From 3b265058a5020002ac5cff5aba8d9d28e2935c3d Mon Sep 17 00:00:00 2001 From: linpeilie Date: Sat, 16 Mar 2024 22:22:17 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=94=A8=E4=BE=8B=E3=80=81?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/.vuepress/config/en/series.ts | 1 + docs/.vuepress/styles/index.css | 3 + docs/README.md | 15 +++ docs/en/guide/class-convert.md | 111 ++++++++++++++++++ docs/en/guide/cycle-avoiding.md | 87 ++++++++++++++ docs/guide/class-convert.md | 108 ++++++++++++++++- .../github/linpeilie/model/DependsSource.java | 16 +++ .../github/linpeilie/model/DependsTarget.java | 12 ++ .../linpeilie/model/EnglishRelease.java | 2 + .../github/linpeilie/model/FrenchRelease.java | 2 - .../linpeilie/annotations/AutoMapping.java | 46 ++++++++ .../annotations/ReverseAutoMapping.java | 46 ++++++++ 12 files changed, 446 insertions(+), 3 deletions(-) create mode 100644 docs/en/guide/cycle-avoiding.md create mode 100644 example/spring-boot-with-lombok/src/main/java/io/github/linpeilie/model/DependsSource.java create mode 100644 example/spring-boot-with-lombok/src/main/java/io/github/linpeilie/model/DependsTarget.java diff --git a/docs/.vuepress/config/en/series.ts b/docs/.vuepress/config/en/series.ts index 3eb7c0e..7cd29c1 100644 --- a/docs/.vuepress/config/en/series.ts +++ b/docs/.vuepress/config/en/series.ts @@ -15,6 +15,7 @@ export const series = { { text: 'Map to object', link: '/en/guide/map-to-class' }, { text: 'Conversion for enum', link: '/en/guide/enum-convert' }, { text: 'Class converted with multiple class', link: '/en/guide/multiple-class-convert' }, + { text: 'Cycle Avoiding', link: '/en/guide/cycle-avoiding' }, { text: 'Api', link: '/en/guide/converter-api' }, { text: 'Configuration', link: '/en/guide/configuration' }, { text: 'Faq', link: '/en/guide/faq' }, diff --git a/docs/.vuepress/styles/index.css b/docs/.vuepress/styles/index.css index e69de29..3685647 100644 --- a/docs/.vuepress/styles/index.css +++ b/docs/.vuepress/styles/index.css @@ -0,0 +1,3 @@ +html { + height: 100%; +} \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 527d2dd..c8ee02f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -120,3 +120,18 @@ implementation group: 'io.github.linpeilie', name: 'mapstruct-plus-spring-boot-s - [RuoYi-Vue-Plus](https://gitee.com/JavaLionLi/RuoYi-Vue-Plus/wikis/pages) - [RuoYi-Cloud-Plus](https://gitee.com/JavaLionLi/RuoYi-Cloud-Plus/wikis/pages) +## 联系我 + +> 个人网站:[代码笔耕](https://easii.gitee.io) + +> 微信交流群 + +![微信交流群](http://cos.easii.cn/wechat_20240315192612.jpg) + +> vx : Clue8a796d01 + +![Clue8a796d01](http://cos.easii.cn/20230609091707.webp) + +> 公众号:**代码笔耕** + +![代码笔耕](http://cos.easii.cn/qrcode_for_gh_c207b35e04b8_344.webp) \ No newline at end of file diff --git a/docs/en/guide/class-convert.md b/docs/en/guide/class-convert.md index c8eb8e8..b1daa66 100644 --- a/docs/en/guide/class-convert.md +++ b/docs/en/guide/class-convert.md @@ -146,6 +146,10 @@ public class QuickStartTest { } ``` +When there are multiple methods in a custom type converter, you can also specify the concrete conversion method with `@AutoMapping` 's `qualifiedByName`. +You can refer to [specify conversion methods](#specify-conversion-methods), sections. + + ## Custom Property conversions When there are inconsistent scenarios for attributes in the two classes, such as name, type, and so on, @@ -344,6 +348,113 @@ public class User { } ``` +### specify conversion methods + +::: info +since 1.4.0 + +Note that this feature needs to be used in conjunction with `@AutoMapper` 's `uses`. +::: + +When an attribute needs to define the transformation logic separately and is complex, you can implement the method first, +specifying it by qualifiedByName. + +For example: + +Need film distribution, need according to the different language, change to the corresponding language title. +There are two conversion methods, one is to convert English to French, and the other is to convert French to English: + + +```java +@Component +@Named("TitleTranslator") +public class Titles { + + @Named("EnglishToFrench") + public String translateTitleEF(String title) { + if ("One Hundred Years of Solitude".equals(title)) { + return "Cent ans de solitude"; + } + return "Inconnu et inconnu"; + } + + @Named("FrenchToEnglish") + public String translateTitleFE(String title) { + if ("Cent ans de solitude".equals(title)) { + return "One Hundred Years of Solitude"; + } + return "Unknown"; + } + +} +``` + +The conversion logic is then applied: + +:::: code-group +::: code-group-item EnglishRelease +```java +@Data +@AutoMapper(target = FrenchRelease.class, uses = Titles.class) +public class EnglishRelease { + + @AutoMapping(qualifiedByName = "EnglishToFrench") + private String title; + +} +``` +::: +::: code-group-item FrenchRelease +```java +@Data +@AutoMapper(target = EnglishRelease.class, uses = Titles.class) +public class FrenchRelease { + + @AutoMapping(qualifiedByName = "FrenchToEnglish") + private String title; + +} +``` +::: +:::: + +### Specifies a dependency between fields + +When the transformation logic for attribute A, which depends on attribute B, can specify that attribute a depends on B, +the transformation will first transform B and then transform A. + +Example: + +:::: code-group +::: code-group-item DependsSource +```java +@Data +@AutoMapper(target = DependsTarget.class) +public class DependsSource { + + private String firstName; + private String lastName; + @AutoMapping(dependsOn = {"firstName", "lastName"}) + private String fullName; + +} +``` +::: +::: code-group-item DependsTarget +```java +@Data +public class DependsTarget { + + private String firstName; + private String lastName; + private String fullName; + +} +``` +::: +:::: + + ## Automatically access the custom converter interface ::: info diff --git a/docs/en/guide/cycle-avoiding.md b/docs/en/guide/cycle-avoiding.md new file mode 100644 index 0000000..24f2f54 --- /dev/null +++ b/docs/en/guide/cycle-avoiding.md @@ -0,0 +1,87 @@ +--- +title: cycle avoiding +order: 7 +category: +- Guide +description: MapStructPlus MapStructPlus类循环嵌套 CycleAvoiding +--- + +## Background + +Class loop nesting is when two classes reference each other. +For example, both the source and target object structures contain bidirectional associations between parent and child objects. +When this occurs, direct conversion results in a stack overflow error. + + +Example: + +```java +@Data +public class TreeNode { + private TreeNode parent; + private List children; +} + +@Data +public class TreeNodeDto { + private TreeNodeDto parent; + private List children; +} +``` + +The `parent` attribute can be of other types, possibly spanning a nested loop formed by a longer chain of attributes. + +To accommodate this situation, the **`AutoMapper`** annotation for MapStructPlus adds the **`cycleAvoiding`** attribute, +which is used for identification and whether loop nesting needs to be avoided. +The default is `false`, which needs to be set to `true` if loop nesting is to be avoided. + + +When configured to `true`, a `CycleAvoidingMappingContext` object is passed through the conversion link of the entire object, +temporarily saving the conversion generated object, this type is returned directly to avoid stack overflow problems. +So, when you configure this property to `true`, there is a bit of performance cost, and if there is no loop nesting, +use the default configuration to avoid unnecessary performance cost. + +## Example + +Using the example above, in the `AutoMapper` annotation, configure the `cycleAvoiding` property to be `true`, as follows: + +```java +@Data +@AutoMapper(target = TreeNodeDto.class, cycleAvoiding = true) +public class TreeNode { + private TreeNode parent; + private List children; +} + +@Data +@AutoMapper(target = TreeNode.class, cycleAvoiding = true) +public class TreeNodeDto { + private TreeNodeDto parent; + private List children; +} +``` + +The compile-generated transformation logic is as follows: + +```java +public TreeNodeDto convert(TreeNode arg0, CycleAvoidingMappingContext arg1) { + TreeNodeDto target = arg1.getMappedInstance(arg0, TreeNodeDto.class); + if (target != null) { + return target; + } + + if (arg0 == null) { + return null; + } + + TreeNodeDto treeNodeDto = new TreeNodeDto(); + + arg1.storeMappedInstance(arg0, treeNodeDto); + + treeNodeDto.setParent(demoConvertMapperAdapterForCycleAvoiding.iglm_TreeNodeToTreeNodeDto(arg0.getParent(), arg1)); + treeNodeDto.setChildren( + demoConvertMapperAdapterForCycleAvoiding.iglm_TreeNodeToTreeNodeDto(arg0.getChildren(), arg1)); + + return treeNodeDto; +} +``` \ No newline at end of file diff --git a/docs/guide/class-convert.md b/docs/guide/class-convert.md index efcacab..d809ab3 100644 --- a/docs/guide/class-convert.md +++ b/docs/guide/class-convert.md @@ -142,6 +142,11 @@ public class QuickStartTest { } ``` +当自定的类型转换器中有多个方法时,还可以通过 `@AutoMapping` 的 `qualifiedByName` 来指定具体的转换方法。 +具体可以参考 [指定转换方法](#指定转换方法) 章节。 + +### + ## 自定义属性转换 当两个类中属性存在不一致的场景时,例如名称、类型等不一致,可以进行自定义转换,通过在属性上面添加 `@AutoMapping`,来配置映射规则。 @@ -334,6 +339,107 @@ public class User { } ``` +### 指定转换方法 + +::: info +since 1.4.0 +需要注意的是,该功能需要结合 `@AutoMapper` 的 `uses` 一起使用。 +::: + +当某个属性需要单独定义转换逻辑,并且比较复杂时,可以先实现该方法,通过 `qualifiedByName` 来指定该方法。 + +例如: + +需要电影发行,需要根据不同的语言,修改为相应的语言标题。这里先实现两个转换方法,一个是将英语转为法语,另一个是将法语转换为英语: + +```java +@Component +@Named("TitleTranslator") +public class Titles { + + @Named("EnglishToFrench") + public String translateTitleEF(String title) { + if ("One Hundred Years of Solitude".equals(title)) { + return "Cent ans de solitude"; + } + return "Inconnu et inconnu"; + } + + @Named("FrenchToEnglish") + public String translateTitleFE(String title) { + if ("Cent ans de solitude".equals(title)) { + return "One Hundred Years of Solitude"; + } + return "Unknown"; + } + +} +``` + +接下来应用该转换逻辑: + +:::: code-group +::: code-group-item EnglishRelease +```java +@Data +@AutoMapper(target = FrenchRelease.class, uses = Titles.class) +public class EnglishRelease { + + @AutoMapping(qualifiedByName = "EnglishToFrench") + private String title; + +} +``` +::: +::: code-group-item FrenchRelease +```java +@Data +@AutoMapper(target = EnglishRelease.class, uses = Titles.class) +public class FrenchRelease { + + @AutoMapping(qualifiedByName = "FrenchToEnglish") + private String title; + +} +``` +::: +:::: + +### 指定属性之间的依赖关系 + +当属性 A 的转换逻辑,依赖于属性 B,可以指定 A 依赖 B,则在进行转换时,会先转换 B,然后再转换 A。 + +示例: + +:::: code-group +::: code-group-item DependsSource +```java +@Data +@AutoMapper(target = DependsTarget.class) +public class DependsSource { + + private String firstName; + private String lastName; + @AutoMapping(dependsOn = {"firstName", "lastName"}) + private String fullName; + +} +``` +::: +::: code-group-item DependsTarget +```java +@Data +public class DependsTarget { + + private String firstName; + private String lastName; + private String fullName; + +} +``` +::: +:::: + ## 自动接入自定义转换接口 ::: info @@ -468,7 +574,7 @@ public class CarDtoToCarMapperImpl implements CarDtoToCarMapper { **在该文中,所有提到的源类指通过 `@AutoMapper` 注解的类;目标类指的是 `@AutoMapper` 中 `target` 属性指定的类型。** ::: -前面提到,当在一个类上面添加 `@AutoMapper` 注解时,默认情况下,除了会生成源类到目标类的转换接口,还会生成目标类到源类的转换接口和实现类,这里需要注意的是,默认情况下生成的该转换接口,并没有任何自定义配置,即使在源类中配置了 `@AutoMapping` 注解。 +前面提到,当在一个类上面添加 `@AutoMapper` 注解时,默认情况下,除了会生成**源类到目标类的转换接口**,还会生成**目标类到源类的转换接口和实现类**,这里需要注意的是,**默认情况下生成的该转换接口,并没有任何自定义配置**,即使在源类中配置了 `@AutoMapping` 注解。 这里要实现目标类到源类的自定义转换配置,可以有两种方式: diff --git a/example/spring-boot-with-lombok/src/main/java/io/github/linpeilie/model/DependsSource.java b/example/spring-boot-with-lombok/src/main/java/io/github/linpeilie/model/DependsSource.java new file mode 100644 index 0000000..bf3abfd --- /dev/null +++ b/example/spring-boot-with-lombok/src/main/java/io/github/linpeilie/model/DependsSource.java @@ -0,0 +1,16 @@ +package io.github.linpeilie.model; + +import io.github.linpeilie.annotations.AutoMapper; +import io.github.linpeilie.annotations.AutoMapping; +import lombok.Data; + +@Data +@AutoMapper(target = DependsTarget.class) +public class DependsSource { + + private String firstName; + private String lastName; + @AutoMapping(dependsOn = {"firstName", "lastName"}) + private String fullName; + +} diff --git a/example/spring-boot-with-lombok/src/main/java/io/github/linpeilie/model/DependsTarget.java b/example/spring-boot-with-lombok/src/main/java/io/github/linpeilie/model/DependsTarget.java new file mode 100644 index 0000000..b3fa6cf --- /dev/null +++ b/example/spring-boot-with-lombok/src/main/java/io/github/linpeilie/model/DependsTarget.java @@ -0,0 +1,12 @@ +package io.github.linpeilie.model; + +import lombok.Data; + +@Data +public class DependsTarget { + + private String firstName; + private String lastName; + private String fullName; + +} diff --git a/example/spring-boot-with-lombok/src/main/java/io/github/linpeilie/model/EnglishRelease.java b/example/spring-boot-with-lombok/src/main/java/io/github/linpeilie/model/EnglishRelease.java index 87f5082..104423a 100644 --- a/example/spring-boot-with-lombok/src/main/java/io/github/linpeilie/model/EnglishRelease.java +++ b/example/spring-boot-with-lombok/src/main/java/io/github/linpeilie/model/EnglishRelease.java @@ -2,6 +2,7 @@ package io.github.linpeilie.model; import io.github.linpeilie.annotations.AutoMapper; import io.github.linpeilie.annotations.AutoMapping; +import io.github.linpeilie.annotations.ReverseAutoMapping; import io.github.linpeilie.mapper.Titles; import lombok.Data; @@ -10,6 +11,7 @@ import lombok.Data; public class EnglishRelease { @AutoMapping(qualifiedByName = "EnglishToFrench") + @ReverseAutoMapping(qualifiedByName = "FrenchToEnglish") private String title; } diff --git a/example/spring-boot-with-lombok/src/main/java/io/github/linpeilie/model/FrenchRelease.java b/example/spring-boot-with-lombok/src/main/java/io/github/linpeilie/model/FrenchRelease.java index fdbd4e3..19f2652 100644 --- a/example/spring-boot-with-lombok/src/main/java/io/github/linpeilie/model/FrenchRelease.java +++ b/example/spring-boot-with-lombok/src/main/java/io/github/linpeilie/model/FrenchRelease.java @@ -6,10 +6,8 @@ import io.github.linpeilie.mapper.Titles; import lombok.Data; @Data -@AutoMapper(target = EnglishRelease.class, uses = Titles.class) public class FrenchRelease { - @AutoMapping(qualifiedByName = "FrenchToEnglish") private String title; } diff --git a/mapstruct-plus/src/main/java/io/github/linpeilie/annotations/AutoMapping.java b/mapstruct-plus/src/main/java/io/github/linpeilie/annotations/AutoMapping.java index df82176..1a84fcf 100644 --- a/mapstruct-plus/src/main/java/io/github/linpeilie/annotations/AutoMapping.java +++ b/mapstruct-plus/src/main/java/io/github/linpeilie/annotations/AutoMapping.java @@ -5,6 +5,8 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.mapstruct.Condition; +import org.mapstruct.Named; @Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.CLASS) @@ -40,10 +42,54 @@ public @interface AutoMapping { */ String defaultValue() default ""; + /** + * String-based form of qualifiers; When looking for a suitable mapping method for a given property, MapStruct will + * only consider those methods carrying directly or indirectly (i.e. on the class-level) a {@link Named} annotation + * for each of the specified qualifier names. + *

+ * Note that annotation-based qualifiers are generally preferable as they allow more easily to find references and + * are safe for refactorings, but name-based qualifiers can be a less verbose alternative when requiring a large + * number of qualifiers as no custom annotation types are needed. + *

+ * Note that {@link #defaultValue()} usage will also be converted using this qualifier. + * + * @return One or more qualifier name(s) + * @see Named + * @since 1.4.0 + */ String[] qualifiedByName() default {}; + /** + * String-based form of qualifiers for condition / presence check methods; + * When looking for a suitable presence check method for a given property, MapStruct will + * only consider those methods carrying directly or indirectly (i.e. on the class-level) a {@link Named} annotation + * for each of the specified qualifier names. + * + * This is similar like {@link #qualifiedByName()} but it is only applied for {@link Condition} methods. + *

+ * Note that annotation-based qualifiers are generally preferable as they allow more easily to find references and + * are safe for refactorings, but name-based qualifiers can be a less verbose alternative when requiring a large + * number of qualifiers as no custom annotation types are needed. + *

+ * + * + * @return One or more qualifier name(s) + * @see #qualifiedByName() + * @see Named + * @since 1.4.0 + */ String[] conditionQualifiedByName() default {}; + /** + * One or more properties of the result type on which the mapped property depends. The generated method + * implementation will invoke the setters of the result type ordered so that the given dependency relationship(s) + * are satisfied. Useful in case one property setter depends on the state of another property of the result type. + *

+ * An error will be raised in case a cycle in the dependency relationships is detected. + * + * @return the dependencies of the mapped property + * @since 1.4.0 + */ String[] dependsOn() default {}; } diff --git a/mapstruct-plus/src/main/java/io/github/linpeilie/annotations/ReverseAutoMapping.java b/mapstruct-plus/src/main/java/io/github/linpeilie/annotations/ReverseAutoMapping.java index 17cd299..f913226 100644 --- a/mapstruct-plus/src/main/java/io/github/linpeilie/annotations/ReverseAutoMapping.java +++ b/mapstruct-plus/src/main/java/io/github/linpeilie/annotations/ReverseAutoMapping.java @@ -5,6 +5,8 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.mapstruct.Condition; +import org.mapstruct.Named; /** * 由目标类生成当前类的配置 @@ -67,10 +69,54 @@ public @interface ReverseAutoMapping { */ String defaultValue() default ""; + /** + * String-based form of qualifiers; When looking for a suitable mapping method for a given property, MapStruct will + * only consider those methods carrying directly or indirectly (i.e. on the class-level) a {@link Named} annotation + * for each of the specified qualifier names. + *

+ * Note that annotation-based qualifiers are generally preferable as they allow more easily to find references and + * are safe for refactorings, but name-based qualifiers can be a less verbose alternative when requiring a large + * number of qualifiers as no custom annotation types are needed. + *

+ * Note that {@link #defaultValue()} usage will also be converted using this qualifier. + * + * @return One or more qualifier name(s) + * @see Named + * @since 1.4.0 + */ String[] qualifiedByName() default {}; + /** + * String-based form of qualifiers for condition / presence check methods; + * When looking for a suitable presence check method for a given property, MapStruct will + * only consider those methods carrying directly or indirectly (i.e. on the class-level) a {@link Named} annotation + * for each of the specified qualifier names. + * + * This is similar like {@link #qualifiedByName()} but it is only applied for {@link Condition} methods. + *

+ * Note that annotation-based qualifiers are generally preferable as they allow more easily to find references and + * are safe for refactorings, but name-based qualifiers can be a less verbose alternative when requiring a large + * number of qualifiers as no custom annotation types are needed. + *

+ * + * + * @return One or more qualifier name(s) + * @see #qualifiedByName() + * @see Named + * @since 1.4.0 + */ String[] conditionQualifiedByName() default {}; + /** + * One or more properties of the result type on which the mapped property depends. The generated method + * implementation will invoke the setters of the result type ordered so that the given dependency relationship(s) + * are satisfied. Useful in case one property setter depends on the state of another property of the result type. + *

+ * An error will be raised in case a cycle in the dependency relationships is detected. + * + * @return the dependencies of the mapped property + * @since 1.4.0 + */ String[] dependsOn() default {}; } -- Gitee