64

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?

Karim Tawfik
  • 1,286
  • 1
  • 11
  • 21

8 Answers8

49

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);

}
thunderhook
  • 497
  • 5
  • 21
Bob
  • 585
  • 1
  • 5
  • 9
  • I confirm that this solution is also working with Spring, using Mapstruct 1.1.0.Final – Anthony Richir Oct 04 '18 at 12:52
  • 16
    Thanks. Sucks that you can't use constructor injection though. – Josh M. Aug 09 '19 at 17:31
  • Does this work in Unit Test without loading spring context? – Łukasz Rzeszotarski Nov 06 '19 at 12:06
  • How can I inject MyService in constructor using this solution? – christopher.online Mar 13 '21 at 12:55
  • 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()))"). – Nick G. Dec 16 '21 at 18:49
  • 2
    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. – Wes Grant Aug 12 '22 at 20:38
37

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.

Gunnar
  • 18,095
  • 1
  • 53
  • 73
  • 7
    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 autowired – Brett Y Oct 24 '16 at 21:47
  • 5
    Ah, that's an interesting point if you *only* want to use it in an expression. Could you open an issue in [our tracker](https://github.com/mapstruct/mapstruct/issues)? Thanks! – Gunnar Oct 25 '16 at 12:02
  • for 'uses=' usually add other mapper, I suggest change interface to abstract class, as Bob answered bellow – Omid Ashouri Nov 11 '20 at 07:41
  • 1
    The issue was created as [mapstruct#938](https://github.com/mapstruct/mapstruct/issues/938). It was closed with a suggestion to use `@Context` and a handwritten method. – M. Justin Jan 06 '21 at 18:32
  • 1
    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))")"``` – Nashev May 10 '21 at 08:17
29

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.

Sjaak
  • 3,602
  • 17
  • 29
  • 1
    I would have loved this solution. Sadly this doesn't work when using a builder (version 1.3.0.Beta2) because a setter is needed – thunderhook Jan 09 '19 at 12:19
  • @thunderhook: could you check whether you can pass the builder as `@MappingTarget` ? – Sjaak Jan 12 '19 at 09:56
  • 1
    that's an excellent suggestion. I tried it and it works like a charm. The ``map()`` is getting called before the builders ``build()`` method. – thunderhook Jan 13 '19 at 21:01
  • Does this work in Unit Test without loading spring context? – Łukasz Rzeszotarski Nov 06 '19 at 12:07
  • 3
    doesn't work with mapstruct 1.3.1.Final. '@AferMapping' method with '@Context' MyMapper myMapper has been ignored – advortsov Jul 09 '20 at 10:11
  • Did you add a context to the calling method as well? – Sjaak Jul 26 '20 at 22:10
  • 1
    doesn't work with mapstruct 1.4.1.Final. '@AferMapping' method with '@Context' MyMapper myMapper has been ignored – Omid Ashouri Nov 14 '20 at 07:27
  • 1
    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. – Sjaak Nov 21 '20 at 09:55
28

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, 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
@Mapper
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);

}
daniu
  • 14,137
  • 4
  • 32
  • 53
Cmyker
  • 2,318
  • 1
  • 26
  • 29
23

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.

Jim Cox
  • 974
  • 7
  • 10
  • 2
    This is the best answer in my opinion – bluelurker Nov 25 '20 at 14:53
  • This implementation seems to override the mapping for every field, I believe the original question was regarding one specific field. – Manuel Pap Dec 07 '20 at 09:38
  • 1
    @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. – Priidu Neemre Jan 13 '21 at 08:58
  • Absolutely lovely solution, way more elegant than any other. – Christophe Schutz Mar 09 '21 at 14:55
  • 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. – Federico Cristina Jan 14 '22 at 13:39
  • @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? – Simone Colnaghi May 12 '22 at 14:36
  • 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 appreciated – DockYard Aug 22 '22 at 15:24
  • 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. – Jim Cox Aug 24 '22 at 15:12
  • MapStruct 1.5.4 This solution fails to work with 1.5.4 - generated MapperImpl is just an empty extension of the decorator class – Markus Fried Apr 20 '23 at 09:43
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;
Arsalan Siddiqui
  • 181
  • 2
  • 11
1

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

@Mapper(
    uses = {
        //Other mappings..
    },
    componentModel = MappingConstants.ComponentModel.SPRING)
Kobynet
  • 983
  • 11
  • 23
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.

Anibal Anto
  • 99
  • 1
  • 2