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的屬性時要刪除掉生成的類文件,然后在啟動項目,不然可能會在開發調試中報異常。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM