天天看點

基于grpc從零開始搭建一個準生産分布式應用(6) - 02 - MapStruct資料轉換

開始前必讀:​​基于grpc從零開始搭建一個準生産分布式應用(0) - quickStart​​ 

一、基礎轉換

1.1、基礎類型

基本類型、包裝類、BigDecimal轉String預設使用DecimalFormat格式化,@Mapping#numberFormat可以指定格式,Date轉String預設使用SimpleDateFormat格式化,如預設格式不符要求,可以用,@Mapping可以指定格式。

  1. enum和String:可直接轉枚舉定義的值,不支援MappingConstants.ANY_REMAINING
  2. BigDecimal(等)、基本資料類(包括包裝類)和String:
  3. java.time.LocalDateTime(等)和String:
  4. java.util.Date轉String:Date->String,
  5. java.sql.Timestamp和java.util.Date:

注意在@Mapping#target、source設定的屬性的含義。mapstruct并不是按照設定的屬性找到字段,在找此字段的getter/setter方法的,而是直接通過get/set字首 + 屬性值找到相應的方法。是以如果屬性名與getter和setter方法名不對象,則會報錯。

@Data
public class TransformBO {
    private String level;
    private int price;
    private String date;
    private Date timestamp;
    private String localDateTime
}
@Data
public class TransformPO {
    private LevelEnum level;
    private BigDecimal price;
    private Date date;
    private Timestamp timestamp;
    private LocalDateTime localDateTime;
}

@Mapper
public interface TestMapper {
    TransformBO toTransformBO(TransformPO transformPO);
    TransformPO toTransformPO(TransformBO transformPO);
}

//@Mapping(target = "date", dateFormat = "yyyy-MM-dd HH:mm")
//@Mapping(target = "price", numberFormat = "$#.00")

TransformBO transformBO = new TransformBO();
transformBO.setLevel("PERFECT");
transformBO.setPrice(12);
transformBO.setDate("2022-01-31 01:30:19");
transformBO.setTimestamp(new Date(System.currentTimeMillis()));
transformBO.setLocalDateTime("2022-01-31T01:34:47.986");
System.out.println(mapper.toTransformPO(transformBO));

-->互轉後Date的出入值是比較大的,需要特殊處理
TransformBO(level=PERFECT, price=12, date=2022-01-31 01:35:21, timestamp=2022-01-31 01:35:21.024, localDateTime=2022-01-31T01:35:21.107)
TransformPO(level=PERFECT, price=12, date=Mon Jan 31 01:30:19 CST 2022, timestamp=2022-01-31 01:35:21.138, localDateTime=2022-01-31T01:34:47.986)      

1.2、Booble

private Boolean delete;
private String delete;


transformPO.setDelete(false);
//正常輸出      

二、集合轉換

2.1、List

這個比較簡單,需要定義一個集合類和一個單體轉換類,類的名字随意,但建議起成對的名字,便于記錄。

@Mapper
public interface TestMapper {
    TestBO testToBO(TestPO testPO);
    List<TestBO> testToBOS(List<TestPO> testPOS);
}      

2.2、Map

@Mapper
public interface TestMapper {
    TestBO testToBO(TestPO testPO);
    Map<TestBO, TestBO> testToBOS(Map<TestPO, TestPO> testPOS); 
}      

2.3、Enum

枚舉通常是直接映射到同名的枚舉對象中,不同名需@ValueMapping指定,并且源和目标上可以設定

  • MappingConstants.ANY_REMAINING:隻能用在source上,辨別source中除了同名自動映射和指定映射外,其餘所有對象都映射到指定的target對象上;
  • MappingConstants.ANY_UNMAPPED:隻能用在source上,不能和MappingConstants.ANY_REMAINING同時使用,會将source除指定對象(不包括同名自動映射)的剩餘對象映射到target對象,隻是為了省事;
  • MappingConstants.NULL:設定在source上表示source是null時,會被映射到指定的target;設定在target上表示source對象映射到null。
enum LevelEnum {
    PERFECT(1, "完美"),
    PASS(2, "合格"),
    normal_status(3, "普通"),
    failed(4, "不及格"),
    normal(5, "正常");
}

enum DisableStatus {
    able_status(1, "完美"),
    disable_status(2, "合格"),
    normal_status(3, "普通"),
    failed_status(4, "不及格"),
    ok_status(5, "還行"),
    fine_status(6, "可以");
}

@Mapper
interface TestMapper {
    @ValueMappings({
            @ValueMapping(source = "able_status", target = "PERFECT"),//一對一映射
            @ValueMapping(source = MappingConstants.NULL, target = "PASS"),//
            @ValueMapping(source = "failed_status", target = MappingConstants.NULL),//==在新代碼中failed_status=null
            @ValueMapping(source = MappingConstants.ANY_REMAINING, target = "normal"),//==default=normal
    })
    LevelEnum toEnum(DisableStatus disableStatus);
}      
public class TestMapperImpl implements TestMapper {
    @Override
    public LevelEnum toEnum(DisableStatus disableStatus) {
        if ( disableStatus == null ) {
            return LevelEnum.PASS;//--------
        }
        LevelEnum levelEnum;

        switch ( disableStatus ) {
            case able_status: levelEnum = LevelEnum.PERFECT;
                break;
            case failed_status: levelEnum = null;
                break;
            case normal_status: levelEnum = LevelEnum.normal_status;
                break;
            default: levelEnum = LevelEnum.normal;
        }
        return levelEnum;
    }
}      
//生成的代碼
@Mapper
public interface TestMapper {
    @ValueMappings({
            @ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "normal_status"),
    })
    LevelEnum toEnum(DisableStatus disableStatus);
}
 
@Component
public class TestMapperImpl implements TestMapper {
    @Override
    public LevelEnum toEnum(DisableStatus disableStatus) {
        if ( disableStatus == null ) {
            return null;
        } 
        LevelEnum levelEnum;
        switch ( disableStatus ) {
            default: levelEnum = LevelEnum.normal_status;
        }
        return levelEnum;
    }
}      

2.3.1、自定義名稱轉換

可以通過删除或添加源枚舉字元串的前字尾來映射目标枚舉對象,這也是一種快捷方式。@EnumMapping#nameTransformationStrategy支援的參數有:suffix(添加源字尾)、stripSuffix(删除源字尾)、prefix(添加源字首)、stripPrefix(删除源字首)。

@Mapper
public interface TestMapper {
    @EnumMapping(nameTransformationStrategy = "stripSuffix", configuration = "_status")
    LevelEnum toEnum(DisableStatus disableStatus);
}
//比如原Enum值為
public enum LevelEnum { 
    able_status(1, "完美")
}
//生成的源碼為
@Override
    public LevelEnum toEnum(DisableStatus disableStatus) {
        if ( disableStatus == null ) {
            return null;
        }
        LevelEnum levelEnum; 
        switch ( disableStatus ) {
            case able_status: levelEnum = LevelEnum.able;
            break;
            case disable_status: levelEnum = LevelEnum.disable;
            break;
            case normal_status: levelEnum = LevelEnum.normal;
            break;
            case failed_status: levelEnum = LevelEnum.failed;
            break;
            case ok_status: levelEnum = LevelEnum.ok;
            break;
            case fine_status: levelEnum = LevelEnum.fine;
            break;
            default: throw new IllegalArgumentException( "Unexpected enum constant: " + disableStatus );
        }
        return levelEnum;
    }      

三、資料映射政策

3.1、@mappingControl屬性映射優先級政策(不建議修改)

在mapStruct架構中可以從【全局-類-屬性】等地方可以配置映射政策,@MappingConfig,@Mapper,@BeanMapping,@Mapping越往後的優先級越高,這4個注解都有mappingControl屬性,預設值是MappingControl.class,可選值還有NoComplexMapping.class、DeepClone.class,用于控制每個字段屬性映射的方式;

3.1.1、MappingControl.class的優先級邏輯如下:

  1. MappingControl.Use.DIRECT——直接複制具有相同屬性類型的字段,但如果有自定義的映射配置@Mapping和導入了其他Mapper的自定義的映射,會将直接指派覆寫;自定義的映射配置@Mapping優先級高于導入了其他Mapper的自定義的映射;
  2. MappingControl.Use.MAPPING_METHOD——屬性類型不同的字段,會去查找将源屬性的類型作為參數,将目标屬性的類型作為傳回的映射方法(包括自定義方法、抽象類實作方法和接口預設方法);
  3. MappingControl.Use.BUILT_IN_CONVERSION——如果不存在上述方法,查找内置轉換方法,就像BigDecimal轉String;
  4. MappingControl.Use.COMPLEX_MAPPING——如果内置轉換方法不存在,将進行一些複雜轉換,也就是結合現有的映射方法和内置轉換,互相嵌套生成一個能夠映射的方法;
  5. 如果都沒有,則會自動生成子映射方法,就像上面的例子若是不指定TestPO到TestBO的接口方法,則也會主動生成;
  6. 若這也沒法生成則編譯就會報錯了。

3.1.2、NoComplexMapping.class的優先級邏輯如下:

  1. MappingControl.Use.DIRECT——直接複制具有相同屬性類型的字段,但如果有自定義的映射配置@Mapping和導入了其他Mapper的自定義的映射,會将直接指派覆寫;自定義的映射配置@Mapping優先級高于導入了其他Mapper的自定義的映射;
  2. MappingControl.Use.MAPPING_METHOD——屬性類型不同的字段,會去查找将源屬性的類型作為參數,将目标屬性的類型作為傳回的映射方法(包括自定義方法、抽象類實作方法和接口預設方法);
  3. MappingControl.Use.BUILT_IN_CONVERSION——如果不存在上述方法,查找内置轉換方法,就像BigDecimal轉String;
  4. 如果都沒有,則會自動生成子映射方法,就像上面的例子若是不指定TestPO到TestBO的接口方法,則也會主動生成;
  5. 若這也沒法生成則編譯就會報錯了。

3.1.3、DeepClone.class的優先級邏輯如下:

  • MappingControl.Use.MAPPING_METHOD——屬性類型不同的字段,會去查找将源屬性的類型作為參數,将目标屬性的類型作為傳回的映射方法(包括自定義方法、抽象類實作方法和接口預設方法)。
//比如使用DeepClone的mappingControl,這樣我們就隻能進行MappingControl.Use.MAPPING_METHOD映射了
@Mapper(mappingControl = DeepClone.class)
public interface TestMapper {
 
    @Mapping(target = "localDateTime", dateFormat = "yyyy-MM-dd HH")
    TransformBO toTransformBO(TransformPO transformPO);
}      

3.2、@TargetType泛型的應用-将映射目标類型傳遞給自定義映射器

就是在使用@Mapper#uses()時,使用的自定義映射器中的方法可以接受Class<T>對象,要使用 @TargetType标注。可用于一些通用設定。

@Data
public class BasePO {
    private Long id;
}
 @Data
public class TestFivePO {
    private TestFourPO test;
}
@EqualsAndHashCode(callSuper = true)
@Data
public class TestFourPO extends BasePO {
}

//-----
@Data
public class BaseBO {
    private Long id;
    private String name;
}
@Data
public class TestSevenBO {
    private TestSixBO test;
}
@EqualsAndHashCode(callSuper = true)
@Data
public class TestSixBO extends BaseBO {

}
------------源碼實作--------------------------------

@Mapper(uses = {BaseMapper.class})
public interface TestMapper { 
    TestSevenBO testToBO(TestFivePO testPO);
}

@Mapper
public class BaseMapper {
    public <T extends BaseBO> T testToBO(BasePO basePO, @TargetType Class<T> baseBOClass)  {
        try {
            T t = baseBOClass.newInstance();
            t.setId(basePO.getId());
            return t;
        } catch (Exception e) {
            return null;
        }
    }
}
------------生成的源碼實作--------------------------------
@Component
public class TestMapperImpl implements TestMapper {
    @Autowired
    private BaseMapper baseMapper;

    public TestMapperImpl() {
    }

    public TestSevenBO testToBO(TestFivePO testPO) {
        if (testPO == null) {
            return null;
        } else {
            TestSevenBO testSevenBO = new TestSevenBO();
            testSevenBO.setTest((TestSixBO)this.baseMapper.testToBO(testPO.getTest(), TestSixBO.class));
            return testSevenBO;
        }
    }
}      

3.3、@Context将上下文ThreadLocal或狀态對象傳遞給自定義方法

就是線程間共享值的一種方式,用了ThreadLocal特性。

@Data
public class ThreadLocalContext {
    private ThreadLocal<Object> threadLocal;
    public ThreadLocalContext() {
        threadLocal = new ThreadLocal<>();
    }
}
 
@Mapper
public class BaseMapper {
    public TestSixBO toTestSixBOWithContext(TestFourPO testFourPO, @Context ThreadLocalContext threadLocalContext) {
        TestSixBO testBO = new TestSixBO();
        testBO.setId(testFourPO.getId());
        testBO.setName((String) threadLocalContext.getThreadLocal().get());
        return testBO;
    }
}
 
@Mapper(uses = {BaseMapper.class})
public interface TestMapper {
    TestSevenBO testToBO(TestFivePO testPO, @Context ThreadLocalContext threadLocalContext);
}
 
@Component
public class TestMapperImpl implements TestMapper {
    @Autowired
    private BaseMapper baseMapper;
    @Override
    public TestSevenBO testToBO(TestFivePO testPO, ThreadLocalContext threadLocalContext) {
        if ( testPO == null ) {
            return null;
        }
        TestSevenBO testSevenBO = new TestSevenBO();
        testSevenBO.setTest( baseMapper.toTestSixBOWithContext( testPO.getTest(), threadLocalContext ) );
        return testSevenBO;
    }
}
 
ThreadLocalContext threadLocalContext = new ThreadLocalContext();
threadLocalContext.getThreadLocal().set("xx");
 
TestFivePO testFivePO = new TestFivePO();
TestFourPO testFourPO = new TestFourPO();
testFourPO.setId(1L);
testFivePO.setTest(testFourPO);
System.out.println(testMapper.testToBO(testFivePO, threadLocalContext));

---
TestSevenBO(test=TestSixBO(super=BaseBO(id=1, name=xx)))      

3.4、@Qualifier差別相同參數類型和傳回類型的映射方法

@Mapper
public class BaseMapper {
    public String toString1(String name) {
        return name + "1";
    }
    public String toString2(String name) {
        return name + "2";
    }
}
toString1、toString2方法的參數類型和傳回類型相同,編譯直接報錯 
@Mapper(uses = {BaseMapper.class})
public interface TestMapper {
    TestSixBO testToBO(TestFourPO testPO); 
}      

3.4.1、解決方案1-自定義注解ElementType.METHOD

@Qualifier
@Target(ElementType.METHOD) //ElementType.TYPE
@Retention(RetentionPolicy.CLASS)
public @interface StringToString1 {
}
@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface StringToString2 {
}
------------------------------
@Mapper(uses = {BaseMapper.class})
public interface TestMapper {
    @Mapping(target = "name", qualifiedBy = {StringToString1.class})//qualifiedBy屬性
    TestSixBO testToBO(TestFourPO testPO);
}
 
@Mapper
public class BaseMapper {
    @StringToString1
    public String toString1(String name) {
        return name + "1";
    }
    @StringToString2
    public String toString2(String name) {
        return name + "2";
    }
}
------------------------生成的代碼如下------------------------
@Component
public class TestMapperImpl implements TestMapper {
 
    @Autowired
    private BaseMapper baseMapper;
 
    @Override
    public TestSixBO testToBO(TestFourPO testPO) {
        if ( testPO == null ) {
            return null;
        }
 
        TestSixBO testSixBO = new TestSixBO();
        // 使用qualifiedBy中設定的注解的方法
        testSixBO.setName( baseMapper.toString1( testPO.getName() ) );
        testSixBO.setId( testPO.getId() );
 
        return testSixBO;
    }
}      

3.4.2、解決方案2-自定義注解ElementType.TYPE

@Qualifier
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface StringToString1 {
}
 
@Qualifier
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface StringToString2 {
}
 
 
@Mapper
@StringToString1//----------------------
public class BaseMapper {
    public String toString1(String name) {
        return name + "1";
    }
}
 
@Mapper
@StringToString2
public class BaseMapper2 { 
    public String toString2(String name) {
        return name + "2";
    }
}
 
@Mapper(uses = {BaseMapper.class, BaseMapper2.class})
public interface TestMapper {
    @Mapping(target = "name", qualifiedBy = {StringToString2.class})
    TestSixBO testToBO(TestFourPO testPO);
}      

3.4.3、解決方案3-@Named取代定義一個@Qualifier的接口

//若類上注解了@Named,mapper中引入了此類,要想使用引入類中自定義的映射方法,映射字段時就必須指定qualifiedByName
@Mapper
@Named("StringToString1")
public class BaseMapper {
    public String toString1(String name) {
        return name + "1";
    }
}
 
@Mapper
@Named("StringToString2")
public class BaseMapper2 {
    public String toString2(String name) {
        return name + "2";
    }
}
 
@Mapper(uses = {BaseMapper.class, BaseMapper2.class})
public interface TestMapper {
    @Mapping(target = "name", qualifiedByName = {"StringToString2"})//qualifiedByName屬性
    TestSixBO testToBO(TestFourPO testPO);
 
}      

四、集合政策

通過@Mapping#collectionMappingStrategy設定集合的映射政策:CollectionMappingStrategy.ACCESSOR_ONLY(預設)、CollectionMappingStrategy.SETTER_PREFERRED、CollectionMappingStrategy.ADDER_PREFERRED、CollectionMappingStrategy.TARGET_IMMUTABLE。

public class TestPOS {
 
    private List<TestPO> list; 
    public List<TestPO> getList() {
        return list;
    }
    public void addList(TestPO testPO) {
        this.list.add(testPO);
    }
}
 
public class TestBOS {
    private List<TestBO> list;
    public List<TestBO> getList() {
        return list;
    } 
    public void addList(TestBO testBO) {
        this.list.add(testBO);
    }
}
 
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
public interface TestMapper {
    TestBO toBO(TestPO testPO);
    TestBOS toBOS(TestPOS testPOS);
}      

4.1、使用預設集合政策CollectionMappingStrategy.ACCESSOR_ONLY

@Data
public class TestPOS {
    private List<TestPO> list;
} 
@Data
public class TestBOS {
    private List<TestBO> list;
}
 
@Mapper
public interface TestMapper {
    TestBO toBO(TestPO testPO);
    void toBOS(TestPOS testPOS, @MappingTarget TestBOS testBOS);
}      
testBOS.getList().clear();
testBOS.getList().addAll( list )//先清空再添加      

4.2、使用CollectionMappingStrategy.ADDER_PREFERRED政策

手動建立add方法,使用CollectionMappingStrategy.ADDER_PREFERRED

public class TestBOS {
    private List<TestBO> oneTest_;
    public List<TestBO> getOneTestList() {
        return oneTest_;
    }
    public void addOneTest(TestBO testBO) {//手動建立了此方法
        oneTest_.add(testBO);
    }
}

@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
public interface TestMapper {
    @Mapping(target = "oneTest_", source = "tests") //這處的oneTest_要改成oneTestList才能正确調用addOneTest方法
    TestBOS toBean(TestPOS testPO);
}      

自定義add方法,使用CollectionMappingStrategy.ADDER_PREFERRED

public class TestBOS {
    private List<Integer> oneTest_;
    public List<TestBO> getOneTestList() {
        return new ArrayList<>();
    }
    public void addOneTest(Integer integer) {//入參不一樣
        oneTest_.add(integer);
    }
}
//testBOS.getOneTestList().addAll( list )
public class TestBOS {
    private List<Integer> oneTest_;
    public List<TestBO> getOneTestList() {
        return new ArrayList<>();
    }
    public void addOneTest(TestBO testBO) {//入參不一樣
        oneTest_.add(testBO.getId().intValue());
    }
}
//testBOS.addOneTest( testPOToTestBO( test ) )      

隻有add方法,使用CollectionMappingStrategy.ADDER_PREFERRED政策

TestBOS testBOS = new TestBOS();
 
        if ( testPOS.getList() != null ) {
            for ( TestPO list : testPOS.getList() ) {
                testBOS.addList( toBO( list ) );
            }
        }      

去掉add方法,使用CollectionMappingStrategy.ACCESSOR_ONLY政策

TestBOS testBOS = new TestBOS();
 
        if ( testBOS.getList() != null ) {
            List<TestBO> list = testPOListToTestBOList( testPOS.getList() );
            if ( list != null ) {
                testBOS.getList().addAll( list );
            }
        }
//以上結論是一樣的,都是保持原集合内容不變,再增加新内容。      

4.3、使用CollectionMappingStrategy.TARGET_IMMUTABLE政策

@Data
public class TestPOS {
    private List<TestPO> list;
} 
@Data
public class TestBOS {
    private List<TestBO> list;
}
 
@Mapper(collectionMappingStrategy = CollectionMappingStrategy.TARGET_IMMUTABLE)
public interface TestMapper {
    TestBO toBO(TestPO testPO);
    void toBOS(TestPOS testPOS, @MappingTarget TestBOS testBOS);
}      
@Override
    public void toBOS(TestPOS testPOS, TestBOS testBOS) {
        if ( testPOS == null ) {
            return;
        }
        testBOS.setList( testPOListToTestBOList( testPOS.getList() ) );
    }
    //這個直接使用setter方式,會覆寫掉原集合的内容      

繼續閱讀