前言
在一次需求的開發過程中,發現一個物件插入資料庫時某個欄位總是為空。簡化後的程式碼如下:
@Autowired
private PersonService personService;
public void test1(){
Person person = personService.findById(1L);
PersonDto personDto = PersonMapper.INSTANCE.personToPersonDto(person);
personService.insert(personDto);
}
這麼簡單的邏輯按理說不會出么蛾子啊,我先排查了資料庫裡person id=1的記錄發現值是有的啊,然後又排查了我的insert方法,也是沒問題的。
經過一段時間的排查,才發現是
PersonDto personDto = PersonMapper.INSTANCE.personToPersonDto(person);
這行程式碼的問題。證據截圖如下:
前面的時候addTeacherNum還有值,轉化後怎麼又沒值了呢?
大家看到這裡肯定猜測是不是我屬性名不對,或者屬性型別不對。我甚至還刪除了之後用複製的方式來保證沒有手敲敲錯的情況。
完全是一模一樣的屬性啊。
我們知道mapstruct是編譯時通過我們的PersonMapper介面來實現實現類,實現類裡是setter、getter方法來實現的。於是我開啟了PersonMapper的實現類準備一探究竟:
public class PersonMapperImpl implements PersonMapper {
public PersonMapperImpl() {
}
public PersonDto personToPersonDto(Person person) {
if (person == null) {
return null;
} else {
PersonDtoBuilder personDto = PersonDto.builder();
personDto.name(person.getName());
return personDto.build();
}
}
}
竟然沒有對我這個屬性addTeacherNum進行賦值。這讓我百思不得其解。只能去看看原始碼,試圖找出原因。
如何除錯Maven外掛
前面我們提到mapstruct是在程式碼編譯的時候就開始生成程式碼了,於是我們需要對maven編譯期進行除錯。方法如下:
- maven debug命令
mvnDebug clean compile
- idea遠端debug
新建一個remote,然後修改埠為8000,然後在執行maven命令的同時,啟動這個remote即可。
原始碼解析
斷點應該打在哪裡呢?
我們檢視mapstruct的結構,一般先從配置的檔案入手
找到了這個MappingProcessor類,我們可以看到這裡面有個process方法,裡面又呼叫瞭如下的這個方法:
private void processMapperTypeElement(ProcessorContext context, TypeElement mapperTypeElement) {
Object model = null;
for ( ModelElementProcessor<?, ?> processor : getProcessors() ) {
try {
model = process( context, processor, mapperTypeElement, model );
}
catch ( AnnotationProcessingException e ) {
//省略
}
}
}
這段程式碼其實就是呼叫getProcessors()方法拿到多個processor,然後遍歷呼叫。而這個getProcessors()就是從配置檔案裡通過SPI的方式載入物件。
這裡面我們重點關注這個Processor:MapperCreationProcessor。它的process方法如下:
@Override
public Mapper process(ProcessorContext context, TypeElement mapperTypeElement, List<SourceMethod> sourceModel) {
this.elementUtils = context.getElementUtils();
this.typeUtils = context.getTypeUtils();
this.messager =
new MapperAnnotatedFormattingMessenger( context.getMessager(), mapperTypeElement, context.getTypeUtils() );
this.options = context.getOptions();
this.versionInformation = context.getVersionInformation();
this.typeFactory = context.getTypeFactory();
this.accessorNaming = context.getAccessorNaming();
MapperOptions mapperOptions = MapperOptions.getInstanceOn( mapperTypeElement, context.getOptions() );
List<MapperReference> mapperReferences = initReferencedMappers( mapperTypeElement, mapperOptions );
MappingBuilderContext ctx = new MappingBuilderContext(
typeFactory,
elementUtils,
typeUtils,
messager,
accessorNaming,
context.getEnumMappingStrategy(),
context.getEnumTransformationStrategies(),
options,
new MappingResolverImpl(
messager,
elementUtils,
typeUtils,
typeFactory,
new ArrayList<>( sourceModel ),
mapperReferences,
options.isVerbose()
),
mapperTypeElement,
//sourceModel is passed only to fetch the after/before mapping methods in lifecycleCallbackFactory;
//Consider removing those methods directly into MappingBuilderContext.
Collections.unmodifiableList( sourceModel ),
mapperReferences
);
this.mappingContext = ctx;
return getMapper( mapperTypeElement, mapperOptions, sourceModel );
}
getMapper裡面有一段這個方法引起我的注意:
List<MappingMethod> mappingMethods = getMappingMethods( mapperOptions, methods );
猜測這段就是獲取要寫入的set、get方法。
於是一路跟蹤:
發現mapstruct裡面把方法分為了下面四類,而我的addTeacherNum屬性通過lombok生成的方法methodType被分到了ADDER裡面。
而在生成我的Mapper實現類的時候它會只過濾setter方法。
List<Accessor> candidates = new ArrayList<>( getSetters() );
至此真相大白。