32

I'd like to combine MapStruct mappers with Spring's Conversion model. So I declare every Mapper interface as an extension of Spring's Converter:

@Mapper
public interface CarMapper extends Converter<Car, CarDto> {    
    @Override
    CarDto convert(Car car);    
}

I can then use the mapper beans by injecting the standard ConversionService:

class CarWarehouse {
    @Autowired
    private ConversionService conversionService;

    ...

    public CarDto getCarInformation(Car car) {
        return conversionService.convert(car, CarDto.class);
    }
}

This works nicely, but I'm wondering whether there's a way to avoid injecting some Mappers into others directly via the uses attribute. What I'd like to do is tell a Mapper to use the ConversionService for employing another mapper. However, since the ConversionService's convert method doesn't match MapStruct's standard pattern for a mapping method, the code generation plugin doesn't recognise that it can use the service when looking for a submapping. Basically, what I want to do is write

@Mapper(uses=ConversionService.class)
public interface ParentMapper extends Converter<Parent, ParentDto>

instead of

@Mapper(uses={ChildMapper1.class, ChildMapper2.class, ChildMapper3.class})
public interface ParentMapper extends Converter<Parent, ParentDto>

Is there a way to achieve this?

Edit

Since it's been asked, let's say I've got a CarMapper defined as above, with the types Car and CarDto having an attribute wheel of type Wheel and WheelDto, respectively. Then I'd like to be able to define another Mapper like this:

@Mapper
public interface WheelMapper extends Converter<Wheel, WheelDto> {    
    @Override
    WheelDto convert(Wheel wheel);    
}

Right now, I'd have to add this Mapper explicitly:

@Mapper(uses = WheelMapper.class)
public interface CarMapper extends Converter<Car, CarDto>

Which would then give the generated CarMapperImpl an @Autowired member of type WheelMapper which would be called in order to map the attribute wheel.

However, what I'd like is that the generated code would look somewhat like this:

@Component
public class CarMapperImpl implements CarMapper {
    @Autowired
    private ConversionService conversionService;
    @Override
    public CarDto convert(Car car) {
        CarDto carDto = new CarDto();
        carDto.setWheel(conversionService.convert(car.getWheel(), WheelDto.class);
        return carDto;
    }
}
buræquete
  • 14,226
  • 4
  • 44
  • 89
Ray
  • 3,084
  • 2
  • 19
  • 27

3 Answers3

21

It's been more than a year since I asked this question, but now we've come up with an answer inside the MapStruct project itself - the MapStruct Spring Extensions project.

A CarMapper example is provided as an example within the project.

M. Justin
  • 14,487
  • 7
  • 91
  • 130
Ray
  • 3,084
  • 2
  • 19
  • 27
  • 7
    This Stack Overflow answer would be improved with an inline code example of use solving the OP's specific question. I don't have the time at the moment to play around with it and test it out to make sure it works, and I don't want to post it without making sure it's good. – M. Justin Dec 30 '20 at 16:01
1

You can just skip passing a WheelMapper entirely, when you just have a CarMapper the generated CarMapperImpl will contain a logic to map Wheel <-> WheelDto as well. No need to pass anything to uses, making your issue obsolete.

carDto.setWheel( wheelToWheelDto( car.getWheel() ) );

with a method like;

protected WheelDto wheelToWheelDto(Wheel wheel) {
    if ( wheel == null ) {
        return null;
    }

    WheelDto wheelDto = new WheelDto();

    wheelDto.setName( wheel.getName() );

    return wheelDto;
}

I did try to achieve an intelligent injection of ConversionService through MapStruct, but it is not possible I think. You'd need support from MapStruct to achieve such a feat. It does not even consider injecting ConversionService. Maybe a custom generic mapper that is already implemented and uses ConversionService might work, but I was unable to do that! Though I don't see any reason for it since MapStruct is already creating all necessary smaller mappers from the parent mapper...

buræquete
  • 14,226
  • 4
  • 44
  • 89
  • What about attributes that are shared between different master types? Let's say there's a Van with type Wheel as well. In these situations, I typically do specify separate mappers for these shared types. I know the example isn't absolutely convincing, but there are situations where it just comes natural to do so. – Ray Sep 27 '19 at 09:19
  • @Ray you can have a `Wheel` class, then extend it and have `VanWheel`, and `CarWheel` maybe? There are other solutions to that issue, but overall I'd suggest sticking with what `MapStruct` has without any hack solution – buræquete Sep 27 '19 at 09:21
  • @Ray plus with what you are trying to do, you'll have to pass `ConversionService` to both `VanMapper` & `CarMapper`, how can it guess that one has to use mapper1 and other mapper2 for `Wheel` fields other that specifically setting different classes for it as I suggested? – buræquete Sep 29 '19 at 09:09
  • There is a way to achieve what you're explaining in the final paragraph with some additional effort. I'll see if I can explain this in more detail when I get the time. – Ray Oct 02 '19 at 11:43
0

Frankly, I doubt you can achieve automatic wiring of ConversionService into generated mappers by MapStruct. The way that you described in the question (that wires individual mappers through uses annotation attribute), probably, the best that MapStruct can give out of the box.

However, there is workaround, if you absolutely need to use ConversionService to perform conversion for some DTOs (e.g. if you have some legacy converters, that you don't want to refactor to mappers). Basically, you can use combination of Mappers.getMapper static factory to get instance of ConversionService and default method in the mapper interface, to use ConversionService instance:

@Mapper(componentModel = "spring")
public interface CarMapper extends Converter<Car, CarDto> {

    ConversionService CONVERSION_SERVICE = Mappers.getMapper(ConversionService.class);

    @Override
    default CarDto convert(Car car) {
        if (car == null) {
            return null;
        }

        CarDto carDto = new CarDto();

        carDto.setEngine(CONVERSION_SERVICE.convert(car.getEngine(), EngineDto.class));
        carDto.setWheel(CONVERSION_SERVICE.convert(car.getWheel(), WheelDto.class));

        return carDto;
    }
}

Note: as you can see, workaround requires to write CarMapper code. So, in my opinion, the solution with uses annotation attribute is cleaner approach. For example, you get almost the same result, by defining following interface:

@Mapper(componentModel = "spring", 
        uses = {EngineMapper.class, WheelMapper.class}, 
        injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface CarMapper extends Converter<Car, CarDto> {
    @Override
    CarDto convert(Car car);

Generated mapper:

@Component
public class CarMapperImpl implements CarMapper {

    private final EngineMapper engineMapper;
    private final WheelMapper wheelMapper;

    @Autowired
    public CarMapperImpl(EngineMapper engineMapper, WheelMapper wheelMapper) {

        this.engineMapper = engineMapper;
        this.wheelMapper = wheelMapper;
    }

    @Override
    public CarDto convert(Car car) {
        if (car == null) {
            return null;
        }

        CarDto carDto = new CarDto();

        carDto.setEngine(engineMapper.convert(car.getEngine()));
        carDto.setWheel(wheelMapper.convert(car.getWheel()));

        return carDto;
    }
}
Oleksii Zghurskyi
  • 3,967
  • 1
  • 16
  • 17
  • As for the existing mechanisms, you're probably right. The easiest way is just to ignore the `ConversionService` for the child mappers altogether. However, MapStruct gurus came up with another idea in the Gitter chat. I'll see if I can find some time to explain this in a separate post. – Ray Oct 02 '19 at 11:41
  • Cool! Can you share the link to the Gitter? Just interested to find the better way - it's frequent use-case for me also. – Oleksii Zghurskyi Oct 02 '19 at 12:28
  • 1
    It involves writing your own additional Annotation Processor to generate a "bridge class": https://gitter.im/mapstruct/mapstruct-users – Ray Oct 02 '19 at 12:32