MapStruct作为一款高效的Java对象映射工具,默认提供了基于字段名匹配的自动映射能力,但在实际业务场景中,经常会遇到需要根据源对象的多个字段组合计算、或者经过复杂逻辑处理才能得到目标字段值的情况,这时候就需要借助表达式调用自定义方法来实现字段级的动态映射。

MapStruct表达式的基本使用规则
MapStruct支持在@Mapping注解的expression属性中编写Java表达式,表达式的返回值会直接赋值给对应的目标字段。表达式的语法格式为java(自定义方法的调用逻辑),其中自定义方法可以是映射接口中定义的默认方法,也可以是外部工具类的静态方法。
表达式的语法要求
- 表达式必须以
java(开头,以)结尾,中间是合法的Java代码片段 - 如果调用的是映射接口自身的默认方法,直接使用方法名加参数即可
- 如果调用的是外部类的静态方法,需要写全类路径加方法名,或者先通过
import导入类 - 表达式中的参数可以是源对象的字段,直接使用字段名即可
定义自定义方法实现动态映射
下面通过一个具体的业务场景来演示完整实现过程:假设我们有两个对象,UserSource是源对象,包含firstName、lastName、age三个字段;UserTarget是目标对象,包含fullName、ageDesc两个字段,其中fullName需要拼接firstName和lastName,ageDesc需要根据age的值返回不同的描述文本。
定义源对象和目标对象
首先定义两个实体类:
// 源对象
public class UserSource {
private String firstName;
private String lastName;
private Integer age;
// 省略getter、setter方法
}
// 目标对象
public class UserTarget {
private String fullName;
private String ageDesc;
// 省略getter、setter方法
}
定义映射接口并编写自定义方法
在MapStruct的映射接口中,我们可以定义默认方法来实现自定义逻辑,然后通过表达式调用这些方法:
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target = "fullName", expression = "java(buildFullName(source.getFirstName(), source.getLastName()))")
@Mapping(target = "ageDesc", expression = "java(buildAgeDesc(source.getAge()))")
UserTarget toTarget(UserSource source);
/**
* 拼接全名
*/
default String buildFullName(String firstName, String lastName) {
if (firstName == null && lastName == null) {
return "";
}
if (firstName == null) {
return lastName;
}
if (lastName == null) {
return firstName;
}
return firstName + " " + lastName;
}
/**
* 根据年龄生成描述
*/
default String buildAgeDesc(Integer age) {
if (age == null) {
return "年龄未知";
}
if (age < 18) {
return "未成年";
} else if (age <= 60) {
return "成年";
} else {
return "老年";
}
}
}
上面的代码中,我们在@Mapping注解的expression属性里分别调用了接口中定义的两个默认方法,传入源对象的对应字段作为参数,方法的返回值会自动赋值给目标字段。
调用外部工具类的静态方法
如果自定义逻辑比较复杂,或者需要在多个映射接口中复用,也可以把逻辑放到外部工具类中,作为静态方法调用:
// 工具类
public class UserMappingUtil {
public static String buildFullName(String firstName, String lastName) {
if (firstName == null && lastName == null) {
return "";
}
if (firstName == null) {
return lastName;
}
if (lastName == null) {
return firstName;
}
return firstName + " " + lastName;
}
}
然后在映射接口中通过表达式调用静态方法,需要写全类路径:
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(componentModel = "spring")
public interface UserMapper {
@Mapping(target = "fullName", expression = "java(com.example.util.UserMappingUtil.buildFullName(source.getFirstName(), source.getLastName()))")
UserTarget toTarget(UserSource source);
}
如果觉得写全类路径比较繁琐,也可以在映射接口上通过@Mapper的import属性导入工具类:
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import com.example.util.UserMappingUtil;
@Mapper(componentModel = "spring", imports = UserMappingUtil.class)
public interface UserMapper {
@Mapping(target = "fullName", expression = "java(UserMappingUtil.buildFullName(source.getFirstName(), source.getLastName()))")
UserTarget toTarget(UserSource source);
}
注意事项
- 表达式中的
source是默认的源对象引用名,如果映射方法的参数名不是source,需要对应修改,比如参数是user,则表达式里用user.getFirstName() - 自定义方法的参数类型需要和源对象字段的类型匹配,否则会编译报错
- 如果自定义方法可能返回
null,需要确保目标字段的类型可以接收null,或者做好空值处理 - 表达式中的Java代码会在编译期生成到映射实现类中,不会在运行时动态执行,所以不需要担心性能问题
测试验证
编写测试代码验证映射效果:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
public void testToTarget() {
UserSource source = new UserSource();
source.setFirstName("张");
source.setLastName("三");
source.setAge(25);
UserTarget target = userMapper.toTarget(source);
System.out.println(target.getFullName()); // 输出:张 三
System.out.println(target.getAgeDesc()); // 输出:成年
}
}
通过测试可以看到,自定义方法通过表达式被正确调用,目标字段的值符合预期,实现了字段级的动态映射效果。