你能配置 Spring 控制器特定的 Jackson 反序列化吗?

Posted

技术标签:

【中文标题】你能配置 Spring 控制器特定的 Jackson 反序列化吗?【英文标题】:Can you configure Spring controller specific Jackson deserialization? 【发布时间】:2016-10-17 01:55:23 【问题描述】:

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

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

您能否仅为特定控制器配置自定义反序列化?

【问题讨论】:

写一个对象映射器怎么样?我想你可以在里面添加你需要的反序列化逻辑。 问题不在于创建对象映射器。我的问题是如何在每个控制器的基础上而不是在 Web 应用程序中全局配置对象映射器。 我理解你的问题,我建议编写一个可用于所有控制器的对象映射器,但我可以根据它收到的请求反序列化对象。 好的。所以也许为了让事情更清楚,它是 java.lang.String 的自定义反序列化器。在我的用例中,是否将其应用于所有控制器的所有字符串可能并不重要,但我更愿意将其限制为特定的控制器。 嗨,马克,自从你提出问题后,你有没有找到办法做到这一点?我正在努力寻找实现这一目标的方法 【参考方案1】:

要拥有不同的反序列化配置,您必须拥有不同的 ObjectMapper 实例,但开箱即用的 Spring 使用 MappingJackson2HttpMessageConverter,它被设计为仅使用一个实例。

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

从 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 将如下所示:

@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 实现

最后一块是确定应该使用哪个映射器的逻辑,它将包含在ObjectMapperResolver接口中。它只包含一种查找方法:

public interface ObjectMapperResolver 

  ObjectMapper getObjectMapper();


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

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

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

你可以创建接口,类似于ObjectMapperResolver:

public interface ObjectMapperConfigurer 

  void configureObjectMapper(ObjectMapper objectMapper);


然后以RequstMatchers 为键制作此实例的映射,并将其放入类似于RequestMatcherObjectMapperResolverFilter/HandlerInterceptor/ControllerAdvice

附:如果您想进一步探索动态ObjectMapper 配置,我可以建议我的旧答案here。它描述了如何在运行时生成动态@JsonFilters。它还包含我在 cmets 中建议的带有扩展 MappingJackson2HttpMessageConverter 的旧方法。

【讨论】:

在我的情况下,有必要使用 WebMvcConfigurerAdapter.extendMessageConverters 而不是 WebMvcConfigurerAdapter.configureMessageConverters 因为否则默认转换器会丢失。使用 extendMessageConverters 时,我只是过滤了传入的列表,在添加我自己的实现之前删除了所有其他 MappingJackson2HttpMessageConverter-s。 我尝试了您的第一个解决方案,就像here 中的解释一样。它工作正常。这很棒。但是......我怎样才能让它同时与 @CustomRequestBody@Valid 一起工作?似乎@Valid 需要@RequestBody@CustomRequestBody 不适用于@RequestBody 我也想知道为什么@CustomRequestBody 不适用于@RequestBody【参考方案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);

【讨论】:

如果我没记错的话,这种方法的行为如下:Spring 将使用现有的转换器来读取请求正文,然后这个建议才会开始删除结果并再次读取请求。除了请求被转换两次这一事实之外,这还有两个副作用:您可能会在 getReader 上获得 IllegalStateException,因为 getInputStream 已经被调用(可修复),如果现有转换器无法使用,您将获得 HttpMessageNotReadableException读取请求。【参考方案3】:

您可以为您的字符串数据创建自定义反序列化器。

自定义反序列化器

public class CustomStringDeserializer extends JsonDeserializer<String> 

  @Override
  public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException 

    String str = p.getText();

    //return processed String
  

现在假设字符串存在于 POJO 中,在变量上方使用 @JsonDeserialize 注释:

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

现在,当您将其作为响应返回时,它将按照您在 CustomDeserializer 中完成的方式进行反序列化。

希望对你有帮助。

【讨论】:

是的,但要求我不要更改 POJO。【参考方案4】:

你可以试试Message Converters。 他们有关于 http 输入请求的上下文(例如,文档参见 here、JSON)。如何自定义可以看here。 您可以使用特殊 URI 检查 HttpInputMessage 的想法,这些 URI 在您的控制器中使用并根据需要转换字符串。 您可以为此创建特殊注释,扫描包并自动执行。

注意

您可能不需要实现 ObjectMappers。您可以使用简单的默认 ObjectMapper 来解析字符串,然后根据需要转换字符串。 在这种情况下,您将创建一次 RequestBody。

【讨论】:

【参考方案5】:

您可以为要反序列化的每种不同类型的请求参数定义一个 POJO。然后,假设 POJO 中的字段名称与 JSON 请求中的字段名称匹配,以下代码会将 JSON 中的值提取到您定义的对象中。

ObjectMapper mapper = new ObjectMapper(); 
YourPojo requestParams = null;

try 
    requestParams = mapper.readValue(JsonBody, YourPOJO.class);

 catch (IOException e) 
    throw new IOException(e);

【讨论】:

POJO 已经存在。我正在寻找自定义其中特定字段的反序列化,但在特定于控制器的基础上并且不触及 POJO。

以上是关于你能配置 Spring 控制器特定的 Jackson 反序列化吗?的主要内容,如果未能解决你的问题,请参考以下文章

从数据库中动态检索 Spring Boot CORS 配置以获取控制器中的特定方法

Spring Boot REST 端点忽略 Content-Type

如何让 Spring MVC 读取日期为“2019-3-29”格式的路径参数?

如何在spring boot中实现用户级别的特定授权?

Swagger + spring boot + jwt + 如何禁用特定 API 的授权按钮

你能说说Spring框架中Bean的生命周期吗?