更加优雅的类型转换工具
类型转换
-
在日常开发中,我们经常面临需要进行类型转换的情况,例如将数据库中的持久化对象(PO)转换为传输对象(DTO)或视图对象(VO)。这些类之间可能存在一些差异:
- 字段名称不同
- 例如,公司表中存储的是
name
字段,但需要返回的视图对象中的字段为companyName
。 - 又或者,用户表中有
companyId
字段,但并不希望将该字段返回给前端。
- 例如,公司表中存储的是
- 字段类型不同
- 例如,数据库中的
id
存储为bigint
,但实际想要返回的数据类型是string
(因为在JavaScript中解析长整型可能会导致精度丢失)。 - 或者,日期类型存储在数据库中,但想要返回一个字符串类型。
- 例如,数据库中的
- 数据来源不同
- 例如,用户类中包含公司信息,但在用户视图对象中我们可能只需要公司的名称,而不是整个公司对象。
传统解决方案的问题
在解决这些问题的过程中,通常会采用以下传统方法:
- 手动编写类型转换
- 可以在Service层或者通过创建一个专门的Converter类来进行手动转换,也可以使用构造函数或者成员方法进行转换。然而,这种方式存在一些问题:
- 代码冗余
- 编写类型转换通常繁琐且重复,这些代码往往是固定的,但又是不可或缺的。
- 维护困难
- 如果类的字段发生变化,需要修改涉及这个字段的大量转换方法,这是一项繁重的任务。
- 代码冗余
- 可以在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对象
- 源对象:输入的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工具,来进行对象转换
-