code

@ExceptionHandler(Spring REST)에서 @RequestBody를 얻는 방법

starcafe 2023. 3. 29. 21:40
반응형

@ExceptionHandler(Spring REST)에서 @RequestBody를 얻는 방법

spring-web-4.3이 포함된 Spring Boot 1.4.1을 사용하고 있습니다.에 대한 주석을 단 수업이 있다.@ControllerAdvice및 으로 주석을 단 메서드@ExceptionHandler서비스 코드에 의해 발생하는 예외를 처리합니다.이러한 예외를 처리할 때,@RequestBodyPUT 및 POST 운영 요청의 일부였기 때문에 문제의 원인이 된 요청 기관을 확인할 수 있습니다.이 경우 진단에 매우 중요합니다.

Per Spring Docs 메서드시그니처@ExceptionHandler방법에는 다양한 것이 포함될 수 있습니다.HttpServletRequest요청 본문은 보통 여기서 입수할 수 있습니다.getInputStream()또는getReader()단, 컨트롤러 메서드가 요청 본문을 다음과 같이 해석하는 경우"@RequestBody Foo fooBody"우리 모두가 그렇듯이HttpServletRequest's예외 핸들러 메서드가 호출될 때까지 입력 스트림 또는 판독기가 이미 닫혀 있습니다.기본적으로 요청 본문은 스프링에서 이미 읽었으며, 이는 여기에 설명된 문제와 유사합니다.요청 본문을 한 번만 읽을 수 있는 것은 서블릿을 사용하는 일반적인 문제입니다.

불행하게도@RequestBody예외 핸들러 방식에서 사용할 수 있는 옵션 중 하나가 아닙니다.그렇다면 사용할 수 있습니다.

추가할 수 있습니다.InputStream예외 핸들러 메서드로 이행합니다만, 이것은 Http ServletRequest의 InputStream과 같은 것이 되어, 같은 문제가 발생합니다.

저는 또한 현재의 요청을 받아보려고 했습니다.((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()이는 현재 요구를 취득하기 위한 또 다른 트릭이지만 결국 Spring이 예외 핸들러 메서드로 전달되는Http ServletRequest와 같은 문제가 발생하게 됩니다.

요청 내용을 읽고 여러 번 읽을 수 있도록 캐시하는 필터 체인에 커스텀 요청 래퍼를 삽입하는 것과 관련된 몇 가지 솔루션에 대해 읽은 적이 있습니다.이 솔루션은 단순히 로깅을 구현하기 위해 필터/요구/응답 체인 전체를 중단하고 싶지 않으며(또한 성능 또는 안정성 문제가 발생할 수 있음), 업로드된 문서와 같은 대규모 요청이 있는 경우 이를 메모리에 캐싱하고 싶지 않기 때문에 좋아하지 않습니다.게다가, 봄은 아마 그...@RequestBody찾을 수만 있다면 이미 어딘가에 캐싱되어 있을 거야

, 솔루션에서는, 「 」를 사용하는 합니다.ContentCachingRequestWrapper봄 수업이지만 내 경험상 이것은 효과가 없다.문서화되어 있지 않을 뿐만 아니라 소스 코드를 보면 요청 본문이 아닌 파라미터만 캐시하는 것처럼 보입니다.이 클래스에서 요청 본문을 가져오려고 하면 항상 빈 문자열이 생성됩니다.

그래서 제가 놓친 다른 선택지를 찾고 있습니다.읽어주셔서 감사합니다.

응답을 수락하면 전달하기 위한 새로운 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);

request body 객체를 request scope 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;
    }
}

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

5인칭 답변에 대한 개선 사항입니다.
요청 본문을 입수하여 예외 핸들러 클래스 내 어디에서나 사용할 수 있습니다.

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

}

다음은 일부 필드 검증 제어에 사용한 Kotlin 구문 해결 방법입니다.

의 「Default(디폴트)」를 .handleMethodArgumentNotValid(...)@RestControllerAdvice동일한 요청 본문 개체에 포함된 필드를 체계적으로 기록합니다.

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
        }
    }

HttpServerRequest 에의 액세스 방법에 대해서는, 여기를 참조해 주세요.https://stackoverflow.com/a/61813076/1036433

언급URL : https://stackoverflow.com/questions/43502332/how-to-get-the-requestbody-in-an-exceptionhandler-spring-rest

반응형