Jplus 框架说明

Stella981
• 阅读 681

Jplus-core说明

声明

jplus 是一个java框架,web只是他的一个模块,他并非重复造轮子,初衷是希望把所有好的合适的框架集合到一起,能发挥出对开发者更大的作用。
框架思想主要学习黄勇 的smart,推荐大家去看看;

关于Jplus

  1. 支持零配置,所有配置可由.properties或代码管理。
  2. 提供可以媲美Spring的 IOC、AOP、MVC,及各种强大的第三方定制插件。
  3. 快速开发,框架~200Kb,所有插件可插拔。

jplus 简介

Jplus 框架说明

项目启动顺序:
初始化 -->CoreLoader -->BeanHandle -->ActionHandle -->PluginHandle -->AopHandle -->IocHandle -->完成

关于Bean管理
项目启动后CoreLoader 加载 BeanHandle,同时BeanHandle 初始化ConfigHandle, 获取一系列相应的用户配置信息properties。
然后扫描 ${app.scan.pkg} 包中所有带@Component注解的class,完成bean的发现。
PS:在项目中运行时获取 bean 对象可以使用:BeanHandle.getBean(Classcls);

1. 快速创建一个jplusMvc项目

  1. 首先我们创建一个simple Maven Project
    然后第一步创建一个HelloAction.java用于接收用户请求:

    package com.demo.mvc.action; import com.jplus.core.mvc.WebUtil; import com.jplus.core.mvc.annotation.Controller; import com.jplus.core.mvc.annotation.Request;

    @Controller public class HelloAction {

     @Request.All("{msg}")
     public void index(String msg) {
         System.err.println("== This is Restful Action ===========");
         WebUtil.writeHTML(msg);
     }
    

    }

  2. 然后我们看见,包名是com.demo开始的,现在配置注解扫描配置,在project的classpath目录下,添加文件:app.properties

    app.scan.pkg=com.demo

  3. [附加]其实到第二步,项目就可以启动了,但为了debug方便,添加日志配置,在classpath目录下,添加:log4j.xml

    <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/" debug="false">

  4. 最后添加Tomcat容器,启动访问:localhost:8080/hello试试吧~

2. 关于JplusMvc的相关配置

  1. 关于MVC注解

    • 注解:**@Controller**
      使用说明:用于定义Action配置。类上加上此注解,该类将被定义为Action处理的接收类。
      使用方式:直接在请求处理类上加上此注解即可。

    • 注解:**@Request**
      使用说明:用于定义请求的UrlMapping,对用户的请求根据路径做路由分发。
      使用方式:见下方代码,默认提供六种配置方式,对应于不同的http请求:
      @Request("xxx") => 用于公共请求前缀,作用于类上
      @Request.All("xxx")=> 用于接受所有匹配的请求,作用于方法上
      @Request.Get("xxx")=> 用于接受Http get请求,作用于方法上
      @Request.Post("xxx")=> 用于接受Http post请求,作用于方法上
      @Request.Put("xxx")=> 用于接受Http put请求,作用于方法上
      @Request.Delete("xxx")=> 用于接受Http delete请求,作用于方法上

      package com.demo.mvc.action; import com.jplus.core.mvc.WebUtil; import com.jplus.core.mvc.annotation.Controller; import com.jplus.core.mvc.annotation.Request; @Controller @Request("user/") public class UserAction {

      @Request.All("info") private void info() { WebUtil.writeHTML("This is Page info"); }

      @Request.Get("{id}") private void getUser(int id) { WebUtil.writeHTML("This is a get user info by id,id:"+id); }

      @Request.Post("add") private void addUser() { WebUtil.writeHTML("This is add Page"); }

      @Request.Put("edit") private void editUser() { WebUtil.writeHTML("This is edit Page"); }

      @Request.Delete("{id}") private void delUser(int id) { WebUtil.writeHTML("This is del Page,user id is :" + id); } }

    • 注解:**@Param**
      使用说明:用于设置请求入参,通过入参名称,获取restful/get/post形式的参数。
      使用方式:见下方代码:

      /** * 发现和上面getUser的方法的区别了么, * 上面直接取值,使用的下标取值,取的restful请求中的占位符{xx}中的值,然后按下标依次取,支持类型:int/long/Double/BigDecimal/String * 下面通过@Param取值,使用名称key取值,可以取restful/get/post...等各种方式提交的值,支持类型:int/long/Double/BigDecimal/String */ @Request.Get("{id}") private void getUser2(@Param("id") int userId) { WebUtil.writeHTML("This is a get user info by id,id:"+userId); }

    • 注解:**@Form**
      使用说明:用于标示POJO实体对象的请求入参,作用于实体类上。
      使用方式:当一个pojo实体对象为入参时(form表单提交),框架会自动将入参填充到对象中去。见下方代码:

      ==>UserForm.java @Form public class User { private int id; private String name; private String age; /** ... omit get/set/toString method ... */

      }

      ==> UserAction.java @Request.Post("add") private void addUser(User user) { System.err.println("I can get form param:"+ user.toString()); WebUtil.writeHTML("This is add Page"); }

    • 注解:**@Service**
      使用说明:用于标记实现类,供框架扫描,作为IOC注入到其他类中,作用于类上,一般用于Service层。又一个默认参数[名称],非必填。
      使用方式:@Service用于标记扫描,等待其他类使用@Autowired 注入,
      @Service可以添加参数名 @Service("xxx"),然后注入的地方使用@Autowired("xxx")进行注入,见下方代码:

      ==> UserService.java @Service public class UserService {

      public Result add(User form) { Result res = new Result().OK(); System.err.println("Add Service :" + form.toString()); return res; } }


      ==> UserAction.java

      @Controller @Request("user/") public class UserAction { private @Autowired UserService userService;

      @Request.Post("add") private Result addUser(User form) { return userService.add(form); } }

  2. 关于MVC上下文

    • DataContext.java
      DataContext为整个MVC的上下文容器,生产周期为当前请求。
      里面封装了Cookie,HttpServletRequest,HttpServletResponse,ServletContext,Session等相关对象的处理。

    • WebUtil.java
      Web 操作工具类,依赖于DataContext,提供了取参,输出,转发请求等实用功能。

    • 视图输出:View.java
      视图输出,将请求执行完毕,返回到JSP页面进行渲染,然后返回也客户端页面。
      默认视图地址[在properties中配置]:app.view.path=/WEB-INF/jsp/

      @Controller @Request("hello/") public class HelloAction { @Request.All("page") public View page() { System.err.println("== This is page Action ==========="); //跳转到 : /WEB-INF/jsp/hello/page.jsp return new View(); } }

    • JSON输出:Result.java
      JSON输出,将请求执行完毕,返回到前端JSON字符串。
      推荐使用Result作为方法返回值。用户也可以自定义任何对象输出,默认都为JSON格式。

      @Request.Get("JSON") public Result getJSON() { System.err.println("== This is page Action ==========="); // 返回JSON : {"code":1,"obj":"请求成功"} return new Result().OK().DATA("请求成功"); }

3. 关于JplusAop的相关配置

说明:使用jdk,Cglib动态代理技术实现,采用责任链模式,递归代理。
默认AOP提供三种代理:

  1. AspectProxy.java 切面代理,提供6种切点方法:

    //开始 public void begin() //拦截[根据boolean判断是否继续执行方法] public boolean intercept(Class cls, Method method, Object\[\] params) //方法执行前 public void before(Class cls, Method method, Object[] params) throws Throwable //方法执行后 public void after(Class cls, Method method, Object\[\] params, Object result) //抛出异常 public void error(Class cls, Method method, Object[] params, Throwable e) //结束 public void end()

  • 使用方式:
    对自定义切面类继承AspectProxy抽象类,并重写目标方法, 然后对该类添加@Aspect注解。

    • 注解:@Aspect _(该注解有三个属性值:)_:
      pkg:想要被代理类所在的包名。
      cls:想要被代理的具体类,和pkg二选一。
      annotation:想要被代理的类是有指定注解的类

    • 注解:@AspectOrder
      该注解是@Aspect的补充,对链式AOP做排序,若有@AspectOrder注解,则优先比较(序号的值越小越靠前)

    • 代码示例:

      package com.demo.mvc.aop; import java.lang.reflect.Method;

      import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.jplus.core.aop.AspectProxy; import com.jplus.core.aop.annotation.Aspect; import com.jplus.core.mvc.DataContext; import com.jplus.core.mvc.annotation.Controller;

      /** * AOP拦截器演示
      * 测试拦截指定包名下所有的Action * @author Yuanqy */ @Aspect(pkg = "com.demo.mvc.action", annotation = Controller.class) public class ActionInterceptor extends AspectProxy { private Logger logger = LoggerFactory.getLogger(getClass());

      ThreadLocal curTimes = new ThreadLocal();

      @Override public void before(Class<?> cls, Method method, Object[] params) throws Throwable { curTimes.set(System.currentTimeMillis()); logger.info("===Is new RequestURI:" + DataContext.getRequest().getRequestURI()); logger.info("===Action:{}.{}", cls.getName(), method.getName()); }

      @Override public void after(Class<?> cls, Method method, Object[] params, Object result) throws Throwable { logger.info("===Action is complete,time:{}ms \n", (System.currentTimeMillis() - curTimes.get())); curTimes.remove(); } }

  1. TransactionProxy.java 事务代理:

    • 使用方式:
      在想要被事务代理的类、或类的方法上添加注解:**@Transaction**即可。

    • **注解:@Transaction**:
      类似于Spring的事务,支持事务传播,支持配置事务隔离级别(默认level=4)
      作用于类上,表示当前类中所有方法开启事务。
      作用于方法上,表示当前方法开启事务。

    • 代码示例:

      @Service public class UserService {

      @Transaction
      public Result add(User form) {
          Result res = new Result().OK();
          // TODO something...
          return res;
      }
      

      }

  2. PluginProxy.java 插件代理:

    • 使用方式:
      他是对于上诉两种代理的补充,可以用于扩展任意规则、任意场景代理实现。
      该类仅有一个获取代理链方法:

      /** * 具体代理被执行时,会执行实现了Proxy接口的doProxy()方法. * 使用方式:请参考:com.jplus.j2cache.proxy.J2CachePluginProxy * 原理: * 项目启动时,先扫描缓存bean,当AopHandle运行时,执行插件代理的getTargetClassList方法。 * 该方法由开发人员自己定义筛选规则,将满足条件的对象bean集合返回,做AOP代理; * 在项目运行期,代理的对象执行时,通过链式代理,找到当前类,执行doProxy()方法,完成代理操作。 */ public abstract List<Class<?>getTargetClassList()

4. 关于JplusIOC的相关配置

框架中,ioc初始化位于项目启动最后阶段。 之前的MVC,AOP,Plugin 已经是创建了所有项目中所需的bean对象, 在最后要做的就是把这些 对象 互相关联起来,建立原本的依赖关系。

  • **注解:@Autowired**:

    • 使用方式:

      //通过接口注入,框架会找到容器中的第一个接口实现类进行注入,实现类必须>=1,否则异常 private @Autowired IIocDemo demo;// 通过接口注入

      //通过指定接口实现注入,实现类上必须标明当前名称@Service("xxx")才能注入成功 private @Autowired("xxx") IIocDemo demo;// 通过指定接口实现注入

      //这个没什么好说的,直接注入目标类即可 //PS.只要是bean容器管理了的对象,都可通过@Autowired注入。 private @Autowired IocDemoImpl demo;// 通过指定对象注入

  • **注解:@Value**:

    • 使用方式:

      //方式1:@Value("xxx") //方式2:@Value("${xxx}") 两种方式效果一致 private @Value("app.scan.pkg") String scanPkg;// 注入配置文件

  • **注解:@Component**:
    @Component注解是其他注解的元注解。jplus的扫描规则是扫描所有继承或使用Component注解的类,然后加入bean管理。

    • 使用方式:

      /** * 属于Bean扫描注解,任何需要加入IOC的组件类,都 可以添加该注解。 * 该注解提供两个参数[都非必填项]: * initMethod:填入当前类的方法名,项目启动后会执行该方法 * destroyMethod:填入当前类的方法名,项目注销前会执行该方法 */ @Component(initMethod = "init", destroyMethod = "destroy") public class InitComponent { public void init() { System.err.println("== 项目启动后会执行该方法 =="); }

      public void destroy() { System.err.println("== 项目注销前会执行该方法 =="); } }

  • 代码示例:

    /** * 接口类 */ public interface IIocDemo { Result addUser(User user); Result getUser(int id); }

    /** * 实现类 */ //@Service @Service("iocDemoImpl") public class IocDemoImpl implements IIocDemo {

    private Map<Integer, User> map = new HashMap<>();
    
    @Override
    public Result addUser(User user) {
      map.put(user.getId(), user);
      return new Result().OK().DATA("新增成功");
    }
    
    @Override
    public Result getUser(int id) {
      if (map.containsKey(id))
      return new Result().OK().DATA(map.get(id));
      else
      return new Result().FAIL().DATA("数据不存在");
    }
    

    }

    /** * 在Action中注入 */ @Controller @Request("ioc/") public class IocAction {

    private @Autowired IIocDemo ioc1;            // 通过接口注入
    private @Autowired("iocDemoImpl") IIocDemo ioc2;    // 通过指定接口实现注入
    private @Autowired IocDemoImpl ioc3;        // 通过指定对象注入
    
    private @Value("app.scan.pkg") String scanPkg;
    
    @Request.Get("test")
    private void iocTest() {
      System.out.println(ioc1.addUser(new User(1, "张三")));
      System.out.println(ioc2.getUser(1));
      System.out.println(ioc3.getUser(1));
    
      WebUtil.writeTEXT("OK");
    }
    

    }

5. 关于Jplus Junit的相关配置

jplus框架默认集成 Junit4 单元测试框架。

  • 使用方式:

    测试类添加两个注解:@RunWith [Junit框架注解]、@ContextConfiguration [Jplus框架注解] 即可。
    RunWith注解大家可以百度下使用方式,现在说明下ContextConfiguration注解。

    • 注解:@ContextConfiguration
      ContextConfiguration用于配置配置文件路径。通过参数locations配置,说明如下:

      "file:/app.properties"            //系统根目录 
      "app.properties"            //项目根目录 
      "/app.properties"            //项目WEB-INF下 
      "classpath:app.properties"        //项目ClassPath下
      
      //如果有多个配置文件,可以用 ; 分号分割,如下:
      "file:/db.properties;classpath:app.properties"    //多个配置文件.一个文件目录下;一个classpath目录
      
    • 代码示例:

      /** * JplusJunit4 测试框架IOC功能 */ @RunWith(JplusJunit4Runner.class) @ContextConfiguration(locations = "classpath:app.properties") public class JunitIOC {

      private @Autowired("iocDemoImpl") IIocDemo ioc3; private @Autowired IocDemoImpl ioc2;

      private @Value("app.scan.pkg") String scanPkg;

      @Test public void test() throws InterruptedException { System.out.println("添加用户:" + ioc3.addUser(new User(1001, "Jplus"))); System.out.println("获取用户:" + ioc2.getUser(1001)); System.out.println("app.scan.pkg=" + scanPkg); Thread.currentThread().join(); } }

6. 关于JplusMVC的后端参数验证

对于请求入参,JPlus可以通过拦截器做公共参数校验,对于form表单对象,可以使用工具类进行校验。

  • 使用方式:
    对于form表单对象(可以理解为添加了@Form注解的pojo类),当该对象作为请求入参时,正常情况参数的校验都是固定的字段,固定的规则。而且开发人员也不想在业务代码里面进行入参判断。这时候就可以使用:**Result res= VerifyUtil.doVerify(form);**通过返回Result判断校验结果。
    原理:对入参对象的字段进行处理,判断是否有@V注解,对@V注解的字段进行判断处理。

  • 注解 @V

    注解名

    默认表达式

    默认返回描述

    是否可为空

    IsNotNull

    \w+

    字段{}不能为空

    FALSE

    IsCN

    ^[\u4e00-\u9fa5]+$

    字段{}不是汉字

    TRUE

    IsDigits

    ^[0-9]*$

    字段{}不是纯数字

    TRUE

    IsFloat

    ^[0-9]*$

    字段{}不是小数

    TRUE

    IsEmail

    ^\w+([-+.]\w+)_@\w+([-.]\w+)_\.\w+([-.]\w+)*$

    邮箱格式不正确

    TRUE

    IsIdCardNo

    ^(\d{6})()?(\d{4})(\d{2})(\d{2})(\d{3})(\w)$

    身份证号不正确

    TRUE

    IsPhone

    ^[1][0-9]{10}$

    手机号码不正确

    TRUE

    IsPwd

    ^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,16}$

    密码格式不正确

    TRUE

    IsTel

    ^(0\d{2}-\d{8}(-\d{1,4})?)|(0\d{3}-\d{7,8}(-\d{1,4})?)$

    电话号码不正确

    TRUE

    IsZipCode

    ^[0-9]{6}$

    邮政编码不正确

    TRUE

    Custom

     

    未定义

    TRUE

    PS:@V.Custom()用于给用户自定义校验规则及返回说明。还支持JPEL表达式。

  • 代码示例:

    /** * Form入参表单 */ @Form public class User { @V.IsNotNull @V.IsDigits private int id; //表示id必须为数字,不能为空 private String name; @V.Custom(regex = "el:{}>=18 && {}<30", retmsg = "年龄必须在18~30岁之间",isNull=false) private String age; //表示 自定义用JPEL表达式判断年龄范围,不能为空 @V.Custom(regex="^[A-Za-z]+$",retmsg="请输入正确的URL") private String url; //表示 自定义用正则表达式判断 @V.IsPhone private String phone; //表示 必须为手机号,可为空 } //====================================================== @Controller @Request("user/") public class UserAction { private @Autowired UserService userService; @Request.Post("add") private Result addUser(User form) { //执行入参验证 Result res = VerifyUtil.doVerify(form); if (res.IsOK()) return userService.add(form); return res; } }

7. 附加:关于Jplus的JPEL表达式的使用

  • 使用逆波兰算法实现,相比js引擎,效率提升>15倍,后续会逐渐优化,支持特性:

    • 算数运算表达式:
      加(+)、减(-)、乘(*)、除(/)、求余(%)、幂(^)运算
    • 关系表达式:
      等于(==)、不等于(!=)、大于(>)、大于等于(>=)、小于(<)、小于等于(<=)
    • 逻辑表达式:
      且(&&)、或( || )、非( ! )、true、false
    • 括号优先级表达式:
      使用“(表达式)”构造,括号里的具有高优先级。
    • 不支持三目运算,不支持文本字符串
  • 使用方式:

    //参见代码:com.jplus.core.util.JPEL.java

    String calc = "1+2*(3+4)-5+(-2)*8/2+8 == 5*6/2-5 && 10%4*3>-1+2*3"; System.err.println("中缀表达式:" + calc); // == 逆波兰 ======================== long t = System.currentTimeMillis(); System.out.println("逆波兰=" + excute(calc) + "\ttime:" + (System.currentTimeMillis() - t)); // == JS引擎========================== t = System.currentTimeMillis(); System.out.println("JS引擎=" + getSE().eval(calc) + "\ttime:" + (System.currentTimeMillis() - t));

8. 附加:关于Jplus多数据源的配置

多数据源可以支持申明式配置编程式配置两种,具体方法参考jplus-orm说明文档。

  • 编程式配置 实例:

    /** * JplusJunit4 测试Jplus-core编程式多数据源切换 */ @RunWith(JplusJunit4Runner.class) @ContextConfiguration(locations = "classpath:app.properties") public class JplusDDS {

     @Test
     public void test() {
         JdbcTemplate jt = null;
         try {
             // 1.创建N个数据源,可选用C3p0,Druid...都可以
             DataSource ds1 = new C3p0Plugin("jdbc:mysql://139.196.18.60:3306", "root", "password", "com.mysql.jdbc.Driver").getDataSource();
             DataSource ds2 = new C3p0Plugin("jdbc:mysql://192.168.1.16:3306", "root", "password", "com.mysql.jdbc.Driver").getDataSource();
             // 2.将N个数据源放入DynamicDataSource中,并定义key名称
             Map<String, DataSource> map = new HashMap<String, DataSource>();
             map.put("readone", ds1);
             map.put("readtwo", ds2);
             DynamicDataSource dds = new DynamicDataSource(map);
             // 3.创建一个JdbcTemplate,使用数据源为DynamicDataSource
             jt = new JdbcTemplate(dds);
    
             // 4.循环测试数据源切换\[ps.我事先准备了两个不同版本的mysql\]
             for (int i = 0; i < 3; i++) {
                 if (i == 2)
                     jt.openTransaction(); // 最后一次开启事务
                 DataSourceHolder.setDbType("readone");
                 System.err.println("\[切换1\]:" + JSON.toJSONString(jt.query("SHOW VARIABLES LIKE 'version';", new Object\[\] {})));
                 DataSourceHolder.setDbType("readtwo");
                 System.err.println("\[切换2\]:" + JSON.toJSONString(jt.query("SHOW VARIABLES LIKE 'version';", new Object\[\] {})));
                 if (i == 2)
                     jt.commit(); // 提交事务
             }
         } catch (Exception e) {
             e.printStackTrace();
             if (jt != null)
                 jt.rollback(); // 回滚事务
         }
    
     }
    

    }

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Stella981 Stella981
3年前
Python之time模块的时间戳、时间字符串格式化与转换
Python处理时间和时间戳的内置模块就有time,和datetime两个,本文先说time模块。关于时间戳的几个概念时间戳,根据1970年1月1日00:00:00开始按秒计算的偏移量。时间元组(struct_time),包含9个元素。 time.struct_time(tm_y
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Stella981 Stella981
3年前
Spring Boot日志集成
!(https://oscimg.oschina.net/oscnet/1bde8e8d00e848be8b84e9d1d44c9e5c.jpg)SpringBoot日志框架SpringBoot支持JavaUtilLogging,Log4j2,Lockback作为日志框架,如果你使用star
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这