信頼性は、大規模な金融および分析プラットフォームの資産です。当社が企業プラットフォームを再設計したとき、顧客を不満にした主なものは、パフォーマンスやユーザーインターフェイスではなく、修正されていない欠陥がプロセスを破壊したことを学びました。 Spring Boot や他のフレームワークにおける類似のパターンでは、ストレスの下に置かれたときにシステムが予想通り行動することを確認します。 @ControllerAdvice Why This Matters REST API を構築する場合 あなたはすぐにこの問題に直面します: Spring Boot “ ” How do I handle errors neatly without writing repetitive try-catch blocks everywhere? Imagine having 誰でも失敗できるA 無効な入力または欠けているリソース バラバラなスタックの痕跡を返す代わりに、あなたは . 50+ endpoints ゼロポインター例外 consistent, meaningful, and client-friendly error responses それがどこ すべてのエラー処理を 1 か所に集中します。 見よう これを一歩一歩、現実世界の実例で構築する。 @ControllerAdvice how Use Case — User Management REST API We will create a それが支持する: simple User API Fetching users Creating users Simulating failures for testing Our goal → build a that returns clean JSON responses. unified global error-handling layer Step 1: Project Setup Dependencies (Maven): <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Validation --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> This brings in web + validation support. Then create your main app: @SpringBootApplication public class ExceptionHandlerDemoApplication { public static void main(String[] args) { SpringApplication.run(ExceptionHandlerDemoApplication.class, args); } } Step 2: Define the Entity — User.java @Data @AllArgsConstructor @NoArgsConstructor public class User { private int id; @NotBlank(message = "Name cannot be blank") private String name; @Min(value = 18, message = "Age must be at least 18") private int age; } ●なぜ: ** 単純な検証を追加するため、入力が無効な場合に自動的に Method Argument Not Valid Exception を起動できます。 Step 3: Custom Exception for Missing Data public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String message) { super(message); } } public class InvalidRequestException extends RuntimeException { public InvalidRequestException(String message) { super(message); } } ●なぜ: ** 一般的な例外の代わりに「ユーザーが見つかりません」の状況をきれいに表します。 Step 4: Build the Controller @RestController @RequestMapping("/api/users") public class UserController { // Simple GET that throws ResourceNotFoundException for id > 100 @GetMapping("/{id}") public String getUser(@PathVariable("id") @Min(1) Integer id) { if (id > 100) { throw new ResourceNotFoundException("User with id " + id + " not found"); } return "User-" + id; } // Create user example to demonstrate validation public static record CreateUserRequest( @NotBlank(message = "name is required") String name, @Min(value = 18, message = "age must be >= 18") int age) {} @PostMapping @ResponseStatus(HttpStatus.CREATED) public String createUser(@RequestBody @Valid CreateUserRequest body) { if ("bad".equalsIgnoreCase(body.name())) { throw new InvalidRequestException("Name 'bad' is not allowed"); } return "created:" + body.name(); } // Endpoint to force a server error for demo @GetMapping("/boom") public void boom() { throw new IllegalStateException("simulated server error"); } } ●なぜ: ** 現実的なシナリオ - 見つからない、検証エラー、およびランタイムエラー - 私たちのグローバルトレーダーが管理します。 Step 5: Create a Standard Error Model @Data @AllArgsConstructor @NoArgsConstructor public class ErrorResponse { private OffsetDateTime timestamp; private int status; private String error; private String message; private String path; private List<FieldError> fieldErrors;} ●なぜ: **すべてのAPIは同じ構造でエラーを返す必要があります - これは生産におけるモニタリングとデバッグを改善します。 Step 6: Implement @ControllerAdvice (Global Handler) @ControllerAdvice public class GlobalExceptionHandler { private final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); // Handle custom validation exceptions @ExceptionHandler(InvalidRequestException.class) public ResponseEntity<ErrorResponse> handleInvalidRequest(InvalidRequestException ex, HttpServletRequest req) { log.debug("InvalidRequestException: {}", ex.getMessage()); ErrorResponse body = new ErrorResponse(OffsetDateTime.now(), HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase(), ex.getMessage(), req.getRequestURI()); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body); } // Resource not found -> 404 @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex, HttpServletRequest req) { log.debug("ResourceNotFoundException: {}", ex.getMessage()); ErrorResponse body = new ErrorResponse(OffsetDateTime.now(), HttpStatus.NOT_FOUND.value(), HttpStatus.NOT_FOUND.getReasonPhrase(), ex.getMessage(), req.getRequestURI()); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); } // Validation errors from @Valid on request bodies @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex, HttpServletRequest req) { log.debug("Validation failed: {}", ex.getMessage()); List<ErrorResponse.FieldError> fieldErrors = ex.getBindingResult().getFieldErrors().stream() .map(fe -> new ErrorResponse.FieldError(fe.getField(), fe.getRejectedValue(), fe.getDefaultMessage())) .collect(Collectors.toList()); ErrorResponse body = new ErrorResponse(OffsetDateTime.now(), HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase(), "Validation failed", req.getRequestURI()); body.setFieldErrors(fieldErrors); return ResponseEntity.badRequest().body(body); } // Type mismatch for method args (?id=abc) @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ResponseEntity<ErrorResponse> handleTypeMismatch(MethodArgumentTypeMismatchException ex, HttpServletRequest req) { log.debug("Type mismatch: {}", ex.getMessage()); ErrorResponse body = new ErrorResponse(OffsetDateTime.now(), HttpStatus.BAD_REQUEST.value(), HttpStatus.BAD_REQUEST.getReasonPhrase(), ex.getMessage(), req.getRequestURI()); return ResponseEntity.badRequest().body(body); } // No handler found (404 for unmatched endpoints) @ExceptionHandler(NoHandlerFoundException.class) public ResponseEntity<ErrorResponse> handleNoHandler(NoHandlerFoundException ex, HttpServletRequest req) { log.debug("NoHandlerFound: {} {}", ex.getHttpMethod(), ex.getRequestURL()); ErrorResponse body = new ErrorResponse(OffsetDateTime.now(), HttpStatus.NOT_FOUND.value(), HttpStatus.NOT_FOUND.getReasonPhrase(), "Endpoint not found", req.getRequestURL().toString()); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body); } // Generic fallback @ExceptionHandler(Exception.class) public ResponseEntity<ErrorResponse> handleAll(Exception ex, HttpServletRequest req) { log.error("Unhandled exception: ", ex); ErrorResponse body = new ErrorResponse(OffsetDateTime.now(), HttpStatus.INTERNAL_SERVER_ERROR.value(), HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), "An internal error occurred", req.getRequestURI()); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body); } } Why: @ControllerAdvice → すべてのコントローラでグローバル化します。 @ExceptionHandler → 特定の例外タイプをキャッチします。 buildResponse() ヘルパーは、コードを DRY でクリーンにします。 Step 7: Test Scenarios GET /api/users/1 応答コード: 200 - 成功 ♪♪♪ ♪♪♪ GET /api/users/999 404 Resource Not Found (リソースが見つかりませんでした) ♪♪♪ ♪♪♪ Validation Error POST / API / ユーザー {"name":"", "age": 15} Response: ♪♪♪ ♪♪♪ Unexpected Error GET /api/users/boom ♪♪♪ ♪♪♪ Why This Approach Works in Real Projects Problem Solution Too many try-catch blocks Centralized handling with @ControllerAdvice Inconsistent responses Unified ErrorResponse structure Hard to debug Standardized messages with timestamps and paths Client confusion Clear, meaningful messages for each failure type 「Try-Catch Blocks」 @ControllerAdvice による集中管理 矛盾した反応 Unified ErrorResponse 構造 デバッグ Hard to Debug タイムスタンプとパスによる標準化されたメッセージ 顧客混乱 各失敗タイプのための明確で有意義なメッセージ Real-World Usage Scenarios 銀行API:認証エラー(無効なアカウント番号など)がサービスを崩壊させないようにします。 電子商取引:製品未発見または支払いエラーを優雅に処理します。 Data Microservices: 入力データの検証に失敗したときに構造化されたメッセージを返します。 API Gateway:複数のマイクロサービスで一貫した応答 Final Thoughts @ControllerAdvice、 @ExceptionHandler、およびシンプルなエラー応答モデルを組み合わせると、以下のことが得られます。 クリーンコード 持続的な API 体験 生産におけるデバッグの容易化 これは、Spring Bootの開発における最もシンプルで最も強力なデザインパターンの一つです。