使用Jackson Mixins和MappingJacksonHttpMessageConverter和Spring MVC

我马上就会得到我真正的问题/问题, 有没有办法在HttpMessageConverter中访问控制器处理程序方法的注释 ? 我很确定答案是否定的(在浏览Spring的源代码之后)。

使用MappingJacksonHttpMessageConverter时,有没有其他方法可以使用Jackson Mixins配对? 我已经基于MappingJacksonHttpMessageConverter实现了我自己的HttpMessageConverter来“升级”它以使用Jackson 2.0。

Controller.class

@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 getListOfFoo(@PathVariable("id") Integer id) { return MyServiceImpl.getInstance().getBarObj(id).getFoos(); } } 

@JsonFilter是我希望传递给mapper的自定义注释,然后可以自动将其直接提供给ObjectMapper。

MappingJacksonHttpMessageConverter.class

 public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConverter { ... @Override protected void writeInternal(Object object, HttpOutputMessage outputMessage) { //Obviously, no access to the HandlerMethod here. } ... } 

我为这个答案进行了广泛的搜索。 到目前为止,我只看到人们在Controller的处理方法中将他们的对象序列化为JSON(在每种方法中反复违反DRY原则 )。 或直接注释其数据对象(没有关于如何公开对象的解耦或多种配置)。

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

这不是理想的解决方案。 看我的第二个答案。

我使用ModelAndViewResolver解决了这个问题。 您可以直接使用AnnotationMethodHandlerAdapter注册这些,并且知道在默认处理发生之前它们将始终首先启动。 因此,Spring的文档 –

 /** * Set a custom ModelAndViewResolvers to use for special method return types. * 

Such a custom ModelAndViewResolver will kick in first, having a chance to resolve * a return value before the standard ModelAndView handling kicks in. */ public void setCustomModelAndViewResolver(ModelAndViewResolver customModelAndViewResolver) { this.customModelAndViewResolvers = new ModelAndViewResolver[] {customModelAndViewResolver}; }

看看ModelAndViewResolver接口,我知道它包含了将一些function扩展到处理程序方法工作方式所需的所有参数。

 public interface ModelAndViewResolver { ModelAndView UNRESOLVED = new ModelAndView(); ModelAndView resolveModelAndView(Method handlerMethod, Class handlerType, Object returnValue, ExtendedModelMap implicitModel, NativeWebRequest webRequest); } 

看看resolveModelAndView中所有那些美味的参数! 我几乎可以访问Spring知道的所有请求。 以下是我实现接口的方式与MappingJacksonHttpMessageConverter非常相似,除了以单向方式(向外):

 public class JsonModelAndViewResolver implements ModelAndViewResolver { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); public static final MediaType DEFAULT_MEDIA_TYPE = new MediaType("application", "json", DEFAULT_CHARSET); private boolean prefixJson = false; public void setPrefixJson(boolean prefixJson) { this.prefixJson = prefixJson; } /** * Converts Json.mixins() to a Map * * @param jsonFilter Json annotation * @return Map of Target -> Mixin classes */ protected Map, Class> getMixins(Json jsonFilter) { Map, Class> mixins = new HashMap, Class>(); if(jsonFilter != null) { for(JsonMixin jsonMixin : jsonFilter.mixins()) { mixins.put(jsonMixin.target(), jsonMixin.mixin()); } } return mixins; } @Override public ModelAndView resolveModelAndView(Method handlerMethod, Class handlerType, Object returnValue, ExtendedModelMap implicitModel, NativeWebRequest webRequest) { if(handlerMethod.getAnnotation(Json.class) != null) { try { HttpServletResponse httpResponse = webRequest.getNativeResponse(HttpServletResponse.class); httpResponse.setContentType(DEFAULT_MEDIA_TYPE.toString()); OutputStream out = httpResponse.getOutputStream(); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setMixInAnnotations(getMixins(handlerMethod.getAnnotation(Json.class))); JsonGenerator jsonGenerator = objectMapper.getJsonFactory().createJsonGenerator(out, JsonEncoding.UTF8); if (this.prefixJson) { jsonGenerator.writeRaw("{} && "); } objectMapper.writeValue(jsonGenerator, returnValue); out.flush(); out.close(); return null; } catch (JsonProcessingException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } return UNRESOLVED; } } 

上面使用的唯一自定义类是我的注释类@Json ,它包含一个名为mixins参数。 这是我在Controller端实现这一点的方法。

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

这是一些非常棒的简单。 ModelAndViewResolver会自动将返回对象转换为JSON并应用带注释的混合。

由于新的3.0标签不允许直接配置ModelAndViewResolver,因此必须恢复到Spring 2.5配置方式的“向下”(如果你称之为)。 也许他们只是忽略了这一点?

我的旧配置(使用Spring 3.1风格)

  

我的新配置(使用Spring 2.5风格

        

^^ 3.0+没有办法连接自定义ModelAndViewResolver。 因此,切换回旧式。

这是自定义注释:

JSON

 @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Json { /** * A list of Jackson Mixins. * 

* {@link http://wiki.fasterxml.com/JacksonMixInAnnotations} */ JsonMixin[] mixins() default {}; }

JsonMixin

 public @interface JsonMixin { public Class target(); public Class mixin(); } 

在发布下面的答案后,我改变了我的做法。 我使用了HandlerMethodReturnValueHandle r。 我必须创建一个程序化的Web配置来覆盖订单,因为最后会触发自定义返回值处理程序。 我需要在默认值之前触发它们。

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

希望这会导致某人的方向比我下面的答案更好。

这允许我将任何对象直接序列化为JSON。 在@RequestMapping中有produce =“application / json”,那么我总是将返回值序列化为JSON。

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

现在,您可以在Spring控制器中使用无缝的POJO到JSON de / serialization,而无需任何样板代码。

额外:我有的参数解析器将检查参数的@Id标签,如果JSON包含Id的密钥,则检索实体并将JSON应用于持久对象。 巴姆。

 /** * De-serializes JSON to a Java Object. * 

* 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 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 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 parse(String json, Class 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; } }