Mapstruct - как я могу внедрить пружинную зависимость в классе Generated Mapper

Мне нужно внедрить класс обслуживания Spring в сгенерированную реализацию Mapper, чтобы я мог использовать его через

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

Это применимо в Mapstruct-1.0?

8 ответов

Решение

Это должно быть возможно, если вы объявите Spring как компонентную модель и добавите ссылку на тип myservice:

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

Этот механизм предназначен для предоставления доступа к другим методам отображения, вызываемым сгенерированным кодом, но вы также должны иметь возможность использовать их в выражении таким же образом. Просто убедитесь, что вы используете правильное имя сгенерированного поля со ссылкой на сервис.

Как комментирует brettanomyces, сервис не будет внедрен, если он не используется в операциях отображения, кроме выражений.

Единственный способ найти это:

  • Преврати мой интерфейс маппера в абстрактный класс
  • Введите сервис в абстрактный класс
  • Сделайте его защищенным, чтобы "реализация" абстрактного класса имела доступ

Я использую CDI, но это должно быть то же самое с 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(obj);

}

В дополнение к ответам, приведенным выше, стоит добавить, что в mapstruct mapper существует более чистый способ использования службы Spring, который в большей степени соответствует концепции дизайна "разделение задач", называемый "квалификатор". Для простоты я предпочитаю именованный квалификатор, как указано здесь http://mapstruct.org/documentation/stable/reference/html/. Примером будет:

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

}

Вот как вы используете его в вашем картографе:

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

}

Начиная с версии 1.2 это можно решить с помощью комбинации @AfterMapping и @Context.. Вот так:

@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() ) );
   }
 }

Служба может быть передана как контекст.

Лучше было бы использовать @Context класс, который обернуть MyService вместо прохождения MyService непосредственно. @AfterMapping Метод может быть реализован в этом "контекстном" классе: void map( @MappingTarget Target.X target, Source.ID source ) сохранение логики сопоставления от логики поиска. Ознакомьтесь с этим примером в репозитории примеров MapStruct.

Я использую Mapstruct 1.3.1 и обнаружил, что эту проблему легко решить с помощью декоратора.

Пример:

@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 сгенерирует 2 класса и пометит FooMapper, расширяющий FooMapperDecorator, как компонент @Primary.

Я просмотрел все ответы на этот вопрос, но не смог заставить все работать. Я копнул немного дальше и смог решить эту проблему очень легко. Все, что вам нужно сделать, это убедиться, что:

  1. Ваша компонентная модель установлена ​​как «весна».
  2. Вы используете абстрактный класс для своего картографа.
  3. Определите именованный метод, в котором вы будете использовать внедренный bean-компонент (в примере его appProperties используется внутри метода MapSource ).
       @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();
   } }

Также помните, что ваш картограф теперь стал весенним бобом. Вам нужно будет внедрить его в свой вызывающий класс следующим образом:

      private final MyMapper myMapper;

Начиная с версии Mapstruct 1.5.0, вы можете использовать константу для генерации модели компонента Spring.

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

я не могу использоватьcomponentModel="spring"потому что я работаю в большом проекте, который его не использует. Многие картографы включают мой картограф сMappers.getMapper(FamilyBasePersonMapper.class), этот экземпляр не является bean-компонентом Spring, и поле в моем преобразователе равно null.

Я не могу модифицировать все мапперы, которые используют мой маппер. И я не могу использовать конкретный конструктор с инъекциями или Spring@Autowiredвнедрение зависимости.

Решение, которое я нашел: использование экземпляра компонента Spring без прямого использования Spring:

Вот компонент Spring, который регистрирует себя первым экземпляром (экземпляр Spring):

      @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) {
        //...
    }

    //...

}

Вот Mapper, который использует Spring Bean черезgetFirstInstanceметод:

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

   //...

Возможно, это не лучшее решение. Но это помогло уменьшить влияние изменений в окончательном разрешении.

Другие вопросы по тегам