MapStruct文档(十三)——问题分析

 

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行只要targetWriteAccessortargetReadAccessor有一个不为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方法。

这里TypeMirrorBOList的,是一个声明类型;所以会调用到TypeMatcher中重写的visitDeclared方法。

253行调用的方法使用Types#isAssignable判断一个TypeMirror是否可用分配给另一个,这里使用JavacTypes#isAssignable判断返回了true,也就是说BOList是可以分配给List<BaseBO>,之后会去判断类型的实际类型参数,就比如泛型,List<BaseBO>返回的是BaseBOBOList返回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属性字段的值。

AssignmentWrapperAssignment的包装类,其本身可以包含其他的Assignment对象,构成例如复杂嵌套映射,其子类有SetterWrapper等。

这之后进入MappingResolverImpl#getTargetAssignment方法,通过源类型和目标类型得到一个合适的分配对象。核心处理方法是MappingResolverImpl#getBestMatch,会得到MethodMethod<T1 extends Method, T2 extends Method>对象;处理的逻辑是,例如A->C,会从所有候选方法中找B->C的方法,这命名为Y方法,之后再从所有候选方法中找到一个A->B的方法,这命名为X方法;之后将这两个方法封装成Assignment对象

这里会将上面的assigment包装成一个SetterWrapper对象。

在生成的这个Mappermethods对象属性中有个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还是AnyySourceType也是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方法时,由于basecom.google.protobuf.AnytargetType是泛型,不能分配,使用返回-1了。从而得知匹配packGeneratedMessageV3方法时会返回1。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值