linpeilie / mapstruct-plus

MapStruct Plus is an enhancement to the MapStruct framework. It can automatically generate the transformation operation between two classes through an annotation, omitting the operation of defining the interface of MapStruct, makes Java type conversion easy and elegant.
https://mapstruct.plus/
Apache License 2.0
274 stars 22 forks source link

兼容不可变对象 #33

Closed babyfish-ct closed 1 year ago

babyfish-ct commented 1 year ago

AutoMappergenerator类,存在如下两个行为

builder.addMethod(addConvertMethodSpec(Collections.singletonList(source), metadata.getFieldMappingList(),
     metadata.getTargetClassName())); //没问题
builder.addMethod(addConvertMethodSpec(Arrays.asList(source, target), metadata.getFieldMappingList(),
    metadata.getTargetClassName())); //有可能有问题

第一个行为生成无@MappingTarget的版本,没有任何问题 第二个行为生成带@MappingTarget的版本,如果相关类采用不可变对象设计,会导致问题问题。

不可变对象有多种,比如

建议支持一个开关,给用户一个关闭第二个行为的机会。

linpeilie commented 1 year ago

MappingTarget目的是为了不生成新的对象,而直接给现有的对象属性赋值,不可变对象会有影响吗。

方便举个例子看一下么

babyfish-ct commented 1 year ago

是的,@MappingTarget的目的是为了修改已有对象。

但问题是,对象可能是不可变的,比如Java语言的record类型。

babyfish-ct commented 1 year ago

也就是说,@MappingTarget本身,就是在假设用户的的Target类型一定是传统get/set风格的POJO,再假设数据是可以直接修改的。

sunshio commented 1 year ago

建议在@AutoMapper注解中加一个选项,用来控制是否是否生成@MappingTarget的版本,这个问题是我在整合jimmer和mapstruct-plus的时候发现的

linpeilie commented 1 year ago

我考虑下实现方案。

如果这个对象不可变,第一个方法不会报错么

sunshio commented 1 year ago

上面说错了,是@AutoMapping注解,是的第一个方法是生成一个新的对象,不会报错

sunshio commented 1 year ago

不可变对象,比如String,一旦被“修改”就会产生新的对象

linpeilie commented 1 year ago

就是可以忽略生成带MappingTarget的方法对吧。我考虑下如何做

babyfish-ct commented 1 year ago

第一个方法不报错的原因这样的。MapStruct本身靠build模式支持不可变对象,并且它通过此方法内置了5种Java领域的不可变对象方案。Jimmer的不可变对象远比它内置的这些强大,自然不可能用MapStruct内置的这些简单不可变对象框架,所以定义了一套全新的不可变对象技术体系,并且利用MapStruct留下的annotation processor扩展点教会MapStruct如何处理这种全新的对象风格。

即,无论非传统non-pojo-style对象如何设计,都可以扩展mapstruct让第一个方法能正确工作。但是,不可变对象无法用@MappingTarget模式是谁也无法改变的。

babyfish-ct commented 1 year ago

是的,不生成MappingTarget版本即可。

比如,给@AutoMapping一个boolean immutable() default false

linpeilie commented 1 year ago

方便提供一个测试的类么

babyfish-ct commented 1 year ago

虽然此问题是因不可变对象而起,但不需要用不可变对象来测试和验证这个新加的功能。

就mapstructplus首页那个入门介绍

@AutoMapper(target = UserDto.class)
public class User {
    // ...
}

@AutoMapper加一个boolean targetImmutable() default false 上述代码即可改成

@AutoMapper(target = UserDto.class, targetImmutable = true)
public class User {
    // ...
}

保证生成的MapStruct Mapper没有@MappingTarget UserDto existingUserDto即可。

如果UserDto真的是不可变类型,无论是mapstruct内置的行为,还是用户对mapstruct的扩展,都能正确生成正常转化逻辑, 那是mapstruct内置行为或用户扩展行为的责任。mapstructplus本身只需要保证targetImmutable = true时不生成含MappingTarget参数的方法即可

linpeilie commented 1 year ago

最终生成的方法,直接返回 target,这样子没问题吧,就不再尝试修改了

babyfish-ct commented 1 year ago

你是说类似这样吗?

default void assignTo(S source, @MappingTarget TImmutable target) {
    // Donithing
}

给了默认实现,所以MapStruct不会去研究怎么生成代码,自然不会报错。但总感觉怪怪的,没用的东西,就不要放到这里了。

仅留下

TImmutable convert(S source)

让mapstruct或基于它的扩展去生成代码即可

linpeilie commented 1 year ago
default Timutable assignTo(S source, @MappingTarget Timutable target) {
    return target;
}

应该是这个样子的,父接口不能动,所以这个方法删不掉

sunshio commented 1 year ago

这样子是不行的,因为这个问题会在编译是时候报错,编译根本通不过,必须有不带@MappingTarget的版本

sunshio commented 1 year ago

https://gitee.com/mayihua/msp-jimmer

sunshio commented 1 year ago

1686984971566 1686984935661

sunshio commented 1 year ago

image

linpeilie commented 1 year ago

这个项目访问不了。 另外,你发的这个截图,与不可变对象没啥关系吧,只是属性没有对应上

linpeilie commented 1 year ago

把这几个类的代码发一下吧,我看一下

sunshio commented 1 year ago

我代码仓库上面已经贴了:https://gitee.com/mayihua/msp-jimmer

sunshio commented 1 year ago

gitee权限已修改

sunshio commented 1 year ago

你上面的做法好像是可以的,给了默认实现,ms编译就不报错了

linpeilie commented 1 year ago

看了下这个仓库的代码,Book 类是接口么,如果是接口,BookInput怎样转成Book的呢

sunshio commented 1 year ago

这个项目访问不了。 另外,你发的这个截图,与不可变对象没啥关系吧,只是属性没有对应上

你先看下我的代码,你现在对jimmer的不可变对象还没什么概念

babyfish-ct commented 1 year ago

关于应该是这个样子的,父接口不能动,所以这个方法删不掉

提供两个父接口即可

interace ImmutableBaseMapper<S, T> {
    T convert(S source);
}

interface BaseMapper<S, T> extends ImmutableBaseMapper<S, T> {
    void assignTo(S source, @MappingTarget target);
}

根据@AutoMapper上targetImmutable是否为true,决定选用哪个基接口即可。

linpeilie commented 1 year ago

我看了下你的用法, 其实没有那么复杂, 直接判断target是接口的话,直接不处理,你目前的用法,都不用修改配置项

sunshio commented 1 year ago

看了下这个仓库的代码,Book 类是接口么,如果是接口,BookInput怎样转成Book的呢

是jimmer用ms的扩展点以builder方式实现了bookInput->book,book接口可以像普通类一样用ms

linpeilie commented 1 year ago

没有判断是否接口的API,我考虑下实现

sunshio commented 1 year ago

我看了下你的用法, 其实没有那么复杂, 直接判断target是接口的话,直接不处理,你目前的用法,都不用修改配置项

babyfish的意思是支持“不可变对象”,而不是仅仅解决jimmer这一个问题(java领域还有其他不可变对象),所以他想预留针对不可变对象的接口方法

babyfish-ct commented 1 year ago

image

这些是MapStruct内置的不可变对象支持(Lombok可以生成不可变对象),当然,MapStruct文档列举的只是纯Java的,如果算上Kotlin的,至少十几个不可变对象相关的框架(但,无论多少种,都可以扩展mapstruct予以支持)。

因Jimmer的需求非常强大,整个行业找不到任何不可变框架能满足,所以Jimmer选择创建一套全新的不可变对方方案:为JVM移植immerjs。在真个过程中,Jimmer应自身特点,不可变对象让用户声明为接口(Java下annotaton processor, Kotlin下KSP去实现这些接口,但用户感知不到)

但,这不代表,Java下所有不可变框架的都是定义接口的,很多框架还是定义class而非interface。至于不可变遍对象框架有多少种、用户会选用什么,你完全无法预料。

所以,你需要支持一个普适性的特征:只要用户表明target type是immutable的,就不得能生成带@MappingTarget参数的方法。至于用户采用何种不可变对象技术方案,不重要,而且太多了,你相关管也管不过来。

linpeilie commented 1 year ago

懂你的意思,我测试了下你提供的代码。 并没有因为不可变对象而报错,例如BookInputToBook,编译生成的转换为:

    @Override
    public Book convert(BookInput arg0) {
        if ( arg0 == null ) {
            return null;
        }

        BookDraft.MapStruct book = new BookDraft.MapStruct();

        book.id( arg0.getId() );

        return book.build();
    }

    @Override
    public Book convert(BookInput arg0, Book arg1) {
        if ( arg0 == null ) {
            return arg1;
        }

        return arg1;
    }

没有找到赋值的方法,看起来并没有啥问题

babyfish-ct commented 1 year ago

就是这段

BookDraft.MapStruct book = new BookDraft.MapStruct();
book.id( arg0.getId() );
return book.build();

很明显,一个笨重的builder模式,mapstruct只支持builder拓展,jimmer只能选择如此用如此不雅的方式拓展它。

linpeilie commented 1 year ago

我们的问题不是第二个方法么,我现在都不清楚为啥编译会有问题了 😶‍🌫️

babyfish-ct commented 1 year ago

我看你说没找赋值语句,理解错了

sunshio commented 1 year ago

我们的问题不是第二个方法么,我现在都不清楚为啥编译会有问题了 😶‍🌫️ image

编译问题是ms报的,因为第二个方法没有提供默认实现的情况下,ms就会自己实现,但ms自己确实就找不到store.id这个属性所以编译报错

为什么jimmer不提供第二个方法的实现?因为不可变对象无法提供,即便提供了,也不符合@MappingTarget的语义

sunshio commented 1 year ago

懂你的意思,我测试了下你提供的代码。 并没有因为不可变对象而报错,例如BookInputToBook,编译生成的转换为:

    @Override
    public Book convert(BookInput arg0) {
        if ( arg0 == null ) {
            return null;
        }

        BookDraft.MapStruct book = new BookDraft.MapStruct();

        book.id( arg0.getId() );

        return book.build();
    }

    @Override
    public Book convert(BookInput arg0, Book arg1) {
        if ( arg0 == null ) {
            return arg1;
        }

        return arg1;
    }

没有找到赋值的方法,看起来并没有啥问题

没报错?你可能要拉一下我gitee最新的代码,主要是加了

image