簡體   English   中英

如何在@ExceptionHandler (Spring REST) 中獲取@RequestBody

[英]How to get the @RequestBody in an @ExceptionHandler (Spring REST)

我正在使用 Spring Boot 1.4.1,其中包括 spring-web-4.3.3。 我有一個用@ControllerAdvice注釋的 class 和用@ExceptionHandler注釋的方法來處理服務代碼拋出的異常。 在處理這些異常時,我想記錄作為 PUT 和 POST 操作請求一部分的@RequestBody ,這樣我就可以看到導致問題的請求正文,這在我的案例中對於診斷至關重要。

根據Spring 文檔@ExceptionHandler方法的方法簽名可以包括各種內容,包括HttpServletRequest 請求主體通常可以通過getInputStream()getReader()從此處獲取,但如果我的 controller 方法像我的所有方法一樣解析請求主體,如"@RequestBody Foo fooBody" ,則HttpServletRequest's輸入 stream 或閱讀器已被關閉我的異常處理程序方法被調用的時間。 本質上,請求正文已被 Spring 讀取,類似於此處描述的問題。 使用 servlets 時的一個常見問題是請求正文只能讀取一次。

不幸的是, @RequestBody不是異常處理程序方法可用的選項之一,如果是的話我可以使用它。

我可以將InputStream添加到異常處理程序方法中,但這最終與 HttpServletRequest 的 InputStream 相同,因此存在相同的問題。

我還嘗試使用((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()獲取當前請求,這是獲取當前請求的另一個技巧,但這最終與 Spring 傳遞給異常處理程序方法的 HttpServletRequest 相同,因此有同樣的問題。

我已經閱讀了一些類似這樣的解決方案, 這些解決方案涉及在過濾器鏈中插入自定義請求包裝器,該包裝器將讀取請求的內容並緩存它們,以便可以多次讀取它們。 我不喜歡這個解決方案,因為我不想中斷整個過濾器/請求/響應鏈(並可能引入性能或穩定性問題)只是為了實現日志記錄,如果我有任何大型請求,例如上傳的文檔(其中我願意),我不想在 memory 中緩存它。此外,如果我只能找到它,Spring 可能已經將@RequestBody緩存在某個地方。

順便說一下,許多解決方案建議使用ContentCachingRequestWrapper Spring class 但根據我的經驗,這是行不通的。 除了沒有記錄外,查看它的源代碼看起來它只緩存參數,而不緩存請求體。 嘗試從此 class 獲取請求正文始終會導致空字符串。

所以我正在尋找我可能錯過的任何其他選項。 謝謝閱讀。

您可以將請求主體對象引用到請求范圍的 bean。 然后在您的異常處理程序中注入該請求范圍的 bean 以檢索請求正文(或您希望引用的其他請求上下文 bean)。

// @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;
    }
}

接受的答案會創建一個新的 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);

您應該能夠使用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;
    }
}

只是對quintencls 答案的增強
我得到了請求正文,可以在異常處理程序 class 中的任何地方使用它。

@ControllerAdvice
public class CustomErrorHandler extends ResponseEntityExceptionHandler implements RequestBodyAdvice {
    
    ...

    private Object reqBody;

    ...

    @ExceptionHandler(NoSuchElementException.class)
    public ResponseEntity<Object> handleNoSuchElementException(final NoSuchElementException ex,
            final WebRequest request) {
        System.out.println("===================================" + reqBody);
        return handleNotFoundException(ex, request);
    }

    ...

    @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) throws IOException {
        return inputMessage;
    }

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

        // capture request body here to use in our controller advice class
        this.reqBody = body;

        return body;
    }

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

}

請參閱此處: https ://stackoverflow.com/a/61813076/1036433 - 獲取訪問 HttpServerRequest 的干凈方法。

這是我用於某些字段驗證控制的 Kotlin 語法解決方案。

我需要從@RestControllerAdvice增強默認的handleMethodArgumentNotValid(...)方法,以系統地記錄嵌入在同一請求正文對象中的字段。

override fun handleMethodArgumentNotValid(e: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatus, request: WebRequest): ResponseEntity<Any> {
        val error = e.bindingResult.fieldErrors.first()
        val requestBody = try {
            val field = error.javaClass.getDeclaredField("violation").apply { trySetAccessible() }
            ((field[error] as ConstraintViolationImpl<Any>).rootBean as MyRequestBodyObject)
        } catch (ex : Exception) {
            //do some failsafe here
        }
    }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM