简体   繁体   English

使用Spring在Rest API中返回实体

[英]Returning entities in Rest API with Spring

Creating a restful api for a web application in Spring is pretty easy. 在Spring中为Web应用程序创建一个宁静的api非常容易。 Let's say we have a Movie entity, with a name, year, list of genres and list of actors. 假设我们有一个电影实体,其中包含名称,年份,类型列表和演员列表。 In order to return a list of all movies in json format, we just create a method in some controller that will query a database and return a list as a body of ResponseEntity. 为了以json格式返回所有电影的列表,我们只是在某个控制器中创建一个方法,该方法将查询数据库并将列表作为ResponseEntity的主体返回。 Spring will magically serialize it, and all works great :) Spring将神奇地对其进行序列化,并且一切正常:)

But, what if I, in some case, want that list of actors in a movie to be serialized, and not in other? 但是,如果我在某些情况下希望序列化电影中的那个演员列表而不是其他电影中的那个演员,该怎么办? And in some other case, alongside the fields of the movie class, I need to add some other properties, for each movie in the list, which values are dynamically generated? 在其他情况下,除了电影类的字段外,我还需要为列表中的每个电影添加其他一些属性,这些属性是动态生成的?

My current solution is to use @JsonIgnore on some fields or to create a MovieResponse class with fields like in Movie class and additional fields that are needed, and to convert from Movie to MovieResponse class each time. 我当前的解决方案是在某些字段上使用@JsonIgnore或创建具有Movie类和其他所需字段等字段的MovieResponse类,并每次将Movie转换为MovieResponse类。

Is there a better way to do this? 有一个更好的方法吗?

The point of the JSONIgnore annotation is to tell the DispatcherServlet (or whatever component in Spring handles rendering the response) to ignore certain fields if those fields are null or otherwise omitted. JSONIgnore批注的要点是告诉DispatcherServlet(或Spring中处理响应的任何组件)忽略某些字段(如果这些字段为null或以其他方式省略)。

This can provide you with some flexibility in terms of what data you expose to the client in certain cases. 在某些情况下,这可以使您在向客户端公开哪些数据方面具有一定的灵活性。

Downside to JSONIgnore: JSONIgnore的缺点:

However, there are some downsides to using this annotation that I've recently encountered in my own projects. 但是,在我自己的项目中最近遇到的使用此批注存在一些缺点。 This applies mainly to the PUT method and cases where the object that your controller serializes data to is the same object that is used to store that data in the database. 这主要适用于PUT方法以及控制器将数据序列化到的对象与用于在数据库中存储该数据的对象相同的情况。

The PUT method implies that you're either creating a new collection on the server or are replacing a collection on the server with the new collection you're updating. PUT方法意味着您要么在服务器上创建一个新集合,要么用要更新的新集合替换服务器上的一个集合。

Example of Replacing a Collection on the server: 在服务器上替换集合的示例:

Imagine that you're making a PUT request to your server, and the RequestBody contains a serialized Movie entity, but this Movie entity contains no actors because you've omitted them! 假设您要向服务器发出PUT请求,并且RequestBody包含序列化的Movie实体,但是此Movie实体不包含任何actor,因为您已将它们省略了! Later on down the road, you implement a new feature that allows your users to edit and correct spelling errors in the Movie description, and you use PUT to send the Movie entity back to the server, and you update the database. 稍后,您将实现一项新功能,该功能使您的用户可以编辑和更正“影片”描述中的拼写错误,并使用PUT将“影片”实体发送​​回服务器,并更新数据库。

But, let's say that -- because it's been so long since you added JSONIgnore to your objects -- you've forgotten that certain fields are optional. 但是,可以这么说-因为自从向对象添加JSONIgnore以来已经很久了-您已经忘记了某些字段是可选的。 In the client side, you forget to include the collection of actors, and now your user accidentally overwrites Movie A with actors B, C, and D, with Movie A with no actors whatsoever! 在客户端,您忘记了包括演员的集合,现在您的用户意外地用演员B,C和D覆盖了电影A,而没有演员的电影A覆盖了电影A!

Why is JSONIgnore opt-in? 为什么选择JSONIgnore?

It stands to reason that the intention behind forcing you to opt-out of making certain fields required is precisely so that these types of data integrity issues are avoided. 可以合理地认为,迫使您选择退出某些必填字段的意图恰恰是为了避免此类数据完整性问题。 In a world where you're not using JSONIgnore, you guarantee that your data can never be replaced with partial data unless you explicitly set that data yourself. 在不使用JSONIgnore的世界中,除非您自己明确设置数据,否则您可以保证永远不会将数据替换为部分数据。 With JSONIgnore, you remove these safeguards. 使用JSONIgnore,您可以删除这些保护措施。

With that said, JSONIgnore is very valuable, and I use it myself in precisely the same manner to reduce the size of the payload sent to the client. 话虽如此,JSONIgnore非常有价值,我自己以完全相同的方式使用它来减小发送给客户端的有效负载的大小。 However, I'm beginning to rethink this strategy and instead opt for one where I use POJO classes in a separate layer for sending data to the frontend than what I use to interact with the database. 但是,我开始重新考虑这种策略,而是选择一种方法,即在单独的层中使用POJO类将数据发送到前端,而不是使用与数据库进行交互的方法。

Possible Better Setup?: 可能更好的设置?:

The ideal setup -- from my experience dealing with this particular problem -- is to use Constructor injection for your Entity objects instead of setters. 理想的设置(根据我对这个特定问题的处理经验)是对您的Entity对象(而不是setter)使用构造函数注入。 Force yourself to have to pass in every parameter at instantiation time so that your entities are never partially filled. 强制您自己必须在实例化时传递每个参数,以便您的实体永远不会被部分填充。 If you try to partially fill them, the compiler stops you from doing something you may regret. 如果尝试部分填充它们,编译器将阻止您执行可能会后悔的事情。

For sending data to the client side, where you may want to omit certain pieces of data, you could use a separate, disconnected entity POJO, or use a JSONObject from org.json. 为了将数据发送到客户端,您可能希望省略某些数据,可以使用单独的,断开连接的实体POJO,或使用org.json中的JSONObject。

When sending data from the client to the server, your frontend entity objects receive the data from the model database layer, partially or full, since you don't really care if the frontend gets partial data. 从客户端向服务器发送数据时,您的前端实体对象部分或全部接收来自模型数据库层的数据,因为您实际上并不关心前端是否获得部分数据。 But then when storing the data in the datastore, you would first fetch the already-stored object from the datastore, update its properties, and then store it back in the datastore. 但是,当将数据存储在数据存储区中时,您将首先从数据存储区中获取已存储的对象,更新其属性,然后再将其存储回数据存储区中。 In other words, if you were missing the actors, it wouldn't matter because the object you're updating from the datastore already has the actors assigned to it's properties. 换句话说,如果您缺少参与者,那没关系,因为您要从数据存储区更新的对象已经为其属性分配了参与者。 Thus, you only replace the fields that you explicitly intend to replace. 因此,仅替换您明确打算替换的字段。

While there would be more maintenance overhead and complexity to this setup, you would gain a powerful advantage: The Java compiler would have your back! 尽管此设置会有更多的维护开销和复杂性,但您将获得强大的优势:Java编译器将为您提供支持! It won't let you or even a hapless colleague do anything in the code that might compromise the data in the datastore. 它不会让您甚至是一个倒霉的同事都无法在代码中做任何可能破坏数据存储中数据的事情。 If you attempt to create an entity on the fly in your model layer, you'll be forced to use the constructor, and forced to provide all of the data. 如果尝试在模型层中动态创建实体,则将被迫使用构造函数,并被迫提供所有数据。 If you don't have all of the data and cannot instantiate the object, then you'll either need to pass empty values (which should signal a red flag to you) or fetch that data from the datastore first. 如果您没有所有数据并且无法实例化该对象,那么您要么需要传递空值(应该向您发出红色标记),要么首先从数据存储中获取该数据。

I ran into this problem, and really wanted to keep using @JsonIgnore, but also use the entities/POJO's to use in the JSON calls. 我遇到了这个问题,我真的想继续使用@JsonIgnore,但也要在JSON调用中使用实体/ POJO。

After a lot of digging I came up with the solution of automatically retrieving the ignored fields from the database, on every call of the object mapper. 经过大量的挖掘,我想到了在对象映射器的每次调用中自动从数据库中检索被忽略字段的解决方案。

Ofcourse there are some requirements which are needed for this solution. 当然,此解决方案需要一些要求。 Like you have to use the repository, but in my case this works just the way I need it. 就像您必须使用存储库一样,但就我而言,这正是我需要的方式。

For this to work you need to make sure the ObjectMapper in MappingJackson2HttpMessageConverter is intercepted and the fields marked with @JsonIgnore are filled. 为此,您需要确保MappingJackson2HttpMessageConverter中的ObjectMapper被拦截并且标有@JsonIgnore的字段被填充。 Therefore we need our own MappingJackson2HttpMessageConverter bean: 因此,我们需要自己的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;
    }
}

Each JSON request is than converted into an object by our own objectMapper, which fills the ignored fields by retrieving them from the repository: 然后,每个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));
        }
    }

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. 也许并非在所有情况下都是最干净的解决方案,但就我而言,它只是按照我想要的方式起作用。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM