Mapstruct @ Spring에 대한 심층 분석

(Miguel Duque) (2020 년 12 월 14 일)

Unsplash의 Beau Swierstra 의 사진 figcaption>

자바 개발자라면 POJO 매핑이 다층 애플리케이션을 개발할 때 표준 작업이라는 것을 확실히 알고 있습니다.
이러한 매핑을 수동으로 작성하는 것은 지루하고 개발자에게 불쾌한 작업은 오류가 발생하기 쉽습니다.

MapStruct 는 컴파일 중에 매퍼 클래스 구현을 생성하는 오픈 소스 Java 라이브러리입니다. 안전하고 쉬운 방법입니다.

이 기사에서는이 강력한 라이브러리를 활용하여 정기적으로 손으로 작성하는 상용구 코드의 양을 크게 줄이는 방법에 대한 예제를 따를 것입니다.

색인

  • 기본 매핑
  • 다른 속성 이름 매핑
  • 모든 속성이 매핑되었는지 확인
  • 하위 항목 속성 매핑
  • 전체 하위 항목 매핑 — 다른 매퍼 사용
  • 사용자 지정 방법으로 매핑
  • @BeforeMapping @AfterMapping
  • 추가 매개 변수를 사용한 매핑
  • 매핑 방법에 대한 종속성 주입
  • 업데이트
  • 패치 업데이트

기본 매핑

Doctor 클래스를 포함하는 기본 모델로 애플리케이션을 시작하겠습니다. Google 서비스는 모델 계층에서이 클래스를 검색 한 다음 DoctorDto 클래스.

모델 클래스 :

@Data
public class Doctor {
private int id;
private String name;
}

Dto 클래스 :

@Data
public class DoctorDto {
private int id;
private String name;
}

이 작업을 수행하려면 Mapper 인터페이스를 만들어야합니다.

@Mapper(componentModel = "spring")
public interface DoctorMapper {
DoctorDto toDto(Doctor doctor);
}

두 클래스가 동일한 속성 이름 ( id name ), mapstruct는 생성 된 클래스에 두 필드의 매핑을 포함합니다.

@Component
public class DoctorMapperImpl implements DoctorMapper {

@Override
public DoctorDto toDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}

DoctorDto doctorDto = new DoctorDto();

doctorDto.setId( doctor.getId() );
doctorDto.setName( doctor.getName() );

return doctorDto;
}
}

componentModel을 추가하여 = “spring”, 생성 된 매퍼는 Spring Bean이되고 다른 Bean과 마찬가지로 @Autowired 주석으로 검색 할 수 있습니다.

@Service
public class DoctorService {

private final DoctorMapper doctorMapper;
private final DoctorRepository doctorRepository;

@Autowired
public DoctorService(DoctorMapper doctorMapper) {
this.doctorMapper = doctorMapper;
this.doctorRepository = doctorRepository;
}

public DoctorDto getDoctor(Integer id) {
Doctor doctor = doctorRepository.findById(id);
return doctorMapper.toDto(doctor);
}
}

다른 속성 이름 매핑

Doctor iv id =에 속성 전화 를 포함하는 경우 “ba87a1cd20”> 클래스 :

@Data
public class Doctor {
private int id;
private String name;
private String phone;
}

연락처 에 매핑됩니다. 의사 Dto :

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
}

매퍼가 자동으로 매핑 할 수 없습니다. 이렇게하려면이 매핑에 대한 규칙을 만들어야합니다.

@Mapper(componentModel = "spring")
public interface DoctorMapper {

@Mapping(source = "phone", target = "contact")
DoctorDto toDto(Doctor doctor);
}

모든 속성이 매핑되었는지 확인

원하는 경우 대상 속성을 매핑하는 것을 잊지 않도록 매퍼에서 unmappedTargetPolicy 옵션을 구성 할 수 있습니다.

@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface DoctorMapper {

@Mapping(source = "phone", target = "contact")
DoctorDto toDto(Doctor doctor);
}

이 구성에서 제거하면

@Mapping (source = “phone”, target = “contact”)

다음으로 컴파일하는 동안 코드가 실패합니다. 오류 :

매핑되지 않은 대상 속성 : “contact”. DoctorDto toDto (Doctor doctor);

어떤 이유로 든 대상을 무시하려는 경우 속성에 다음을 추가 할 수 있습니다.

@Mapping (target = “contact”, ignore = true)

마찬가지로 모든 소스 속성은 unmappedSourcePolicy 옵션을 구성하여 매핑됩니다.

하위 엔티티 속성 매핑

대부분 필요한 클래스 지도에 하위 개체가 포함되어 있습니다. . 예를 들면 다음과 같습니다.

@Data
public class Doctor {
private int id;
private String name;
private String phone;
private Speciality speciality;
}@Data
public class Speciality {
private int id;
private String name;
}

그리고 Dto에서는 전체 전문 분야 대신 이름 만 필요합니다.

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
private String specialityName;
}

이 상황은 mapstruct 에서도 간단합니다.

@Mapping(source = "phone", target = "contact")
@Mapping(source = "speciality.name", target = "specialityName")
DoctorDto toDto(Doctor doctor);

전체 하위 항목 매핑 — 다른 항목 사용 매퍼

이전과 마찬가지로 Doctor 클래스에는 하위 개체가 있습니다. 주소 :

@Data
public class Doctor {
private int id;
private String name;
private String phone;
private Speciality speciality;
private Address address;
}

하지만이 경우 새 주소에 매핑하고 싶습니다. DoctorDto 클래스의 개체 :

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
private String specialityName;
private AddressDto address;
}

To Address와 AddressDto 클래스 간의 매핑을 수행하려면 다른 매퍼 인터페이스를 만들어야합니다.

@Mapper(componentModel = "spring")
public interface AddressMapper {
AddressDto toDto(Address address);
}

그런 다음 DoctorMapper Doctor 에서 DoctorDto 로.매퍼 구성의 “ uses” 옵션을 사용하여 수행 할 수 있습니다.

@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = {AddressMapper.class})
public interface DoctorMapper {

@Mapping(source = "phone", target = "contact")
@Mapping(source = "speciality.name", target = "specialityName")
DoctorDto toDto(Doctor doctor);
}

DoctorMapperImpl 는 Autowire를 사용하고 AddressMapper :

@Component
public class DoctorMapperImpl implements DoctorMapper {

@Autowired
private AddressMapper addressMapper;

@Override
public DoctorDto toDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}

DoctorDto doctorDto = new DoctorDto();

doctorDto.setSpecialityName( doctorSpecialityName(doctor));
doctorDto.setContact( doctor.getPhone() );
doctorDto.setId( doctor.getId() );
doctorDto.setName( doctor.getName() );
doctorDto.setAddress(
addressMapper.toDto( doctor.getAddress() ) );


return doctorDto;
}

...
}

맞춤 방법을 사용한 매핑

이제 닥터 수업 :

@Data
public class Doctor {
private int id;
private String name;
private String phone;
private Speciality speciality;
private Address address;
private List patients;
}

그러나 DoctorDto 우리는 환자 수만 원합니다.

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
private String specialityName;
private AddressDto address;
private int numPatients;
}

이 매핑에는 2 명이 필요합니다. 사물 :

  • @Named 주석이있는 사용자 지정 메서드
  • 매핑 주석의 qualifiedByName 구성
@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = {AddressMapper.class})
public interface DoctorMapper {

@Mapping(source = "phone", target = "contact")
@Mapping(source = "speciality.name", target = "specialityName")
@Mapping(
source = "patients",
target = "numPatients",
qualifiedByName = "countPatients")

DoctorDto toDto(Doctor doctor);

@Named("countPatients")
default int getNumPatients(List patients) {
if(patients == null) {
return 0;
}
return patients.size();
}
}

@BeforeMapping @AfterMapping

이전 예 ( 환자 나열 – int numPatients )는 @BeforeMapping em을 사용하여 수행 할 수도 있습니다. > 및 @AfterMapping

@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = {AddressMapper.class})
public interface DoctorMapper {

@BeforeMapping
default void setList(Doctor doctor) {
if (doctor != null && doctor.getPatients() == null) {
doctor.setPatients(new ArrayList());
}
}

@Mapping(source = "phone", target = "contact")
@Mapping(source = "speciality.name", target = "specialityName")
@Mapping(target = "numPatients", ignore = true)
DoctorDto toDto(Doctor doctor);

@AfterMapping
default void setNumPatients(Doctor doctor,
@MappingTarget DoctorDto doctorDto) {
doctorDto.setNumPatients(doctor.getPatients().size());
}
}

다음 메서드는 생성 된 매핑 메서드의 시작과 끝에서 호출됩니다.

@Override
public DoctorDto toDto(Doctor doctor) {
setList( doctor );

if ( doctor == null ) {
return null;
}

DoctorDto doctorDto = new DoctorDto();

doctorDto.setSpecialityName( doctorSpecialityName( doctor ) );
doctorDto.setContact( doctor.getPhone() );
doctorDto.setId( doctor.getId() );
doctorDto.setName( doctor.getName() );
doctorDto.setAddress( addressMapper.toDto(doctor.getAddress()));

setNumPatients( doctor, doctorDto );

return doctorDto;
}

추가 매개 변수를 사용한 매핑

매퍼가 엔티티 외에 추가 매개 변수를 받아야하는 상황을 처리하는 방법을 확인하겠습니다.

이 경우 Doctor 클래스는 도시 ID 만 가져올 수 있습니다.

@Data
public class Doctor {
private int id;
private String name;
private String phone;
private Speciality speciality;
private Address address;
private List patients;
private int cityId;
}

하지만이를 도시 이름에 매핑해야합니다.

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
private String specialityName;
private AddressDto address;
private int numPatients;
private String cityName;
}

서비스는 도시 목록을 가져 와서 매퍼에게 전달합니다.

@Service
public class DoctorService {

private final DoctorMapper doctorMapper;
private final DoctorRepository doctorRepository;

@Autowired
public DoctorService(DoctorMapper doctorMapper) {
this.doctorMapper = doctorMapper;
this.doctorRepository = doctorRepository;
}

public DoctorDto getDoctor(Integer id) {
Doctor doctor = doctorRepository.findById(id);
List cities = getCities();
return doctorMapper.toDto(doctor, cities);
}
}

매퍼에서 다음을 수행해야합니다.

  • @Context 주석으로 추가 매개 변수 (도시 목록)를 표시
  • 사용자 지정 매핑을 처리하는 방법
  • 맞춤형 ma에서 컨텍스트 매개 변수 (도시 목록)를 요청 핑 방법
@Mapping(source = "phone", target = "contact")
@Mapping(source = "speciality.name", target = "specialityName")
@Mapping(target = "numPatients", ignore = true)
@Mapping(source = "cityId",
target = "cityName",
qualifiedByName = "cityName")

DoctorDto toDto(Doctor doctor, @Context List cities);

@Named("cityName")
default String getCityName(int cityId, @Context List cities) {
return cities.stream()
.filter(city -> city.getId() == cityId)
.findAny()
.map(City::getName)
.orElse(null);
}

매핑 방법에 대한 종속성 주입

사용자 지정 매핑 방법에 필요한 상황에 처하게 될 것입니다. 또 다른 빈 (다른 매퍼, 저장소, 서비스 등).

이러한 상황에서는 해당 빈을 매퍼에 자동 연결해야하므로이를 수행하는 방법의 예를 살펴 보겠습니다.

이 예에서 Patient 클래스는 추상 클래스가됩니다.

@Data
public abstract class Patient {
private int id;
private String name;
private int age;
}

두 가지 구현이 포함되어 있습니다.

public class Man extends Patient {
}public class Woman extends Patient {
}

이전에 보셨 듯이 Doctor 엔티티 :

@Data
public class Doctor {
private int id;
private String name;
private String phone;
private Speciality speciality;
private Address address;
private List patients;
private int cityId;
}

그러나 PatientDto 는 각 구체적인 클래스에 대해 고유 한 목록을 필요로합니다.

@Data
public class DoctorDto {
private int id;
private String name;
private String contact;
private String specialityName;
private AddressDto address;
private int numPatients;
private String cityName;
private PatientsDto patients;
}

그리고

@Data
public class PatientsDto {
private List men = new ArrayList();
private List women = new ArrayList();

public void addMan(ManDto manDto) {
men.add(manDto);
}

public void addWoman(WomanDto womanDto) {
women.add(womanDto);
}
}

따라서 이러한 구체적인 클래스를 매핑하려면 먼저 creatin g 두 명의 매퍼 :

@Mapper(componentModel = "spring")
public interface WomanMapper {
WomanDto toDto(Woman woman);
}@Mapper(componentModel = "spring")
public interface ManMapper {
ManDto toDto(Man man);
}

그러나 목록 환자 PatientsDto 환자 또한 새로 생성 된 매퍼를 사용하는 다른 매퍼 ( WomanMapper ManMapper ), DoctorMapper

PatientsMapper 가 사용하기 때문에 인터페이스를 만드는 대신 WomanMapper ManMapper 를 사용해야하며 추상 클래스를 만들어야합니다.

@Mapper(componentModel = "spring")
public abstract class PatientsMapper {

@Autowired
private ManMapper manMapper;

@Autowired
private WomanMapper womanMapper;

public PatientsDto toDto(List patients) {
PatientsDto patientsDto = new PatientsDto();
for (Patient patient : patients) {
if (patient instanceof Man) {
patientsDto.addMan(
manMapper.toDto((Man) patient));
} else if (patient instanceof Woman) {
patientsDto.addWoman(
womanMapper.toDto((Woman) patient));
}
}
return patientsDto;
}
}

마지막으로 DoctorMapper PatientsMapper 를 사용하도록하려면 몇 가지 구성을 추가해야합니다.

@Mapper(
componentModel = "spring",
unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = {AddressMapper.class, PatientsMapper.class})
public interface DoctorMapper {
...
}

patients 변수는 Entity 및 Dto 클래스에서 동일한 이름을 갖기 때문에 다른 것을 지정할 필요가 없습니다.

이것은 최종 결과입니다. 생성 된 클래스 :

@Component
public class DoctorMapperImpl implements DoctorMapper {

@Autowired
private AddressMapper addressMapper;
@Autowired
private PatientsMapper patientsMapper;


@Override
public DoctorDto toDto(Doctor doctor, List cities) {
if (doctor == null) {
return null;
}

DoctorDto doctorDto = new DoctorDto();

doctorDto.setNumPatients(
getNumPatients(doctor.getPatients()));
doctorDto.setCityName(
getCityName(doctor.getCityId(), cities));
doctorDto.setSpecialityName(doctorSpecialityName(doctor));
doctorDto.setContact(doctor.getPhone());
doctorDto.setId(doctor.getId());
doctorDto.setName(doctor.getName());
doctorDto.setAddress(
addressMapper.toDto(doctor.getAddress()));
doctorDto.setPatients(
patientsMapper.toDto(doctor.getPatients()));


return doctorDto;
}
...
}@Component
public class ManMapperImpl implements ManMapper {

@Override
public ManDto toDto(Man man) {
if ( man == null ) {
return null;
}

ManDto manDto = new ManDto();

manDto.setId( man.getId() );
manDto.setName( man.getName() );
manDto.setAge( man.getAge() );

return manDto;
}
}@Component
public class WomanMapperImpl implements WomanMapper {

@Override
public WomanDto toDto(Woman woman) {
if ( woman == null ) {
return null;
}

WomanDto womanDto = new WomanDto();

womanDto.setId( woman.getId() );
womanDto.setName( woman.getName() );
womanDto.setAge( woman.getAge() );

return womanDto;
}
}

업데이트

Mapstruct도 업데이트를 처리하는 쉬운 방법입니다. Doctor 항목을 DoctorDto , 다음을 생성하기 만하면됩니다.

@Mapping(source = "contact", target = "phone")
@Mapping(source = "specialityName", target = "speciality.name")
void updateEntity(DoctorDto doctorDto,
@MappingTarget Doctor doctor);

생성 된 구현에서 볼 수 있듯이 모든 변수 (null 또는 null 아님) :

@Override
public void updateEntity(DoctorDto doctorDto, Doctor doctor) {
if ( doctorDto == null ) {
return;
}

if ( doctor.getSpeciality() == null ) {
doctor.setSpeciality( new Speciality() );
}
doctorDtoToSpeciality( doctorDto, doctor.getSpeciality() );
doctor.setPhone( doctorDto.getContact() );
doctor.setId( doctorDto.getId() );
doctor.setName( doctorDto.getName() );
if ( doctorDto.getAddress() != null ) {
if ( doctor.getAddress() == null ) {
doctor.setAddress( new Address() );
}
addressDtoToAddress( doctorDto.getAddress(), doctor.getAddress() );
}
else {
doctor.setAddress( null );
}
}

패치 업데이트

이전 예에서 살펴본 것처럼 기본 업데이트 방법은 null 인 경우에도 모든 속성을 매핑합니다.따라서 패치 업데이트 만 수행하려는 상황에 처한 경우 (null이 아닌 값만 업데이트) nullValuePropertyMappingStrategy 를 사용해야합니다.

@BeanMapping(nullValuePropertyMappingStrategy = 
NullValuePropertyMappingStrategy.IGNORE)

@Mapping(source = "contact", target = "phone")
@Mapping(source = "specialityName", target = "speciality.name")
void updatePatchEntity(DoctorDto doctorDto,
@MappingTarget Doctor doctor);

생성 된 메서드는 값을 업데이트하기 전에 null 검사를 수행합니다.

@Override
public void updatePatchEntity(DoctorDto doctorDto, Doctor doctor) {
if ( doctorDto == null ) {
return;
}

if ( doctorDto.getContact() != null ) {
doctor.setPhone( doctorDto.getContact() );
}
doctor.setId( doctorDto.getId() );
if ( doctorDto.getName() != null ) {
doctor.setName( doctorDto.getName() );
}
if ( doctorDto.getAddress() != null ) {
if ( doctor.getAddress() == null ) {
doctor.setAddress( new Address() );
}
addressDtoToAddress(
doctorDto.getAddress(),
doctor.getAddress() );
}
if ( doctor.getSpeciality() == null ) {
doctor.setSpeciality( new Speciality() );
}
doctorDtoToSpeciality1( doctorDto, doctor.getSpeciality() );
}

결론

이 도움말 Mapstruct 라이브러리를 활용하여 안전하고 우아한 방식으로 상용구 코드를 크게 줄이는 방법을 설명했습니다.

예제에서 볼 수 있듯이 Mapstruct는 다음에서 생성 할 수있는 방대한 기능 및 구성 세트를 제공합니다. 쉽고 빠른 방법으로 기본에서 복잡한 매퍼까지.