你能配置 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();
如果您没有很多使用自定义映射器的用例,您可以简单地使用 ReqeustMatcher
s 作为键来制作预配置实例的映射。像这样的:
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);
然后以RequstMatcher
s 为键制作此实例的映射,并将其放入类似于RequestMatcherObjectMapperResolver
的Filter
/HandlerInterceptor
/ControllerAdvice
。
附:如果您想进一步探索动态ObjectMapper
配置,我可以建议我的旧答案here。它描述了如何在运行时生成动态@JsonFilter
s。它还包含我在 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”格式的路径参数?