きっかけ
昔の自分の制作物を見ると、“ここはもっとシンプルにまとめられるな”と思った物を見掛ける事はありますが、特に例外処理がもっさりしないようにできそうな匂いがしたので、コツをまとめてみました。
モデル、或いはORMでのやり取りでのゲッターの例外処理 に焦点を当ててみようと思います。
Springモデルのゲッターでの例外処理については、エンティティクラスで行うか、サービスクラスで行うかというのは設計思想によって異なります。
一般的には、以下のようなPros&Consが考えられます:
1.エンティティクラスでの例外処理
- メリット:カプセル化の原則に沿っている。データの整合性をエンティティ自身が保証する
- デメリット:エンティティに例外処理のロジックが増える
2.サービスクラスでの例外処理
- メリット:ビジネスロジックの一部として統一的に例外処理できる
- デメリット:言及されたように、同様の例外処理が複数箇所で必要になり冗長になる可能性がある
多くの場合、Spring環境ではサービス層で例外処理を行い、@ControllerAdvice
や@ExceptionHandler
を使用してグローバルな例外ハンドリングを実装するのがスマートなようです。
特にドメイン駆動設計(DDD)を採用している場合は、エンティティ内での例外処理(値オブジェクトのバリデーションなど)と、サービス層での業務例外処理を適切に使い分けることがセオリーとなります。
共通の例外処理ユーティリティクラスを作成するという方法もありますが、どのアプローチが最適かは、アプリケーションの複雑さやチームの設計方針によって変わることでしょう。
Springでのグローバルな例外処理の実装
Springでグローバルな例外処理を実装する際の主要なアプローチを紹介します。
これにより、コントローラーやサービスクラスで冗長な例外処理を書く必要がなくなります。
1. @ControllerAdviceと@ExceptionHandlerを使用した例外処理
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
/*
* 特定の例外をハンドリング
*/
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFoundException(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
ex.getMessage(),
System.currentTimeMillis());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
/*
* IllegalArgumentExceptionをハンドリング
*/
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ErrorResponse> handleIllegalArgumentException(IllegalArgumentException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
ex.getMessage(),
System.currentTimeMillis());
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
/*
* その他の例外をハンドリング
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"予期せぬエラーが発生しました。",
System.currentTimeMillis());
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
2. エラーレスポンスクラスの定義
public class ErrorResponse {
private int status;
private String message;
private long timestamp;
public ErrorResponse(int status, String message, long timestamp) {
this.status = status;
this.message = message;
this.timestamp = timestamp;
}
// getterとsetterを省略
}
3. カスタム例外クラスの作成
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
4. サービスクラスでの例外のスロー例
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("ユーザーが見つかりません。ID: " + id));
}
public User updateUser(Long id, User user) {
User existingUser = getUserById(id); // 既に例外処理を含むメソッドを呼び出し
// 更新処理...
return userRepository.save(existingUser);
}
}
5. 特定のコントローラーに限定した例外処理
@RestControllerAdvice(assignableTypes = {UserController.class})
public class UserControllerAdvice {
@ExceptionHandler(UserValidationException.class)
public ResponseEntity<ErrorResponse> handleUserValidationException(UserValidationException ex) {
ErrorResponse error = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
ex.getMessage(),
System.currentTimeMillis());
return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
}
}
6. ResponseStatusExceptionの活用
Spring 5からはResponseStatusException
を使用することも可能です:
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
try {
return userService.getUserById(id);
} catch (ResourceNotFoundException ex) {
throw new ResponseStatusException(
HttpStatus.NOT_FOUND, "ユーザーが見つかりません", ex);
}
}
7. Spring Boot 2.3以降でのErrorControllerの実装
@RestController
public class CustomErrorController implements ErrorController {
@RequestMapping("/error")
public ResponseEntity<ErrorResponse> handleError(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
Exception exception = (Exception) request.getAttribute("javax.servlet.error.exception");
ErrorResponse error = new ErrorResponse(
statusCode,
exception != null ? exception.getMessage() : "Unknown error",
System.currentTimeMillis());
return new ResponseEntity<>(error, HttpStatus.valueOf(statusCode));
}
@Override
public String getErrorPath() {
return "/error";
}
}
これらのアプローチを組み合わせることで、コードの冗長性を減らしつつ、適切な例外処理を実装するテクニックの一つになるかもしれません。