Как получить @RequestBody в @ExceptionHandler (Spring REST)

Я использую Spring Boot 1.4.1, который включает spring -web-4.3.3. У меня есть класс, аннотированный с помощью @ControllerAdvice и методы, аннотированные с помощью @ExceptionHandler, для обработки исключений, переданных служебным кодом. При обработке этих исключений я хотел бы зарегистрировать @RequestBody, который был частью запроса на операции PUT и POST, чтобы я мог видеть тело запроса, которое вызвало проблему, которая в моем случае имеет решающее значение для диагностики.

Per Spring Docs подпись метода для методов @ExceptionHandler может включать в себя различные вещи, включая HttpServletRequest. Тело запроса обычно может быть получено здесь через getInputStream() или getReader(), но если мои методы контроллера анализируют тело запроса, например "@RequestBody Foo fooBody", как все мое, входной поток или считыватель HttpServletRequest's уже закрыт вызывается метод моего обработчика исключений. По существу тело запроса уже было прочитано Spring, как описано в здесь. Общей проблемой, связанной с сервлетами, является то, что тело запроса можно прочитать только один раз.

К сожалению, @RequestBody не является одним из параметров, доступных для метода обработчика исключений, если бы тогда я мог его использовать.

Я могу добавить InputStream к методу обработчика исключений, но это заканчивается тем же, что и HttpServletRequest InputStream, и поэтому имеет ту же проблему.

Я также попытался получить текущий запрос с ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(), который является еще одним трюком для получения текущего запроса, но в итоге это тот же HttpServletRequest, что Spring переходит в метод обработчика исключений и поэтому имеет ту же проблему.

Я прочитал несколько решений, таких как this и , которые включают в себя вставку специальной оболочки запроса в цепочку фильтров, которая будет считывать содержимое запроса и кэшировать их, чтобы их можно было читать более одного раза. Мне не нравится это решение, потому что я не хочу прерывать всю цепочку фильтров/запросов/ответов (и, возможно, создавать проблемы с производительностью или стабильностью), только для реализации ведения журнала, и если у меня есть большие запросы, такие как загруженные документы (которые Я), я не хочу кэшировать это в памяти. Кроме того, Spring, вероятно, имеет @RequestBody где-то кэшированный, если бы я мог его найти.

Кстати, многие решения рекомендуют использовать класс ContentCachingRequestWrapper Spring, но по моему опыту это не работает. Помимо того, что он не документирован, глядя на его исходный код, похоже, что он кэширует только параметры, но не тело запроса. Попытка получить тело запроса из этого класса всегда приводит к пустой строке.

Итак, я ищу любые другие варианты, которые я, возможно, пропустил. спасибо за чтение.

Ответ 1

Вы можете ссылаться на объект тела запроса на bean с запросом. Затем добавьте этот обработчик bean в свой обработчик исключений, чтобы получить тело запроса (или другой запрос-контекст beans, который вы хотите ссылаться).

// @Component
// @Scope("request")
@ManagedBean
@RequestScope
public class RequestContext {
    // fields, getters, and setters for request-scoped beans
}

@RestController
@RequestMapping("/api/v1/persons")
public class PersonController {

    @Inject
    private RequestContext requestContext;

    @Inject
    private PersonService personService;

    @PostMapping
    public Person savePerson(@RequestBody Person person) throws PersonServiceException {
         requestContext.setRequestBody(person);
         return personService.save(person);
    }

}

@ControllerAdvice
public class ExceptionMapper {

    @Inject
    private RequestContext requestContext;

    @ExceptionHandler(PersonServiceException.class)
    protected ResponseEntity<?> onPersonServiceException(PersonServiceException exception) {
         Object requestBody = requestContext.getRequestBody();
         // ...
         return responseEntity;
    }
}

Ответ 2

Принятый ответ создает новый POJO для передачи информации, но такого же поведения можно добиться, не создавая дополнительные объекты, повторно используя http-запрос.

Пример кода для сопоставления контроллера:

public ResponseEntity savePerson(@RequestBody Person person, WebRequest webRequest) {
    webRequest.setAttribute("person", person, RequestAttributes.SCOPE_REQUEST);

А позже в классе/методе ExceptionHandler вы можете использовать:

@ExceptionHandler(Exception.class)
public ResponseEntity exceptionHandling(WebRequest request,Exception thrown) {

    Person person = (Person) request.getAttribute("person", RequestAttributes.SCOPE_REQUEST);

Ответ 3

Вы должны иметь возможность получать содержимое тела запроса с помощью интерфейса RequestBodyAdvice. Если вы реализуете это в классе, аннотированном @ControllerAdvice, он должен быть выбран автоматически.

Чтобы получить другую информацию запроса, такую как метод HTTP и параметры запроса, я использую перехватчик. Я собираю всю эту информацию запроса для сообщения об ошибках в переменной ThreadLocal, которую я очищаю на ловушке afterCompletion в том же перехватчике.

Класс ниже реализует это и может использоваться в вашем ExceptionHandler для получения всей информации запроса:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class RequestInfo extends HandlerInterceptorAdapter implements RequestBodyAdvice {
    private static final Logger logger = LoggerFactory.getLogger(RequestInfo.class);
    private static final ThreadLocal<RequestInfo> requestInfoThreadLocal = new ThreadLocal<>();

    private String method;
    private String body;
    private String queryString;
    private String ip;
    private String user;
    private String referrer;
    private String url;

    public static RequestInfo get() {
        RequestInfo requestInfo = requestInfoThreadLocal.get();
        if (requestInfo == null) {
            requestInfo = new RequestInfo();
            requestInfoThreadLocal.set(requestInfo);
        }
        return requestInfo;
    }

    public Map<String,String> asMap() {
        Map<String,String> map = new HashMap<>();
        map.put("method", this.method);
        map.put("url", this.url);
        map.put("queryParams", this.queryString);
        map.put("body", this.body);
        map.put("ip", this.ip);
        map.put("referrer", this.referrer);
        map.put("user", this.user);
        return map;
    }

    private void setInfoFromRequest(HttpServletRequest request) {
        this.method = request.getMethod();
        this.queryString = request.getQueryString();
        this.ip = request.getRemoteAddr();
        this.referrer = request.getRemoteHost();
        this.url = request.getRequestURI();
        if (request.getUserPrincipal() != null) {
            this.user = request.getUserPrincipal().getName();
        }
    }

    public void setBody(String body) {
        this.body = body;
    }

    private static void setInfoFrom(HttpServletRequest request) {
        RequestInfo requestInfo = requestInfoThreadLocal.get();
        if (requestInfo == null) {
            requestInfo = new RequestInfo();
        }
        requestInfo.setInfoFromRequest(request);
        requestInfoThreadLocal.set(requestInfo);
    }

    private static void clear() {
        requestInfoThreadLocal.remove();
    }

    private static void setBodyInThreadLocal(String body) {
        RequestInfo requestInfo = get();
        requestInfo.setBody(body);
        setRequestInfo(requestInfo);
    }

    private static void setRequestInfo(RequestInfo requestInfo) {
        requestInfoThreadLocal.set(requestInfo);
    }

    // Implementation of HandlerInterceptorAdapter to capture the request info (except body) and be able to add it to the report in case of an error

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        RequestInfo.setInfoFrom(request);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception exception) {
        RequestInfo.clear();
    }

    // Implementation of RequestBodyAdvice to capture the request body and be able to add it to the report in case of an error

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return inputMessage;
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        RequestInfo.setBodyInThreadLocal(body.toString());
        return body;
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        return body;
    }
}