it-roy-ru.com

Spring Boot: упаковка JSON-ответа в динамические родительские объекты

У меня есть спецификация API REST, которая взаимодействует с внутренними микросервисами, которые возвращают следующие значения:

В ответах "коллекций" (например, 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. Однако, перевод этой модели в код оказывается очень трудным.

Допустим, для отдельных ответов у меня есть следующий объект модели 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 (и, возможно, класс BaseCollectionsResponse extends ResourceSupport), который могут расширяться всеми моими моделями, но, видя, как Джексон выводит ключи JSON из определений объектов, мне нужно добавить что-то вроде Javaassist, чтобы добавить поля для базовых классов ответа в среде исполнения - грязный хак, от которого я хотел бы держаться как можно дальше от людей.

  • Есть ли более простой способ сделать это? К сожалению, у меня может быть переменное число объектов JSON верхнего уровня в ответе через год, поэтому я не могу использовать что-то вроде SerializationConfig.Feature.WRAP_ROOT_VALUE Джексона, потому что это оборачивает все в один объект корневого уровня (насколько я в курсе).

  • Возможно ли что-то вроде @JsonProperty для уровня класса (в отличие от просто уровня метода и поля)?

13
user991710

Есть несколько возможностей.

Вы можете использовать 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());
    }
}

Вы также можете использовать аннотацию Джексона @JsonTypeInfo на уровне класса

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

Лично я не возражаю против дополнительных классов Dto, вам нужно создать их только один раз, и затраты на обслуживание практически отсутствуют. И если вам нужно выполнить тесты MockMVC, вам, скорее всего, понадобятся классы для десериализации ваших ответов JSON для проверки результатов.

Как вы, вероятно, знаете, среда Spring обрабатывает сериализацию/десериализацию объектов в слое HttpMessageConverter, так что это правильное место для изменения способа сериализации объектов.

Если вам не нужно десериализовывать ответы, можно создать универсальную оболочку и пользовательский HttpMessageConverter (и поместить его перед MappingJackson2HttpMessageConverter в списке конвертера сообщений). Как это:

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 в конфигурации Spring, которая расширяет WebMvcConfigurerAdapter путем переопределения configureMessageConverters(). Имейте в виду, что при этом отключается автоматическое обнаружение конвертеров по умолчанию, поэтому вам, вероятно, придется добавить значение по умолчанию самостоятельно (проверьте исходный код Spring на WebMvcConfigurationSupport#addDefaultHttpMessageConverters(), чтобы увидеть значения по умолчанию. Если вы расширяете WebMvcConfigurationSupport вместо WebMvcConfigurerAdapter, вы можете вызвать addDefaultHttpMessageConverters напрямую (лично я предпочитаю использование WebMvcConfigurationSupport вместо WebMvcConfigurerAdapter, если мне нужно что-то настроить, но есть некоторые незначительные последствия для этого, о которых вы, вероятно, можете прочитать в других статьях.

4
Klaus Groenbaek

Я думаю, вы ищете Пользовательский Джексон Сериализатор . С простой реализацией кода один и тот же объект может быть сериализован в разных структурах

пример: https://stackoverflow.com/a/10835504/814304http://www.davismol.net/2015/05/18/jackson-create-and- зарегистрировать пользовательский json-сериализатор-с-stdserializer-and-simplemodule-classes/

1
iMysak

Джексон не имеет большой поддержки динамических/переменных структур JSON, поэтому любое решение, которое выполнит нечто подобное, будет довольно хакерским, как вы упомянули. Насколько я знаю и из того, что я видел, стандартный и наиболее распространенный метод использует классы-обертки, как вы в настоящее время. Классы-обертки действительно складываются, но если вы проявите творческий подход к своей неотъемлемости, вы сможете найти некоторые общие черты между классами и, таким образом, уменьшить количество классов-оболочек. В противном случае вы могли бы посмотреть на написание пользовательских рамок.

1
Trevor Bye