5

在 Spring 中为 web 应用程序创建一个 restful api 非常容易。假设我们有一个 Movie 实体,带有名称、年份、类型列表和演员列表。为了以 json 格式返回所有电影的列表,我们只需在某个控制器中创建一个方法,该方法将查询数据库并返回一个列表作为 ResponseEntity 的主体。Spring 会神奇地序列化它,一切都很好:)

但是,如果我,在某些情况下,希望电影中的演员列表被连载,而不是在其他的?在其他情况下,除了电影类的字段,我需要为列表中的每部电影添加一些其他属性,哪些值是动态生成的?

我目前的解决方案是在某些字段上使用 @JsonIgnore 或创建一个包含 Movie 类中的字段和所需的其他字段的 MovieResponse 类,并且每次都从 Movie 转换为 MovieResponse 类。

有一个更好的方法吗?

4

2 回答 2

2

JSONIgnore 注释的重点是告诉 DispatcherServlet(或 Spring 中处理呈现响应的任何组件)如果这些字段为空或以其他方式省略,则忽略某些字段。

这可以为您在某些情况下向客户端公开哪些数据方面提供一些灵活性。

JSONIgnore 的缺点:

但是,我最近在自己的项目中遇到了使用此注释的一些缺点。这主要适用于 PUT 方法以及控制器将数据序列化到的对象与用于在数据库中存储该数据的对象相同的情况。

PUT 方法意味着您要么在服务器上创建一个新集合,要么将服务器上的一个集合替换为您正在更新的新集合。

替换服务器上的集合的示例:

想象一下,您正在向您的服务器发出 PUT 请求,并且 RequestBody 包含一个序列化的 Movie 实体,但该 Movie 实体不包含任何演员,因为您省略了它们!稍后,您实现了一项新功能,允许您的用户编辑和更正电影描述中的拼写错误,您使用 PUT 将电影实体发送回服务器,并更新数据库。

但是,让我们这么说——因为您已经很久没有向对象添加 JSONIgnore 了——您忘记了某些字段是可选的。在客户端,您忘记包含演员集合,现在您的用户不小心用演员 B、C 和 D 覆盖了电影 A,而电影 A 没有任何演员!

为什么选择加入 JSONIgnore?

按理说,强迫您选择不填写某些字段的意图正是为了避免这些类型的数据完整性问题。在您不使用 JSONIgnore 的世界中,您可以保证您的数据永远不会被部分数据替换,除非您自己明确设置该数据。使用 JSONIgnore,您可以删除这些保护措施。

话虽如此,JSONIgnore 非常有价值,我自己以完全相同的方式使用它来减少发送到客户端的有效负载的大小。但是,我开始重新考虑这种策略,而是选择在一个单独的层中使用 POJO 类来将数据发送到前端,而不是我用来与数据库交互的策略。

可能更好的设置?:

理想的设置——根据我处理这个特定问题的经验——是为你的实体对象使用构造函数注入而不是设置器。强迫自己在实例化时必须传入每个参数,这样您的实体就不会被部分填充。如果您尝试部分填充它们,编译器会阻止您做一些您可能会后悔的事情。

为了将数据发送到客户端,您可能希望省略某些数据,您可以使用单独的、断开连接的实体 POJO,或使用 org.json 中的 JSONObject。

当从客户端向服务器发送数据时,您的前端实体对象从模型数据库层接收部分或全部数据,因为您并不关心前端是否获取部分数据。但是,当在数据存储中存储数据时,您将首先从数据存储中获取已存储的对象,更新其属性,然后将其存储回数据存储中。换句话说,如果您缺少演员,那也没关系,因为您从数据存储更新的对象已经将演员分配给它的属性。因此,您只需替换您明确打算替换的字段。

虽然此设置会有更多的维护开销和复杂性,但您将获得一个强大的优势:Java 编译器将为您提供支持!它不会让您甚至倒霉的同事在代码中做任何可能危及数据存储中数据的事情。如果您尝试在模型层中动态创建实体,您将被迫使用构造函数,并被迫提供所有数据。如果您没有所有数据并且无法实例化对象,那么您需要传递空值(这应该向您发出危险信号)或首先从数据存储中获取该数据。

于 2012-05-25T01:53:55.167 回答
0

我遇到了这个问题,真的很想继续使用@JsonIgnore,但也使用实体/POJO 在 JSON 调用中使用。

经过大量挖掘后,我想出了在每次调用对象映射器时自动从数据库中检索被忽略字段的解决方案。

当然,此解决方案需要一些要求。就像您必须使用存储库一样,但在我的情况下,这正是我需要的方式。

为此,您需要确保 MappingJackson2HttpMessageConverter 中的 ObjectMapper 被截获,并且标有 @JsonIgnore 的字段已填充。因此我们需要我们自己的 MappingJackson2HttpMessageConverter bean:

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;
    }
}

每个 JSON 请求都由我们自己的 objectMapper 转换为一个对象,它通过从存储库中检索忽略的字段来填充它们:



    /**
     * 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));
        }
    }

在所有情况下,也许不是最干净的解决方案,但在我的情况下,它可以按照我希望它工作的方式来解决问题。

于 2015-03-18T13:18:19.327 回答