상속과 함께 MapStruct 및 Lombok 사용

Lombok 및 MapStruct는 데이터 개체로 작업할 때 매우 유용한 두 가지 도구입니다. Lombok은 getter, setter, builder 및 생성자를 제공하므로 이러한 반복적인 메서드를 수동으로 작성하거나 생성하지 않아도 됩니다. MapStruct는 Kafka 이벤트 개체에서 웹 서비스로 보낼 요청 개체로 변환하는 것과 같이 두 개체 간에 변환하는 코드를 생성합니다. 그러나 이 게시물은 Lombok 및 MapStruct를 사용하여 하위 클래스인 두 객체 간에 변환하는 매우 구체적인 사용 사례에 관한 것입니다. 예를 들어, VehicleDto를 확장하는 CarDto를 VehicleEvent를 확장하는 CarEvent로 변환합니다. 단순화를 위해 먼저 이러한 병렬 객체가 동일한 이름과 동일한 유형을 가진 정확히 동일한 필드를 가지고 있다고 가정합니다.

내 페어링 파트너와 내가 이 사용 사례를 만났을 때 우리는 처음에 당황했습니다. 우리의 결합된 Google Fu 노력도 좋은 해결책을 찾지 못했습니다. 그러나 결국 문제를 발견했습니다a PR for MapStruct that seemed to be the solution! 하지만 물론 그렇게 간단했다면 지금 이 글을 쓰지 않았을 것입니다. 해당 PR에서 제공하는 SubClassMapping 주석은 매우 유용하지만 하위 클래스의 모든 필드에 대한 특정 Mapping 주석이 필요합니다. 우리는 보다 우아한 솔루션을 원했습니다.

추가 조사 후 Lombok에서 SuperBuilder 주석을 제공한다는 사실을 발견했습니다. 이를 부모 클래스와 자식 클래스 모두에 추가하면 부모 클래스와 자식 클래스에서 선언된 필드를 설정하는 데 사용할 수 있는 자식 클래스용 빌더가 생성됩니다. 그러나 이를 사용하여 하위 클래스의 인스턴스에서 상위 클래스의 필드를 설정할 수 있지만 자동 생성된 MapperImpl은 하위 클래스에서 빌더를 호출할 때만 상위 클래스 필드를 인식했습니다.

마지막으로 우리는 빌더를 비활성화하라는 BeanMapping 주석을 매퍼 메서드에 추가했습니다. 이것은 트릭을했다! 다음은 솔루션을 보여주는 몇 가지 예제 코드입니다.

차량Dto




import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@Data
@SuperBuilder
@NoArgsConstructor
public class VehicleDto {

    private String vin;
    private int numCylinders;

}


CarDto




import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@Data
@SuperBuilder
@EqualsAndHashCode(callSuper=true)
@NoArgsConstructor
public class CarDto extends VehicleDto {

    private String make;
    private String model;
    private int numDoors;

}


차량 이벤트




import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@Data
@SuperBuilder
@NoArgsConstructor
public class VehicleEvent {

    private String vin;
    private int numCylinders;

}


자동차 이벤트




import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;

@Data
@SuperBuilder
@EqualsAndHashCode(callSuper=true)
@NoArgsConstructor
public class CarEvent extends VehicleEvent {

    private String make;
    private String model;
    private int numDoors;

}


비히클맵퍼




import org.mapstruct.BeanMapping;
import org.mapstruct.Builder;
import org.mapstruct.Mapper;
import org.mapstruct.SubclassMapping;

@Mapper
public interface VehicleMapper {
    @BeanMapping(builder = @Builder( disableBuilder = true ))
    @SubclassMapping(source = CarDto.class, target = CarEvent.class)
    VehicleEvent toEventData(VehicleDto vehicleDto);
}


VehicleMapperImpl(MapStruct에서 생성)




import javax.annotation.processing.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2022-09-21T17:00:25-0400",
    comments = "version: 1.5.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.5.1.jar, environment: Java 11.0.15 (Amazon.com Inc.)"
)
public class VehicleMapperImpl implements VehicleMapper {

    @Override
    public VehicleEvent toEventData(VehicleDto vehicleDto) {
        if ( vehicleDto == null ) {
            return null;
        }

        if (vehicleDto instanceof CarDto) {
            return carDtoToCarEvent( (CarDto) vehicleDto );
        }
        else {
            VehicleEvent vehicleEvent = new VehicleEvent();

            vehicleEvent.setVin( vehicleDto.getVin() );
            vehicleEvent.setNumCylinders( vehicleDto.getNumCylinders() );

            return vehicleEvent;
        }
    }

    protected CarEvent carDtoToCarEvent(CarDto carDto) {
        if ( carDto == null ) {
            return null;
        }

        CarEvent carEvent = new CarEvent();

        carEvent.setVin( carDto.getVin() );
        carEvent.setNumCylinders( carDto.getNumCylinders() );
        carEvent.setMake( carDto.getMake() );
        carEvent.setModel( carDto.getModel() );
        carEvent.setNumDoors( carDto.getNumDoors() );

        return carEvent;
    }
}


VehicleMapper 테스트




import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class VehicleMapperTest {

    @Test
    void shouldMapParentAndChildFieldsWhenMappingChild() {
        CarDto carDto = CarDto.builder()
                .numCylinders(6)
                .vin("2FTJW36M6LCA90573")
                .make("Chevrolet")
                .model("Malibu")
                .numDoors(4)
                .build();
        VehicleMapper mapper = new VehicleMapperImpl();

        CarEvent carEvent = (CarEvent) mapper.toEventData(carDto);

        assertThat(carEvent.getNumCylinders()).isEqualTo(carDto.getNumCylinders());
        assertThat(carEvent.getNumDoors()).isEqualTo(carDto.getNumDoors());
    }
}


보시다시피 매퍼가 사용하지 않더라도 SuperBuilder 주석을 유지하기로 선택했습니다. 주로 이것은 쉬운 테스트를 허용합니다. 그러나 SuperBuilder 주석이 있는 경우 NoArgsConstructor 주석도 필요합니다. 그렇지 않으면 다음 코드를 생성합니다.

        CarEvent.CarEventBuilder<?, ?> b = null;

        CarEvent carEvent = new CarEvent( b );


null 빌더에서 NullPointerException이 발생합니다.

예제 코드에서와 같이 두 필드 집합이 채워지는지 확인하기 위해 부모 클래스와 자식 클래스에서 하나 이상의 값을 테스트하는 것이 좋습니다. 생성된 코드를 테스트하는 것은 약간 중복되는 것처럼 보이지만 이렇게 하면 변경 사항이 기존 기능을 중단하는지 빠르게 찾고 예상 동작을 문서화하는 것과 같은 테스트의 모든 일반적인 이점뿐만 아니라 주석의 올바른 조합이 있는지 확인할 수 있습니다.

좋은 웹페이지 즐겨찾기