12.1、AbstractProcessor
AbstractProcessor可以在编译时获取注解,生成代码。
主要的方法有:
init:做一些初始化操作。
process:核心处理。返回是boolean类型,false表示继续由其他处理器处理该“元素(包、类、方法、变量等)”上的注解类型(当前要处理的注解集合)。
getSupportedAnnotationTypes:获取可以处理的注解。可以在AbstractProcessor的实现类上使用@SupportedAnnotationTypes取代此方法。
getSupportedSourceVersion:获取java版本号。可以在AbstractProcessor的实现类上使用@SupportedSourceVersion取代此方法。
要想在编译时生成代码还需要在resources/META-INF/services创建javax.annotation.processing.Processor文件,文件内容就是自定义注解处理器的全限定类名。
12.2、编译时DEBUG
这里使用的是2017.3.5版本的IDEA。
1、首先创建一个远程配置。端口8000。
2、在需要调试的AbstractProcessor实现类加上断点。这里是mapstruct的注解处理器实现类。
3、到自己的项目根目录下,以mvnDebug方式操作项目。这里只是编译项目。
mvnDebug clean compile
会在命令行打印。
监听8000端口,等待运行。
4、以Debug方式运行1中的配置。
这样命令就会继续往下执行,编译项目。
断点也可以进去了。
12.3、映射时字段名的处理
定义一些基础类
@Data
@ToString
public class BasePO {
private Long id;
}
@Data
@ToString
public class BaseBO {
private Long id;
}
@Data
public class BeanTestPO {
private String name;
private BasePO basePO;
}
@Data
public class BeanTestBO {
private String name;
private BaseBO baseBO;
}
public class BaseMapper {
@ObjectFactory
public ProtocolStringList createProtocolStringList(List<String> list) {
return new LazyStringArrayList(list.size());
}
public static byte[] toByte(ByteString bytes) {
return bytes.toByteArray();
}
// any转javabean的中间转换方法
public static <T extends GeneratedMessageV3> T unpack(Any any, @TargetType Class<T> clazz) {
T unpack;
try {
unpack = any.unpack(clazz);
} catch (InvalidProtocolBufferException e) {
return null;
}
return unpack;
}
// protobuf转javabean的中间转换方法
public static Any packGeneratedMessageV3(GeneratedMessageV3 message) {
return Any.pack(message);
}
public static Date toDateFromString(String dateString, String format) {
if (StringUtils.isEmpty(dateString)) {
return null;
}
Date date;
try {
date = new SimpleDateFormat(format).parse(dateString);
} catch (ParseException e) {
return null;
}
return date;
}
}
@Mapper(uses = {BaseMapper.class}, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
public interface TestTwoMapper {
@Mapping(target = "name", source = "name")
@Mapping(target = "baseBO.id", constant = "1L")
BeanTestBO testToBO(BeanTestPO testPO);
}
mapstruct的注解处理器的实现类是MappingProcessor.class,它的核心处理中并没有具体的逻辑,其中通过ServiceLoader.load加载所有ModelElementProcessor.class的实现类。
每个实现类负责不同的功能,核心方法是process()。
就第1.7章的属性名问题看一下源码是如何处理的。
先看一下handleDefinedMapping方法的调用栈如下。最开始还是从MappingProcessor中调用的MapperCreationProcessor中的一个逻辑处理。
其中几个关键的参数类型说明如下:
TypeElement:继承自Element(表示程序中包、类、方法、成员变量、函数参数、泛型类型),typeElement表示一个类或接口元素,注解是一个接口元素。
ProcessorContext:处理器上下文,里面封装了处理过程中用到的一些工具类(比如获取Element、Types:元素的类型、枚举映射策略,用于代码生成时的一些参数选项,打印编译时的提示)等。
ModelElementProcessor:模型元素处理器,实现类有不同的功能:比如构造mapper对象(这里是指在处理器中的将@Mapper注解的元素转换成的对象的实现),对mapper进行检查,生成实现类等。
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 ) {
processingEnv.getMessager()
.printMessage(
Kind.ERROR,
e.getMessage(),
e.getElement(),
e.getAnnotationMirror(),
e.getAnnotationValue()
);
break;
}
}
}
private <P, R> R process(ProcessorContext context, ModelElementProcessor<P, R> processor,
TypeElement mapperTypeElement, Object modelElement) {
@SuppressWarnings("unchecked")
P sourceElement = (P) modelElement;
return processor.process( context, mapperTypeElement, sourceElement );
}
...
会对每一个注解了@Mapper的元素进行处理。这里看到通过一个循环,使用不同的ModelElementProcessor实现类对元素进行处理,ModelElementProcessor#process方法会接受一个P泛型表示处理器要处理的参数类型,返回一个R泛型表示处理器返回的类型;第一个ModelElementProcessor子类(MethodRetrievalProcessor)会接受一个null参数,将返回值(List<SourceMethod>,如下图:返回的是@Mapper注解元素中的方法和导入的BaseMapper中的方法)作为后一个ModelElementProcessor子类的入参,后一个处理器是MapperCreationProcessor,用于构造Mapper对象。
中间的ModelElementProcessor子类基本都返回Mapper类型的结果,这就类似于责任链模式,每个ModelElementProcessor子类仅处理自己能处理的部分,不能处理的交给下一个ModelElementProcessor子类。例如我们使用的spring方式的注入,其中会由SpringComponentProcessor处理,一个很明显的结果就是实现类会被加上@Component注解。
一直处理到ModelElementProcessor子类(MapperRenderingProcessor)会生成实现类文件;MapperRenderingProcessor#createSourceFile->ModelWriter#writeModel->FreeMarkerWritable#write->FreeMarkerModelElementWriter#write->Template#process。其中的FreeMarkerWritable#getTemplateName会获取GeneratedType.ftl的模板名,再通过Configuration#getTemplate获取模板,模板中的变量来自Mapper对象。
<#if hasPackageName()>
package ${packageName};
</#if>
<#list importTypeNames as importedType>
import ${importedType};
</#list>
<#if !generatedTypeAvailable>/*</#if>
@Generated(
value = "org.mapstruct.ap.MappingProcessor"<#if suppressGeneratorTimestamp == false>,
date = "${.now?string("yyyy-MM-dd'T'HH:mm:ssZ")}"</#if><#if suppressGeneratorVersionComment == false>,
comments = "version: ${versionInformation.mapStructVersion}, compiler: ${versionInformation.compiler}, environment: Java ${versionInformation.runtimeVersion} (${versionInformation.runtimeVendor})"</#if>
)<#if !generatedTypeAvailable>
*/</#if>
<#list annotations as annotation>
<#nt><@includeModel object=annotation/>
</#list>
<#lt>${accessibility.keyword} class ${name}<#if superClassName??> extends ${superClassName}</#if><#if interfaceName??> implements ${interfaceName}</#if> {
<#list fields as field><#if field.used><#nt> <@includeModel object=field/>
</#if></#list>
<#if constructor??><#nt> <@includeModel object=constructor/></#if>
<#list methods as method>
<#nt> <@includeModel object=method/>
</#list>
}
@includeModel使用了实现TemplateDirectiveModel(ModelIncludeDirective)来处理自定义标签,其逻辑是每次再调用FreeMarkerWritable#write->FreeMarkerModelElementWriter#write->Template#process处理当前属性名的模板和bean对象(比如<@includeModel object=annotation/>处理Annotation.class和Annotation.ftl),一直一直循环嵌套调用同样的操作,直到处理完所以自定义标签;其初始化设置是在ModelWriter的静态代码块的Configuration类型的变量中。
最后由MapperServiceProcessor处理,只有在使用默认的注入方式,且有自定义实现类配置(@Mapper#implementationName)时,才会有相应的逻辑处理——在META-INF/services/下生成相应的类;
返回null表示结束。
再说一下BeanMappingMethod的继承结构。
Writable:用于将元素写入到字符流中。
FreeMarkerWritable:使用FreeMarker模板引擎来输出。生成源文件使用的是Filer#createSourceFile方法,然后使用freemarker模板向源文件写入内容。
ModelElement:使用模型元素的基类,将模型元素写入到源码文件中。其实现类有:Type(类型元素模型)、Annotation(注解元素模型)、MappingMethod等
MappingMethod:mapper中声明或引用的方法元素模型。其实现类有:ValueMappingMethod(值映射方法,例如枚举映射枚举)、NestedPropertyMappingMethod(嵌套属性映射方法)、NormalTypeMappingMethod等。
NormalTypeMappingMethod:主要的映射方法。其实现类有:MapMappingMethod(map映射方法)、ContainerMappingMethod(迭代器或流映射方法)、BeanMappingMethod(bean对象映射方法)等。
BeanMappingMethod:对象属性映射方法。
最后爬一下源码,能发现其中是如何处理属性字段值的。在BeanMappingMethod#handleDefinedMapping方法中有这样一段。
从926行开始看,拿到目标对象属性的第一个名。
这个targetRef的类型是TargetReference,debug时可以看到章节前定义的对象的结构,如下:
对象属性的字段会有pathProperties属性,propertyEntries表示属性嵌套,例如source = "in.propA.propB",则propertyEntries[0]=propA,propertyEntries[1]=propB。
parameter会在使用@MappingTarget注解更新时赋值,就是更新方法中被更新的对象那个参数。
928~931行用于检测此属性名是否是不需要处理的属性名,unprocessedDefinedTargets是一个map,用于存放不需要处理的属性名,key就是属性名,value是嵌套中的属性引用,通常是有嵌套属性设了常量或表达式时才会有这个map。就是上面映射@Mapping(target = "baseBO.id", constant = "1L")。
933~934行用于获取目标属性名的读写器。Accessor用于从bean的属性字段读写操作,或向bean的属性字段读写操作。
比如我们将BeanTestBO改成这样。
public interface BOList extends List<String> {
}
public class BeanTestBO {
private String name;
public String getFullName() {
return name;
}
public void setNickName(String name) {
this.name = name;
}
}
DEBUG到这里是得到的targetWriteAccessor和targetReadAccessor都是null;
再修改testToBO方法上的@Mapping注解
@Mapper(uses = {BaseMapper.class}, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
public interface TestTwoMapper {
@Mapping(target = "nickName", source = "name")
BeanTestBO testToBO(BeanTestPO testPO);
}
得到的结果targetReadAccessor为空,targetWriteAccessor不为空。
再修改改testToBO方法上的@Mapping注解
@Mapper(uses = {BaseMapper.class}, nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS,
collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
public interface TestTwoMapper {
@Mapping(target = "fullName", source = "name")
BeanTestBO testToBO(BeanTestPO testPO);
}
得到的结果targetWriteAccessor为空,targetReadAccessor不为空
936~937行只要targetWriteAccessor和targetReadAccessor有一个不为null就不会进行条件逻辑中,就不会有Message.BEANMAPPING_UNKNOWN_PROPERTY_IN_RESULTTYPE( "Unknown property \"%s\" in result type %s. Did you mean \"%s\"?")的错误。而944行的判断,就上面说过的,非嵌套属性不会有pathPropertie。
targetWriteAccessor来自unprocessedTargetProperties(Map<String, Accessor),其创建来自以下代码。
意思就是获取当前元素(目标对象)的所有方法,过滤出setter的方法。
targetReadAccessor来自目标类型对象中的getPropertyReadAccessors方法。其处理方式基本同targetWriteAccessor。
12.4、List的属性字段映射在用工厂方法下的问题
看下面一个列子
@Data
public class BeanTestPO {
private List<BasePO> itemList;
}
@Data
public class BeanTestBO {
private List<BaseBO> itemList;
}
@Mapper(uses = {BaseMapper.class})
public interface TestMapper {
BeanTestBO testToBO(BeanTestPO testPO);
}
public class BaseMapper {
@ObjectFactory
public BOList createProtocolStringList() {
return null;
}
}
映射的结果中,工厂方法会被调用导致出错。
分析下为什么会这样呢?
源码一直爬下去会在MethodMatcher#matchResultType发现
returnTypeMatcher(TypeMatcher)继承SimpleTypeVisitor6<Boolean, TypeMirror>。
TypeMirror表示Java编程语言中的一种类型。 类型包括原始类型,声明类型(类和接口类型),数组类型,类型变量和空类型。 还表示通配符类型参数,可执行文件的签名和返回类型以及对应于程序包和关键字void伪类型。
TypeVisitor类型的访问者,以访问者设计模式的风格。 实现此接口的类用于在编译时类型类型未知时对类型进行操作。 当访问者被传递到类型的accrpt
方法时,调用最适用于该类型的visitXXX方法。
SimpleTypeVisitor6继承AbstractTypeVisitor6(其重写的visit方法的实现逻辑就是直接调用TypeMirror#accept)继承TypeVisitor。
看一下上面163行之后的调用栈。经过java编译器内部的code包处理会invoke到相依的类型visit方法。
这里TypeMirror是BOList的,是一个声明类型;所以会调用到TypeMatcher中重写的visitDeclared方法。
253行调用的方法使用Types#isAssignable判断一个TypeMirror是否可用分配给另一个,这里使用JavacTypes#isAssignable判断返回了true,也就是说BOList是可以分配给List<BaseBO>,之后会去判断类型的实际类型参数,就比如泛型,List<BaseBO>返回的是BaseBO,BOList返回null,按理这里是要判断泛型中的参数类型是否可分配,但是t(目标类型)是直接取自身类型也就是是个BOList,其父类才是List<String>,匹配父类的参数类型才会判断失败;这里最终在266行返回true,visitedAssignability是判断当前方法的返回类型是否是Void,是为Assignability.VISITED_ASSIGNABLE_TO,判断List<BaseBO>是否有类型参数。源码这里有注释,认为List<E>匹配list E,正如List<BaseBO>和BOList,但是却没有考虑BOList父类到底是个什么参数类型,不过下面也说明了不建议将一个原始类型参数和有参数的类型(泛型)匹配。但我依旧认为这里是bug。List<BaseBO>反过来赋予BOList是不会转换成功的,也就是说不存在bug。
这个问题我在官方的github上已提交,确实之前也有人反映这样的问题,官方会在下个版本修复。
为什么要说这个问题,因为protobuf中映射String集合时,生成的字段属性是LazyStringArrayList,要使用工厂方法,这在第11.1章已说明;当又有其他类型的集合字段时就会也调用工厂方法,导致出错。
12.5、属性的复杂映射
先看一个例子
public class BasePO {
}
@Data
public class BeanTestPO {
private BasePO base;
}
public class BaseVO {
}
public class BeanTestVO {
private BaseVO base;
}
public class BaseMapper2 {
public static BaseVO toVO(BaseBO baseBO) {
return new BaseVO();
}
public static BaseBO toBO(BasePO basePO) {
return new BaseBO();
}
}
@Mapper(uses = BaseMapper2.class)
public interface TestTwoMapper {
BeanTestVO testToBO(BeanTestPO testPO);
}
编译的结果
@Component
public class TestTwoMapperImpl implements TestTwoMapper {
@Override
public BeanTestVO testToBO(BeanTestPO testPO) {
if ( testPO == null ) {
return null;
}
BeanTestVO beanTestVO = new BeanTestVO();
beanTestVO.setBase( BaseMapper2.toVO( BaseMapper2.toBO( testPO.getBase() ) ) );
return beanTestVO;
}
}
beanTestVO.setBase( BaseMapper2.toVO( BaseMapper2.toBO( testPO.getBase() ) ) );从TestPO转成TestVO经过了复杂的映射。
看一下这是怎么生成的。
爬一下源码,发现propertyMappings字段设置是在PropertyMapping#build方法中
这段是创建一个Assignment(一个接口,可以通过源对象对目标对象分配操作,MethodReference、AssignmentWrapper都实现了该接口)对象,确切说是SetterWrapper中的decoratedAssigment属性字段的值。
AssignmentWrapper是Assignment的包装类,其本身可以包含其他的Assignment对象,构成例如复杂嵌套映射,其子类有SetterWrapper等。
这之后进入MappingResolverImpl#getTargetAssignment方法,通过源类型和目标类型得到一个合适的分配对象。核心处理方法是MappingResolverImpl#getBestMatch,会得到MethodMethod<T1 extends Method, T2 extends Method>对象;处理的逻辑是,例如A->C,会从所有候选方法中找B->C的方法,这命名为Y方法,之后再从所有候选方法中找到一个A->B的方法,这命名为X方法;之后将这两个方法封装成Assignment对象。
这里会将上面的assigment包装成一个SetterWrapper对象。
在生成的这个Mapper中methods对象属性中有个propertyMappings对象,这个就是生成的实现类中映射方法中要进行转换的字段属性对象,里面有个assigment字段,类型是SetterWrapper,这就是给目标对象属性字段进行set设置的包装类,里面的decoratedAssigment字段就是用到的给目标对象属性字段进行设置所调用的方法,正如上图框出来的toVO;它是一个MethodReference对象,它代表着本身就可以是一个方法,它也可以关联一个MethodReference对象,表示通过什么方法映射成其本身,正如上图框出来的toBO。
当得到这Mapper对象,在最后通过FreeMarker生成实现类时,会加载SetterWrapper.ftl模板,里面导入CommonMacros.ftl模板。
<#import "../macro/CommonMacros.ftl" as lib>
<@lib.handleExceptions>
<@lib.sourceLocalVarAssignment/>
<@lib.handleSourceReferenceNullCheck>
<#if ext.targetBeanName?has_content>${ext.targetBeanName}.</#if>${ext.targetWriteAccessorName}<@lib.handleWrite><@lib.handleAssignment/></@lib.handleWrite>;
</@lib.handleSourceReferenceNullCheck>
</@lib.handleExceptions>
<#macro handleWrite><#if fieldAssignment> = <#nested><#else>( <#nested> )</#if></#macro>
<#macro handleAssignment>
<@includeModel object=assignment
targetBeanName=ext.targetBeanName
existingInstanceMapping=ext.existingInstanceMapping
targetReadAccessorName=ext.targetReadAccessorName
targetWriteAccessorName=ext.targetWriteAccessorName
targetType=ext.targetType/>
handleWrite标签里面有个<#nested>嵌套外面的<@lib.handleAssignment/>,其中object=assignment正如上面所说是个MethodReference类型,所以有加载了MethodReference.ftl,其内部又有MethodReference类型的assignment字段,又会加载MethodReference.ftl,这样就生成了复杂映射。
12.6、protobuf中的Any问题
定义一些基础类
syntax = "proto3";
import "any.proto";
option java_package = "com.ljc.orika.proto3";
option java_multiple_files = true;
option java_outer_classname = "TestAll";
message Test3 {
google.protobuf.Any details = 1;
}
message Item3 {
}
@Data
public class ItemDTO {
}
@Data
public class TestFivePO {
private Any details;
}
@Data
public class Test3DTO {
private ItemDTO details;
}
public class BaseMapper {
public static <T extends GeneratedMessageV3> T unpack(Any any, @TargetType Class<T> clazz) {
return null;
}
public static <T extends GeneratedMessageV3> Any pack(T message) {
return Any.pack(message);
}
public static Any packGeneratedMessageV3(GeneratedMessageV3 message) {
return pack(message);
}
}
@Mapper(uses = {BaseMapper.class})
public interface TestMapper {
ItemDTO toItemDTO(Item3 item);
Test3DTO toDTO(TestFivePO test);
}
调试编译时,运行到MappingResolverImpl#getBestMatch方法查找最合适的方法时。
在之前查找Y方法时,已经得到了
因为会循环添加到yCandidates中所有合适的方法;在遍历Y方法到unpack这一项时,736行查找X方法,此时sourceType还是Any,ySourceType也是Any;在BaseMapper类只有一个unpack方法时会被匹配到,因为Any也是GeneratedMessageV3的子类。
所有最终得到的yCandidates、xCandidates、typesInTheMiddle
这里的typesInTheMiddle被覆盖了一次,之前的是
所以在776~783行的循环中会进行三者的匹配,因为没有匹配而清空xCandidates,最终没有匹配到方法。
而当在BaseMapper类添加pack方法时,736行查找X方法时得到将是pack方法,后面也不会再被覆盖。得到的yCandidates不变、xCandidates、typesInTheMiddle
typesInTheMiddle第二项虽然不会被匹配到,到第一项还是正确匹配的,所以可以正常转换。
所以关键是736行查找X方法。所以进入其中的方法,看到
这里的逻辑就是通过各种选择器选择合适的方法。
最后会在InheritanceSelector选择器中的得到结果,主要是用来选择有继承关系的类,以继承层数最少的为结果。
其中有个方法
大致的意思就是如果类型相同返回0;如果有继承关系返回1,多级依次增加1;如果类型不能分配返回-1。这里在处理pack方法时,由于base是com.google.protobuf.Any,targetType是泛型,不能分配,使用返回-1了。从而得知匹配packGeneratedMessageV3方法时会返回1。