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.
Я просмотрел все ответы на этот вопрос, но не смог заставить все работать. Я копнул немного дальше и смог решить эту проблему очень легко. Все, что вам нужно сделать, это убедиться, что:
- Ваша компонентная модель установлена как «весна».
- Вы используете абстрактный класс для своего картографа.
- Определите именованный метод, в котором вы будете использовать внедренный 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);
//...
Возможно, это не лучшее решение. Но это помогло уменьшить влияние изменений в окончательном разрешении.