一、什么是MapStruct

多层应用程序通常需要在不同的对象模型(例如实体和DTO)之间进行映射。编写这样的映射代码是一项乏味且容易出错的任务。MapStruct旨在通过尽可能使其自动化来简化这项工作。

MapStruct是一个代码生成器,它基于约定重于配置的方法极大地简化了Java Bean类型之间映射的实现。生成的映射代码直接使用对象的方法,因此速度快、类型安全且易于理解。

官网地址:https://mapstruct.org/

二、为什么要使用MapStruct

  • 高性能,直接进行方法调用而非反射
  • 编译时类型安全,只有相互映射的对象和属性才能进行映射
  • 构建时可以发现错误,如映射不完整以及不正确

三、如何使用MapStruct

3.1 引入依赖
    <dependency>
      <groupId>org.mapstruct</groupId>
      <artifactId>mapstruct</artifactId>
        <version>1.4.2.Final</version>
    </dependency>
    <dependency>
      <groupId>org.mapstruct</groupId>
      <artifactId>mapstruct-processor</artifactId>
        <version>1.4.2.Final</version>
    </dependency>
3.2 创建对象
public class Car {
 
    private String make;
    private int numberOfSeats;
    private CarType type;
 
    //constructor, getters, setters etc.
}
public class CarDto {
 
    private String make;
    private int seatCount;
    private String type;
 
    //constructor, getters, setters etc.
}
public enum CarType {
 
    SEDAN;
}
3.3 创建Mapper
@Mapper
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
 
    @Mapping(source = "numberOfSeats", target = "seatCount")
    CarDto carToCarDto(Car car);
}
3.4 测试
@Test
public void shouldMapCarToDto() {
    //given
    Car car = new Car( "Morris", 5, CarType.SEDAN );
 
    //when
    CarDto carDto = CarMapper.INSTANCE.carToCarDto( car );
 
    //then
    assertThat( carDto ).isNotNull();
    assertThat( carDto.getMake() ).isEqualTo( "Morris" );
    assertThat( carDto.getSeatCount() ).isEqualTo( 5 );
    assertThat( carDto.getType() ).isEqualTo( "SEDAN" );
}

可以在maven编译后目录看到MapStruct帮我们自动实现了CarMapper接口

public class CarMapperImpl implements CarMapper {
    public CarMapperImpl() {
    }

    public CarDto carToCarDto(Car car) {
        if (car == null) {
            return null;
        } else {
            CarDto carDto = new CarDto();
            carDto.setSeatCount(car.getNumberOfSeats());
            carDto.setMake(car.getMake());
            if (car.getType() != null) {
                carDto.setType(car.getType().name());
            }

            return carDto;
        }
    }
}
3.5 更多使用技巧

参考对应版本的官方参考指南:https://mapstruct.org/documentation/reference-guide/

四、MapStruct实现原理

4.1 什么是JSR

Java Specification Request(简称:JSR)是指Java规范请求,由JCP成员使用JSR规范请求作为正式的规范文档向委员会提交的Java发展议案,经过一系列的流程后,如果这些议案都通过审查,最终会加入到未来的Java体系中。

JCP官网:https://www.jcp.org/en/home/index

常见的 JSR

  • Bean Validation 1.0 (JSR 303), Hibernate Validator 是 Bean Validation 的参考实现
  • Dependency Injection for Java 1.0 (JSR 330),Spring实现了该规范
4.2 JSR 269: Pluggable Annotation Processing API

在JDK 1.6中实现了JSR-269规范 Pluggable Annotations Processing API(插入式注解处理API),提供了一组插入式注解处理器的标准API在编译期间对注解进行处理。

javac编译流程:

opkqsc7d0z.png

我们可以把它看做是一组编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素。

有了编译器注解处理的标准API后,我们的代码才有可能干涉编译器的行为,由于语法树中的任意元素,甚至包括代码注释都可以在插件之中访问到,所以通过插入式注解处理器实现的插件在功能上有很大的发挥空间。只要有足够的创意,程序员可以使用插入式注解处理器来实现许多原本只能在编码中完成的事情。

更多细节可以参考深入理解Java虚拟机:JVM高级特性与最佳实践(第3版) 第10章 前端编译与优化

4.3 基于JSR 269实现的框架
  • MapStruct
  • Lombok
  • AutoService

五、自己实现一个简单的MapStruct

5.1 自定义注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface RabbMapper {
}
5.2 实现Processor
// 指定要处理的注解
@SupportedAnnotationTypes({"xyz.lazyrabbit.annotation.RabbMapper"})
// 指定支持的JDK版本
@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
public class RabbMapperProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        System.err.println("调用RabbMapperProcessor");
        for (TypeElement annotation : annotations) {
            for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
                MapperUtils.generateClass(processingEnv, element);
            }
        }
        return true;
    }
}
5.3 SPI配置

创建META-INF/services/javax.annotation.processing.Processor文件,指定自定义Processor类路径

xx.xx.xx.RabbMapperProcessor
5.4 maven配置
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>2.5.1</version>
        <configuration>
          <source>8</source>
          <target>8</target>
          <!-- 禁用注解处理器 -->
          <compilerArgument>-proc:none</compilerArgument>
        </configuration>
      </plugin>
    </plugins>
  </build>
5.5 实现生成class的逻辑

定义方法对象

public class Method {

    /**
     * 方法名称
     */
    private String methodName;
    /**
     * 返回值类型
     */
    private String returnType;
    /**
     * 参数类型
     */
    private String variableType;
    /**
     * 参数名称
     */
    private String variableName;
    /**
     * 返回值字段名称列表
     */
    private List<String> returnFieldList;
    
    //...省略getset方法
}

工具类

public class MapperUtils {

    /**
     * 获取Mapper
     *
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T getMapper(Class<T> clazz) {
        try {
            String className = clazz.getName() + "Impl";
            return (T) Class.forName(className).newInstance();
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取后缀
     *
     * @param fullName
     * @return
     */
    public static String getLastName(String fullName) {
        return fullName.substring(fullName.lastIndexOf(".") + 1);
    }

    /**
     * 首字母大写
     *
     * @param str
     * @return
     */
    public static String captureFirst(String str) {
        char[] chars = str.toCharArray();
        chars[0] -= 32;
        return String.valueOf(chars);
    }

    /**
     * 首字母小写
     *
     * @param str
     * @return
     */
    public static String lowerFirst(String str) {
        char[] chars = str.toCharArray();
        chars[0] += 32;
        return String.valueOf(chars);
    }

    /**
     * 使用freemaker模板生成文件
     *
     * @param data
     * @return
     */
    public static String generator(Map<String, Object> data) {
        try (StringWriter stringWriter = new StringWriter();) {
            Configuration conf = new Configuration(Configuration.VERSION_2_3_31);
            conf.setDirectoryForTemplateLoading(new File(MapperUtils.class.getClassLoader().getResource("template").getPath()));
            conf.setObjectWrapper(new DefaultObjectWrapper(Configuration.VERSION_2_3_31));
            Template temp = conf.getTemplate("struct.java.ftl");
            temp.process(data, stringWriter);
            return stringWriter.toString();
        } catch (Exception e) {
            return e.toString();
        }
    }

    /**
     * 根据element信息生成实现类
     *
     * @param processingEnv
     * @param element
     */
    public static void generateClass(ProcessingEnvironment processingEnv, Element element) {
        if (element.getKind() == ElementKind.INTERFACE) {
            Set<String> importClassSet = new HashSet<>();
            List<Method> MethodList = new ArrayList<>();
            String className = element.getSimpleName().toString();
            String packageName = processingEnv.getElementUtils().getPackageOf(element).toString();
            List<? extends Element> enclosedElements = element.getEnclosedElements();
            for (Element enclosedElement : enclosedElements) {
                // 遍历接口的所有方法
                if (enclosedElement.getKind() == ElementKind.METHOD) {
                    ExecutableElement executableElement = (ExecutableElement) enclosedElement;
                    // 获取方法参数
                    List<? extends VariableElement> parameters = executableElement.getParameters();
                    String variableName = null;
                    String variableType = null;
                    for (VariableElement parameter : parameters) {
                        variableType = MapperUtils.getLastName(parameter.asType().toString());
                        variableName = parameter.getSimpleName().toString();
                        importClassSet.add(parameter.asType().toString());
                    }
                    // 获取方法返回值类型
                    TypeMirror returnType = executableElement.getReturnType();
                    importClassSet.add(returnType.toString());
                    String returnClassName = MapperUtils.getLastName(returnType.toString());
                    DeclaredType declaredType = (DeclaredType) returnType;
                    Element returnElement = declaredType.asElement();
                    List<? extends Element> returnElementEnclosedElements = returnElement.getEnclosedElements();
                    List<String> fieldList = new ArrayList<>();
                    for (Element returnElementEnclosedElement : returnElementEnclosedElements) {
                        // 遍历返回值的字段
                        if (ElementKind.FIELD == returnElementEnclosedElement.getKind()) {
                            String fieldName = MapperUtils.captureFirst(returnElementEnclosedElement.getSimpleName().toString());
                            fieldList.add(fieldName);
                        }
                    }
                    // 构建方法对象
                    Method Method = new Method();
                    Method.setMethodName(enclosedElement.getSimpleName().toString());
                    Method.setReturnType(returnClassName);
                    Method.setReturnFieldList(fieldList);
                    Method.setVariableType(variableType);
                    Method.setVariableName(variableName);
                    MethodList.add(Method);
                }
            }
            try {
                // 生成子类
                Map<String, Object> data = new HashMap();
                data.put("classPath", packageName);
                data.put("className", className);
                data.put("importList", importClassSet);
                data.put("methodList", MethodList);
                String result = MapperUtils.generator(data);
                System.err.println(data);
                System.err.println(result);
                JavaFileObject javaFileObject = processingEnv.getFiler().
                        createSourceFile(packageName + "." + className + "Impl");
                try (Writer writer = javaFileObject.openWriter()) {
                    writer.write(result);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

freemarker模板

package ${classPath};

<#list importList as import>
import ${import};
</#list>

/**
 * generated by @RabbMapper
 * @since ${.now}
 */
public class ${className}Impl implements ${className} {

<#list methodList as method>
    @Override
    public ${method.returnType} ${method.methodName}(${method.variableType} ${method.variableName}) {
        ${method.returnType} ${method.returnType?uncap_first} = new ${method.returnType}();
<#list method.returnFieldList as returnField>
        ${method.returnType?uncap_first}.set${returnField}(${method.variableName}.get${returnField}());
</#list>
        return ${method.returnType?uncap_first};
    }

</#list>
}
5.6 测试使用

定义对象

public class UserDO {

    private String name;
    private Integer age;
    private String password;
    private LocalDateTime createTime;
    // 省略getset
}
public class UserQO {

    private String name;
    private Integer age;
    private String password;
    private LocalDateTime createTime;
    // 省略getset
}
public class UserVO {

    private String name;
    private Integer age;
    private LocalDateTime createTime;
    // 省略getset
}

定义接口

@RabbMapper
public interface UserStruct {

    UserStruct INSTANCE = MapperUtils.getMapper(UserStruct.class);

    UserDO toDO(UserQO userQO);

    UserVO toVO(UserDO userDO);
}

测试使用

@Test
public void shouldMapCarToDto() {
    UserQO userQO = new UserQO();
        userQO.setName("拉布拉多");
        userQO.setPassword("123456");
        userQO.setAge(18);
        userQO.setCreateTime(LocalDateTime.now().minusYears(18l));

        UserDO userDO = UserStruct.INSTANCE.toDO(userQO);
        System.out.println(userDO.toString());

        UserVO userVO = UserStruct.INSTANCE.toVO(userDO);
        System.out.println(userVO.toString());
}

项目编译后可以看到生成的实现类

/**
 * generated by @RabbMapper
 * @since 2023-12-7 20:13:51
 */
public class UserStructImpl implements UserStruct {

    @Override
    public UserDO toDO(UserQO userQO) {
        UserDO userDO = new UserDO();
        userDO.setName(userQO.getName());
        userDO.setAge(userQO.getAge());
        userDO.setPassword(userQO.getPassword());
        userDO.setCreateTime(userQO.getCreateTime());
        return userDO;
    }

    @Override
    public UserVO toVO(UserDO userDO) {
        UserVO userVO = new UserVO();
        userVO.setName(userDO.getName());
        userVO.setAge(userDO.getAge());
        userVO.setCreateTime(userDO.getCreateTime());
        return userVO;
    }
}