将Jackson Mixins与MappingJacksonHttpMessageConverter和Spring MVC结合使用


问题内容

我将立即解决我的真实问题/问题, 是否可以通过HttpMessageConverter访问控制器的处理程序方法上的注释
?我很确定答案是否定的(在浏览完Spring的源代码之后)。

使用MappingJacksonHttpMessageConverter时,还有其他使用配对的Jackson

Mixins的方法吗?我已经基于MappingJacksonHttpMessageConverter实现了自己的HttpMessageConverter,以“升级”它以使用Jackson
2.0。

控制器类

@Controller
public class Controller {

    @JsonFilter({ @JsonMixin(target=MyTargetObject.class, mixin=MyTargetMixin.class) })
    @RequestMapping(value="/my-rest/{id}/my-obj", method=RequestMethod.GET, produces="application/json")
    public @ResponseBody List<MyTargetObject> getListOfFoo(@PathVariable("id") Integer id) {
        return MyServiceImpl.getInstance().getBarObj(id).getFoos();
    }
}

@JsonFilter 是我希望传递给映射器的自定义注释,然后可以将其直接直接自动馈送到ObjectMapper。

MappingJacksonHttpMessageConverter.class

public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

    ...

    @Override
    protected void writeInternal(Object object, HttpOutputMessage outputMessage) {

            //Obviously, no access to the HandlerMethod here.

    }

    ...
}

我一直在寻找这个答案。到目前为止,我只看到人们在Controller的处理方法内部将对象序列化为JSON(在每种方法中都反复违反DRY原理)。或直接注释其数据对象(无需解耦或如何公开对象的多种配置)。

可能是无法在HttpMessageConverter中完成。还有其他选择吗?拦截器可以访问HandlerMethod,但不能访问处理程序方法的返回对象。


问题答案:

在发布以下答案后,我更改了操作方式。我用了一个HandlerMethodReturnValueHandle
我必须创建一个程序化的Web配置来覆盖该顺序,因为自定义返回值处理程序是最后触发的。我需要在默认设置之前触发它们。

@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
   ...
}

希望这将使某人朝比我在下面的答案更好的方向前进。

这使我可以将任何对象直接序列化为JSON。在@RequestMapping中产生了=“ application /
json”之后,我总是将返回值序列化为JSON。

除了使用之外,我对参数绑定也做了同样的事情HandlerMethodArgumentResolver。只需使用您选择的注释为您的类添加注释(我使用了JPA
@Entity,因为我通常会序列化成模型)。

现在,您可以在Spring控制器中实现无缝POJO到JSON反序列化,而无需任何样板程序代码。

奖励:我使用的参数解析器将检查参数的@Id标记,如果JSON包含ID的键,则将检索实体并将JSON应用于持久对象。am

/**
 * De-serializes JSON to a Java Object.
 * <p>
 * Also provides handling of simple data type validation.  If a {@link JsonMappingException} is thrown then it
 * is wrapped as a {@link ValidationException} and handled by the MVC/validation framework.
 *
 * @author John Strickler
 * @since 2012-08-28
 */
public class EntityArgumentResolver implements HandlerMethodArgumentResolver {

    @Autowired
    private SessionFactory sessionFactory;

    private final ObjectMapper objectMapper = new ObjectMapper();

    private static final Logger log = Logger.getLogger(EntityArgumentResolver.class);

    //whether to log the incoming JSON
    private boolean doLog = false;

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().getAnnotation(Entity.class) != null;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
        String requestBody = IOUtils.toString(request.getReader());
        Class<?> targetClass = parameter.getParameterType();
        Object entity = this.parse(requestBody, targetClass);
        Object entityId = getId(entity);

        if(doLog) {
            log.info(requestBody);
        }

        if(entityId != null) {
            return copyObjectToPersistedEntity(entity, getKeyValueMap(requestBody), entityId);
        } else {
            return entity;
        }
    }


    /**
     * @param rawJson a json-encoded string
     * @return a {@link Map} consisting of the key/value pairs of the JSON-encoded string
     */
    @SuppressWarnings("unchecked")
    private Map<String, Object> getKeyValueMap(String rawJson) throws JsonParseException, JsonMappingException, IOException {
        return objectMapper.readValue(rawJson, HashMap.class);
    }


    /**
     * Retrieve an existing entity and copy the new changes onto the entity.
     *
     * @param changes a recently deserialized entity object that contains the new changes
     * @param rawJson the raw json string, used to determine which keys were passed to prevent
     *                copying unset/null values over to the persisted entity
     * @return the persisted entity with the new changes copied onto it
     * @throws NoSuchMethodException
     * @throws SecurityException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     * @throws IllegalArgumentException
     */
    private Object copyObjectToPersistedEntity(Object changesObject, Map<String, Object> changesMap, Object id) throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException, InvocationTargetException {

        Session session = sessionFactory.openSession();

        Object persistedObject =
                session.get(changesObject.getClass(), (Serializable) id);

        session.close();

        if(persistedObject == null) {
            throw new ValidationException(changesObject.getClass().getSimpleName() + " #" + id + " not found.");
        }

        Class<?> clazz = persistedObject.getClass();

        for(Method getterMethod : ReflectionUtils.getAllDeclaredMethods(clazz)) {

            Column column = getterMethod.getAnnotation(Column.class);

            //Column annotation is required
            if(column == null) {
                continue;
            }

            //Is the field allowed to be updated?
            if(!column.updatable()) {
                continue;
            }

            //Was this change a part of JSON request body?
            //(prevent fields false positive copies when certain fields weren't included in the JSON body)
            if(!changesMap.containsKey(BeanUtils.toFieldName(getterMethod))) {
                continue;
            }

            //Is the new field value different from the existing/persisted field value?
            if(ObjectUtils.equals(getterMethod.invoke(persistedObject), getterMethod.invoke(changesObject))) {
                continue;
            }

            //Copy the new field value to the persisted object
            log.info("Update " + clazz.getSimpleName() + "(" + id + ") [" + column.name() + "]");

            Object obj = getterMethod.invoke(changesObject);

            Method setter = BeanUtils.toSetter(getterMethod);

            setter.invoke(persistedObject, obj);

        }

        return persistedObject;
    }


    /**
     * Check if the recently deserialized entity object was populated with its ID field
     *
     * @param entity the object
     * @return an object value if the id exists, null if no id has been set
     */
    private Object getId(Object entity) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException {

        for(Method method : ReflectionUtils.getAllDeclaredMethods(entity.getClass())) {
            if(method.getAnnotation(Id.class) != null) {
                method.setAccessible(true);
                return method.invoke(entity);
            }
        }

        return null;
    }


    private <T> T parse(String json, Class<T> clazz) throws JsonParseException, IOException {
        try {
            return objectMapper.readValue(json, clazz);
        } catch(JsonMappingException e) {
            throw new ValidationException(e);
        }
    }

    public void setDoLog(boolean doLog) {
        this.doLog = doLog;
    }

}