Mapstruct - How can I inject a spring dependency in the Generated Mapper class
Asked Answered
B

10

72

I need to inject a spring service class in the generated mapper implementation, so that I can use it via

   @Mapping(target="x", expression="java(myservice.findById(id))")"

Is this applicable in Mapstruct-1.0?

Burrows answered 6/8, 2016 at 18:18 Comment(0)
E
45

It should be possible if you declare Spring as the component model and add a reference to the type of myservice:

@Mapper(componentModel="spring", uses=MyService.class)
public interface MyMapper { ... }

That mechanism is meant for providing access to other mapping methods to be called by generated code, but you should be able to use them in the expression that way, too. Just make sure you use the correct name of the generated field with the service reference.

Electorate answered 9/8, 2016 at 6:43 Comment(5)
I have the same issue and it appears that the classes declared with 'uses' will only be autowired if one of their methods is used as a source->target mapping, so if their only use is in an expression they will not get autowiredLienlienhard
Ah, that's an interesting point if you only want to use it in an expression. Could you open an issue in our tracker? Thanks!Electorate
for 'uses=' usually add other mapper, I suggest change interface to abstract class, as Bob answered bellowAngeliqueangelis
The issue was created as mapstruct#938. It was closed with a suggestion to use @Context and a handwritten method.Gherlein
Just for note: this uses works for me with a mapping like @Mapping(target="x", source="id") instead of @Mapping(target="x", expression="java(myservice.findById(id))")"Frisse
B
58

As commented by brettanomyces, the service won't be injected if it is not used in mapping operations other than expressions.

The only way I found to this is :

  • Transform my mapper interface into an abstract class
  • Inject the service in the abstract class
  • Make it protected so the "implementation" of the abstract class has access

I'm using CDI but it should be the samel with Spring :

@Mapper(
        unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE,
        componentModel = "spring",
        uses = {
            // My other mappers...
        })
public abstract class MyMapper {

    @Autowired
    protected MyService myService;

    @Mappings({
        @Mapping(target="x", expression="java(myservice.findById(obj.getId())))")
    })
    public abstract Dto myMappingMethod(Object obj);

}
Beacham answered 11/4, 2017 at 10:30 Comment(6)
I confirm that this solution is also working with Spring, using Mapstruct 1.1.0.FinalMcnew
Thanks. Sucks that you can't use constructor injection though.Staffman
Does this work in Unit Test without loading spring context?Exist
How can I inject MyService in constructor using this solution?Burchette
So far that's the most elegant solution, thank you for posting! Can it be extended to map the entity itself somehow? I.e. if Dto.id field is set, it will fetch the entity from the database? Sort of @Mapping(target="this", expression="java(myotherservice.getById(obj.getId()))").Epimenides
This may be obvious but it stumped me for a few minutes. In order for this to work in Spring, you must also inject the mapper in your service instead of using the INSTANCE mapper class.Voroshilovgrad
E
45

It should be possible if you declare Spring as the component model and add a reference to the type of myservice:

@Mapper(componentModel="spring", uses=MyService.class)
public interface MyMapper { ... }

That mechanism is meant for providing access to other mapping methods to be called by generated code, but you should be able to use them in the expression that way, too. Just make sure you use the correct name of the generated field with the service reference.

Electorate answered 9/8, 2016 at 6:43 Comment(5)
I have the same issue and it appears that the classes declared with 'uses' will only be autowired if one of their methods is used as a source->target mapping, so if their only use is in an expression they will not get autowiredLienlienhard
Ah, that's an interesting point if you only want to use it in an expression. Could you open an issue in our tracker? Thanks!Electorate
for 'uses=' usually add other mapper, I suggest change interface to abstract class, as Bob answered bellowAngeliqueangelis
The issue was created as mapstruct#938. It was closed with a suggestion to use @Context and a handwritten method.Gherlein
Just for note: this uses works for me with a mapping like @Mapping(target="x", source="id") instead of @Mapping(target="x", expression="java(myservice.findById(id))")"Frisse
A
40

What's worth to add in addition to the answers above is that there is more clean way to use spring service in mapstruct mapper, that fits more into "separation of concerns" design concept that avoids mixing mappers and spring beans, called "qualifier". Easy re-usability in other mappers as a bonus. For sake of simplicity I prefer named qualifier as noted here http://mapstruct.org/documentation/stable/reference/html/#selection-based-on-qualifiers Example would be:

import org.mapstruct.Mapper;
import org.mapstruct.Named;
import org.springframework.stereotype.Component;

@Component
public class EventTimeQualifier {

    private EventTimeFactory eventTimeFactory; // ---> this is the service you want yo use

    public EventTimeQualifier(EventTimeFactory eventTimeFactory) {
        this.eventTimeFactory = eventTimeFactory;
    }

    @Named("stringToEventTime")
    public EventTime stringToEventTime(String time) {
        return eventTimeFactory.fromString(time);
    }

}

This is how you use it in your mapper:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper(componentModel = "spring", uses = EventTimeQualifier.class)
public interface EventMapper {

    @Mapping(source = "checkpointTime", target = "eventTime", qualifiedByName = "stringToEventTime")
    Event map(EventDTO eventDTO);

}
Auk answered 22/11, 2018 at 11:28 Comment(5)
How to use the EventTimeQualifier in aftermapping?Passible
Autowire as argument maybe?Auk
The @Mapper annotation at the EventTimeQualifier does not seem to be necessary. It worked for me without it because of the componentModel="spring".Frimaire
Thank you for pointing it out, it was written against mapstruct 1.2 and it was not working without, makes sense to take the @mapper out nowAuk
The best solution in my opinion.Igbo
C
32

Since 1.2 this can be solved with a combination of @AfterMapping and @Context.. Like this:

@Mapper(componentModel="spring")
public interface MyMapper { 

   @Mapping(target="x",ignore = true)
   // other mappings
   Target map( Source source, @Context MyService service);

   @AfterMapping
   default void map( @MappingTarget Target.X target, Source.ID source, @Context MyService service) {
        target.set( service.findById( source.getId() ) );
   }
 }

The service can be passed as context.

A nicer solution would be to use an @Context class which wrap MyService in stead of passing MyService directly. An @AfterMapping method can be implemented on this "context" class: void map( @MappingTarget Target.X target, Source.ID source ) keeping the mapping logic clear of lookup logic. Checkout this example in the MapStruct example repository.

Cindiecindra answered 11/7, 2018 at 19:27 Comment(8)
I would have loved this solution. Sadly this doesn't work when using a builder (version 1.3.0.Beta2) because a setter is neededVeronicaveronika
@thunderhook: could you check whether you can pass the builder as @MappingTarget ?Cindiecindra
that's an excellent suggestion. I tried it and it works like a charm. The map() is getting called before the builders build() method.Veronicaveronika
Does this work in Unit Test without loading spring context?Exist
doesn't work with mapstruct 1.3.1.Final. '@AferMapping' method with '@Context' MyMapper myMapper has been ignoredCeltic
Did you add a context to the calling method as well?Cindiecindra
doesn't work with mapstruct 1.4.1.Final. '@AferMapping' method with '@Context' MyMapper myMapper has been ignoredAngeliqueangelis
It's a bit difficult to reply to "it doesn't work" without knowing what does not work. Please checkout the example provided in the link. It still works in 1.4.1.Final. If there's an issue in MapStruct, please raise an issue.Cindiecindra
D
24

I am using Mapstruct 1.3.1 and I have found this problem is easy to solve using a decorator.

Example:

@Mapper(unmappedTargetPolicy = org.mapstruct.ReportingPolicy.IGNORE,
 componentModel = "spring")
@DecoratedWith(FooMapperDecorator.class)
public interface FooMapper {

    FooDTO map(Foo foo);
}
public abstract class FooMapperDecorator implements FooMapper{

    @Autowired
    @Qualifier("delegate")
    private FooMapper delegate;

    @Autowired
    private MyBean myBean;

    @Override
    public FooDTO map(Foo foo) {

        FooDTO fooDTO = delegate.map(foo);

        fooDTO.setBar(myBean.getBar(foo.getBarId());

        return fooDTO;
    }
}

Mapstruct will generate 2 classes and mark the FooMapper that extends FooMapperDecorator as the @Primary bean.

Dimorph answered 13/2, 2020 at 21:51 Comment(9)
This is the best answer in my opinionHolzman
This implementation seems to override the mapping for every field, I believe the original question was regarding one specific field.Sulfamerazine
@ManolisPap: not really, read the documentation for @DecoratedWith carefully. You inject the generated mapper (i.e. delegate) and apply it first. Then, perform your customizations in the decorator method body.Lucillelucina
Absolutely lovely solution, way more elegant than any other.Eutherian
Great approach. Worked perfectly. I had to add @Primary to the Interface in order to avoid the following error: Field mapper in (someClass) required a single bean, but 2 were found.Circumjacent
@Jim Cox I have the MyBean Class marked as @Service. In the FooMapperDecorator the @Autowired of that is not working. The myBean is null :( Does anyone know why?Nominal
This does not work me for some reason. In my mapstruct generated impl class, "MyBean myBean" is not present at all and only "FooMapper delegate" is present. Any idea what could be wrong with this. I love this solution and would be great if I could implement this. Any help would be appreciatedAnisaanise
Make sure that you have configured the mapstruct-processor; if you're using maven that is done in the maven-compiler-plugin. Also, if you are using an IDE, I have found that on occasion the IDE will NOT rebuild the mappers after a coding change and that I need to clean the project before seeing my changes.Dimorph
MapStruct 1.5.4 This solution fails to work with 1.5.4 - generated MapperImpl is just an empty extension of the decorator classCarbazole
S
2

I went through all the responses in this question but was not able to get things working. I dug a little further and was able to solve this very easily. All you need to do is to make sure that:

  1. Your componentModel is set as "spring"
  2. You are using an abstract class for your mapper.
  3. Define a named method where you will use your injected bean (in the example, its appProperties getting used inside the mapSource method)
@Mapper(componentModel = "spring") 
public abstract class MyMapper {

   @Autowired
   protected AppProperties appProperties;

   @Mapping(target = "account", source = "request.account")
   @Mapping(target = "departmentId", source = "request.departmentId")
   @Mapping(target = "source", source = ".", qualifiedByName = "mapSource")
   public abstract MyDestinationClass getDestinationClass(MySourceClass request);

   @Named("mapSource")
   String mapSource(MySourceClass request) {
      return appProperties.getSource();
   } }

Also, remember, that your mapper is now a spring bean. You will need to inject it your calling class as follows:

private final MyMapper myMapper;
Streetwalker answered 15/3, 2023 at 22:1 Comment(0)
K
2

Since mapstruct 1.5.0 you can use a constant for spring componentmodel generation

@Mapper(
    uses = {
        //Other mappings..
    },
    componentModel = MappingConstants.ComponentModel.SPRING)
Khartoum answered 3/4, 2023 at 7:30 Comment(1)
Or you can set it in your pom e never chance your code.Interlanguage
M
0

I can't use componentModel="spring" because I work in a large project that doesn't use it. Many mappers includes my mapper with Mappers.getMapper(FamilyBasePersonMapper.class), this instance is not the Spring bean and the @Autowired field in my mapper is null.

I can't modifiy all mappers that use my mapper. And I can't use particular constructor with the injections or the Spring's @Autowired dependency injection.

The solution that I found: Using a Spring bean instance without using Spring directly:

Here is the Spring Component that regist itself first instance (the Spring instance):

@Component
@Mapper
public class PermamentAddressMapper {
    @Autowired
    private TypeAddressRepository typeRepository;

    @Autowired
    private PersonAddressRepository personAddressRepository;

    static protected PermamentAddressMapper FIRST_INSTANCE;

    public PermamentAddressMapper() {
        if(FIRST_INSTANCE == null) {
            FIRST_INSTANCE = this;
        }
    }

    public static PermamentAddressMapper getFirstInstance(){
        return FIRST_INSTANCE;
    }

    public static AddressDTO idPersonToPermamentAddress(Integer idPerson) {
        //...
    }

    //...

}

Here is the Mapper that use the Spring Bean accross getFirstInstance method:

@Mapper(uses = { NationalityMapper.class, CountryMapper.class, DocumentTypeMapper.class })
public interface FamilyBasePersonMapper {

    static FamilyBasePersonMapper INSTANCE = Mappers.getMapper(FamilyBasePersonMapper.class);

    @Named("idPersonToPermamentAddress")
    default AddressDTO idPersonToPermamentAddress(Integer idPerson) {
        return PermamentAddressMapper.getFirstInstance()
            .idPersonToPermamentAddress(idPersona);
    }

    @Mapping(
        source = "idPerson",
        target="permamentAddres", 
        qualifiedByName="idPersonToPermamentAddress" )
    @Mapping(
        source = "idPerson",
        target = "idPerson")
    FamilyDTO toFamily(PersonBase person);

   //...

Maybe this is not the best solution. But it has helped to decrement the impact of changes in the final resolution.

Mate answered 8/8, 2022 at 23:4 Comment(1)
any way to do it with Constructor injection?Methylene
S
0

in my case my mapper was no importing List.class in the autogenerated file so I added (imports = List.class), ie:

@Mapper(imports = List.class)
public interface MyMapper {
  [...]
}
Slideaction answered 26/1 at 11:19 Comment(0)
A
0

It's very simple:

@Mapper(componentModel = "spring")
public abstract class SimpleMapper {

   @Autowired
   protected Myservice service;
}

The most important is:

  • SimpleMapper must be scaned by scanBasePackages.
  • Do NOT use Mappers.getMapper(SimpleMapper.class), use normal field injection or constructor injection while using SimpleMapper.

Ref: mapstruct

Acquirement answered 2/4 at 8:9 Comment(1)
This is Bobs answer, answered Apr 11, 2017 at 10:30...Harvest

© 2022 - 2024 — McMap. All rights reserved.