V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
simonlu9
V2EX  ›  Java

神奇的 beanUtils.copyProperties 目标与源对象属性类型不一致但可以 set 成功

  •  
  •   simonlu9 · 2022-08-02 16:42:02 +08:00 · 2140 次点击
    这是一个创建于 874 天前的主题,其中的信息可能已经有所发展或是发生改变。
    public class CourseCategoryResponse {
    
        private String id;
        @ApiModelProperty("分类名称")
        private String name;
        @ApiModelProperty("分类图标")
        private String image;
    
        @ApiModelProperty("英文名称")
        private String nameEn;
    
        @ApiModelProperty("子分类")
        private List<CourseCategoryResponse> children;
        @ApiModelProperty("课程列表")
        private List<CourseSimpleResponse> coures;
    }
    
    public class Category {
    
        @Id
        @Column(length = 37)
        @GeneratedValue(generator = "jpa-uuid")
        private String id;
    
        @ApiParam("主体")
        @Column(length = 32)
        private String subject;
    
    
        @ApiParam("分类名称")
        @Column(length = 32)
        private String name;
       
        private List<Category> children;
        
     }
     
     ==========调用代码=====================
     
      PropertyDescriptor propertyDescriptors = BeanUtils.getPropertyDescriptor(CourseCategoryResponse.class,"children");
            CourseCategoryResponse courseCategoryResponse = new CourseCategoryResponse();
            Category category = new Category();
            category.setName("123");
            Category children = new Category();
            children.setName("234");
            category.setChildren(List.of(children));
            PropertyDescriptor source = BeanUtils.getPropertyDescriptor(Category.class,"children");
    
            Object value = source.getReadMethod().invoke(category);
           propertyDescriptors.getWriteMethod().invoke(courseCategoryResponse,value);
           System.out.println(courseCategoryResponse);
           
           
     打印结果 courseCategoryResponse 的 children 是有值的,我想这个为什么可以 set 进去
    第 1 条附言  ·  2022-08-04 17:15:32 +08:00

    经过一段源码阅读,很好奇fastjson是怎样序列化,它是怎样判断children类型,先通过反射类的方法和属性,然后生成一堆拼接json的代码,为什么要这样,生成一个类就可以重用,不用每次都反射,效率太慢,mapStuct也是这样原理 其中这段代码

     List var13 = (List)var10.getChildren();
    

    通过asm

    
    
    
    
    public class ASMSerializer_1_CourseCategoryResponse extends JavaBeanSerializer implements ObjectSerializer {
        public Type children_asm_fieldType = ASMUtils.getMethodType(CourseCategoryResponse.class, "getChildren");
        public ObjectSerializer children_asm_list_item_ser_;
        public ObjectSerializer children_asm_ser_;
        public Type coures_asm_fieldType = ASMUtils.getMethodType(CourseCategoryResponse.class, "getCoures");
        public ObjectSerializer coures_asm_list_item_ser_;
        public ObjectSerializer coures_asm_ser_;
    
        public ASMSerializer_1_CourseCategoryResponse(SerializeBeanInfo var1) {
            super(var1);
        }
    
        public void write(JSONSerializer var1, Object var2, Object var3, Type var4, int var5) throws IOException {
            if (var2 == null) {
                var1.writeNull();
            } else {
                SerializeWriter var9 = var1.out;
                if (!this.writeDirect(var1)) {
                    this.writeNormal(var1, var2, var3, var4, var5);
                } else if (var9.isEnabled(32768)) {
                    this.writeDirectNonContext(var1, var2, var3, var4, var5);
                } else {
                    CourseCategoryResponse var10 = (CourseCategoryResponse)var2;
                    if (!this.writeReference(var1, var2, var5)) {
                        if (var9.isEnabled(2097152)) {
                            this.writeAsArray(var1, var2, var3, var4, var5);
                        } else {
                            SerialContext var11 = var1.getContext();
                            var1.setContext(var11, var2, var3, 0);
                            char var12 = '{';
                            String var6 = "children";
                            List var13 = (List)var10.getChildren();
           
    
    
    
    第 2 条附言  ·  2022-09-12 14:42:06 +08:00

    这个还有补充一点,beanUtis.copyPropertites里面是有判断类型是否能够赋值的

    	for (PropertyDescriptor targetPd : targetPds) {
    			Method writeMethod = targetPd.getWriteMethod();
    			if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
    				PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
    				if (sourcePd != null) {
    					Method readMethod = sourcePd.getReadMethod();
    					if (readMethod != null &&
    							ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
    						
    					}
    				}
    			}
    		}
    
    public static boolean isAssignable(Class<?> lhsType, Class<?> rhsType) {
            Assert.notNull(lhsType, "Left-hand side type must not be null");
            Assert.notNull(rhsType, "Right-hand side type must not be null");
            if (lhsType.isAssignableFrom(rhsType)) {
                return true;
            } else {
                Class resolvedPrimitive;
                if (lhsType.isPrimitive()) {
                    resolvedPrimitive = (Class)primitiveWrapperTypeMap.get(rhsType);
                    if (lhsType == resolvedPrimitive) {
                        return true;
                    }
                } else {
                    resolvedPrimitive = (Class)primitiveTypeToWrapperMap.get(rhsType);
                    if (resolvedPrimitive != null && lhsType.isAssignableFrom(resolvedPrimitive)) {
                        return true;
                    }
                }
    
                return false;
            }
        }
    

    关键是上面的 isAssignable 方法,当List<Category> 赋给 List<CategoryResponse>,他们比较的是List类型,绝对不会比较List里面的类型,实在也是坑之一,所以就会能够产生上述结果

    7 条回复    2022-08-03 22:48:50 +08:00
    simonlu9
        1
    simonlu9  
    OP
       2022-08-02 17:09:20 +08:00
    set 是可以 set 成功的,但是 get 会报错,json 序列化也是可以看到有值,是个坑
    quericy
        2
    quericy  
       2022-08-02 19:04:42 +08:00
    因为 List 里的泛型擦除了
    sLvxq6Ya
        3
    sLvxq6Ya  
       2022-08-02 19:12:42 +08:00   ❤️ 1
    对 BeanUtils 不是很熟悉,推测你这个问题应该是因为 Java 的泛型擦除
    Java 的泛型只在编译期进行类型检查,而 BeanUtils 的 Set 是通过运行时反射,不需要经过类型检查

    所以执行的时候就相当于给 courseCategoryResponse 的 List<Object> children(原本声明要求 List<CourseCategoryResponse>) 赋值了一个 List<Object> (实际上是 List<Category>), set 时都是 List<Object>不会报错,但是 get 时你可能通过指定变量类型做了一个类型转换,就会报错。
    xaplux
        4
    xaplux  
       2022-08-03 08:38:29 +08:00
    2 楼正解
    lifespy
        5
    lifespy  
       2022-08-03 14:30:52 +08:00
    歪个楼
    @ApiParam 和 @ApiModelProperty 有什么区别吗
    Saxton
        6
    Saxton  
       2022-08-03 22:46:17 +08:00 via iPhone
    2 楼正解,这就是反射带来的影响
    Saxton
        7
    Saxton  
       2022-08-03 22:48:50 +08:00 via iPhone   ❤️ 1
    另外我不推荐用 beanUtils 来实现对象的拷贝,本身反射是一种非常危险的行为,并不能在编译的时候发现问题,推荐楼主使用 mapStruct ,编译时会自动生成一个 set 方法
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1339 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 17:26 · PVG 01:26 · LAX 09:26 · JFK 12:26
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.