大規模なRESTサービスサーバーをセットアップしようとしています。私たちはSpring Boot 1.2.1、Spring 4.1.5、そしてJava 8を使用しています。私たちのコントローラは@RestControllerと標準の@RequestMappingアノテーションを実装しています。
私の問題は、Spring Bootがコントローラ例外のデフォルトリダイレクトを/error
に設定することです。ドキュメントから:
Spring Bootはデフォルトで/ errorマッピングを提供しています。これは賢明な方法ですべてのエラーを処理し、サーブレットコンテナの「グローバル」エラーページとして登録されます。
Node.jsを使ってRESTアプリケーションを書いて何年にもわたって、これは私にとっては賢明なこと以外何でもありません。サービスエンドポイントが生成した例外はすべて応答に返されます。回答を探しているだけで、リダイレクトに対して何のアクションも実行できない、またはほとんどないAngularまたはJQuery SPAコンシューマにリダイレクトを送信する理由を理解できません。 。
私がやりたいことは、例外を取ることができるグローバルエラーハンドラを設定することです - 意図的にリクエストマッピングメソッドから投げられるか、Springによって自動生成され(リクエストパスシグネチャのハンドラメソッドが見つからない場合404) MVCリダイレクトを行わずにクライアントに送信する標準フォーマットのエラー応答(400、500、503、404)。具体的には、エラーを取り、それをUUIDでNoSQLに記録してから、JSON本体のログエントリのUUIDを使用して正しいHTTPエラーコードをクライアントに返します。
ドキュメントはこれを行う方法についてあいまいされています。自分で ErrorController の実装を作成するか ControllerAdvice を使用する必要があるように思われますが、これまでに示した例のすべてに、何らかのエラーマッピングへの応答の転送が含まれています。助けにはならない。他の例では、 "Throwable"を一覧表示してすべてを取得するのではなく、処理するすべての例外タイプを一覧表示する必要があることをお勧めします。
誰かが私が逃したことを私に言ったり、Node.jsがより扱いやすいだろうという連鎖を示唆せずにこれを行う方法について正しい方向に私を向けることができますか?
新しい答え(2016-04-20)
Spring Bootの使い方1.3.1。リリース
Newステップ1 - application.propertiesに次のプロパティを追加するのは簡単で邪魔になりません。
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false
(下記のように)既存のDispatcherServletインスタンスを変更するよりもはるかに簡単です。 - JO '
完全なRESTfulアプリケーションで作業する場合は、静的リソースの処理にSpring Bootのデフォルト設定を使用している場合はリソースハンドラが要求を処理することになるため、静的リソースの自動マッピングを無効にすることが非常に重要です。 **これは、アプリケーション内の他のどのハンドラによっても処理されていない要求をすべて処理するため、ディスパッチャサーブレットは例外をスローする機会がないことを意味します。
新しい回答(2015-12-04)
Spring Bootの使い方1.2.7。リリース
新しいステップ1 - / "throExceptionIfNoHandlerFound"フラグを設定する邪魔にならない方法を見つけました。以下の(ステップ1)のDispatcherServlet置換コードを、アプリケーション初期化クラスでこれに置き換えます。
@ComponentScan()
@EnableAutoConfiguration
public class MyApplication extends SpringBootServletInitializer {
private static Logger LOG = LoggerFactory.getLogger(MyApplication.class);
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(MyApplication.class, args);
DispatcherServlet dispatcherServlet = (DispatcherServlet)ctx.getBean("dispatcherServlet");
dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
}
この場合、既存のDispatcherServletにフラグを設定しています。これは、Spring Bootフレームワークによる自動設定を保持します。
もう1つ見つけたことがあります - @EnableWebMvcアノテーションはSpring Bootにとって致命的です。はい、そのアノテーションは以下で説明されるようにすべてのコントローラ例外を捕らえることができるようなことを可能にしますが、それはまたSpring Bootが通常提供するであろう有用な自動設定の多くを殺します。 Spring Bootを使用するときは、この注釈を慎重に使用してください。
元の答え:
ここに掲載されている解決策についてさらに多くの調査とフォローアップを行って(ヘルプに感謝します)、少しの実行時トレースでSpringコードをトレースした後、ついに私はすべての例外を処理する設定を見つけました。 404を含みます。
ステップ1 - "ハンドラが見つかりません"状況でMVCを使用しないようにSpringBootに指示します。クライアントにビューのリダイレクトを "/ error"に戻す代わりに、Springが例外をスローするようにします。これを行うには、設定クラスの1つにエントリが必要です。
// NEW CODE ABOVE REPLACES THIS! (2015-12-04)
@Configuration
public class MyAppConfig {
@Bean // Magic entry
public DispatcherServlet dispatcherServlet() {
DispatcherServlet ds = new DispatcherServlet();
ds.setThrowExceptionIfNoHandlerFound(true);
return ds;
}
}
この欠点は、デフォルトのディスパッチャサーブレットに代わることです。これはまだ問題になっていません。副作用や実行の問題は発生していません。他の理由でディスパッチャサーブレットを使って何か他のことをするつもりなら、これがそれらをする場所です。
ステップ2 - これでスプリングブートはハンドラが見つからないときに例外をスローするようになりました。その例外は他のものと一緒に統一された例外ハンドラで扱うことができます。
@EnableWebMvc
@ControllerAdvice
public class ServiceExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(Throwable.class)
@ResponseBody
ResponseEntity<Object> handleControllerException(HttpServletRequest req, Throwable ex) {
ErrorResponse errorResponse = new ErrorResponse(ex);
if(ex instanceof ServiceException) {
errorResponse.setDetails(((ServiceException)ex).getDetails());
}
if(ex instanceof ServiceHttpException) {
return new ResponseEntity<Object>(errorResponse,((ServiceHttpException)ex).getStatus());
} else {
return new ResponseEntity<Object>(errorResponse,HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Override
protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
Map<String,String> responseBody = new HashMap<>();
responseBody.put("path",request.getContextPath());
responseBody.put("message","The URL you have reached is not in service at this time (404).");
return new ResponseEntity<Object>(responseBody,HttpStatus.NOT_FOUND);
}
...
}
ここでは "@EnableWebMvc"アノテーションが重要だと思います。これなしではうまくいきません。そしてそれだけです - あなたのSpring起動アプリは404を含む、上記のハンドラクラスの中のすべての例外を捕らえるでしょう。
最後のポイント - スローされたエラーをキャッチするためにこれを取得する方法がないようです。私はアスペクトを使用してエラーをキャッチして上記のコードで処理できる例外に変換するという賢い考えを持っていますが、実際にそれを実装しようとする時間はまだありません。これが誰かに役立つことを願っています。
どんなコメント/訂正/強化でも評価されるでしょう。
Spring Boot 1.4以降では、定型コードを削除するのに役立つ、より簡単な例外処理のための新しいクールなクラスが追加されました。
例外処理のために新しい@RestControllerAdvice
が提供されています。これは@ControllerAdvice
と@ResponseBody
の組み合わせです。この新しいアノテーションを使うとき、@ResponseBody
メソッドの@ExceptionHandler
を削除することができます。
すなわち.
@RestControllerAdvice
public class GlobalControllerExceptionHandler {
@ExceptionHandler(value = { Exception.class })
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiErrorResponse unknownException(Exception ex, WebRequest req) {
return new ApiErrorResponse(...);
}
}
@EnableWebMvc
アノテーションと以下をapplication.propertiesに追加することで404エラーを処理するのに十分でした:spring.mvc.throw-exception-if-no-handler-found=true
あなたはここでソースを見つけて遊ぶことができます:
https://github.com/magiccrafter/spring-boot-exception-handling
ResponseEntityExceptionHandler
はあなたの要求を満たしていると思います。 HTTP 400のサンプルコード:
@ControllerAdvice
public class MyExceptionHandler extends ResponseEntityExceptionHandler {
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ExceptionHandler({HttpMessageNotReadableException.class, MethodArgumentNotValidException.class,
HttpRequestMethodNotSupportedException.class})
public ResponseEntity<Object> badRequest(HttpServletRequest req, Exception exception) {
// ...
}
}
あなたはこれをチェックすることができます ポスト
これはもっと古い質問ですが、私はこれについて私の考えを共有したいと思います。私はそれがあなた方の何人かに役立つことを願っています。
私は現在、Spring Framework 4.3.7.RELEASEと一緒にSpring Boot 1.5.2.RELEASEを利用するREST APIを構築しています。私は(XML構成とは対照的に)Java構成アプローチを使用します。また、私のプロジェクトでは@RestControllerAdvice
アノテーションを使ったグローバルな例外処理メカニズムを使用しています(後述)。
私のプロジェクトはあなたのものと同じ要件を持っています。存在しないURLにリクエストを送信しようとしたときに、私のREST APIがAPIクライアントへのHTTPレスポンスでJSONペイロードを伴うHTTP 404 Not Found
を返すようにします。私の場合、JSONペイロードは次のようになっています(これはSpring Bootのデフォルトのbtwとは明らかに異なります)。
{
"code": 1000,
"message": "No handler found for your request.",
"timestamp": "2017-11-20T02:40:57.628Z"
}
私はついにそれを機能させました。簡単に説明すると、主なタスクは次のとおりです。
NoHandlerFoundException
がスローされるようにしてください(以下のステップ1を参照)。ApiError
)を作成します(手順2を参照)。NoHandlerFoundException
に反応して適切なエラーメッセージをAPIクライアントに返す例外ハンドラを作成します(手順3を参照)。さて、今度は詳細に行きます:
ステップ1:application.propertiesを設定します
プロジェクトのapplication.properties
ファイルに次の2つの設定を追加する必要がありました。
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false
これにより、クライアントが、要求を処理できるコントローラメソッドが存在しないURLにアクセスしようとした場合に、NoHandlerFoundException
がスローされます。
ステップ2:APIエラー用のクラスを作成する
私はEugen Paraschivのブログの この記事 で提案されているものに似たクラスを作りました。このクラスはAPIエラーを表します。この情報は、エラーが発生した場合にHTTP応答本文でクライアントに送信されます。
public class ApiError {
private int code;
private String message;
private Instant timestamp;
public ApiError(int code, String message) {
this.code = code;
this.message = message;
this.timestamp = Instant.now();
}
public ApiError(int code, String message, Instant timestamp) {
this.code = code;
this.message = message;
this.timestamp = timestamp;
}
// Getters and setters here...
}
ステップ3:グローバル例外ハンドラの作成/設定
次のクラスを使用して例外を処理します(簡単にするために、import文、ロギングコード、その他の関連性のないコードを削除しました)。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ApiError noHandlerFoundException(
NoHandlerFoundException ex) {
int code = 1000;
String message = "No handler found for your request.";
return new ApiError(code, message);
}
// More exception handlers here ...
}
ステップ4:テストを書く
確実にしたいのですが、失敗した場合でも、APIは常に正しいエラーメッセージを呼び出し側のクライアントに返します。したがって、私はこのようなテストを書きました:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SprintBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ActiveProfiles("dev")
public class GlobalExceptionHandlerIntegrationTest {
public static final String ISO8601_DATE_REGEX =
"^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$";
@Autowired
private MockMvc mockMvc;
@Test
@WithMockUser(roles = "DEVICE_SCAN_HOSTS")
public void invalidUrl_returnsHttp404() throws Exception {
RequestBuilder requestBuilder = getGetRequestBuilder("/does-not-exist");
mockMvc.perform(requestBuilder)
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.code", is(1000)))
.andExpect(jsonPath("$.message", is("No handler found for your request.")))
.andExpect(jsonPath("$.timestamp", RegexMatcher.matchesRegex(ISO8601_DATE_REGEX)));
}
private RequestBuilder getGetRequestBuilder(String url) {
return MockMvcRequestBuilders
.get(url)
.accept(MediaType.APPLICATION_JSON);
}
@ActiveProfiles("dev")
アノテーションは省略することができます。私は異なるプロファイルを扱うときだけそれを使います。 RegexMatcher
はカスタムの Hamcrestのマッチャーです 私はタイムスタンプフィールドをよりよく扱うために使います。これがそのコードです(私はそれを見つけました here ):
public class RegexMatcher extends TypeSafeMatcher<String> {
private final String regex;
public RegexMatcher(final String regex) {
this.regex = regex;
}
@Override
public void describeTo(final Description description) {
description.appendText("matches regular expression=`" + regex + "`");
}
@Override
public boolean matchesSafely(final String string) {
return string.matches(regex);
}
// Matcher method you can call on this matcher class
public static RegexMatcher matchesRegex(final String string) {
return new RegexMatcher(regex);
}
}
私の側からのいくつかのさらなるメモ:
@EnableWebMvc
アノテーションを設定することを提案しました。これは私の場合は必要ありませんでした。このコードはどうですか?私は404エラーをキャッチするためにフォールバックリクエストマッピングを使います。
@Controller
@ControllerAdvice
public class ExceptionHandlerController {
@ExceptionHandler(Exception.class)
public ModelAndView exceptionHandler(HttpServletRequest request, HttpServletResponse response, Exception ex) {
//If exception has a ResponseStatus annotation then use its response code
ResponseStatus responseStatusAnnotation = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class);
return buildModelAndViewErrorPage(request, response, ex, responseStatusAnnotation != null ? responseStatusAnnotation.value() : HttpStatus.INTERNAL_SERVER_ERROR);
}
@RequestMapping("*")
public ModelAndView fallbackHandler(HttpServletRequest request, HttpServletResponse response) throws Exception {
return buildModelAndViewErrorPage(request, response, null, HttpStatus.NOT_FOUND);
}
private ModelAndView buildModelAndViewErrorPage(HttpServletRequest request, HttpServletResponse response, Exception ex, HttpStatus httpStatus) {
response.setStatus(httpStatus.value());
ModelAndView mav = new ModelAndView("error.html");
if (ex != null) {
mav.addObject("title", ex);
}
mav.addObject("content", request.getRequestURL());
return mav;
}
}
デフォルトでは、Spring Bootはjsonにエラーの詳細を提供します。
curl -v localhost:8080/greet | json_pp
[...]
< HTTP/1.1 400 Bad Request
[...]
{
"timestamp" : 1413313361387,
"exception" : "org.springframework.web.bind.MissingServletRequestParameterException",
"status" : 400,
"error" : "Bad Request",
"path" : "/greet",
"message" : "Required String parameter 'name' is not present"
}
あらゆる種類のリクエストマッピングエラーに対しても機能します。この記事をチェックしてください http://www.jayway.com/2014/10/19/spring-boot-error-responses/ /
作成したい場合はNoSQLに記録してください。ログを記録する場所に@ControllerAdviceを作成してから、例外を再スローすることができます。ドキュメントに例があります https://spring.io/blog/2013/11/01/exception-handling-in-spring-mvc
RESTコントローラの場合、Zalando Problem Spring Web
を使用することをお勧めします。
https://github.com/zalando/problem-spring-web
Spring Bootが自動設定を埋め込むことを目的としている場合、このライブラリは例外処理のためにより多くのことを行います。依存関係を追加するだけです。
<dependency>
<groupId>org.zalando</groupId>
<artifactId>problem-spring-web</artifactId>
<version>LATEST</version>
</dependency>
そして、あなたの例外に対して1つ以上のアドバイス特性を定義します(またはデフォルトで提供されるものを使用します)。
public interface NotAcceptableAdviceTrait extends AdviceTrait {
@ExceptionHandler
default ResponseEntity<Problem> handleMediaTypeNotAcceptable(
final HttpMediaTypeNotAcceptableException exception,
final NativeWebRequest request) {
return Responses.create(Status.NOT_ACCEPTABLE, exception, request);
}
}
それから、例外処理に対するコントローラのアドバイスを次のように定義できます。
@ControllerAdvice
class ExceptionHandling implements MethodNotAllowedAdviceTrait, NotAcceptableAdviceTrait {
}
@RestControllerAdviceはSpring Framework 4.3の新機能であり、分野横断的な解決策によってRestfulApiで例外を処理します。
package com.khan.vaquar.exception;
import javax.servlet.http.HttpServletRequest;
import org.owasp.esapi.errors.IntrusionException;
import org.owasp.esapi.errors.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.khan.vaquar.domain.ErrorResponse;
/**
* Handles exceptions raised through requests to spring controllers.
**/
@RestControllerAdvice
public class RestExceptionHandler {
private static final String TOKEN_ID = "tokenId";
private static final Logger log = LoggerFactory.getLogger(RestExceptionHandler.class);
/**
* Handles InstructionExceptions from the rest controller.
*
* @param e IntrusionException
* @return error response POJO
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IntrusionException.class)
public ErrorResponse handleIntrusionException(HttpServletRequest request, IntrusionException e) {
log.warn(e.getLogMessage(), e);
return this.handleValidationException(request, new ValidationException(e.getUserMessage(), e.getLogMessage()));
}
/**
* Handles ValidationExceptions from the rest controller.
*
* @param e ValidationException
* @return error response POJO
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = ValidationException.class)
public ErrorResponse handleValidationException(HttpServletRequest request, ValidationException e) {
String tokenId = request.getParameter(TOKEN_ID);
log.info(e.getMessage(), e);
if (e.getUserMessage().contains("Token ID")) {
tokenId = "<OMITTED>";
}
return new ErrorResponse( tokenId,
HttpStatus.BAD_REQUEST.value(),
e.getClass().getSimpleName(),
e.getUserMessage());
}
/**
* Handles JsonProcessingExceptions from the rest controller.
*
* @param e JsonProcessingException
* @return error response POJO
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = JsonProcessingException.class)
public ErrorResponse handleJsonProcessingException(HttpServletRequest request, JsonProcessingException e) {
String tokenId = request.getParameter(TOKEN_ID);
log.info(e.getMessage(), e);
return new ErrorResponse( tokenId,
HttpStatus.BAD_REQUEST.value(),
e.getClass().getSimpleName(),
e.getOriginalMessage());
}
/**
* Handles IllegalArgumentExceptions from the rest controller.
*
* @param e IllegalArgumentException
* @return error response POJO
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = IllegalArgumentException.class)
public ErrorResponse handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException e) {
String tokenId = request.getParameter(TOKEN_ID);
log.info(e.getMessage(), e);
return new ErrorResponse( tokenId,
HttpStatus.BAD_REQUEST.value(),
e.getClass().getSimpleName(),
e.getMessage());
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = UnsupportedOperationException.class)
public ErrorResponse handleUnsupportedOperationException(HttpServletRequest request, UnsupportedOperationException e) {
String tokenId = request.getParameter(TOKEN_ID);
log.info(e.getMessage(), e);
return new ErrorResponse( tokenId,
HttpStatus.BAD_REQUEST.value(),
e.getClass().getSimpleName(),
e.getMessage());
}
/**
* Handles MissingServletRequestParameterExceptions from the rest controller.
*
* @param e MissingServletRequestParameterException
* @return error response POJO
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(value = MissingServletRequestParameterException.class)
public ErrorResponse handleMissingServletRequestParameterException( HttpServletRequest request,
MissingServletRequestParameterException e) {
String tokenId = request.getParameter(TOKEN_ID);
log.info(e.getMessage(), e);
return new ErrorResponse( tokenId,
HttpStatus.BAD_REQUEST.value(),
e.getClass().getSimpleName(),
e.getMessage());
}
/**
* Handles NoHandlerFoundExceptions from the rest controller.
*
* @param e NoHandlerFoundException
* @return error response POJO
*/
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(value = NoHandlerFoundException.class)
public ErrorResponse handleNoHandlerFoundException(HttpServletRequest request, NoHandlerFoundException e) {
String tokenId = request.getParameter(TOKEN_ID);
log.info(e.getMessage(), e);
return new ErrorResponse( tokenId,
HttpStatus.NOT_FOUND.value(),
e.getClass().getSimpleName(),
"The resource " + e.getRequestURL() + " is unavailable");
}
/**
* Handles all remaining exceptions from the rest controller.
*
* This acts as a catch-all for any exceptions not handled by previous exception handlers.
*
* @param e Exception
* @return error response POJO
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(value = Exception.class)
public ErrorResponse handleException(HttpServletRequest request, Exception e) {
String tokenId = request.getParameter(TOKEN_ID);
log.error(e.getMessage(), e);
return new ErrorResponse( tokenId,
HttpStatus.INTERNAL_SERVER_ERROR.value(),
e.getClass().getSimpleName(),
"An internal error occurred");
}
}
Httpステータスコードに従って応答したい人のために、ErrorController
の方法を使うことができます。
@Controller
public class CustomErrorController extends BasicErrorController {
public CustomErrorController(ServerProperties serverProperties) {
super(new DefaultErrorAttributes(), serverProperties.getError());
}
@Override
public ResponseEntity error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status.equals(HttpStatus.INTERNAL_SERVER_ERROR)){
return ResponseEntity.status(status).body(ResponseBean.SERVER_ERROR);
}else if (status.equals(HttpStatus.BAD_REQUEST)){
return ResponseEntity.status(status).body(ResponseBean.BAD_REQUEST);
}
return super.error(request);
}
}
ここでのResponseBean
は私の応答用のカスタムポジョです。
dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
と@EnableWebMvc @ControllerAdvice
による解決策はSpring Boot 1.3.1で私のために働きましたが、1.2.7では働きませんでした