Вопрос по spring, rest, json, java – Возврат сущностей в Rest API с помощью Spring

4

Создать полноценный API для веб-приложения в Spring довольно просто. Допустим, у нас есть сущность Movie с именем, годом, списком жанров и списком актеров. Чтобы вернуть список всех фильмов в формате json, мы просто создадим метод в каком-то контроллере, который будет запрашивать базу данных и возвращать список в виде тела ResponseEntity. Spring волшебным образом его сериализует, и все прекрасно работает

Но что, если я в некоторых случаях хочу, чтобы этот список актеров в фильме был сериализован, а не в других? И в каком-то другом случае, наряду с полями класса фильма, мне нужно добавить некоторые другие свойства для каждого фильма в списке, какие значения генерируются динамически?

Моим текущим решением является использование @JsonIgnore в некоторых полях или создание класса MovieResponse с такими полями, как в классе Movie, и дополнительных необходимых полей, а также каждый раз для преобразования из класса Movie в класс MovieResponse.

Есть лучший способ сделать это

+ 1 - Отличный вопрос. Дайте мне знать, если вам нужны какие-либо разъяснения в моем ответе, так как он довольно длинный! jmort253

Ваш Ответ

2   ответа
2

чтобы сообщить DispatcherServlet (или любому другому компоненту в Spring, обрабатывающему отображение ответа) игнорировать определенные поля, если эти поля являются нулевыми или иным образом опущены.

Это может предоставить вам некоторую гибкость в отношении того, какие данные вы предоставляете клиенту в определенных случаях.

Вниз к JSONIgnore:

Однако есть некоторые недостатки в использовании этой аннотации, с которой я недавно столкнулся в своих собственных проектах. Это относится главным образом к методу PUT и к случаям, когда объект, к которому ваш контроллер сериализует данные, является тем же объектом, который используется для хранения этих данных в базе данных.

Метод PUT подразумевает, что вы либо создаете новую коллекцию на сервере, либо Замена коллекция на сервере с новой обновляемой коллекцией.

Пример замены коллекции на сервере:

Представьте, что вы делаете запрос PUT на свой сервер, а RequestBody содержит сериализованную сущность Movie, но эта сущность Movie не содержит актеров, потому что вы их опустили! Позже в будущем вы реализуете новую функцию, которая позволяет пользователям редактировать и исправлять орфографические ошибки в описании фильма, и вы используете PUT для отправки сущности фильма обратно на сервер и обновляете базу данных.

Но, скажем так - потому что прошло так много времени с тех пор, как вы добавили JSONIgnore к своим объектам - вы забыли, что некоторые поля являются необязательными. На стороне клиента вы забыли включить коллекцию актеров, и теперь ваш пользователь случайно переписывает Фильм A с актерами B, C и D, с Фильмом A без каких-либо актеров!

Почему JSONIgnore подписался?

Разумеется, намерение заставить вас отказаться от обязательного заполнения определенных полей именно для того, чтобы избежать подобных проблем с целостностью данных. В мире, где вы не используете JSONIgnore, вы гарантируете, что ваши данные никогда не будут заменены частичными данными, если вы не Явно установите эти данные самостоятельно. С JSONIgnore вы удаляете эти гарантии.

При этом JSONIgnore очень ценен, и я использую его точно так же, чтобы уменьшить размер полезной нагрузки, отправляемой клиенту. Однако я начинаю переосмысливать эту стратегию и вместо этого выбираю ту, в которой я использую классы POJO в отдельном слое для отправки данных во внешний интерфейс, чем то, что я использую для взаимодействия с базой данных.

Возможно, лучшая настройка ?:

По моему опыту, связанному с этой конкретной проблемой, идеальная установка - использовать инжектор Constructor для объектов Entity вместо сеттеров. Заставьте себя передавать каждый параметр во время создания экземпляра, чтобы ваши сущности никогда не были частично заполнены. Если вы попытаетесь частично заполнить их, компилятор не даст вам сделать то, о чем вы можете сожалеть.

Для отправки данных на клиентскую сторону, где вы можете пропустить определенные фрагменты данных, вы можете использовать отдельную отдельную сущность POJO или использовать JSONObject из org.json.

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

Хотя эта установка потребует дополнительных затрат на обслуживание и сложностей, вы получите мощное преимущество: компилятор Java будет у тебя спина! Это не позволит вам или даже несчастному коллеге сделать что-то в коде, что может поставить под угрозу данные в хранилище данных. Если вы попытаетесь создать сущность на лету в слое модели, вы будете вынуждены использовать конструктор и предоставить все данные. Если у вас нет всех данных и вы не можете создать экземпляр объекта, то вам нужно либо передать пустые значения (которые должны сигнализировать вам о красном флаге), либо сначала получить эти данные из хранилища данных.

Спасибо за подробный ответ. Как я и думал, одним из решений является использование аннотации JsonIgnore или, например, Джексон миксин, или иметь отдельный объект для каждой сущности, который будет иметь только необходимые поля для отправки клиенту. Zeljko
Пожалуйста! Удачи jmort253
0

sonIgnore, но также использовать сущности / POJO для использования в вызовах JSON.

После долгих поисков я пришел к решению автоматически извлекать игнорируемые поля из базы данных при каждом вызове объектного преобразователя.

Конечно, есть некоторые требования, которые необходимы для этого решения. Как будто вы должны использовать репозиторий, но в моем случае это работает так, как мне нужно.

Чтобы это работало, вам нужно убедиться, что ObjectMapper в MappingJackson2HttpMessageConverter перехвачен и поля, помеченные @JsonIgnore, заполнены. Поэтому нам нужен наш собственный компонент MappingJackson2HttpMessageConverter:

<p></p>

<pre><code>public class MvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        for (HttpMessageConverter converter : converters) {
            if (converter instanceof MappingJackson2HttpMessageConverter) {
                ((MappingJackson2HttpMessageConverter)converter).setObjectMapper(objectMapper());
            }
        }
    }

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new FillIgnoredFieldsObjectMapper();
        Jackson2ObjectMapperBuilder.json().configure(objectMapper);

        return objectMapper;
    }
}
</code></pre>

Each JSON request is than converted into an object by our own objectMapper, which fills the ignored fields by retrieving them from the repository:



    /**
     * Created by Sander Agricola on 18-3-2015.
     *
     * When fields or setters are marked as @JsonIgnore, the field is not read from the JSON and thus left empty in the object
     * When the object is a persisted entity it might get stored without these fields and overwriting the properties
     * which where set in previous calls.
     *
     * To overcome this property entities with ignored fields are detected. The same object is than retrieved from the
     * repository and all ignored fields are copied from the database object to the new object.
     */
    @Component
    public class FillIgnoredFieldsObjectMapper extends ObjectMapper {
        final static Logger logger = LoggerFactory.getLogger(FillIgnoredFieldsObjectMapper.class);

        @Autowired
        ListableBeanFactory listableBeanFactory;

        @Override
        protected Object _readValue(DeserializationConfig cfg, JsonParser jp, JavaType valueType) throws IOException, JsonParseException, JsonMappingException {
            Object result = super._readValue(cfg, jp, valueType);
            fillIgnoredFields(result);

            return result;
        }

        @Override
        protected Object _readMapAndClose(JsonParser jp, JavaType valueType) throws IOException, JsonParseException, JsonMappingException {
            Object result = super._readMapAndClose(jp, valueType);
            fillIgnoredFields(result);

            return result;
        }

        /**
         * Find all ignored fields in the object, and fill them with the value as it is in the database
         * @param resultObject Object as it was deserialized from the JSON values
         */
        public void fillIgnoredFields(Object resultObject) {
            Class c = resultObject.getClass();
            if (!objectIsPersistedEntity(c)) {
                return;
            }

            List ignoredFields = findIgnoredFields(c);
            if (ignoredFields.isEmpty()) {
                return;
            }

            Field idField = findIdField(c);
            if (idField == null || getValue(resultObject, idField) == null) {
                return;
            }

            CrudRepository repository = findRepositoryForClass(c);
            if (repository == null) {
                return;
            }

            //All lights are green: fill the ignored fields with the persisted values
            fillIgnoredFields(resultObject, ignoredFields, idField, repository);
        }

        /**
         * Fill the ignored fields with the persisted values
         *
         * @param object Object as it was deserialized from the JSON values
         * @param ignoredFields List with fields which are marked as JsonIgnore
         * @param idField The id field of the entity
         * @param repository The repository for the entity
         */
        private void fillIgnoredFields(Object object, List ignoredFields, Field idField, CrudRepository repository) {
            logger.debug("Object {} contains fields with @JsonIgnore annotations, retrieving their value from database", object.getClass().getName());

            try {
                Object storedObject = getStoredObject(getValue(object, idField), repository);
                if (storedObject == null) {
                    return;
                }

                for (Field field : ignoredFields) {
                    field.set(object, getValue(storedObject, field));
                }
            } catch (IllegalAccessException e) {
                logger.error("Unable to fill ignored fields", e);
            }
        }

        /**
         * Get the persisted object from database.
         *
         * @param id The id of the object (most of the time an int or string)
         * @param repository The The repository for the entity
         * @return The object as it is in the database
         * @throws IllegalAccessException
         */
        @SuppressWarnings("unchecked")
        private Object getStoredObject(Object id, CrudRepository repository) throws IllegalAccessException {
            return repository.findOne((Serializable)id);
        }

        /**
         * Get the value of a field for an object
         *
         * @param object Object with values
         * @param field The field we want to retrieve
         * @return The value of the field in the object
         */
        private Object getValue(Object object, Field field) {
            try {
                field.setAccessible(true);
                return field.get(object);
            } catch (IllegalAccessException e) {
                logger.error("Unable to access field value", e);
                return null;
            }
        }

        /**
         * Test if the object is a persisted entity
         * @param c The class of the object
         * @return true when it has an @Entity annotation
         */
        private boolean objectIsPersistedEntity(Class c) {
            return c.isAnnotationPresent(Entity.class);
        }

        /**
         * Find the right repository for the class. Needed to retrieve the persisted object from database
         *
         * @param c The class of the object
         * @return The (Crud)repository for the class.
         */
        private CrudRepository findRepositoryForClass(Class c) {
            return (CrudRepository)new Repositories(listableBeanFactory).getRepositoryFor(c);
        }

        /**
         * Find the Id field of the object, the Id field is the field with the @Id annotation
         *
         * @param c The class of the object
         * @return the id field
         */
        private Field findIdField(Class c) {
            for (Field field : c.getDeclaredFields()) {
                if (field.isAnnotationPresent(Id.class)) {
                    return field;
                }
            }

            return null;
        }

        /**
         * Find a list of all fields which are ignored by json.
         * In some cases the field itself is not ignored, but the setter is. In this case this field is also returned.
         *
         * @param c The class of the object
         * @return List with ignored fields
         */
        private List findIgnoredFields(Class c) {
            List ignoredFields = new ArrayList();
            for (Field field : c.getDeclaredFields()) {
                //Test if the field is ignored, or the setter is ignored.
                //When the field is ignored it might be overridden by the setter (by adding @JsonProperty to the setter)
                if (fieldIsIgnored(field) ? setterDoesNotOverrideIgnore(field) : setterIsIgnored(field)) {
                    ignoredFields.add(field);
                }
            }
            return ignoredFields;
        }

        /**
         * @param field The field we want to retrieve
         * @return True when the field is ignored by json
         */
        private boolean fieldIsIgnored(Field field) {
            return field.isAnnotationPresent(JsonIgnore.class);
        }

        /**
         * @param field The field we want to retrieve
         * @return true when the setter is ignored by json
         */
        private boolean setterIsIgnored(Field field) {
            return annotationPresentAtSetter(field, JsonIgnore.class);
        }

        /**
         * @param field The field we want to retrieve
         * @return true when the setter is NOT ignored by json, overriding the property of the field.
         */
        private boolean setterDoesNotOverrideIgnore(Field field) {
            return !annotationPresentAtSetter(field, JsonProperty.class);
        }

        /**
         * Test if an annotation is present at the setter of a field.
         *
         * @param field The field we want to retrieve
         * @param annotation The annotation looking for
         * @return true when the annotation is present
         */
        private boolean annotationPresentAtSetter(Field field, Class annotation) {
            try {
                Method setter = getSetterForField(field);
                return setter.isAnnotationPresent(annotation);
            } catch (NoSuchMethodException e) {
                return false;
            }
        }

        /**
         * Get the setter for the field. The setter is found based on the name with "set" in front of it.
         * The type of the field must be the only parameter for the method
         *
         * @param field The field we want to retrieve
         * @return Setter for the field
         * @throws NoSuchMethodException
         */
        @SuppressWarnings("unchecked")
        private Method getSetterForField(Field field) throws NoSuchMethodException {
            Class c = field.getDeclaringClass();
            return c.getDeclaredMethod(getSetterName(field.getName()), field.getType());
        }

        /**
         * Build the setter name for a fieldName.
         * The Setter name is the name of the field with "set" in front of it. The first character of the field
         * is set to uppercase;
         *
         * @param fieldName The name of the field
         * @return The name of the setter
         */
        private String getSetterName(String fieldName) {
            return String.format("set%C%s", fieldName.charAt(0), fieldName.substring(1));
        }
    }

Maybe not the most clean solution in all cases, but in my case it does the trick just the way I want it to work.

Похожие вопросы