Can you configure Spring controller specific Jackson deserialization?

后端 未结 5 822
误落风尘
误落风尘 2020-12-15 16:42

I need to add a custom Jackson deserializer for java.lang.String to my Spring 4.1.x MVC application. However all answers (such as this) refer to configuring the ObjectMapper

相关标签:
5条回答
  • 2020-12-15 17:29

    To have different deserialization configurations you must have different ObjectMapper instances but out of the box Spring uses MappingJackson2HttpMessageConverter which is designed to use only one instance.

    I see at least two options here:

    Move away from MessageConverter to an ArgumentResolver

    Create a @CustomRequestBody annotation, and an argument resolver:

    public class CustomRequestBodyArgumentResolver implements HandlerMethodArgumentResolver {
    
      private final ObjectMapperResolver objectMapperResolver;
    
      public CustomRequestBodyArgumentResolver(ObjectMapperResolver objectMapperResolver) {
        this.objectMapperResolver = objectMapperResolver;
      }
    
      @Override
      public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.getParameterAnnotation(CustomRequestBody.class) != null;
      }
    
      @Override
      public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        if (this.supportsParameter(methodParameter)) {
          ObjectMapper objectMapper = objectMapperResolver.getObjectMapper();
          HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
          return objectMapper.readValue(request.getInputStream(), methodParameter.getParameterType());
        } else {
          return WebArgumentResolver.UNRESOLVED;
        }
      }
    }
    

    @CustomRequestBody annotation:

    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface CustomRequestBody {
    
      boolean required() default true;
    
    }
    

    ObjectMapperResolver is an interface we will be using to resolve actual ObjectMapper instance to use, I will discuss it below. Of course if you have only one use case where you need custom mapping you can simply initialize your mapper here.

    You can add custom argument resolver with this configuration:

    @Configuration
    public class WebConfiguration extends WebMvcConfigurerAdapter {
    
      @Bean
      public CustomRequestBodyArgumentResolver customBodyArgumentResolver(ObjectMapperResolver objectMapperResolver) {
        return new CustomRequestBodyArgumentResolver(objectMapperResolver)
      } 
    
      @Override
      public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {       
        argumentResolvers.add(customBodyArgumentResolver(objectMapperResolver()));
      }
    }
    

    Note: Do not combine @CustomRequestBody with @RequestBody, it will be ignored.

    Wrap ObjectMapper in a proxy that hides multiple instances

    MappingJackson2HttpMessageConverter is designed to work with only one instance of ObjectMapper. We can make that instance a proxy delegate. This will make working with multiple mappers transparent.

    First of all we need an interceptor that will translate all method invocations to an underlying object.

    public abstract class ObjectMapperInterceptor implements MethodInterceptor {
    
      @Override
      public Object invoke(MethodInvocation invocation) throws Throwable {
        return ReflectionUtils.invokeMethod(invocation.getMethod(), getObject(), invocation.getArguments());
      } 
    
      protected abstract ObjectMapper getObject();
    
    }
    

    Now our ObjectMapper proxy bean will look like this:

    @Bean
    public ObjectMapper objectMapper(ObjectMapperResolver objectMapperResolver) {
      ProxyFactory factory = new ProxyFactory();
      factory.setTargetClass(ObjectMapper.class);
      factory.addAdvice(new ObjectMapperInterceptor() {
    
          @Override
          protected ObjectMapper getObject() {
            return objectMapperResolver.getObjectMapper();
          }
    
      });
    
      return (ObjectMapper) factory.getProxy();
    }
    

    Note: I had class loading issues with this proxy on Wildfly, due to its modular class loading, so I had to extend ObjectMapper (without changing anything) just so I can use class from my module.

    It all tied up together using this configuration:

    @Configuration
    public class WebConfiguration extends WebMvcConfigurerAdapter {
    
      @Bean
      public MappingJackson2HttpMessageConverter jackson2HttpMessageConverter() {
        return new MappingJackson2HttpMessageConverter(objectMapper(objectMapperResolver()));
      }
    
      @Override
      public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(jackson2HttpMessageConverter());
      }
    }
    

    ObjectMapperResolver implementations

    Final piece is the logic that determines which mapper should be used, it will be contained in ObjectMapperResolver interface. It contains only one look up method:

    public interface ObjectMapperResolver {
    
      ObjectMapper getObjectMapper();
    
    }
    

    If you do not have a lot of use cases with custom mappers you can simply make a map of preconfigured instances with ReqeustMatchers as keys. Something like this:

    public class RequestMatcherObjectMapperResolver implements ObjectMapperResolver {
    
      private final ObjectMapper defaultMapper;
      private final Map<RequestMatcher, ObjectMapper> mapping = new HashMap<>();
    
      public RequestMatcherObjectMapperResolver(ObjectMapper defaultMapper, Map<RequestMatcher, ObjectMapper> mapping) {
        this.defaultMapper = defaultMapper;
        this.mapping.putAll(mapping);
      }
    
      public RequestMatcherObjectMapperResolver(ObjectMapper defaultMapper) {
        this.defaultMapper = defaultMapper;
      }
    
      @Override
      public ObjectMapper getObjectMapper() {
        ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = sra.getRequest();
        for (Map.Entry<RequestMatcher, ObjectMapper> entry : mapping.entrySet()) {
          if (entry.getKey().matches(request)) {
            return entry.getValue();
          }
        }
        return defaultMapper;
      }
    
    }
    

    You can also use a request scoped ObjectMapper and then configure it on a per-request basis. Use this configuration:

    @Bean
    public ObjectMapperResolver objectMapperResolver() {
      return new ObjectMapperResolver() {
        @Override
        public ObjectMapper getObjectMapper() {
          return requestScopedObjectMapper();
        }
      };
    }
    
    
    @Bean
    @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
    public ObjectMapper requestScopedObjectMapper() {
      return new ObjectMapper();
    }
    

    This is best suited for custom response serialization, since you can configure it right in the controller method. For custom deserialization you must also use Filter/HandlerInterceptor/ControllerAdvice to configure active mapper for current request before the controller method is triggered.

    You can create interface, similar to ObjectMapperResolver:

    public interface ObjectMapperConfigurer {
    
      void configureObjectMapper(ObjectMapper objectMapper);
    
    }
    

    Then make a map of this instances with RequstMatchers as keys and put it in a Filter/HandlerInterceptor/ControllerAdvice similar to RequestMatcherObjectMapperResolver.

    P.S. If you want to explore dynamic ObjectMapper configuration a bit further I can suggest my old answer here. It describes how you can make dynamic @JsonFilters at run time. It also contains my older approach with extended MappingJackson2HttpMessageConverter that I suggested in comments.

    0 讨论(0)
  • 2020-12-15 17:30

    You can create custom deserializer for your String data.

    Custom Deserializer

    public class CustomStringDeserializer extends JsonDeserializer<String> {
    
      @Override
      public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
    
        String str = p.getText();
    
        //return processed String
      }
    

    }

    Now suppose the String is present inside a POJO use @JsonDeserialize annotation above the variable:

    public class SamplePOJO{
      @JsonDeserialize(using=CustomStringDeserializer.class)
      private String str;
      //getter and setter
    }
    

    Now when you return it as a response it will be Deserialized in the way you have done it in CustomDeserializer.

    Hope it helps.

    0 讨论(0)
  • 2020-12-15 17:31

    Probably this would help, but it ain't pretty. It would require AOP. Also I did not validate it. Create a @CustomAnnotation.

    Update your controller:

    void someEndpoint(@RequestBody @CustomAnnotation SomeEntity someEntity);
    

    Then implemment the AOP part:

    @Around("execution(* *(@CustomAnnotation (*)))")
    public void advice(ProceedingJoinPoint proceedingJoinPoint) {
      // Here you would add custom ObjectMapper, I don't know another way around it
      HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
      String body = request .getReader().lines().collect(Collectors.joining(System.lineSeparator()));
    
      SomeEntity someEntity = /* deserialize */;
      // This could be cleaner, cause the method can accept multiple parameters
      proceedingJoinPoint.proceed(new Object[] {someEntity});
    }
    
    0 讨论(0)
  • 2020-12-15 17:33

    You could try Message Converters. They have a context about http input request (for example, docs see here, JSON). How to customize you could see here. Idea that you could check HttpInputMessage with special URIs, which used in your controllers and convert string as you want. You could create special annotation for this, scan packages and do it automatically.

    Note

    Likely, you don't need implementation of ObjectMappers. You can use simple default ObjectMapper to parse String and then convert string as you wish. In that case you would create RequestBody once.

    0 讨论(0)
  • 2020-12-15 17:33

    You can define a POJO for each different type of request parameter that you would like to deserialize. Then, the following code will pull in the values from the JSON into the object that you define, assuming that the names of the fields in your POJO match with the names of the field in the JSON request.

    ObjectMapper mapper = new ObjectMapper(); 
    YourPojo requestParams = null;
    
    try {
        requestParams = mapper.readValue(JsonBody, YourPOJO.class);
    
    } catch (IOException e) {
        throw new IOException(e);
    }
    
    0 讨论(0)
提交回复
热议问题