programing

스프링 부트: 동적 상위 개체에서 JSON 응답 래핑

javajsp 2023. 7. 8. 10:37

스프링 부트: 동적 상위 개체에서 JSON 응답 래핑

나는 백엔드 마이크로서비스와 대화하는 REST API 사양을 가지고 있으며, 이는 다음 값을 반환합니다.

"수집" 응답(예: GET/users):

{
    users: [
        {
            ... // single user object data
        }
    ],
    links: [
        {
            ... // single HATEOAS link object
        }
    ]
}

"단일 객체" 응답(예:GET /users/{userUuid}) :

{
    user: {
        ... // {userUuid} user object}
    }
}

이 접근 방식은 단일 응답이 확장 가능하도록 선택되었습니다(예: 다음과 같은 경우).GET /users/{userUuid} 변수를 .?detailedView=true추가 요청 정보가 있을 것입니다.

근본적으로 API 업데이트 간의 변화를 최소화하기 위한 OK 접근법이라고 생각합니다.그러나 이 모델을 코드로 변환하는 것은 매우 어려운 것으로 판명되었습니다.

단일 응답에 대해 단일 사용자를 위한 다음 API 모델 개체가 있다고 가정해 보겠습니다.

public class SingleUserResource {
    private MicroserviceUserModel user;

    public SingleUserResource(MicroserviceUserModel user) {
        this.user = user;
    }

    public String getName() {
        return user.getName();
    }

    // other getters for fields we wish to expose
}

이 방법의 장점은 퍼블릭 게터가 있는 내부적으로 사용되는 모델의 필드만 노출할 수 있고 다른 모델은 노출할 수 없다는 것입니다.그러면 컬렉션 응답에 대해 다음과 같은 래퍼 클래스를 갖게 됩니다.

public class UsersResource extends ResourceSupport {

    @JsonProperty("users")
    public final List<SingleUserResource> users;

    public UsersResource(List<MicroserviceUserModel> users) {
        // add each user as a SingleUserResource
    }
}

단일 개체 응답의 경우 다음과 같습니다.

public class UserResource {

    @JsonProperty("user")
    public final SingleUserResource user;

    public UserResource(SingleUserResource user) {
        this.user = user;
    }
}

은 이것수량을 산출합니다.JSON이 게시물의 상단에 있는 API 사양에 따라 형식이 지정된 응답입니다.이 접근 방식의 장점은 우리가 노출하고자 하는 분야만 노출한다는 것입니다.심각한 단점은 잭슨이 정확한 형식의 응답을 생성하기 위해 읽는 것 외에 식별 가능한 논리적 작업을 수행하지 않는 수많은 래퍼 클래스가 날아다니고 있다는 것입니다.

제 질문은 다음과 같습니다.

  • 어떻게 하면 이 접근법을 일반화할 수 있습니까?로 부탁드립니다.BaseSingularResponse 클스그아마a)BaseCollectionsResponse extends ResourceSupportclass내 모델이 확장할 수 것을 , 는 " 클스래확이할장내수모"와 같은 해야 할 입니다.JavaassistRuntime의 기본 응답 클래스에 필드를 추가합니다. 인간적으로 가능한 한 멀리 떨어져 있고 싶은 더러운 해킹입니다.

  • 이것을 달성하는 더 쉬운 방법이 있습니까? 최상위 있을 도 있기 의 JSON 객체, 1개의 JSON 객체와 같은 할 수 .SerializationConfig.Feature.WRAP_ROOT_VALUE모든 것을 하나의 루트 레벨 개체로 포장하기 때문입니다(내가 알기로는).

  • 아마도 그런 것이 있을 것입니다.@JsonProperty클래스 레벨의 경우(방법 및 필드 레벨과 반대)?

몇 가지 가능성이 있습니다.

를 사용할 수 있습니다.java.util.Map:

List<UserResource> userResources = new ArrayList<>();
userResources.add(new UserResource("John"));
userResources.add(new UserResource("Jane"));
userResources.add(new UserResource("Martin"));
Map<String, List<UserResource>> usersMap = new HashMap<String, List<UserResource>>();
usersMap.put("users", userResources);
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(usersMap));

사용할 수 있습니다.ObjectWriter아래와 같이 사용할 수 있는 응답을 정리합니다.

ObjectMapper mapper = new ObjectMapper();
ObjectWriter writer = mapper.writer().withRootName(root);
result = writer.writeValueAsString(object);

다음은 이 직렬화를 일반화하기 위한 제안입니다.

단순 개체를 처리하는 클래스:

public abstract class BaseSingularResponse {

    private String root;

    protected BaseSingularResponse(String rootName) {
        this.root = rootName;
    }

    public String serialize() {
        ObjectMapper mapper = new ObjectMapper();
        ObjectWriter writer = mapper.writer().withRootName(root);
        String result = null;
        try {
            result = writer.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            result = e.getMessage();
        }
        return result;
    }
}

수집을 처리할 클래스:

public abstract class BaseCollectionsResponse<T extends Collection<?>> {
    private String root;
    private T collection;

    protected BaseCollectionsResponse(String rootName, T aCollection) {
        this.root = rootName;
        this.collection = aCollection;
    }

    public T getCollection() {
        return collection;
    }

    public String serialize() {
        ObjectMapper mapper = new ObjectMapper();
        ObjectWriter writer = mapper.writer().withRootName(root);
        String result = null;
        try {
            result = writer.writeValueAsString(collection);
        } catch (JsonProcessingException e) {
            result = e.getMessage();
        }
        return result;
    }
}

그리고 샘플 애플리케이션:

public class Main {

    private static class UsersResource extends BaseCollectionsResponse<ArrayList<UserResource>> {
        public UsersResource() {
            super("users", new ArrayList<UserResource>());
        }
    }

    private static class UserResource extends BaseSingularResponse {

        private String name;
        private String id = UUID.randomUUID().toString();

        public UserResource(String userName) {
            super("user");
            this.name = userName;
        }

        public String getUserName() {
            return this.name;
        }

        public String getUserId() {
            return this.id;
        }
    }

    public static void main(String[] args) throws JsonProcessingException {
        UsersResource userCollection = new UsersResource();
        UserResource user1 = new UserResource("John");
        UserResource user2 = new UserResource("Jane");
        UserResource user3 = new UserResource("Martin");

        System.out.println(user1.serialize());

        userCollection.getCollection().add(user1);
        userCollection.getCollection().add(user2);
        userCollection.getCollection().add(user3);

        System.out.println(userCollection.serialize());
    }
}

▁the▁use다를 사용할 .@JsonTypeInfo

@JsonTypeInfo(include=As.WRAPPER_OBJECT, use=JsonTypeInfo.Id.NAME)

개인적으로 저는 추가적인 Dto 수업은 상관없고, 당신은 그것들을 한 번만 만들면 되고 유지보수 비용은 거의 또는 전혀 들지 않습니다.그리고 MockMVC 테스트를 수행해야 하는 경우 결과를 확인하기 위해 JSON 응답을 역직렬화하는 클래스가 필요할 가능성이 높습니다.

Spring 프레임워크는 HttpMessageConverter Layer에서 객체의 직렬화/직렬화를 처리하므로 객체의 직렬화 방식을 변경할 수 있습니다.

응답을 역직렬화할 필요가 없는 경우 일반 래퍼와 사용자 지정 HttpMessageConverter를 만들 수 있습니다(그리고 MappingJackson2 앞에 배치).메시지 변환기 목록에 HttpMessageConverter)가 있습니다.다음과 같이:

public class JSONWrapper {

    public final String name;
    public final Object object;

    public JSONWrapper(String name, Object object) {
        this.name = name;
        this.object = object;
    }
}


public class JSONWrapperHttpMessageConverter extends MappingJackson2HttpMessageConverter {

    @Override
    protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        // cast is safe because this is only called when supports return true.
        JSONWrapper wrapper = (JSONWrapper) object;
        Map<String, Object> map = new HashMap<>();
        map.put(wrapper.name, wrapper.object);
        super.writeInternal(map, type, outputMessage);
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        return clazz.equals(JSONWrapper.class);
    }
}

사용자 에 등록해야 . 이 은 HttpMessageConverter를 확장합니다.WebMvcConfigurerAdapter를 하여.configureMessageConverters()이렇게 하면 변환기의 기본 자동 감지가 비활성화되므로, 사용자가 직접 기본값을 추가해야 합니다(스프링 소스 코드:WebMvcConfigurationSupport#addDefaultHttpMessageConverters()기본값을 확인합니다.연장하면WebMvcConfigurationSupport에 신대WebMvcConfigurerAdapter전화하셔도 됩니다addDefaultHttpMessageConverters직접(개인적으로 사용하는 것을 선호합니다.WebMvcConfigurationSupport1파운드가 WebMvcConfigurerAdapter사용자 정의가 필요하지만 이 작업에 대한 몇 가지 사소한 영향이 있을 수 있습니다. 다른 기사에서 이에 대해 읽을 수 있습니다.

Jackson은 동적/가변 JSON 구조를 많이 지원하지 않기 때문에 이와 같은 작업을 수행하는 솔루션은 귀하가 언급한 것처럼 매우 까다로울 것입니다.제가 알기로는, 제가 본 바로는, 표준적이고 가장 일반적인 방법은 현재 당신이 사용하는 래퍼 클래스를 사용하는 것입니다.래퍼 클래스는 추가되지만 고유한 속성으로 창의적이 되면 클래스 간의 공통점을 찾을 수 있으므로 래퍼 클래스의 양을 줄일 수 있습니다.그렇지 않으면 사용자 정의 프레임워크를 작성할 수 있습니다.

사용자 지정 Jackson Serializer를 찾고 있는 것 같습니다.간단한 코드 구현으로 동일한 개체를 서로 다른 구조로 직렬화할 수 있습니다.

예: https://stackoverflow.com/a/10835504/814304 http://www.davismol.net/2015/05/18/jackson-create-and-register-a-custom-json-serializer-with-stdserializer-and-simplemodule-classes/

언급URL : https://stackoverflow.com/questions/41880572/spring-boot-wrapping-json-response-in-dynamic-parent-objects