还在用 BeanUtils.copyProperties 么?来了解一下 MapStruct ?
Thu, Jan 18, 2024
作用
MapStruct 用于简化 Java 对象之间的映射,在编译时生成代码,提供对象映射的性能,并减少手动编写映射代码的工作。
常用注解
@Mapper
MyBatis 中也有同名的注解,但功能不一样。
注解解析:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Mapper {
...
/**
Specifies the component model to which the generated mapper should adhere. Supported values are
default: the mapper uses no component model, instances are typically retrieved via Mappers.getMapper(Class)
cdi: the generated mapper is an application-scoped CDI bean and can be retrieved via @Inject
spring: the generated mapper is a Spring bean and can be retrieved via @Autowired
jsr330: the generated mapper is annotated with @javax.inject.Named and @Singleton, and can be retrieved via @Inject. The annotations will either be from javax.inject or jakarta.inject, depending on which one is available, with javax.inject having precedence.
jakarta: the generated mapper is annotated with @jakarta.inject.Named and @Singleton, and can be retrieved via @Inject.
The method overrides a componentModel set in a central configuration set by config()
Returns:
The component model for the generated mapper.
*/
String componentModel() default MappingConstants.ComponentModel.DEFAULT;
...
}
@Mapping
/**
* Configures the mapping of one bean attribute or enum constant.
* <p>
* The name of the mapped attribute or constant is to be specified via {@link #target()}. For mapped bean attributes it
* is assumed by default that the attribute has the same name in the source bean. Alternatively, one of
* {@link #source()}, {@link #expression()} or {@link #constant()} can be specified to define the property source.
* </p>
* <p>
* In addition, the attributes {@link #dateFormat()} and {@link #qualifiedBy()} may be used to further define the
* mapping.
* </p>
*
* <p>
* <strong>Example 1:</strong> Implicitly mapping fields with the same name:
* </p>
* <pre><code class='java'>
* // Both classes HumanDto and Human have property with name "fullName"
* // properties with the same name will be mapped implicitly
* @Mapper
* public interface HumanMapper {
* HumanDto toHumanDto(Human human)
* }
* </code></pre>
* <pre><code class='java'>
* // generates:
* @Override
* public HumanDto toHumanDto(Human human) {
* humanDto.setFullName( human.getFullName() );
* // ...
* }
* </code></pre>
*
* <p><strong>Example 2:</strong> Mapping properties with different names</p>
* <pre><code class='java'>
* // We need map Human.companyName to HumanDto.company
* // we can use @Mapping with parameters {@link #source()} and {@link #target()}
* @Mapper
* public interface HumanMapper {
* @Mapping(source="companyName", target="company")
* HumanDto toHumanDto(Human human)
* }
* </code></pre>
* <pre><code class='java'>
* // generates:
* @Override
* public HumanDto toHumanDto(Human human) {
* humanDto.setCompany( human.getCompanyName() );
* // ...
* }
* </code></pre>
* <p>
* <strong>Example 3:</strong> Mapping with expression
* <b>IMPORTANT NOTE:</b> Now it works only for Java
* </p>
* <pre><code class='java'>
* // We need map Human.name to HumanDto.countNameSymbols.
* // we can use {@link #expression()} for it
* @Mapper
* public interface HumanMapper {
* @Mapping(target="countNameSymbols", expression="java(human.getName().length())")
* HumanDto toHumanDto(Human human)
* }
* </code></pre>
* <pre><code class='java'>
* // generates:
*@Override
* public HumanDto toHumanDto(Human human) {
* humanDto.setCountNameSymbols( human.getName().length() );
* //...
* }
* </code></pre>
* <p>
* <strong>Example 4:</strong> Mapping to constant
* </p>
* <pre><code class='java'>
* // We need map HumanDto.name to string constant "Unknown"
* // we can use {@link #constant()} for it
* @Mapper
* public interface HumanMapper {
* @Mapping(target="name", constant="Unknown")
* HumanDto toHumanDto(Human human)
* }
* </code></pre>
* <pre><code class='java'>
* // generates
* @Override
* public HumanDto toHumanDto(Human human) {
* humanDto.setName( "Unknown" );
* // ...
* }
* </code></pre>
* <p>
* <strong>Example 5:</strong> Mapping with default value
* </p>
* <pre><code class='java'>
* // We need map Human.name to HumanDto.fullName, but if Human.name == null, then set value "Somebody"
* // we can use {@link #defaultValue()} or {@link #defaultExpression()} for it
* @Mapper
* public interface HumanMapper {
* @Mapping(source="name", target="name", defaultValue="Somebody")
* HumanDto toHumanDto(Human human)
* }
* </code></pre>
* <pre><code class='java'>
* // generates
* @Override
* public HumanDto toHumanDto(Human human) {
* if ( human.getName() != null ) {
* humanDto.setFullName( human.getName() );
* }
* else {
* humanDto.setFullName( "Somebody" );
* }
* // ...
* }
* </code></pre>
*
* <b>IMPORTANT NOTE:</b> the enum mapping capability is deprecated and replaced by {@link ValueMapping} it
* will be removed in subsequent versions.
*
* @author Gunnar Morling
*/
@Repeatable(Mappings.class)
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
public @interface Mapping {
...
}
使用
Apache Maven 设置
...
<properties>
<!-- https://github.com/mapstruct/mapstruct/releases -->
<org.mapstruct.version>1.6.0.Beta1</org.mapstruct.version>
<lombok-mapstruct.version>0.2.0</lombok-mapstruct.version>
</properties>
...
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
...
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<!-- 支持lombok -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Java 代码
public class SimpleSource {
private String name;
private String description;
private Integer simpleLevel;
private UserDTO userDTO;
// getter setter
}
public class SimpleDestination {
private String name;
private String description;
private Integer level;
private User user;
// getter setter
}
public class User {
private Integer id;
private String userName;
// getter setter
}
public class UserDTO {
private Integer id;
private String name;
// getter setter
}
// lombok 对象的使用
@Data
public class SimpleLombokDestination {
private String name;
private String description;
private Integer level;
}
// 这里使用 spring 组件模式,可以更好的和 spring bean 相结合
@Mapper(componentModel = "spring")
public interface SimpleSourceDestinationMapper {
@Mapping(source = "simpleLevel", target = "level")
@Mapping(source = "userDTO", target = "user")
@Mapping(source = "userDTO.name", target = "user.userName")
SimpleDestination fromSource(SimpleSource simpleSource);
@Mapping(source = "level", target = "simpleLevel")
SimpleSource fromDistination(SimpleDestination simpleDestination);
}
编译后自动生成的代码
此代码为编译后自动生成的代码,位置在:target\generated-sources\annotations
中
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
comments = "version: 1.6.0.Beta1, compiler: javac, environment: Java 17.0.7 (Oracle Corporation)"
)
@Component
public class SimpleSourceDestinationMapperImpl implements SimpleSourceDestinationMapper {
@Override
public SimpleDestination fromSource(SimpleSource simpleSource) {
if ( simpleSource == null ) {
return null;
}
SimpleDestination simpleDestination = new SimpleDestination();
simpleDestination.setUser( userDTOToUser( simpleSource.getUserDTO() ) );
simpleDestination.setLevel( simpleSource.getSimpleLevel() );
simpleDestination.setName( simpleSource.getName() );
simpleDestination.setDescription( simpleSource.getDescription() );
return simpleDestination;
}
@Override
public SimpleLombokDestination lombokFromSource(SimpleSource simpleSource) {
if ( simpleSource == null ) {
return null;
}
SimpleLombokDestination simpleLombokDestination = new SimpleLombokDestination();
simpleLombokDestination.setLevel( simpleSource.getSimpleLevel() );
simpleLombokDestination.setName( simpleSource.getName() );
simpleLombokDestination.setDescription( simpleSource.getDescription() );
return simpleLombokDestination;
}
protected User userDTOToUser(UserDTO userDTO) {
if ( userDTO == null ) {
return null;
}
User user = new User();
user.setUserName( userDTO.getName() );
user.setId( userDTO.getId() );
return user;
}
...
}
测试代码
@RunWith(SpringRunner.class)
@SpringBootTest
public class SimpleSourceDestinationMapperTest {
@Autowired
private SimpleSourceDestinationMapper simpleSourceDestinationMapper;
@Test
public void testFromSource() {
SimpleSource simpleSource = new SimpleSource();
simpleSource.setDescription("xxxxxx1");
simpleSource.setName("aaaaaa");
simpleSource.setSimpleLevel(111111);
UserDTO userDTO = new UserDTO();
userDTO.setId(1111);
userDTO.setName("name111111");
simpleSource.setUserDTO(userDTO);
SimpleDestination simpleDestination = simpleSourceDestinationMapper.fromSource(simpleSource);
System.out.println(simpleDestination);
assertEquals(simpleDestination.getDescription(), simpleSource.getDescription());
assertEquals(simpleDestination.getName(), simpleSource.getName());
assertEquals(simpleDestination.getLevel(), simpleSource.getSimpleLevel());
assertEquals(simpleDestination.getUser().getUserName(), simpleSource.getUserDTO().getName());
}
@Test
public void testLombokFromSource() {
SimpleSource simpleSource = new SimpleSource();
simpleSource.setDescription("xxxxxx1");
simpleSource.setName("aaaaaa");
simpleSource.setSimpleLevel(111111);
SimpleLombokDestination simpleDestination = simpleSourceDestinationMapper.lombokFromSource(simpleSource);
assertEquals(simpleDestination.getDescription(), simpleSource.getDescription());
assertEquals(simpleDestination.getName(), simpleSource.getName());
assertEquals(simpleDestination.getLevel(), simpleSource.getSimpleLevel());
}
}
和 BeanUtils.copyProperties 的对比
copyProperties 使用的反射实现,而 MapStruct 是直接生成了代码。所以性能上要比 BeanUtils.copyProperties 好一些
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable, @Nullable String... ignoreProperties) throws BeansException {
...
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
Set<String> ignoredProps = (ignoreProperties != null ? new HashSet<>(Arrays.asList(ignoreProperties)) : null);
CachedIntrospectionResults sourceResults = (actualEditable != source.getClass() ?
CachedIntrospectionResults.forClass(source.getClass()) : null);
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoredProps == null || !ignoredProps.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = (sourceResults != null ?
sourceResults.getPropertyDescriptor(targetPd.getName()) : targetPd);
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null) {
if (isAssignable(writeMethod, readMethod, sourcePd, targetPd)) {
try {
ReflectionUtils.makeAccessible(readMethod);
Object value = readMethod.invoke(source);
ReflectionUtils.makeAccessible(writeMethod);
writeMethod.invoke(target, value);
}
catch (Throwable ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
}
}