Dubbo使用jsr303框架hibernate

Stella981
• 阅读 1222

前言,传递的DTO带有hibernate-validator的注解校验会报错。

Dubbo可以集成jsr303标准规范的验证框架,作为验证框架不二人选的hibernate-validator是大家都会经常在项目中使用的,但是在Dubbo使用是会发生下面这个问题。

问题描述

背景:使用springmvcrestful,使用dubbo做rpc,restful中调用大量的rpc,数据验证会在这两个地方,一个是restful层面,一个是rpc层面,restful层面使用springmvc默认的集成hibernate-validator来实现,参数开启验证只需要加入@Validated param

rpc层面也使用hibernate-validator实现,dubbo中开启validation也有两个方式,一个是在consumer端,一个是在provider端。

当我们在consumer端开启验证时:

<dubbo:reference id="serviceName" interface="com.domain.package.TestService" registry="registry" validation="true"/>

没有任何问题,可以拿到所有的数据校验失败数据。

当我们在provider端开启验证时:

<dubbo:service interface="com.domain.package.TestService" ref="serviceName" validation="true" />

会发生如下异常:

com.alibaba.dubbo.rpc.RpcException: Failed to invoke remote method: sayHello, provider: 

dubbo://127.0.0.1:20831/com.domain.package.TestService?application=dubbo-test-

rest&default.check=false&default.cluster=failfast&default.retries=0&default.timeout=1200000&default.version=1.0

.0&dubbo=2.6.1&interface=com.domain.package.TestService&methods=sayHello&pid=29268&register.ip=192.

168.6.47&side=consumer&timestamp=1524453157718, cause: com.alibaba.com.caucho.hessian.io.HessianFieldException: 

org.hibernate.validator.internal.engine.ConstraintViolationImpl.constraintDescriptor: 

'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated
com.alibaba.com.caucho.hessian.io.HessianFieldException: 

org.hibernate.validator.internal.engine.ConstraintViolationImpl.constraintDescriptor: 

'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated
    at com.alibaba.com.caucho.hessian.io.JavaDeserializer.logDeserializeError(JavaDeserializer.java:167)
    at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize

(JavaDeserializer.java:408)
    at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:273)
    at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:200)
    at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:525)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2791)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2731)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2705)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260)
    at com.alibaba.com.caucho.hessian.io.CollectionDeserializer.readLengthList

(CollectionDeserializer.java:119)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2186)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2057)
    at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize

(JavaDeserializer.java:404)
    at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:273)
    at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:200)
    at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:525)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2791)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2731)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2705)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2260)
    at com.alibaba.dubbo.common.serialize.hessian2.Hessian2ObjectInput.readObject

(Hessian2ObjectInput.java:74)
    at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:90)
    at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:110)
    at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCodec.decodeBody(DubboCodec.java:88)
    at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:121)
    at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:82)
    at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCountCodec.decode(DubboCountCodec.java:44)
    at com.alibaba.dubbo.remoting.transport.netty.NettyCodecAdapter$InternalDecoder.messageReceived

(NettyCodecAdapter.java:133)
    at org.jboss.netty.channel.SimpleChannelUpstreamHandler.handleUpstream

(SimpleChannelUpstreamHandler.java:70)
    at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:564)
    at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:559)
    at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:268)
    at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:255)
    at org.jboss.netty.channel.socket.nio.NioWorker.read(NioWorker.java:88)
    at org.jboss.netty.channel.socket.nio.AbstractNioWorker.process(AbstractNioWorker.java:109)
    at org.jboss.netty.channel.socket.nio.AbstractNioSelector.run(AbstractNioSelector.java:312)
    at org.jboss.netty.channel.socket.nio.AbstractNioWorker.run(AbstractNioWorker.java:90)
    at org.jboss.netty.channel.socket.nio.NioWorker.run(NioWorker.java:178)
    at org.jboss.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108)
    at org.jboss.netty.util.internal.DeadLockProofWorker$1.run(DeadLockProofWorker.java:42)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:744)
Caused by: com.alibaba.com.caucho.hessian.io.HessianProtocolException: 

'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated
    at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:313)
    at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:198)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2789)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2128)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2057)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2101)
    at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2057)
    at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize

(JavaDeserializer.java:404)
    ... 43 more
Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:526)
    at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:309)
    ... 50 more
Caused by: java.lang.NullPointerException
    at org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl.<init>

(ConstraintDescriptorImpl.java:158)
    at org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl.<init>

(ConstraintDescriptorImpl.java:211)
    ... 55 more

问题分析

上面的问题从异常面来看已经很直观了,'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated,这个类无法实例化,那是什么原因导致它无法实例化呢?

Dubbo的序列化协议,默认是hessian,如果没有进行其他协议配置的话,默认使用的就是hessianhessian在反序列化时有个特点需要注意一下,它会在反序列化时取参数最少的构造器来创建对象,有的时候会有很多重载的构造器,因此会有一些参数直接给null,因此可能就会造成一些莫名其妙的问题,就像我们这个问题一样。

那这个问题如何解决呢?接着往下看

解决方案

由于这个是Hessian反序列化问题,因此与Dubbo的版本关系不大,为了验证这个我还专门使用apache dubbo 2.6.1版本测试了一下,问题依旧存在。

方法一:使用无参构造方法来创建对象

既然是hessian反序列化问题,而且它在反序列化时根据构造函数参数个数优先级来取参数最少的,那我们就可以增加一个无参的构造方法来解决这个问题。

但是有的时候我们使用的是第三方的包,不太好增加无参的构造方法,那怎么办的,我们能不能使用其他方法,继续往下看。

方法二:替换jsr303实现框架

既然hibernate-validatororg.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl这个类在使用hessian反序列化存在问题,那我们使用其他jsr303的框架来试试。

jsr303的实现框架有哪些?

  • org.hibernate : hibernate-validator : 5.2.4.Final
  • org.apache.bval : bval-jsr303 : 0.5
  • jersery

bval是apache的一个bean validator的实现,jersery是一个restful的框架为了满足自身的数据验证功能因此增加了jsr303的实现。

由于我们使用的springmvc构建restful因此这里就不考虑jersery,我们就从bval下手来试一试。

在进行了一番配置后(都有哪些配置?)

  • 增加bval包,现在版本是:0.5

    org.apache.bval bval-jsr303 0.5
  • 将bval集成到spring框架中,作为spring的验证框架

这里有两种方式,一种xml配置,一种java config

xml方式:

<mvc:annotation-driven validator="validator"/>  
  
<!-- 数据验证 Validator bean -->  
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">  
    <property name="providerClass" value="org.apache.bval.jsr.ApacheValidationProvider" />  
</bean>  

java config方式: 重写mvcValidator方法

    @Override
    public Validator mvcValidator() {
        Validator validator = super.mvcValidator();
        if (validator instanceof LocalValidatorFactoryBean) {
            LocalValidatorFactoryBean lvfb = (LocalValidatorFactoryBean) validator;
            try {
                String className = "org.apache.bval.jsr303.ApacheValidationProvider";
                Class<?> clazz = ClassUtils.forName(className, WebMvcConfigurationSupport.class.getClassLoader());
                lvfb.setProviderClass(clazz);
            }
            catch (ClassNotFoundException e) {
                //没有找到bval验证框架,走spring默认整合的验证框架:hibernate-validator
                //这里异常没有必要跑出去,直接吃掉
            }
        }
        return validator;
    }

启动后验证功能

但是不好的事情发生了,无法启动报错,错误如下:

java.lang.AbstractMethodError: org.apache.bval.jsr303.ConfigurationImpl.getDefaultParameterNameProvider()

Ljavax/validation/ParameterNameProvider;

经过对spring的资料查找,发现spring从4.0版本往后不在支持集成其他jsr303的框架了,只能使用hibernate-validator,我擦这个有点暴力了。即使自己实现一个jsr303框架也无法再spring中使用,除非不使用spring validator功能,直接使用自己的验证框架来进行验证,这样就无法使用@Validated param方式。

那这种方法只能放弃了。

方法三:修改hibernate-validator的原声类,修改Dubbo ValidationFilter,这也是我最终采用的方法

其实替换jsr303框架不能成功,替换序列化协议应该也可以避免这个问题,只不过替换协议这个一般在维护的项目中不太会选择这样的方式来动刀子,现在开发很多都是分布式服务,序列化反序列化已经无处不在了,因此我建议编写代码时都增加一个无参数的构造方法,养成这样的一个好习惯可以避免很多序列化反序列化框架的坑。而且还有那些有匿名内部类的这种在序列化反序列化也需要注意,不是所有的序列化反序列化框架都支持有匿名类,gson是支持的这个为测试过,我前面也写过一篇博文里面就主要说这个问题,可以查看:《Java中内部类使用注意事项,内部类对序列化与反序列化的影响》

有兴趣的可以看一下我们常用的序列化反序列化类库的一些使用中的注意事项,可以参考这篇文章:《java常用JSON库注意事项总结》

回归话题,上面的问题我们如何解决,最终我们采用重写javax.validation.ConstraintViolation<T>的实现类,替换掉hibernate-validationorg.hibernate.validator.internal.engine.ConstraintViolationImpl,因为ConstraintViolationImpl中有部分对象无法通过hessian反序列化。

我们最终的目标是不管是validation开启在provider端还是consumer端,调用方接收到的参数校验异常数据是一致的。

修改的代码已经提交到apache dubbo,具体查看Pull request:https://github.com/apache/incubator-dubbo/pull/1708

大概的代码如下:

增加类:DubboConstraintViolation实现javax.validation.ConstraintViolation接口

import java.io.Serializable;
import javax.validation.ConstraintViolation;
import javax.validation.Path;
import javax.validation.ValidationException;
import javax.validation.metadata.ConstraintDescriptor;
import com.alibaba.dubbo.common.logger.Logger;
import com.alibaba.dubbo.common.logger.LoggerFactory;


public class DubboConstraintViolation<T> implements ConstraintViolation<T>, Serializable {
    
    static final Logger logger = LoggerFactory.getLogger(DubboConstraintViolation.class.getName());

    private static final long serialVersionUID = -8901791810611051795L;

    private String interpolatedMessage;
    private Object value;
    private Path propertyPath;
    private String messageTemplate;
    private Object[] executableParameters;
    private Object executableReturnValue;
    private int hashCode;

    public DubboConstraintViolation() {
    }
    
    public DubboConstraintViolation(ConstraintViolation<T> violation) {
        this(violation.getMessageTemplate(), violation.getMessage(), violation.getInvalidValue(), violation.getPropertyPath(),
                violation.getExecutableParameters(), violation.getExecutableReturnValue());
    }

    public DubboConstraintViolation(String messageTemplate,
            String interpolatedMessage,
            Object value,
            Path propertyPath,
            Object[] executableParameters,
            Object executableReturnValue) {
        this.messageTemplate = messageTemplate;
        this.interpolatedMessage = interpolatedMessage;
        this.value = value;
        this.propertyPath = propertyPath;
        this.executableParameters = executableParameters;
        this.executableReturnValue = executableReturnValue;
        // pre-calculate hash code, the class is immutable and hashCode is needed often
        this.hashCode = createHashCode();
    }
    
    @Override
    public final String getMessage() {
        return interpolatedMessage;
    }

    @Override
    public final String getMessageTemplate() {
        return messageTemplate;
    }

    @Override
    public final T getRootBean() {
        return null;
    }

    @Override
    public final Class<T> getRootBeanClass() {
        return null;
    }

    @Override
    public final Object getLeafBean() {
        return null;
    }

    @Override
    public final Object getInvalidValue() {
        return value;
    }

    @Override
    public final Path getPropertyPath() {
        return propertyPath;
    }

    @Override
    public final ConstraintDescriptor<?> getConstraintDescriptor() {
        return null;
    }

    @Override
    public <C> C unwrap(Class<C> type) {
        if ( type.isAssignableFrom( ConstraintViolation.class ) ) {
            return type.cast( this );
        }
        throw new ValidationException("Type " + type.toString() + " not supported for unwrapping.");
    }

    @Override
    public Object[] getExecutableParameters() {
        return executableParameters;
    }

    @Override
    public Object getExecutableReturnValue() {
        return executableReturnValue;
    }

    @Override
    // IMPORTANT - some behaviour of Validator depends on the correct implementation of this equals method! (HF)

    // Do not take expressionVariables into account here. If everything else matches, the two CV should be considered
    // equals (and because of the scary comment above). After all, expressionVariables is just a hint about how we got
    // to the actual CV. (NF)
    public boolean equals(Object o) {
        if ( this == o ) {
            return true;
        }
        if ( o == null || getClass() != o.getClass() ) {
            return false;
        }

        DubboConstraintViolation<?> that = (DubboConstraintViolation<?>) o;

        if ( interpolatedMessage != null ? !interpolatedMessage.equals( that.interpolatedMessage ) : that.interpolatedMessage != null ) {
            return false;
        }
        if ( propertyPath != null ? !propertyPath.equals( that.propertyPath ) : that.propertyPath != null ) {
            return false;
        }
        if ( messageTemplate != null ? !messageTemplate.equals( that.messageTemplate ) : that.messageTemplate != null ) {
            return false;
        }
        if ( value != null ? !value.equals( that.value ) : that.value != null ) {
            return false;
        }

        return true;
    }

    @Override
    public int hashCode() {
        return hashCode;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append( "DubboConstraintViolation" );
        sb.append( "{interpolatedMessage='" ).append( interpolatedMessage ).append( '\'' );
        sb.append( ", propertyPath=" ).append( propertyPath );
        sb.append( ", messageTemplate='" ).append( messageTemplate ).append( '\'' );
        sb.append( ", value='" ).append( value ).append( '\'' );
        sb.append( '}' );
        return sb.toString();
    }

    // Same as for equals, do not take expressionVariables into account here.
    private int createHashCode() {
        int result = interpolatedMessage != null ? interpolatedMessage.hashCode() : 0;
        result = 31 * result + ( propertyPath != null ? propertyPath.hashCode() : 0 );
        result = 31 * result + ( value != null ? value.hashCode() : 0 );
        result = 31 * result + ( messageTemplate != null ? messageTemplate.hashCode() : 0 );
        return result;
    }

}

修改com.alibaba.dubbo.validation.filter.ValidationFilter异常处理的部分

这里的变更为捕捉javax.validation.ConstraintViolationException异常,对异常中的Set<ConstraintViolation<String>>数据进行转换,去掉无法反序列化的对象,具体代码如下:

public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
    if (validation != null && !invocation.getMethodName().startsWith("$")
            && ConfigUtils.isNotEmpty(invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.VALIDATION_KEY))) {
        try {
            Validator validator = validation.getValidator(invoker.getUrl());
            if (validator != null) {
                validator.validate(invocation.getMethodName(), invocation.getParameterTypes(), invocation.getArguments());
            }
        } catch (ConstraintViolationException e) {
            Set<ConstraintViolation<?>> set = null;
            //验证set中如果是hibernate-validation实现的类就处理,其他的实现类放过
            Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
            for (ConstraintViolation<?> v : constraintViolations) {
                if (!v.getClass().getName().equals("org.hibernate.validator.internal.engine.ConstraintViolationImpl")) {
                    return new RpcResult(e);
                } else {
                    if (set == null) set = new HashSet<ConstraintViolation<?>>();
                    set.add(new DubboConstraintViolation<>(v));
                }
            }
            return new RpcResult(new ConstraintViolationException(e.getMessage(), set));
        } catch (RpcException e) {
            throw e;
        } catch (Throwable t) {
            return new RpcResult(t);
        }
    }
    return invoker.invoke(invocation);
}

使用这个方法后,在provider端设置validation=trueconsumer端可以正常拿到所有校验数据的异常信息。

总结

我觉得这个方法并不是完美的方法,虽然这个问题是hibernate-validator框架的问题,hibernate-validator出生的年代分布式还不是特别的完善因此没有充分的考虑序列化反序列化问题也很正常,但是作为Dubbo框架在集成jsr303的时候也需要考虑这些问题。具体可以查看Apache DubboPull Requesthttps://github.com/apache/incubator-dubbo/pull/1708

点赞
收藏
评论区
推荐文章
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
Karen110 Karen110
3年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
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之前把这