Для начала стоит оговориться, что данная статья носит сугубо информационный харакер. Я не думаю, что данное решение стоит применять в большинстве задач, так как есть более простые варианты. Конкретно в моём случае, была задача, для которой создание собственных аннотаций и, соответственно, кастомного HttpMessageConverter для сериализации и десериализации body оказалось полезным.

Думаю, что многие из вас сталкивались с добавлением собственных HttpMessageConverter в своём проекте. Ну или хотя бы слышали за такую возможность. Однако, возникают ситуации, когда мы хотим не только добавить свой собственный конвертер, но и создать собственную аннотацию для явного обозначения того, что тело ответа или запроса будут обработаны нестандартными способами Spring. Более того, данные аннотации могут содержать дополнительную информацию, которая потребуется вашему конвертеру.

Как вы увидите дальше, поддержка аннотации для обработки ответа перекликается с поддержкой обработки запроса. Однако, в данной статье мне хотелось бы также продемонстрировать возможность добавление поддержки нового параметра метода контроллера в ситуации, когда из одного тела запроса требуется тянуть два разных объекта.

@CustomResponseBody

Итак, начнем с простого. Создадим аннотацию @CustomResponseBody

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomResponseBody { }

Далее сделаем простой HttpMessageConverter, первоначально создав для него интерфейс (он нам понадобится в дальнейшем). Создадим интерфейс CustomMessageConverter, который будет являться наследником GenericHttpMessageConverter.

public interface CustomMessageConverter 
  extends GenericHttpMessageConverter<Object> { }

В качестве реалиазации самого конвертера можно создать наследника любого нужного вам класса или интерфейса, например, AbstractGenericHttpMessageConverter. Если ваш конвертер будет работать по схожей схеме, что и обычный MappingJackson2HttpMessageConverter, то можно создать наследника именно от него, переопределив метод writeInternal.

public class MappingCustomJackson2HttpMessageConverter 
  extends MappingJackson2HttpMessageConverter implements CustomMessageConverter {

    @Override
    protected void writeInternal(Object o, HttpOutputMessage outputMessage) 
      throws IOException, HttpMessageNotWritableException {
        
      	super.writeInternal(o, outputMessage);
    }
}

Дальше требуется создать наследника класса AbstractMessageConverterMethodProcessor, в котором и будут идентифицироваться наши кастомные аннотации, и переобпределить его абстрактные методы. Рассмотрим пока только supportsReturnType и handleReturnValue. Ведь именно они потребуются для обработки ответа.

public class CustomRequestResponseBodyMethodProcessor 
  extends AbstractMessageConverterMethodProcessor {

    private final CustomMessageConverter converter;

    protected CustomRequestResponseBodyMethodProcessor(
      CustomMessageConverter converter) {
      
        super(List.of(converter));
        this.converter = converter;
    }

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        return returnType.hasMethodAnnotation(CustomResponseBody.class);
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType,
                                  ModelAndViewContainer mavContainer, 
                                  NativeWebRequest webRequest) throws Exception {
        // some code…
    }
}

В приведенном примере я опустил реализацию handleReturnValue, оставляя её на ваше усмотрение. Реализацию данного метода можно взять из класса RequestResponseBodyMethodProcessor, который как раз и отвечает за обработку стандартной аннотации @ResponseBody.

@Override
public void handleReturnValue(@Nullable Object returnValue, 
                              MethodParameter returnType,
      												ModelAndViewContainer mavContainer, 
                              NativeWebRequest webRequest)
      throws IOException, HttpMediaTypeNotAcceptableException, 
HttpMessageNotWritableException {

   mavContainer.setRequestHandled(true);
   ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
   ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

   // Try even with null return value. ResponseBodyAdvice could get involved.
   writeWithMessageConverters(returnValue, returnType, 
                              inputMessage, outputMessage);
}

На последней строчке тут вызывается метод определяющий конвертер для обработки ответа. Однако, полагаю, что в случае, когда вы создаете собственную аннотацию этот метод вам не пригодится и его можно смело опустить, передав параметры напрямую на вход вашему конвертеру, вызвав переопределенный метод write, который сделегирует вызов переопределенному методу writeInternal. Стоит отметить, что из данного метода можно скопипастить немного кода, который поможет вам определить параметры для метода write.

Последним же шагом в данном пункте рассмотрим регистрацию CustomRequestResponseBodyMethodProcessor, предварительно в другой конфигурации создав его бин.

@Configuration
public class CustomHandlerMethodConfiguration {

    private final RequestMappingHandlerAdapter requestMappingHandlerAdapter;
    private final CustomRequestResponseBodyMethodProcessor customRequestResponseBodyMethodProcessor;

    public CustomHandlerMethodConfiguration(RequestMappingHandlerAdapter requestMappingHandlerAdapter, 
                                            CustomRequestResponseBodyMethodProcessor customRequestResponseBodyMethodProcessor) {
        
        this.requestMappingHandlerAdapter = requestMappingHandlerAdapter;
        this.customRequestResponseBodyMethodProcessor = customRequestResponseBodyMethodProcessor;
    }

    @PostConstruct
    public void init() {
        setReturnValueHttpHandler();
    }

    private void setReturnValueHttpHandler() {
        List<HandlerMethodReturnValueHandler> updatedValues = new ArrayList<>();

        updatedValues.add(0, customRequestResponseBodyMethodProcessor);
        updatedValues.addAll(requestMappingHandlerAdapter.getReturnValueHandlers());

        requestMappingHandlerAdapter.setReturnValueHandlers(updatedValues);
    }

}

Как видно из примера, регистрация CustomRequestResponseBodyMethodProcessor происходит путем создания нового списка хендлеров для обработки возвращаемого значения из методов контроллеров и подкладывание его в RequestMappingHandlerAdapter.

После этого, можно смело устанавливать над вашим методом контроллера аннотацию @CustomResponseBody и реализовывать нужную обработку возвращаемого значения в методе writeInternal класса MappingCustomJackson2HttpMessageConverter.

@CustomRequestBody/@CustomArg

В данном пункте, как я и писал раньше, мы разберем возможность переопределения @RequestBody, но с условием, что нам надо из JSON тянуть два объекта и раскидывать их в два разных параметра метода контроллера. Примерно такого вида:

@PostMapping
public ResponseEntity<?> addEntityWithSmthInfo(@CustomRequestBody Entity firstEntity,
                                                    @CustomArg SmthInfo smthInfo) {
    // some code...
}

Можно, конечно, сделать поддержку добавления второй сущности из тела запроса и без дополнительной аннотации, но, на мой взгляд, её наличие только улучшит читабельность вашего кода, продемонстрировав присутствие кастомной логики. Вопрос необходимости такой реализации обуславливается тем, что, возможно, вы захотите сохранять вашу сущность с дополнительным объектом, который получаете из тела запроса, в одной транзакции.

Итак, для переопределения @RequestBody нам надо вернуться к классам CustomRequestResponseBodyMethodProcessor и MappingCustomJackson2HttpMessageConverter. В первом случае требуется реализовать два метода supportsParameter и resolveArgument.

@Override
public boolean supportsParameter(MethodParameter parameter) {
    return parameter.hasParameterAnnotation(CustomRequestBody.class);
}

@Override
public Object resolveArgument(MethodParameter parameter, 
                              ModelAndViewContainer mavContainer, 
                              NativeWebRequest webRequest, 
                              WebDataBinderFactory binderFactory) 
  throws Exception {
// some code….
}

Прежде чем приступать к доработкам в MappingCustomJackson2HttpMessageConverter добавим в интерфейс CustomMessageConverter метод, который будет вызываться для десериализации тела запроса и как раз применяться в вышеприведенном методе resolveArgument.

// CustomMessageConverter.java
public interface CustomMessageConverter 
  extends GenericHttpMessageConverter<Object> {

    EntityWithSmth readWithSmthInfo(Class<?> clazz, 
                                    HttpInputMessage inputMessage) 
      throws Exception;
}

// EntityWithSmth.java
public class EntityWithSmth {
    FirstEntity entity;
    SmthInfo info;
}

Дальше переопределим данный метод в MappingCustomJackson2HttpMessageConverter  и реализуем обработку тела запроса. Там же код за основу можно вытянуть из родительских классов. Например, метод readJavaType из AbstractJackson2HttpMessageConverter. Тут все просто, при десериализации JSON вытягиваем из узлов дерева нужную нам структуру и перегоняем её в нужные объекты. Далее создаем объект EntityWithSmth и возвращаем.

Однако, далее нужно сделать обработку объекта нашего класса EntityWithSmth, который будет возвращаться из метода resolveArgument. Для этого необходимо создать наследника класса ServletInvocableHandlerMethod, в котором, по сути, как и в остальных классах будет копипаста кода из родителя / родителей с небольшими доработками. В нашем же случае требуется переопределить метод getMethodArgumentValues и при проверке параметров метода на основании аннотации определять кем обрабатывать тело нашего запроса. Так как реализация данного метода не маленькая, я оставлю её в репозитории на гитхабе, который был подготовлен для данного материала.

После этого нам необходимо зарегистрировать наш новый класс. Сделать это можно переопределив метод createInvocableHandlerMethod в дочернем классе RequestMappingHandlerAdapter.

public class CustomRequestMappingHandlerAdapter 
  extends RequestMappingHandlerAdapter {
    private final CustomRequestResponseBodyMethodProcessor customRequestResponseBodyMethodProcessor;

    public CustomRequestMappingHandlerAdapter(CustomRequestResponseBodyMethodProcessor customRequestResponseBodyMethodProcessor) {
        this.customRequestResponseBodyMethodProcessor = customRequestResponseBodyMethodProcessor;
    }

    @Override
    protected ServletInvocableHandlerMethod createInvocableHandlerMethod(HandlerMethod handlerMethod) {
        return new CustomServletInvocableHandlerMethod(handlerMethod, 
                                                       customRequestResponseBodyMethodProcessor);
    }
}

И в классе конфигурации реализовать интерфейс WebMvcRegistrations, где переопределить дефолтный метод getRequestMappingHandlerAdapter.

@Configuration
public class CustomWebMvcRegistrationsConfiguration 
  implements WebMvcRegistrations {
    private final CustomRequestResponseBodyMethodProcessor customRequestResponseBodyMethodProcessor;

    public CustomWebMvcRegistrationsConfiguration(CustomRequestResponseBodyMethodProcessor customRequestResponseBodyMethodProcessor) {
        this.customRequestResponseBodyMethodProcessor = customRequestResponseBodyMethodProcessor;
    }
    
    @Override
    public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
        return new CustomRequestMappingHandlerAdapter(customRequestResponseBodyMethodProcessor);
    }
}

Заключение

В заключении, хочу ещё раз отметить, что данная статья является более информационной и, можно сказать, справочной, а не призывом к действию. Но, если вдруг вам захочется “обогощать” ваш ответ какими-то дополнительными данными для сущности и уметь обрабатывать такие запросы, то данная статья станет вам сопрводительным материалом.

Что же касается моей задачи, мы на проекте, используя данный подход, добавляли информацию о правах сущности, работая со Spring Security ACL, где права хранятся в отдельных таблицах. И такой подход позволил нам неявно отдавать информацию о правах не создавая кучу DTO для каждой из защищаемых сущностей.

Продублирую ссылку на репозиторий с простой реализацией данного материала

Комментарии (0)