• MapStruct生成实现类对象的Spring容器对象属性注入问题源码分析


    本文解析MapStruct生成继承类的Spring容器对象属性注入为空问题,并分析了相关源码。给出了一个Spring容器对象属性正确注入例子。

    在领域模型中经常会遇到对象属性的拷贝,对属性的手动赋值会增加不必要的工作量,而使用BeanUtils.copyProperties等工具存在其他问题。除了领域模型,一般MVC项目也会涉及对象属性的复制。org.mapstruct包能完美解决对象的复制,使用上简洁且功能强大,在项目中使用越来越频繁。

    org.mapstruct在生成继承类时,含Spring容器对象的属性

    本文例子采用了web项目,在pom.xml中添加依赖如下:

    <dependency>
    	<groupId>org.mapstruct</groupId>
    	<artifactId>mapstruct-jdk8</artifactId>
    	<version>1.3.1.Final</version>
    </dependency>
    <dependency>
    	<groupId>org.mapstruct</groupId>
    	<artifactId>mapstruct-processor</artifactId>
    	<version>1.3.1.Final</version>
    </dependency>
    

    直接看代码和运行结果

    一、代码

    1、被转换对象

    package com.mingo.exp.mapstruct;
    
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    /**
     * 测试dto
     */
    @Data
    @NoArgsConstructor
    public class TestConvertorDTO {
    
        private String date;
    
        private Integer number;
    
        private Integer version;
    
        public TestConvertorDTO(String date, Integer number, Integer version) {
            this.date = date;
            this.number = number;
            this.version = version;
        }
    }
    

    2、转换后对象

    package com.mingo.exp.mapstruct;
    
    import lombok.Data;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Scope;
    import org.springframework.stereotype.Component;
    
    /**
     * 领域
     * 非单例的prototype
     * 
     * @author Doflamingo
     */
    @Scope("prototype")
    @Data
    @Component
    public class TestConvertorE {
    
        private String date;
    
        private Integer number;
    
        private Integer version;
    
        // Spring bean
    
        /**
         * 本文主要测试该属性是否会被注入Spring容器对象
         */
        @Autowired
        private TestMapStructService structService;
    
        // 领域方法
    
        /**
         * 校验 structService 对象是否注入
         */
        public void testNull() {
            if (null == structService) {
                System.out.println("本例中 Test1Convertor 会打印本行...");
            } else {
                structService.print();
            }
        }
    }
    

    3、mapstruct转换接口Test1Convertor

    @Mapper注解的属性componentModel有几种取值,本文取值为“spring”表示转换生成的对象会被Spring容器所管理。这里会采用默认方式转换生成的对象,如new 0bject()

    package com.mingo.exp.mapstruct;
    
    import org.mapstruct.Mapper;
    
    /**
     * mapstruct 未指定对象生成的Factory
     *
     * @author Doflamingo
     */
    @Mapper(componentModel = "spring")
    public interface Test1Convertor {
    
        /**
         * 转领域对象,不会注入相关Spring容器对象
         *
         * @param orderDO
         * @return
         */
        TestConvertorE doToEntity(TestConvertorDTO orderDO);
    }
    

    4、mapstruct转换接口Test2Convertor

    这里的@Mapper注解属性比Test2Convertor多了uses的取值,uses主要用来指定目标对象的生成工厂,如转换后的目标对象将被EntityObjFactory生成。

    package com.mingo.exp.mapstruct;
    
    import org.mapstruct.Mapper;
    
    /**
     * mapstruct 带Factory
     * EntityObjFactory类用于生成目标对象
     *
     * @author Doflamingo
     */
    @Mapper(componentModel = "spring", uses = EntityObjFactory.class)
    public interface Test2Convertor {
    
        /**
         * 转领域对象,会注入相关Spring容器对象
         *
         * @param orderDO
         * @return
         */
        TestConvertorE doToEntity(TestConvertorDTO orderDO);
    }
    

    5、TestMapStructService测试属性是否会被注入Spring容器对象

    package com.mingo.exp.mapstruct;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    /**
     * mapstruct测试 属性是否会被注入Spring容器对象
     *
     * @author Doflamingo
     */
    @Slf4j
    @Service
    public class TestMapStructService {
    
        @Autowired
        private Test1Convertor test1Convertor;
    
        @Autowired
        private Test2Convertor test2Convertor;
    
        /**
         * 测试方法
         */
        public void print() {
            System.out.println("本例中 Test2Convertor 对象会打印本行...");
        }
    
        /**
         * 测试方法
         */
        public void test() {
            TestConvertorDTO dto = new TestConvertorDTO("20200818", 1, 22);
            // 不会向属性注入Spring容器对象
            TestConvertorE e1 = test1Convertor.doToEntity(dto);
            e1.testNull();
    
            // 会向属性注入Spring容器对象
            TestConvertorE e2 = test2Convertor.doToEntity(dto);
            e2.testNull();
        }
    }
    

    6、EntityObjFactory,mapstruct使用该factory生成目标bean

    EntityObjFactory实现了ApplicationContextAware接口,用于获取Spring容器对象,这样mapstruct就可以借助Spring容器对象生成目标bean,最终再填入属性。含容器注入的属性将被正确注入。

    package com.mingo.exp.mapstruct;
    
    import org.mapstruct.TargetType;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.stereotype.Component;
    
    /**
     * mapstruct 使用该factory
     * 用于获取Spring容器管理的Bean
     *
     * @author Doflamingo
     */
    @Component
    public class EntityObjFactory implements ApplicationContextAware {
        private ApplicationContext applicationContext = null;
    
        @Override
        public void setApplicationContext(ApplicationContext context) {
            if (null == this.applicationContext) {
                this.applicationContext = context;
            }
    
        }
    
        /**
         * 获取Spring容器管理的Bean
         *
         * @param clazz
         * @param <T>
         * @return
         */
        public <T> T createEntity(@TargetType Class<T> clazz) {
            return applicationContext.getBean(clazz);
        }
    }
    

    7、测试类MapStructTest

    package com.mingo.exp.mapstruct;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    
    @SpringBootTest
    @RunWith(SpringRunner.class)
    public class MapStructTest {
    
        @Autowired
        private TestMapStructService structService;
    
        @Test
        public void test() {
            structService.test();
        }
    }
    

    测试类MapStructTest执行结果

    可以看出@Mapper注解未指定uses属性时,生成对象的属性不会被注入Spring容器对象。指定为EntityObjFactory即会正常注入。

    二、mapstruct相关源码分析

    mapstruct原理是在编译时通过注解处理器解析,扫描@Mapper注解和被注解的接口,按照指定属性参数完成被注解的接口的实现类生成。实现类的方法将完成对象属性间的拷贝。

    1、先看编译时生成的实现类

    Test1Convertor实现类:

    Test2Convertor实现类:

    2、mapstruct注解助理类org.mapstruct.ap.MappingProcessor

    MappingProcessor继承了javax.annotation.processing.AbstractProcessor类,AbstractProcessor的方法,这里不做说明,有兴趣可去查看相关文档。这里我只关注public boolean process(final Set annotations, final RoundEnvironment roundEnvironment)

    进入MappingProcessor.process(...)方法

    获取被@Mapper注解的接口信息具体是在MappingProcessor.getMappers(...)方法中这行代码体现

    Set<? extends Element> annotatedMappers = roundEnvironment.getElementsAnnotatedWith( annotation );
    

    进入MappingProcessor.processMapperElements(...)方法,主要逻辑都在该方法中进行

    中途会进入org.mapstruct.ap.internal.processor.MethodRetrievalProcessor.retrieveMethods(...)方法,该方法会决定生成实现类的含有的属性或方法

    先看Test1Convertor接口实现类的处理

    先看Test2Convertor接口实现类的处理

    uses处理逻辑

    // org.mapstruct.ap.internal.processor.MethodRetrievalProcessor.retrieveMethods(...)
    // 这短短代码对@Mapper注解的uses属性进行了处理
    if ( usedMapper.equals( mapperToImplement ) ) {
        // 本例中uses只设置了一个值 mapperConfig.uses()的值即是com.mingo.exp.mapstruct.EntityObjFactory的mapper描述对象
    	for ( DeclaredType mapper : mapperConfig.uses() ) {
    	    // 这里递归处理了com.mingo.exp.mapstruct.EntityObjFactory的mapper描述对象
    	    // 会把EntityObjFactory类里的声明的方法加入到methods里:setApplicationContext、createEntity方法
    		methods.addAll( retrieveMethods(
    			asTypeElement( mapper ),
    			mapperToImplement,
    			mapperConfig,
    			prototypeMethods ) );
    	}
    }
    

    3、Test2Convertor接口实现类生成

    前文说生成实现类核心逻辑是在MappingProcessor.processMapperElements(...)方法中处理,现在看下其中调用的方法MappingProcessor.processMapperTypeElement(...)

    会循环按顺序处理getProcessors()返回的List,org.mapstruct.ap.internal.ModelElementProcessor的实现类如下

    第一个遍历值MethodRetrievalProcessor上面我们已经说过,处理Test2Convertor时生成了三个方法描述(model = process( context, processor, mapperTypeElement, model );),model值将作为下一个遍历值得输入参数。由于@Mapper注解的属性componentModel = "spring",所以Cdi(第三)和Jsr(第四)开头的Processor将会被跳过。

    3.1、第二个遍历值org.mapstruct.ap.internal.processor.MapperCreationProcessor

    这也是为什么生成的Test2Convertor实现类会多一个EntityObjFactory成员变量。这里不包含实现类上的Spring相关注解。

    3.2、第五个遍历值org.mapstruct.ap.internal.processor.SpringComponentProcessor

    org.mapstruct.ap.internal.processor.AnnotationBasedComponentModelProcessor.process(ProcessorContext context, TypeElement mapperTypeElement, Mapper mapper)源码如下

    @Override
    public Mapper process(ProcessorContext context, TypeElement mapperTypeElement, Mapper mapper) {
    	this.typeFactory = context.getTypeFactory();
    
    	MapperConfiguration mapperConfiguration = MapperConfiguration.getInstanceOn( mapperTypeElement );
    
    	String componentModel = mapperConfiguration.componentModel( context.getOptions() );
    	InjectionStrategyPrism injectionStrategy = mapperConfiguration.getInjectionStrategy();
    
        // componentModel不等于"spring"时会直接返回
    	if ( !getComponentModelIdentifier().equalsIgnoreCase( componentModel ) ) {
    		return mapper;
    	}
    
        // 用于实现类上的Spring注解,这里是定值@Component
    	for ( Annotation typeAnnotation : getTypeAnnotations( mapper ) ) {
    		mapper.addAnnotation( typeAnnotation );
    	}
    
    	if ( !requiresGenerationOfDecoratorClass() ) {
    		mapper.removeDecorator();
    	}
    	else if ( mapper.getDecorator() != null ) {
    		adjustDecorator( mapper, injectionStrategy );
    	}
    
        // 用于成员属性的Spring注解,这里是@Autowired
    	List<Annotation> annotations = getMapperReferenceAnnotations();
    	ListIterator<Field> iterator = mapper.getFields().listIterator();
    
        // 用新生成且含Spring注解的Field原来的替换Field
    	while ( iterator.hasNext() ) {
    
    		Field reference = iterator.next();
    		if ( reference instanceof  MapperReference ) {
    			iterator.remove();
    			iterator.add( replacementMapperReference( reference, annotations, injectionStrategy ) );
    		}
    	}
    
        // @Mapper注解默认是InjectionStrategyPrism.FIELD注入
    	if ( injectionStrategy == InjectionStrategyPrism.CONSTRUCTOR ) {
    		buildConstructors( mapper );
    	}
    
    	return mapper;
    }
    

    3.3、第六个遍历值org.mapstruct.ap.internal.processor.MapperRenderingProcessor

    MapperRenderingProcessor.process(ProcessorContext context, TypeElement mapperTypeElement, Mapper mapper)主要用于渲染生成实现类java文件。

    @Override
    public Mapper process(ProcessorContext context, TypeElement mapperTypeElement, Mapper mapper) {
    	if ( !context.isErroneous() ) {
    		writeToSourceFile( context.getFiler(), mapper, mapperTypeElement );
    		return mapper;
    	}
    
    	return null;
    }
    
    private void writeToSourceFile(Filer filer, Mapper model, TypeElement originatingElement) {
    	ModelWriter modelWriter = new ModelWriter();
    
    	createSourceFile( model, modelWriter, filer, originatingElement );
    
    	if ( model.getDecorator() != null ) {
    		createSourceFile( model.getDecorator(), modelWriter, filer, originatingElement );
    	}
    }
    

    3.4、第七个遍历值org.mapstruct.ap.internal.processor.MapperServiceProcessor

    componentModel等于"default"时有效,生成文件放入META-INF/services下。本例中用不到,不会做其他操作

    最后生成的类文件如下

    三、最后

    在添加或修改bean的属性时要删除掉生成的类文件,然后在启动项目,不然可能会在开发调试中报异常。

    原创 Doflamingo https://www.cnblogs.com/doflamingo
  • 相关阅读:
    使用Python的Mock库进行PySpark单元测试
    库龄报表的相关知识
    使用PlanViz进行ABAP CDS性能分析
    Spark SQL中列转行(UNPIVOT)的两种方法
    Spark中的一些概念
    使用Visual Studio Code进行ABAP开发
    2019年的几个目标
    Dom--样式操作
    Dom选择器--内容文本操作
    Javascript面向
  • 原文地址:https://www.cnblogs.com/doflamingo/p/13623867.html
Copyright © 2020-2023  润新知