2012-08-07 56 views
3

我会马上得到我真正的问题/问题,是否有任何方法可以访问HttpMessageConverter中的控制器处理程序方法的注释?我很肯定答案是否定的(在浏览Spring的源代码之后)。使用Jackson Mixins和MappingJacksonHttpMessageConverter&Spring MVC

是否有其他方式使用Jackson Mixins配对使用MappingJacksonHttpMessageConverter?我已经实现了我自己的基于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<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 principle)。或者直接注释它们的数据对象(没有解耦或关于如何暴露对象的多重配置)。

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

回答

2

张贴下面的答案后,我改变了我是如何做到这一点。我用了一个HandlerMethodReturnValueHandle r。我必须创建一个编程Web配置来覆盖订单,因为最后触发自定义返回值处理程序。我需要他们在默认值之前触发。

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

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

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

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

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

Bonus:我的参数解析器会检查参数的@Id标记,如果JSON包含一个Id的键,那么实体被检索并且JSON被应用到持久化对象。巴姆。

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

} 
3

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

我使用ModelAndViewResolver解决了这个问题。您可以直接在AnnotationMethodHandlerAdapter上注册,并知道在默认处理之前他们总是先踢入。因此,Spring的文档 -

/** 
* Set a custom ModelAndViewResolvers to use for special method return types. 
* <p>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界面,我知道,它包含了扩展一些功能到处理方法是如何工作所需的所有参数。

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<Class, Class> 
    * 
    * @param jsonFilter Json annotation 
    * @return Map of Target -> Mixin classes 
    */ 
    protected Map<Class<?>, Class<?>> getMixins(Json jsonFilter) { 

     Map<Class<?>, Class<?>> mixins = new HashMap<Class<?>, 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<MyTargetObject> getListOfFoo(@PathVariable("id") Integer id) { 
     return MyServiceImpl.getInstance().getBarObj(id).getFoos(); 
    } 
} 

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

这个“不利的一面”(如果你这样称呼的话)就是不得不恢复到Spring 2。因为新的3.0标签不允许直接配置ModelAndViewResolver,所以可以采用5种方法来配置它。也许他们只是忽略了这一点?

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

<mvc:annotation-driven /> 

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

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> 
    <property name="customModelAndViewResolvers"> 
     <list> 
      <bean class="my.package.mvc.JsonModelAndViewResolver" /> 
     </list> 
    </property> 
</bean> 

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

这里的定制注释:

的Json

@Target({ElementType.METHOD}) 
@Retention(RetentionPolicy.RUNTIME) 
public @interface Json { 

    /** 
    * A list of Jackson Mixins. 
    * <p> 
    * {@link http://wiki.fasterxml.com/JacksonMixInAnnotations} 
    */ 
    JsonMixin[] mixins() default {}; 

} 

JsonMixin

public @interface JsonMixin { 
    public Class<? extends Serializable> target(); 
    public Class<?> mixin(); 
}