web-dev-qa-db-ja.com

@ExceptionHandlerで@RequestBodyを取得する方法(Spring REST)

Spring-web-4.3.3を含むSpring Boot 1.4.1を使用しています。サービスコードによってスローされた例外を処理する_@ControllerAdvice_アノテーションが付けられたクラスと_@ExceptionHandler_アノテーションが付けられたメソッドがあります。これらの例外を処理するとき、PUTおよびPOST操作の要求の一部であった_@RequestBody_をログに記録したいので、ケースは診断に不可欠です。

Spring Docs ごとに_@ExceptionHandler_メソッドのメソッドシグネチャには、HttpServletRequestを含むさまざまなものを含めることができます。リクエストボディは通常、ここからgetInputStream()またはgetReader()を介して取得できますが、私のコントローラーメソッドが私のすべてのように_"@RequestBody Foo fooBody"_のようなリクエストボディを解析する場合、_HttpServletRequest's_入力ストリームまたはリーダーは、例外ハンドラーメソッドが呼び出されるまでに既に閉じられています。本質的に、リクエストの本文は既に説明されている問題と同様に、Springによって既に読み取られています here 。リクエストボディは一度しか読み取れないことがサーブレットでの作業でよくある問題です。

残念ながら、_@RequestBody_は例外ハンドラメソッドで使用可能なオプションの1つではありません。

InputStreamを例外ハンドラメソッドに追加できますが、これはHttpServletRequestのInputStreamと同じものになるため、同じ問題が発生します。

また、_((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest()_を使用して現在のリクエストを取得しようとしましたが、これは現在のリクエストを取得するための別のトリックですが、これはSpringが例外ハンドラメソッドに渡すHttpServletRequestと同じになるため、同じ問題が発生します。

thisthis のようないくつかのソリューションについて読んだことがあります。これらのソリューションでは、要求の内容を読み取り、キャッシュできるフィルターチェーンにカスタム要求ラッパーを挿入します複数回読んでください。ロギングを実装するためだけにフィルター/リクエスト/レスポンスチェーン全体を中断したり(潜在的にパフォーマンスや安定性の問題を引き起こしたり)、アップロードされたドキュメントなどの大きなリクエストがある場合(これは私はそうする)、私はそれをメモリにキャッシュしたくない。それに、もし私が見つけられただけなら、Springはおそらく_@RequestBody_を既にどこかにキャッシュしているでしょう。

ちなみに、多くのソリューションはContentCachingRequestWrapper Springクラスの使用を推奨していますが、私の経験ではこれは機能しません。文書化されていないことを除けば、ソースコードを見ると、リクエストボディではなくパラメータのみをキャッシュしているように見えます。このクラスからリクエストの本文を取得しようとすると、常に空の文字列になります。

だから私は見逃したかもしれない他のオプションを探しています。読んでくれてありがとう。

24
Uncle Long Hair

リクエストボディオブジェクトをリクエストスコープ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;
    }
}
3
Warren M. Nocos

受け入れられた答えは、物事をやり取りするための新しい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);
2
WeMakeSoftware

RequestBodyAdvice インターフェースを使用して、リクエスト本文のコンテンツを取得できるはずです。 @ ControllerAdvice アノテーションが付けられたクラスにこれを実装すると、自動的に取得されるはずです。

HTTPメソッドやクエリパラメータなどの他のリクエスト情報を取得するには、 interceptor を使用します。私は 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;
    }
}
0
quintencls