2012-08-07 2 views
3

실제 질문/문제 즉시 답변을 드리겠습니다. HttpMessageConverter 내부의 컨트롤러 처리기 메서드에서 주석에 액세스 할 수있는 방법이 있습니까? 나는 대답이 no (Spring의 소스 코드를 살펴본 후)라고 확신한다.Jackson 믹스를 MappingJacksonHttpMessageConverter 및 Spring MVC와 함께 사용

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<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을 사용했다. 사용자 지정 반환 값 처리기가 마지막에 트리거되므로 순서를 재정의하려면 프로그래밍 방식 웹 구성을 만들어야했습니다. 나는 그것들을 디폴트 이전에 트리거 할 필요가 있었다.

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

이렇게하면 내 대답보다 나은 방향으로 누군가가 인도되기를 바랍니다.

이렇게하면 모든 객체를 JSON으로 직접 직렬화 할 수있었습니다. @RequestMapping에서 produce = "application/json"을 얻은 경우에는 항상 JSON으로 반환 값을 serialize합니다.

나는 HandlerMethodArgumentResolver을 사용하는 것을 제외하고 매개 변수 바인딩에 대해 동일한 작업을 수행했습니다. 선택한 주석으로 클래스에 주석을 달아주세요. (모델에 직렬화하기 때문에 JPA @Entity를 사용했습니다).

이제 boilerplater 코드가 필요없이 Spring 컨트롤러에서 POJO에서 JSON으로의 직렬화를 완벽하게 처리 할 수 ​​있습니다.

보너스 : 매개 변수의 @Id 태그를 확인하는 인수 확인자가 JSON에 ID 용 키가 포함되어 있으면 엔티티가 검색되고 JSON이 지속 된 객체에 적용됩니다. Bam.

/** 
* 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으로 직접 등록 할 수 있습니다. 따라서, 봄의 설명서 -

/** 
* 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 인터페이스를 살펴보면, 나는 그것이 핸들러 메소드가 어떻게 작동하는지에 일부 기능을 확장하는 데 필요한 모든 인수를 포함 것을 알고 있었다. resolveModelAndView의 모든 맛있는 인수에

public interface ModelAndViewResolver { 

    ModelAndView UNRESOLVED = new ModelAndView(); 

    ModelAndView resolveModelAndView(Method handlerMethod, 
      Class handlerType, 
      Object returnValue, 
      ExtendedModelMap implicitModel, 
      NativeWebRequest webRequest); 
} 

봐! 나는 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; 
    } 

} 

위에서 사용하는 유일한 사용자 정의 클래스가 mixins라는 하나 개의 매개 변수를 포함하는 내 주석 클래스 @Json입니다. 다음은 컨트롤러 측에서 구현하는 방법입니다.

@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으로 자동 변환하고 주석이 달린 믹스 인을 적용합니다.

하나의 "아래쪽"(이것을 부르면)은 스프링 2로 되돌아 가야합니다.새로운 3.0 태그가 ModelAndViewResolver를 직접 설정하는 것을 허용하지 않기 때문에 이것을 구성하는 5 가지 방법. 어쩌면 그들은 단지 이것을 간과 했는가?

하여 이전 구성 (사용 스프링 3.1 스타일)

<mvc:annotation-driven /> 

나의 새로운 구성 (사용 스프링 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(); 
} 
관련 문제