Ответ 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;
}
}
}
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
в качестве ключей. Что-то вроде этого:
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
для настройки активного Mapper для текущего запроса до запуска метода контроллера.
Вы можете создать интерфейс, похожий на ObjectMapperResolver
:
public interface ObjectMapperConfigurer {
void configureObjectMapper(ObjectMapper objectMapper);
}
Затем создайте карту этих экземпляров с помощью RequstMatcher
в качестве ключей и поместите ее в Filter
/HandlerInterceptor
/ControllerAdvice
, аналогичный RequestMatcherObjectMapperResolver
.
P.S. Если вы хотите более подробно изучить динамическую конфигурацию ObjectMapper
, я могу предложить свой старый ответ здесь. В нем описывается, как вы можете сделать динамический @JsonFilter
во время выполнения. Он также содержит мой более старый подход с расширенным MappingJackson2HttpMessageConverter
, который я предложил в комментариях.