Using Jackson Mixins with MappingJacksonHttpMessageConverter & Spring MVC

I’ll immediately address my question / question, is there any way to access annotations in the controller handler method inside the HttpMessageConverter ? I am sure there is no answer (after going through Spring source code).

Is there any other way to use Jackson Mixins paired when using MappingJacksonHttpMessageConverter ? I have already implemented my own HttpMessageConverter based on MappingJacksonHttpMessageConverter to "upgrade" it to use 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 is a user annotation that I want to pass to a handler, which can then be passed directly to ObjectMapper.

MappingJacksonHttpMessageConverter.class

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

I searched all this for an answer. So far, I've seen people serialize their JSON object inside a controller control method (repeatedly violating the DRY principle in each method). Or annotate your data objects directly (without decoupling or several configurations, how to expose your objects).

Perhaps this cannot be done in the HttpMessageConverter. Are there any other options? Interceptors provide access to HandlerMethod, but not to the returned handler method object.

+3
source share
3 answers

After posting the answer below, I changed how I did it. I used HandlerMethodReturnValueHandle r. I had to create a web-based software configuration to override the order because return value handlers are returned last. I need them to start before defaults.

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

Hope this leads someone in a better direction than my answer below.

This allowed me to serialize any object directly into JSON. In @RequestMapping produces = "application / json", then I always serialize the return value in JSON.

I did the same for parameter binding, except that I used HandlerMethodArgumentResolver . Just comment on your classes with the annotation of your choice (I used JPA @Entity because I usually serialize in the model).

You now have seamless POJOs for JSON de / serialization in your Spring controllers without the need for boiler room code.

Bonus: the argument of the argument that I have will check the @Id tags for the parameter, if the JSON contains the key for the identifier, then the object is retrieved and the JSON is applied to the saved object. 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; } } 
+2
source

This is NOT an ideal solution. See my second answer.

I solved this using ModelAndViewResolver . You can register them directly using the AnnotationMethodHandlerAdapter with a perk, knowing that they will always kick first before processing by default. Hence the Spring documentation is

 /** * 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 looked at the ModelAndViewResolver interface, I knew that it contains all the arguments needed to extend some functions in how the handler works.

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

Take a look at all of these delicious arguments in resolveModelAndView ! I have access to almost everything Spring knows about the request. Here, as I implemented the interface, to act very similar to MappingJacksonHttpMessageConverter with the exception of unidirectional (outward):

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

The only user class used above is my @Json annotation @Json , which includes one parameter named mixins . Here, how I implement it on the controller side.

 @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(); } } 

This is a pretty pretty simplicity. ModelAndViewResolver automatically converts the returned object to JSON and also applies annotated mixes.

One “downside” (if you call it that) should be back to the Spring 2.5 configuration option, since the new 3.0 tag does not allow you to directly configure ModelAndViewResolver. Maybe they just missed it?

My old configuration (using Spring 3.1 style)

 <mvc:annotation-driven /> 

My new configuration (using Spring style 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+ does not have the ability to connect to a custom ModelAndViewResolver. Consequently, the transition back to the old style.

Here's the user annotations:

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(); } 
+3
source

I don’t know if this is due to the version of Spring that I am using (5), or I just did something wrong, but the answers here did not help me. I ended up registering RequestResponseBodyMethodProcessor with the desired ObjectMapper.

PushCard.getObjectMapperForDTO () is my method that returns an ObjectMapper that already has the correct Mixins. Obviously, you can use your own method, which sets it up the way you want.

My configuration class is as follows.

 @EnableWebMvc @Configuration public class SidekickApplicationConfiguration implements WebMvcConfigurer { private static Logger logger = LoggerFactory.getLogger(SidekickApplicationConfiguration.class); @Autowired private RequestMappingHandlerAdapter requestHandler; @Bean RequestResponseBodyMethodProcessor registerReturnValueHandler() { List<HttpMessageConverter<?>> messageConverters = new ArrayList<>(); messageConverters.add(new MappingJackson2HttpMessageConverter(PushCard.getObjectMapperForDTO())); logger.info("Registering RequestResponseBodyMethodProcessor with DTO ObjectMapper..."); RequestResponseBodyMethodProcessor r = new RequestResponseBodyMethodProcessor(messageConverters); requestHandler.setReturnValueHandlers(Arrays.asList(r)); return r; } } 
0
source

All Articles