是否可以配置特定于 Spring 控制器的 Jackson 反序列化?

2022-09-01 04:42:23

我需要将java.lang.String的自定义Jackson反序列化程序添加到我的Spring 4.1.x MVC应用程序中。但是,所有答案(例如都是指为完整的Web应用程序配置ObjectMapper,并且更改将应用于所有控制器中所有@RequestBody的所有字符串。

我只想将自定义反序列化应用于特定控制器中使用的@RequestBody参数。请注意,我没有为特定字符串字段使用@JsonDeserialize批注的选项。

是否可以仅为特定控制器配置自定义反序列化?


答案 1

要具有不同的反序列化配置,您必须具有不同的实例,但开箱即用的Spring使用,该实例设计为仅使用一个实例。ObjectMapperMappingJackson2HttpMessageConverter

我在这里看到至少两个选项:

从 MessageConverter 移动到 ArgumentResolver

创建一个注释和一个参数解析器:@CustomRequestBody

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注解:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomRequestBody {

  boolean required() default true;

}

ObjectMapperResolver是我们将用于解析实际实例的接口,我将在下面讨论它。当然,如果您只有一个需要自定义映射的用例,则只需在此处初始化映射器即可。ObjectMapper

您可以使用以下配置添加自定义参数解析器:

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

注意:不要与 结合使用,它将被忽略。@CustomRequestBody@RequestBody

ObjectMapper 包装在隐藏多个实例的代理中

MappingJackson2HttpMessageConverter设计为仅与 的一个实例一起使用。我们可以使该实例成为代理委托。这将使使用多个映射器变得透明。ObjectMapper

首先,我们需要一个拦截器,它将所有方法调用转换为底层对象。

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

}

现在我们的代理豆将如下所示:ObjectMapper

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

注意:由于它的模块化类加载,我在Wildfly上遇到了这个代理的类加载问题,所以我不得不扩展(不更改任何内容),以便我可以从我的模块中使用类。ObjectMapper

它都使用以下配置捆绑在一起:

@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

public interface ObjectMapperResolver {

  ObjectMapper getObjectMapper();

}

如果您没有很多自定义映射器的使用案例,则可以简单地使用s作为键制作预配置实例的映射。像这样:ReqeustMatcher

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

}

还可以使用限定范围的请求,然后基于每个请求对其进行配置。使用此配置:ObjectMapper

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

这最适合自定义响应序列化,因为您可以直接在控制器方法中对其进行配置。对于自定义反序列化,还必须使用 // 在触发控制器方法之前为当前请求配置活动映射器。FilterHandlerInterceptorControllerAdvice

您可以创建界面,类似于:ObjectMapperResolver

public interface ObjectMapperConfigurer {

  void configureObjectMapper(ObjectMapper objectMapper);

}

然后用 s 作为键制作此实例的映射,并将其放在类似于 的 // 中。RequstMatcherFilterHandlerInterceptorControllerAdviceRequestMatcherObjectMapperResolver

附言:如果你想进一步探索动态配置,我可以在这里建议我的旧答案。它描述了如何在运行时创建动态 s。它还包含我在评论中建议的扩展的旧方法。ObjectMapper@JsonFilterMappingJackson2HttpMessageConverter


答案 2

也许这会有所帮助,但它并不漂亮。这将需要AOP。我也没有验证它。创建一个 .@CustomAnnotation

更新控制器:

void someEndpoint(@RequestBody @CustomAnnotation SomeEntity someEntity);

然后暗示AOP部分:

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

推荐