Mapping an object to an immutable object with builder (using immutables annotation processor) in mapstruct

别说谁变了你拦得住时间么 提交于 2019-12-10 03:36:28

问题


We are using the immutables framework to generate all DTOs. Now we would like to map these objects one to another with mapstruct. But the generated DTOs are immutable and have no setters and no constructor, corresponding to the builder pattern. They are only filled through the corresponding builder accessed by a static builder()-method.

We instead tried to map DTO1 to DTO2.Builder which would work if mapstruct would recognize the setter in the Builder but these do not have void return type but return the Builder itself for fluent concatenation.

So here is the code of the example.

We have two Interfaces

@Value.Immutable
public interface MammalDto {
  public Integer getNumberOfLegs();
  public Long getNumberOfStomachs();
}

and

@Value.Immutable
public interface MammalEntity {
  public Long getNumberOfLegs();
  public Long getNumberOfStomachs();
}

Then we have the Mapper interface for mapstruct:

@Mapper(uses = ObjectFactory.class)
public interface SourceTargetMapper {
  SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );

  ImmutableMammalEntity.Builder toTarget(MammalDto source);
}

For mapstruct to find the Builder we need a Factory:

public class ObjectFactory {

  public ImmutableMammalDto.Builder createMammalDto() {
    return ImmutableMammalDto.builder();
  }

  public ImmutableMammalEntity.Builder createMammalEntity() {
    return ImmutableMammalEntity.builder();
  }
}

In order to generate the code the compiler plugin was instructed to use both annotation processors:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.6.1</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <annotationProcessorPaths>
            <path>
                <groupId>org.immutables</groupId>
                <artifactId>value</artifactId>
                <version>2.2.8</version>
            </path>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>1.2.0.Beta3</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

Note: This will work only with mapstruct version > 1.2.x. Older versions have the problem in a clean build (mvn clean compile) that they do not find the sources that immutables just built. In a second build (without clean) they would find the immutables implementations because they were on the classpath before annotation processors were run. This bug is fixed now.

This works like a charm. First the Immutable implementations of the interfactes are generated and mapstruct uses them to generate the builder.

But the Test shows that no properties are set:

@Test
public void test() {
  MammalDto s = ImmutableMammalDto.builder().numberOfLegs(4).numberOfStomachs(3l).build();
  MammalEntity t = SourceTargetMapper.MAPPER.toTarget(s).build();
    assertThat(t.getNumberOfLegs()).isEqualTo(4);
    assertThat(t.getNumberOfStomachs()).isEqualTo(3);
}

The asserts fail. One look at the mapper generated by mapstruct shows that it has obviously not found any setters:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    //...
)
public class SourceTargetMapperImpl implements SourceTargetMapper {
    private final ObjectFactory objectFactory = new ObjectFactory();

    @Override
    public Builder toTarget(MammalDto source) {
        if ( source == null ) {
            return null;
        }

        Builder builder = objectFactory.createMammalEntity();
        return builder;
    }
}

The empty builder is returned. I think the reason is the setter implementation of the generated builder because it returns itself to create a fluent API:

public final Builder numberOfLegs(Long numberOfLegs) {
  this.numberOfLegs = Objects.requireNonNull(numberOfLegs, "numberOfLegs");
  return this;
}

Is there a way to let mapstruct find these setters? Or even a better way to deal with such immutable objects with builders?

EDIT: As I stated in the comment I ran into Issue #782. In version 1.2.0.Beta3 builders are still not supported. But there are several discussions on this topic so it might be interesting to follow the issue if one has the same problem.


回答1:


Since 1.3 MapStruct supports Immutables. Look here for more details.




回答2:


We had same issue on our project. As workaround we've been using Modifiable implementation of our immutable dto.

You can also try it. It's better that direct usage of builders and object factories.

@Value.Modifiable generates implementation with setters.

@Value.Style(create = "new") generates public no args constructor.

@Value.Immutable
@Value.Modifiable
@Value.Style(create = "new")
public interface MammalEntity {
    public Long getNumberOfLegs();
    public Long getNumberOfStomachs();
}

Then your mapper will be simpler, no need in object factory.

@Mapper
public interface SourceTargetMapper {

  ModifiableMammalEntity toTarget(MammalDto source);
}

In this case MapStruct can see setters in ModifiableMammalEntity

Usage of such mapper will looks like

// Here you don't need to worry about implementation of MammalEntity is. The interface `MammalEntity` is immutable.
MammalEntity mammalEntity = sourceTargetMapper.toTarget(source);



回答3:


You can configure Immutables to generate setters in the builder:

@Value.Immutable
@Value.Style(init = "set*")
public interface MammalEntity {
    public Long getNumberOfLegs();
    public Long getNumberOfStomachs();
}

And you don't need the ObjectBuilder, you can directly use the generated Immutable class

@Mapper(uses = ImmutableMammalEntity.class)
public interface SourceTargetMapper {
    SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );

    ImmutableMammalEntity.Builder toTarget(MammalDto source);
}

You can even define these settings in your own annotation

@Value.Style(init = "set*")
public @interface SharedData {}

and use that instead

@SharedData
@Value.Immutable
public interface MammalEntity {
    public Long getNumberOfLegs();
    public Long getNumberOfStomachs();
}


来源:https://stackoverflow.com/questions/38770813/mapping-an-object-to-an-immutable-object-with-builder-using-immutables-annotati

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!