BeanUtils.copyProperties效率低下原因以及代替方案

9/9/2019 工作经验

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());
1
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);
1
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>
1
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);

}
1
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);
1
2
3

# 2.使用常见场景

# 2.1属性名不一致

如果源对象和目标对象的属性名不同,使用 @Mapping 注解指定映射关系

@Mapper

public interface CarMapper {

@Mapping(source = "manufacturer", target = "brand")

@Mapping(source = "seatCount", target = "numberOfSeats")

CarDto toDto(Car car);

}
1
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);

}
1
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);

}
1
2
3
4
5
6
7
8
9
10
11

# 2.4集合映射

自动转换集合(如 List、Set)

@Mapper

public interface BookMapper {

List<BookDto> toDtoList(List<Book> books);

}
1
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);

}
1
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);

}
1
2
3
4
5
6
7
8
9

# 4.性能优势

编译期生成代码:MapStruct 在编译时生成 Java 类,无需反射。

接近手写代码的性能:生成的代码直接调用 Getter/Setter,效率与手动编写的代码一致。

零运行时依赖:生成的代码不依赖 MapStruct 库。

# 六、总结

MapStruct 通过编译期生成代码解决了反射工具(如 BeanUtils)的性能问题,同时保持了代码的可维护性。适用于高频对象转换场景(如 DTO 与 Entity 的转换),是 Java 对象映射的高性能首选方案