BeanUtils.copyProperties效率低下原因以及代替方案
BeanUtils.copyProperties效率低下原因以及代替方案
近期在线上发现一个定时任务执行时间很长,耗时半个多小时,我们预期几分钟就能执行完成,因此开始排查是哪里的问题。
具体排查过程就不详说了,定位到最后发现是由于BeanUtils.copyProperties的原因,很多选手为了写起来简单,直接用copy,不使用get、set方式,导致浪费了系统很多的性能。
# 一、效率低的主要原因
# 1.反射机制(Reflection)
运行时反射调用:BeanUtils.copyProperties 通过反射动态获取类的属性信息(如字段、Getter/Setter方法),并在运行时调用这些方法。反射操作相比直接代码调用需要更多的系统资源和时间。
反射性能瓶颈:反射需要检查类的元数据、验证访问权限、调用方法时包装参数等,这些操作无法被JIT编译器优化,导致性能显著下降。
# 2.类型检查与转换
动态类型匹配:工具类需要处理不同数据类型之间的转换(如 String 转 Integer、日期格式转换等),这些转换逻辑可能涉及复杂的判断和计算。
额外开销:如果属性类型不匹配,会触发类型转换逻辑,甚至可能抛出异常,进一步增加时间成本。
# 3.安全检查与健壮性处理
属性存在性检查:工具类会检查源对象和目标对象是否存在同名属性,并验证属性是否可读/可写。
异常处理:需要处理 IllegalAccessException、InvocationTargetException 等异常,增加了额外逻辑分支。
# 4.批量处理的开销
如果复制的对象属性数量多(如数十上百个属性),反射操作的累积时间会显著增加。
# 二、性能对比
我简单写了两个方法,一个是单纯用GET、SET赋值,另一个通过copy赋值,且只执行一次,并不是循环n次
这个对象大概有二十多个字段,涉及字符串、整型、字节、日期类型
在自己电脑上跑main方法的结果,耗时和电脑性能也有关系,但是!!!这差距显而易见,差太多了。
我这还是用的spring的copy,如果用Apache的会更慢
# 三、代替方案
# 1.手动编写赋值代码
// 手动调用Setter/Getter,无反射开销
TargetObject target = new TargetObject();
target.setName(source.getName());
target.setAge(source.getAge());
2
3
4
5
优点:性能最优,可控性强。
缺点:代码冗余,维护成本高。
# 2.使用MapStruct(推荐)
@Mapper
public interface ObjectMapper {
ObjectMapper INSTANCE = Mappers.getMapper(ObjectMapper.class);
TargetObject toTarget(SourceObject source);
}
// 使用方式
TargetObject target = ObjectMapper.INSTANCE.toTarget(source);
2
3
4
5
6
7
8
9
10
11
12
13
优点:基于注解生成编译期的Java代码,无反射开销,性能接近手写代码。
缺点:需要配置注解处理器。
# 3.使用性能更高的工具类
Spring BeanUtils:比Apache的实现更高效
Apache Commons PropertyUtils:性能略优于 BeanUtils,但仍依赖反射
Cglib BeanCopier:通过字节码技术动态生成复制类,初始化较慢但后续复制快
# 4.序列化/反序列化
使用JSON工具(如Jackson、Gson)或二进制序列化,适用于需要深拷贝的场景
# 四、最优解决方案MapStruct用法
# 1.使用方式
# 1.1添加依赖
<dependencies>
<!-- MapStruct 核心库 -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<!-- 注解处理器(编译期生成代码) -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
<scope>provided</scope>
</dependency>
</dependencies>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 1.2定义映射接口
创建一个接口,用 @Mapper 标记,定义对象转换方法
@Mapper
public interface UserMapper {
// 单例访问方式(可选)
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
// 对象转换方法:将 UserDto 转换为 UserEntity
UserEntity toEntity(UserDto userDto);
// 反向转换:将 UserEntity 转换为 UserDto
UserDto toDto(UserEntity userEntity);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1.3使用生成的实现类
MapStruct 在编译时会生成接口的实现类(如 UserMapperImpl),可直接调用
UserDto userDto = new UserDto("By", "by@jd.com");
UserEntity userEntity = UserMapper.INSTANCE.toEntity(userDto);
2
3
# 2.使用常见场景
# 2.1属性名不一致
如果源对象和目标对象的属性名不同,使用 @Mapping 注解指定映射关系
@Mapper
public interface CarMapper {
@Mapping(source = "manufacturer", target = "brand")
@Mapping(source = "seatCount", target = "numberOfSeats")
CarDto toDto(Car car);
}
2
3
4
5
6
7
8
9
10
11
# 2.2类型转换
自定义类型转换逻辑
@Mapper
public interface DateMapper {
// 自定义 String 转 Date 的转换方法
default Date stringToDate(String dateStr) throws ParseException {
return new SimpleDateFormat("yyyy-MM-dd").parse(dateStr);
}
@Mapping(source = "dateStr", target = "date")
Event toEntity(EventDto eventDto);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2.3嵌套对象映射
自动处理嵌套对象的映射
@Mapper
public interface OrderMapper {
@Mapping(source = "customer.name", target = "customerName")
@Mapping(source = "product.id", target = "productId")
OrderDto toDto(Order order);
}
2
3
4
5
6
7
8
9
10
11
# 2.4集合映射
自动转换集合(如 List、Set)
@Mapper
public interface BookMapper {
List<BookDto> toDtoList(List<Book> books);
}
2
3
4
5
6
7
# 3.高级配置
# 3.1自定义映射方法
通过 @AfterMapping 或 @BeforeMapping 在映射前后添加额外逻辑
@Mapper
public abstract class ProductMapper {
@AfterMapping
protected void calculateTotal(Product product, @MappingTarget ProductDto productDto) {
productDto.setTotal(product.getPrice() \* product.getQuantity());
}
public abstract ProductDto toDto(Product product);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 3.2依赖注入
支持 Spring、CDI 等依赖注入框架(以 Spring 为例)
@Mapper(componentModel = "spring")
public interface UserMapper {
// Spring 会注入该 Mapper 的实现
UserDto toDto(UserEntity user);
}
2
3
4
5
6
7
8
9
# 4.性能优势
编译期生成代码:MapStruct 在编译时生成 Java 类,无需反射。
接近手写代码的性能:生成的代码直接调用 Getter/Setter,效率与手动编写的代码一致。
零运行时依赖:生成的代码不依赖 MapStruct 库。
# 六、总结
MapStruct 通过编译期生成代码解决了反射工具(如 BeanUtils)的性能问题,同时保持了代码的可维护性。适用于高频对象转换场景(如 DTO 与 Entity 的转换),是 Java 对象映射的高性能首选方案