更加优雅的类型转换工具

类型转换

  • 在日常开发中,我们经常面临需要进行类型转换的情况,例如将数据库中的持久化对象(PO)转换为传输对象(DTO)或视图对象(VO)。这些类之间可能存在一些差异:

    • 字段名称不同
      • 例如,公司表中存储的是 name 字段,但需要返回的视图对象中的字段为 companyName
      • 又或者,用户表中有 companyId 字段,但并不希望将该字段返回给前端。
    • 字段类型不同
      • 例如,数据库中的 id 存储为 bigint,但实际想要返回的数据类型是 string(因为在JavaScript中解析长整型可能会导致精度丢失)。
      • 或者,日期类型存储在数据库中,但想要返回一个字符串类型。
    • 数据来源不同
      • 例如,用户类中包含公司信息,但在用户视图对象中我们可能只需要公司的名称,而不是整个公司对象。

    传统解决方案的问题

    在解决这些问题的过程中,通常会采用以下传统方法:

    • 手动编写类型转换
      • 可以在Service层或者通过创建一个专门的Converter类来进行手动转换,也可以使用构造函数或者成员方法进行转换。然而,这种方式存在一些问题:
        • 代码冗余
          • 编写类型转换通常繁琐且重复,这些代码往往是固定的,但又是不可或缺的。
        • 维护困难
          • 如果类的字段发生变化,需要修改涉及这个字段的大量转换方法,这是一项繁重的任务。

两种拷贝方式

新的解决方案

为了解决上述问题,我们可以考虑使用一些更加优雅的类型转换工具。两种常见的方式是

  • 使用BeanUtils.copyProperties
  • 通过将对象序列化为JSON字符串,然后再反序列化为新对象。

1. BeanUtils.copyProperties

  • 优点
    • 简单易用
      • BeanUtils.copyProperties提供了一个简单的方法,可以方便地将一个对象的属性值拷贝到另一个对象中。
    • 自动匹配字段
      • 这个工具会自动匹配两个对象中相同名称的字段,并进行拷贝,省去了手动编写赋值代码的烦恼。
  • 缺点
    • 字段一一对应
      • 如果字段名称不一致或者需要进行一些特殊的处理,BeanUtils可能就无法满足需求。
    • 性能开销
      • 对于大对象或者频繁调用的情况,使用BeanUtils可能会带来一定的性能开销。
    • 类型转换
      • 当属性名一样但类型不一致时,Spring的BeanUtils.copyProperties方法会尝试进行自动类型转换,如果转换成功则将源对象的属性值赋给目标对象的属性,如果转换失败则会抛出类型转换异常。

2. 序列化为JSON再反序列化

  • 优点

    • 灵活性
      • 通过序列化为JSON字符串再反序列化,可以更加灵活地控制字段的映射关系,进行一些定制化的处理。
    • 解决字段不一致问题
      • 对于字段不一致的情况,可以在序列化和反序列化的过程中进行手动处理,解决命名不同或者类型不同的问题。
  • 缺点

    • 性能开销
      • BeanUtils相似,序列化和反序列化涉及到对象的字符串表示,可能会有一定的性能开销。
    • 额外依赖
      • 需要依赖JSON库来进行序列化和反序列化操作,增加了项目的依赖。

3. 对比:

  • json序列化再反序列化相比起beanUtils来说,增加了可操作性,可以更为便捷地进行配置,指定字段处理的规则
  • json字符串中,仅有基本类型和对象这两种类型,对其进行序列化和反序列化操作,从更低层次上生成对象,避免了很多java类型转换的问题。

通过注解来解决问题

  • 在java中我们可以通过反射的形式来获取类型信息并对字段设置值

  •   MyAnnotation annotation = declaredField.getAnnotation(MyAnnotation.class);
      annotation.name();
      annotation.field();
    

在我的工具中,提供了这样一个工具类和对应的三个注解,用于实现类型转换

@CustomConvent @FromField @SourceField BeanConventUtils.objectConvent(M entity, Class<T> clazz)
  • 什么是源对象和目标对象

  • 因为使用json序列化和反序列化,所以一切操作都涉及到两个json对象

    • 源对象:输入的java对象所生成的json字符串
    • 目标对象:将输出类型所对应的java对象
  • 最简单的类型转换

    • 如果直接使用BeanConventUtils.objectConvent(M entity, Class<T> clazz)进行类型转换,则根据字段匹配默认的序列化反序列化操作,将类型进行转换。

    • 例如

    •   /**
         * @author qcqcqc
         */
        @Data
        @TableName("user")
        public class User implements Serializable {
            private static final long serialVersionUID = 6832388759615000703L;
            @TableId
            private Long id;
            private String name;
            private Long companyId;
            private Date createTime;
        
            @OtODeepSearch(service = CompanyServiceImpl.class)
            @TableField(exist = false)
            private Company company;
        }
      
    • 转换为

    •   /**
         * @author qcqcqc
         */
        @Data
        public class UserVo {
            private String name;
            private String createTime;
            private String companyId;
        }
      
    •   User byId = userService.getById(111);
        UserVo x1 = BeanConventUtils.objectConvent(byId, UserVo.class);
        String x2 = objectMapper.writeValueAsString(x1);
        System.out.println(x2);
        // 这里输出了目标对象的json
      
  • 指定原始字段名称的类型转换

    • 需要使用到@SourceField(name = “”)注解,必须指定name,即原始的字段名称

    •   /**
         * @author qcqcqc
         */
        @Data
        @CustomConvent
        public class UserVo {
            private String name;
            private String createTime;
            private String companyId;
            @SourceField(name = "company")
            private CompanyVo companyInfo;
        }
      
    • 例如这样使用@FromField(fieldPath = "company")指定原来的字段名叫做company

    • 也就是User对象中的company字段其Company类型,也会转换为CompanyVo类型的对象并赋值

    •   /**
         * @author qcqcqc
         */
        @Data
        public class CompanyVo {
            private String id;
            @SourceField(name = "name")
            private String companyName;
            private String testId;
        }
      
    • 这里的companyVo对象也使用了@SourceField注解,来指定原始对象的名称

      • 在反序列化时会继续递归非java包下的对象(自定义类型),并扫描注解进行赋值
      • 所以在UserVo中使用CompanyVo不需要担心其companyInfo字段不完整。
  • 指定原始字段来源来进行类型转换

    • 这个注解的作用类似于SourceField,但是可以指定对象中的对象中的字段,用于更深层次的查找赋值操作

    • 并且支持list类型的数据,进行map操作,生成新的数组

    • 下面举个例子:

      • 原始对象的json字符串:

      •   {
          	"id": 111,
          	"name": "qcqcqc",
          	"companyId": 222,
          	"createTime": "2024-02-24T00:02:58.000+00:00",
          	"company": {
          		"id": 222,
          		"name": "zust",
          		"testId": 111,
          		"test": {
          			"id": 111,
          			"name": "test",
          			"companyId": 0,
          			"company": null
          		},
          		"testList": [{
          			"id": 111,
          			"name": "test",
          			"companyId": 0,
          			"company": null
          		}, {
          			"id": 222,
          			"name": "test2",
          			"companyId": 0,
          			"company": null
          		}, {
          			"id": 333,
          			"name": "test3",
          			"companyId": 0,
          			"company": null
          		}],
          		"tests": [{
          			"id": 555,
          			"name": "test3",
          			"companyId": 222,
          			"company": {
          				"id": 222,
          				"name": "zust",
          				"testId": 111,
          				"test": {
          					"id": 111,
          					"name": "test",
          					"companyId": 0,
          					"company": null
          				},
          				"testList": [{
          					"id": 111,
          					"name": "test",
          					"companyId": 0,
          					"company": null
          				}, {
          					"id": 222,
          					"name": "test2",
          					"companyId": 0,
          					"company": null
          				}, {
          					"id": 333,
          					"name": "test3",
          					"companyId": 0,
          					"company": null
          				}],
          				"tests": [{
          					"id": 555,
          					"name": "test3",
          					"companyId": 222,
          					"company": {
          						"id": 222,
          						"name": "zust",
          						"testId": 111,
          						"test": {
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						},
          						"testList": [{
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 222,
          							"name": "test2",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 333,
          							"name": "test3",
          							"companyId": 0,
          							"company": null
          						}],
          						"tests": [{
          							"id": 555,
          							"name": "test3",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 666,
          							"name": "test4",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 777,
          							"name": "test5",
          							"companyId": 222,
          							"company": null
          						}]
          					}
          				}, {
          					"id": 666,
          					"name": "test4",
          					"companyId": 222,
          					"company": {
          						"id": 222,
          						"name": "zust",
          						"testId": 111,
          						"test": {
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						},
          						"testList": [{
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 222,
          							"name": "test2",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 333,
          							"name": "test3",
          							"companyId": 0,
          							"company": null
          						}],
          						"tests": [{
          							"id": 555,
          							"name": "test3",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 666,
          							"name": "test4",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 777,
          							"name": "test5",
          							"companyId": 222,
          							"company": null
          						}]
          					}
          				}, {
          					"id": 777,
          					"name": "test5",
          					"companyId": 222,
          					"company": {
          						"id": 222,
          						"name": "zust",
          						"testId": 111,
          						"test": {
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						},
          						"testList": [{
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 222,
          							"name": "test2",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 333,
          							"name": "test3",
          							"companyId": 0,
          							"company": null
          						}],
          						"tests": [{
          							"id": 555,
          							"name": "test3",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 666,
          							"name": "test4",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 777,
          							"name": "test5",
          							"companyId": 222,
          							"company": null
          						}]
          					}
          				}]
          			}
          		}, {
          			"id": 666,
          			"name": "test4",
          			"companyId": 222,
          			"company": {
          				"id": 222,
          				"name": "zust",
          				"testId": 111,
          				"test": {
          					"id": 111,
          					"name": "test",
          					"companyId": 0,
          					"company": null
          				},
          				"testList": [{
          					"id": 111,
          					"name": "test",
          					"companyId": 0,
          					"company": null
          				}, {
          					"id": 222,
          					"name": "test2",
          					"companyId": 0,
          					"company": null
          				}, {
          					"id": 333,
          					"name": "test3",
          					"companyId": 0,
          					"company": null
          				}],
          				"tests": [{
          					"id": 555,
          					"name": "test3",
          					"companyId": 222,
          					"company": {
          						"id": 222,
          						"name": "zust",
          						"testId": 111,
          						"test": {
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						},
          						"testList": [{
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 222,
          							"name": "test2",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 333,
          							"name": "test3",
          							"companyId": 0,
          							"company": null
          						}],
          						"tests": [{
          							"id": 555,
          							"name": "test3",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 666,
          							"name": "test4",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 777,
          							"name": "test5",
          							"companyId": 222,
          							"company": null
          						}]
          					}
          				}, {
          					"id": 666,
          					"name": "test4",
          					"companyId": 222,
          					"company": {
          						"id": 222,
          						"name": "zust",
          						"testId": 111,
          						"test": {
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						},
          						"testList": [{
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 222,
          							"name": "test2",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 333,
          							"name": "test3",
          							"companyId": 0,
          							"company": null
          						}],
          						"tests": [{
          							"id": 555,
          							"name": "test3",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 666,
          							"name": "test4",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 777,
          							"name": "test5",
          							"companyId": 222,
          							"company": null
          						}]
          					}
          				}, {
          					"id": 777,
          					"name": "test5",
          					"companyId": 222,
          					"company": {
          						"id": 222,
          						"name": "zust",
          						"testId": 111,
          						"test": {
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						},
          						"testList": [{
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 222,
          							"name": "test2",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 333,
          							"name": "test3",
          							"companyId": 0,
          							"company": null
          						}],
          						"tests": [{
          							"id": 555,
          							"name": "test3",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 666,
          							"name": "test4",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 777,
          							"name": "test5",
          							"companyId": 222,
          							"company": null
          						}]
          					}
          				}]
          			}
          		}, {
          			"id": 777,
          			"name": "test5",
          			"companyId": 222,
          			"company": {
          				"id": 222,
          				"name": "zust",
          				"testId": 111,
          				"test": {
          					"id": 111,
          					"name": "test",
          					"companyId": 0,
          					"company": null
          				},
          				"testList": [{
          					"id": 111,
          					"name": "test",
          					"companyId": 0,
          					"company": null
          				}, {
          					"id": 222,
          					"name": "test2",
          					"companyId": 0,
          					"company": null
          				}, {
          					"id": 333,
          					"name": "test3",
          					"companyId": 0,
          					"company": null
          				}],
          				"tests": [{
          					"id": 555,
          					"name": "test3",
          					"companyId": 222,
          					"company": {
          						"id": 222,
          						"name": "zust",
          						"testId": 111,
          						"test": {
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						},
          						"testList": [{
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 222,
          							"name": "test2",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 333,
          							"name": "test3",
          							"companyId": 0,
          							"company": null
          						}],
          						"tests": [{
          							"id": 555,
          							"name": "test3",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 666,
          							"name": "test4",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 777,
          							"name": "test5",
          							"companyId": 222,
          							"company": null
          						}]
          					}
          				}, {
          					"id": 666,
          					"name": "test4",
          					"companyId": 222,
          					"company": {
          						"id": 222,
          						"name": "zust",
          						"testId": 111,
          						"test": {
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						},
          						"testList": [{
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 222,
          							"name": "test2",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 333,
          							"name": "test3",
          							"companyId": 0,
          							"company": null
          						}],
          						"tests": [{
          							"id": 555,
          							"name": "test3",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 666,
          							"name": "test4",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 777,
          							"name": "test5",
          							"companyId": 222,
          							"company": null
          						}]
          					}
          				}, {
          					"id": 777,
          					"name": "test5",
          					"companyId": 222,
          					"company": {
          						"id": 222,
          						"name": "zust",
          						"testId": 111,
          						"test": {
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						},
          						"testList": [{
          							"id": 111,
          							"name": "test",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 222,
          							"name": "test2",
          							"companyId": 0,
          							"company": null
          						}, {
          							"id": 333,
          							"name": "test3",
          							"companyId": 0,
          							"company": null
          						}],
          						"tests": [{
          							"id": 555,
          							"name": "test3",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 666,
          							"name": "test4",
          							"companyId": 222,
          							"company": null
          						}, {
          							"id": 777,
          							"name": "test5",
          							"companyId": 222,
          							"company": null
          						}]
          					}
          				}]
          			}
          		}]
          	}
          }
        
      • 然后在UserVo上加上这样的字段

      •   /**
           * @author qcqcqc
           */
          @Data
          @CustomConvent
          public class UserVo {
              private String name;
              private String createTime;
              @FromField(fieldPath = "company.tests.$company.tests.$company.tests.$name")
              private List<List<List<String>>> testNames;
          }
        
      • 可以看到这个字段的原始字段来自于company字段的tests字段(List)中每一个对象的company字段的tests字段(List)中每一个对象的company字段中tests字段(List)每一个对象的name字段

        • 这个字段垮了三层列表,属于复杂的字段赋值

        • 查询的结果如下:

        •   {
            	"name": "qcqcqc",
            	"createTime": "2024-02-24 08:02:58",
            	"testNames": [
            		[
            			["test3", "test4", "test5"],
            			["test3", "test4", "test5"],
            			["test3", "test4", "test5"]
            		],
            		[
            			["test3", "test4", "test5"],
            			["test3", "test4", "test5"],
            			["test3", "test4", "test5"]
            		],
            		[
            			["test3", "test4", "test5"],
            			["test3", "test4", "test5"],
            			["test3", "test4", "test5"]
            		]
            	]
            }
          
      • 可以发现所有的字段被正确的一一对应,实现了极其复杂的关系映射

    • 使用

      • $字符用于开发者标识数组泛型对象的字段名
      • 会根据原始对象的json字符串进行匹配,并使用map进行字段获取和汇总成array数组
  • 综合demo

    •   /**
         * @author qcqcqc
         */
        @Data
        @CustomConvent
        public class UserVo {
            private String name;
            private String createTime;
            private String companyId;
            @SourceField(name = "company")
            private CompanyVo companyInfoFromSource;
            @FromField(fieldPath = "company")
            private CompanyVo companyInfoFromField;
            @FromField(fieldPath = "company.name")
            private String companyName;
            @FromField(fieldPath = "company.test.name")
            private String testName;
            @FromField(fieldPath = "company.testList")
            private List<TestVo> testList;
            @FromField(fieldPath = "company.tests.$company.name")
            private List<String> companyNames;
            @FromField(fieldPath = "company.tests.$company.tests.$company.tests.$name")
            private List<List<List<String>>> testNames;
        }
      
    •   {
        	"name": "qcqcqc",
        	"createTime": "2024-02-24 08:02:58",
        	"companyId": "222",
        	"companyInfoFromSource": {
        		"id": "222",
        		"companyName": "zust",
        		"testId": "111"
        	},
        	"companyInfoFromField": {
        		"id": "222",
        		"companyName": "zust",
        		"testId": "111"
        	},
        	"companyName": "zust",
        	"testName": "test",
        	"testList": [{
        		"id": "111",
        		"name": "test",
        		"companyId": "0",
        		"company": null
        	}, {
        		"id": "222",
        		"name": "test2",
        		"companyId": "0",
        		"company": null
        	}, {
        		"id": "333",
        		"name": "test3",
        		"companyId": "0",
        		"company": null
        	}],
        	"companyNames": ["zust", "zust", "zust"],
        	"testNames": [
        		[
        			["test3", "test4", "test5"],
        			["test3", "test4", "test5"],
        			["test3", "test4", "test5"]
        		],
        		[
        			["test3", "test4", "test5"],
        			["test3", "test4", "test5"],
        			["test3", "test4", "test5"]
        		],
        		[
        			["test3", "test4", "test5"],
        			["test3", "test4", "test5"],
        			["test3", "test4", "test5"]
        		]
        	]
        }
      

配置序列化和反序列化器

  • 编写一个类,继承ConventConfig接口,实现里面的方法并将其交由spring管理

  • 例:

  •   /**
       * @author qcqcqc
       */
      @Component
      @Order(1)
      public class DefaultConventConfig implements ConventConfig {
          @Override
          public SimpleModule getConventionModule() {
              SimpleModule simpleModule = new SimpleModule();
              simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
              simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
              // local-datetime序列化
              simpleModule.addSerializer(LocalDateTime.class, LocalDateTimeSerializer.getInstance());
              // local-datetime序列化
              simpleModule.addDeserializer(LocalDateTime.class, LocalDatetimeDeserializer.getInstance());
              // datetime序列化
              simpleModule.addSerializer(Date.class, DateSerializer.getInstance());
              // datetime序列化
              simpleModule.addDeserializer(Date.class, DateDeserializer.getInstance());
              // big-decimal序列化
              simpleModule.addSerializer(BigDecimal.class, BigDecimalSerialize.getInstance());
              return simpleModule;
          }
      }
    

使用其他类型转换工具

  • 在默认配置下,使用jackson进行转换

  • 使用fastjson1(已经编写好工具类)

  •   converter:
        type: fastjson
    
  • 自定义类型转换方法

  • 实现JsonConverter接口,并将其交由spring管理

    • 例:这里以fastjson2为例

    •   /**
         * @author qcqcqc
         */
        @Component
        @ConditionalOnProperty(prefix = "converter", name = "type", havingValue = "fastjson2")
        public class FastJson2Converter implements JsonConverter {
            
            @Override
            public <M, T> T convertValue(M entity, Class<T> clazz) {
                return JSON.parseObject(JSON.toJSONString(entity), clazz);
            }
            
        }
      
    • 这里指定了bean加载的条件,也就是在指定fastjson2的时候使用fastjson2

    • 你也可以使用其他的json工具,来进行对象转换