Karim Tawfik
Karim Tawfik

Reputation: 1506

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

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?

Upvotes: 78

Views: 151683

Answers (10)

Bob
Bob

Reputation: 745

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

}

Upvotes: 64

Cmyker
Cmyker

Reputation: 2568

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

}

Upvotes: 45

Samt
Samt

Reputation: 204

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

Upvotes: -1

Andrea Ciccotta
Andrea Ciccotta

Reputation: 672

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 {
  [...]
}

Upvotes: -1

Kobynet
Kobynet

Reputation: 1058

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

@Mapper(
    uses = {
        //Other mappings..
    },
    componentModel = MappingConstants.ComponentModel.SPRING)

Upvotes: 3

Arsalan Siddiqui
Arsalan Siddiqui

Reputation: 228

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;

Upvotes: 3

Anibal Anto
Anibal Anto

Reputation: 157

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.

Upvotes: -1

Jim Cox
Jim Cox

Reputation: 1044

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.

Upvotes: 25

Sjaak
Sjaak

Reputation: 4160

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.

Upvotes: 34

Gunnar
Gunnar

Reputation: 19020

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.

Upvotes: 51

Related Questions