Java性能优化战术-对象复制
对象复制
当我们需要克隆一个POJO对象,比如从Caffeine缓存里取出一个对象,为了避免业务操作污染这个缓存对象,需要克隆一份。这种场景有
- 电商从缓存里取出一个商品对象,或者库存数据等,进行计算。
- 物联网取出设备的物模型,计算出设备的数字模型。
Java提供了大量的工具来帮助自动完成克隆而不需要手工编写代码。同时手工编写代码主要问题在于当POJO改变的时候,负责克隆此对象的代码未同步修改。如下一个Department对象,需要完成一个复制功能
public class Department {
Integer id;
String name;
List<User> users;
//忽略getter和 setter方法
}通过编码实现最高效,如下代码是一个Department的克隆方法:
// CloneUtil
public Department cloneDepartment(Department source){
Department target = new Department();
target.setId(source.getId());
target.setName(source.getName());
//复制用户
List<User> users = target.getUsers();
if(users!=null){
List userList = new ArrayList(users.size());
for(User user:users){
User targetUser= cloneUser(user);
userList.add(targetUser);
}
target.setUsers(userList);
}
return target;
}
protected User cloneUser(User source){
User target = new User();
target.setId(source.getId());
target.setName(source.getName());
target.setPassword(source.getPassword());
return target;
}如上代码问题在于当对象改变的时候,比如,新增一个type属性,这段cloneDepartment必须调整,添加type属性的赋值,很不幸,忘记复制新增属性是手写克隆代码常见现象。
可以通过JDK对象的序列化,反序列化来实现对象的复制,或者使用JSON序列化&反序列化实现复制。但这些复制方法性能非常糟糕,
public Department cloneByObjectWriter(Department source){
try {
//序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(bos);
os.writeObject(source);
os.close();
//反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream is = new ObjectInputStream(bis);
Department department = (Department)is.readObject();
return department;
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}可以运行CloneUtilTest方法,会发现采用JDK序列化机制性能太糟糕了
Benchmark Mode S Score Units
c.i.c.c.CloneUtilTest.testCloneByHardCode avgt 59.844 ns/op
c.i.c.c.CloneUtilTest.testCloneByJson avgt 1476.493 ns/op
c.i.c.c.CloneUtilTest.testCloneByObjectWriter avgt 22846.874 ns/op性能如此糟糕主要还是因为对于对象的任何属性,都要序列化成byte,然后通过byte 反序列化成对象属性,这是一个非常耗时的操作,使用Jackson序列化反序列化也是复制对象的一个办法,然而性能也很糟糕
因此在关键业务中,不要使用Java的序列化机制来实现对象的的拷贝。
Selma 是一款高效的对象复制和属性映射工具,他实现了JSR269,JSR269允许编译时期执行用户的操作,比如生成一个对象复制工具类。要使用selma,引入下面的依赖。
<dependency>
<groupId>fr.xebia.extras</groupId>
<artifactId>selma-processor</artifactId>
<version>1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>fr.xebia.extras</groupId>
<artifactId>selma</artifactId>
<version>1.0</version>
</dependency>另外一个工具MapStruts也能实现同样的功能
编写一个简单的接口
@Mapper
public interface DepartmentMapper {
Department clone(Department source);
}接口添加一个输入参数是Department ,输出也是Department对象的方法,接口类必须用@Mapper注解,这样,在这个代码被编译的时候,会让Selma生成一个DepartmentMapperSelmaGeneratedClass的类,如下
public final class DepartmentMapperSelmaGeneratedClass
implements DepartmentMapper {
@Override
public final Department clone( Department inDepartment) {
//忽略代码,内容类似CloneUtil.cloneDepartment手写的复制代码
}
public final User asUser(User inUser) {
//忽略代码,内容类似CloneUtil.cloneUser手写的复制代码
}
}Selma会生成一个该接口的实现,放到target/generated-sources/annotations 目录下,这是JSR269规范的默认生成目录,IDE能识别此目录为源代码目录。
使用selma,无需知道这个类存在,调用Selma.builder方法,传入我们定义好的接口即可,返回生成的接口实现类
DepartmentMapper mapper = Selma.builder(DepartmentMapper.class).build();Selma之所以会生成asUser方法,是因为它发现User是一个非Java内置的对象,因此单独生成一个asUser方法用于复制User,这有利于复用这个方法,如果Deparment 除了List属性外,如果还有个属性
private User managaer;那么,Selma可以复用asUser方法,实现manager属性的复制。
可以在Mapper接口定义自己的复制方法,比如,User对象的复制你需要一定个性化扩展,那么,你需要在定义一个Mapper类,我们在DepartmentMapper接口里定义一个
@Mapper(withCustom = UserCustomMapper.class)
public interface DepartmentMapper {
Department clone(Department source);
}
class UserCustomMapper{
public UserCustomMapper(){}
public User toUser(User source){
User target = new User();
target.setId(source.getId());
target.setAge(source.getAge());
return target;
}
}在Selma生成复制代码的时候,如果碰到User对象需要复制,则先检测有没有自定义的类,如果有,再检测自定义的类的方法里有没有入参是User的,如果有,则会调用此方法作为对象克隆方法。
如果对象出现了循环引用,可以使用withCyclicMappings = true
@Mapper(withCyclicMappings = true)
public interface DepartmentMapper {
Department clone(Department source);
}如果不想有些属性被复制,可以使用withIgnoreFields
@Mapper(withIgnoreFields = {"age", "User.password"})
public interface DepartmentMapper {
Department clone(Department source);
}也可以使用完全限定名 com.ibeetl.xxxx.Userer.password。
Selma 不仅仅可以作为对象复制工具,也可以作为一个对象到另外一个对象的映射工具,限于篇幅,不再详细介绍。
性能测试表明,Selma具备与手写代码一样的性能
c.i.c.c.CloneUtilTest.testCloneBySelma avgt 45.338 ns/op