回到实体REST API和Spring(Returning entities in Rest API

2019-07-29 13:02发布

创建于春天的web应用程序中的宁静API是相当容易的。 比方说,我们有一个电影的实体,与流派的名称,年份,列表和演员的名单。 为了返回JSON格式的所有电影的列表,我们只是创造一些控制器的方法,将查询数据库,并返回一个列表作为ResponseEntity的身体。 春天会奇迹般地序列化,所有的伟大工程:)

但是,如果我在某些情况下,想要什么演员该列表中的影片被序列化,而不是其他? 而在其他一些情况下,旁边的电影类的领域,我需要添加一些其他的特性,每部电影的名单,这是动态生成的值?

我目前的解决方案是使用@JsonIgnore在某些领域或创建与像在电影类和增加必要的字段的字段一个MovieResponse类,并从电影转换每次MovieResponse类。

有一个更好的方法吗?

Answer 1:

在JSONIgnore标注的点是告诉DispatcherServlet的(或任何组件在弹簧手柄呈现响应)忽略某些领域,如果这些字段为空或以其他方式省略。

这可以在你暴露在某些情况下在客户端什么样的数据方面为您提供一定的灵活性。

缺点JSONIgnore:

然而,也有一些缺点与使用此批注,我已经在我自己的项目最近遇到。 这主要适用于PUT方法和情况下,您的控制器的数据序列化的对象是用来存储在数据库中的数据相同的对象。

PUT方法意味着你要么在服务器上创建一个新的集合或与您要更新新的集合在服务器上更换的集合。

在服务器上更换收集的例子:

试想一下,你正在做一个PUT请求到服务器,并RequestBody包含序列电影的实体,但是这个电影的实体不包含演员,因为你忽略他们! 后来在路上,你实现一个新功能,允许用户在影片描述编辑和纠正拼写错误,并使用投入到电影实体发送回服务器,并更新数据库。

但是,让我们说 - 因为它已经这么久以来你添加JSONIgnore到你的对象 - 你已经忘记了某些字段是可选的。 在客户端,你忘了,包括演员的收集,现在你的用户意外覆写电影A与演员B,C,和d,与电影A,没有任何演员!

为什么JSONIgnore选择在?

按理说这背后逼你退出需要做某些领域的意图正是从而避免这些类型的数据完整性问题。 在你不使用JSONIgnore的世界,你能保证你的数据就不会与部分数据被替换,除非你明确自己设定的数据。 随着JSONIgnore,删除这些保障措施。

随着中说,JSONIgnore是非常宝贵的,我用它自己在完全相同的方式,以减少向客户端发送有效载荷的大小。 不过,我开始重新考虑这一策略,而是选择一个,我使用POJO类在一个单独的层将数据发送到前端比我用它来与数据库进行交互。

可能更好的设置?:

比较理想的配置 - 从我的经验,处理这方面的问题 - 是使用构造函数注入你的实体对象,而不是制定者。 强迫自己必须在实例化时在每一个参数来传递,使您的实体从不部分填充。 如果尝试部分填充它们,编译器阻止你做一些你可能会后悔。

为了将数据发送到客户端,在那里你可以省掉数据的某些部分,你可以使用一个单独的,断开的实体POJO,或从org.json使用一个JSONObject。

当从客户端发送数据到服务器,您的前端实体对象从模型数据库层接收数据,部分或完全的,因为你真的不关心,如果前端获得的部分数据。 但随后在数据存储中存储数据时,你会首先读取从数据库中已经存储的对象,更新它的属性,然后将它存回的数据存储。 换句话说,如果人失踪演员,这不会有问题,因为你是从数据库中更新对象已分配给它的属性的演员。 因此,你只能更换您明确打算更换的字段。

虽然会有更多的维护开销和复杂性这种设置,您将获得一个强大的优势:Java编译器会支持你! 它不会让你甚至倒霉同事做,可能在数据存储危及数据的代码什么。 如果您尝试创建在模型层飞行的实体,你将被迫使用构造,并被迫提供所有的数据。 如果你没有所有的数据并不能实例化对象,那么你要么需要通过空值(这应该预示着红旗给你),或首先提取从数据存储的数据。



Answer 2:

我就遇到了这个问题,真的想继续使用@JsonIgnore,还利用实体/ POJO的在JSON呼叫使用。

大量挖掘后,我想出了自动从数据库中检索的忽略的字段,在对象映射器的每一个电话的解决方案。

Ofcourse有需要哪些该解决方案的一些要求。 就像你要使用的存储库,但对我来说这没有问题,我需要它的方式。

对于这个工作,你需要确保ObjectMapper在MappingJackson2HttpMessageConverter被拦截并标有@JsonIgnore的字段填写。 因此,我们需要我们自己的MappingJackson2HttpMessageConverter豆:

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

也许不是在所有情况下,最干净的解决方案,但在我的情况下,它的伎俩只是我希望它的工作方式。



文章来源: Returning entities in Rest API with Spring