2026年6月1日月曜日

SpringBoot でエラーハンドリングする方法

SpringBoot でエラーハンドリングする方法

概要

これまではエラーをそのまま返却しており SpringBoot 側はハンドリングされていないエラーはすべて 500 エラーで返します

今回は特定のエラーをハンドリングしエラーレスポンスをカスタムする方法を紹介します

環境

  • macOS 26.4.1
  • openjdk 26.0.1
  • SpringBoot 4.0.6
    • jasypt-spring-boot 4.0.4
    • jackson-databind 2.21.2
  • gradle 9.5.1
  • VSCode 1.121.0
  • MySQL 9.6.0
  • Redis 8.6.3

カスタムエラーの作成

まずはカスタムエラーを作成します
独自の Exception になります

  • vim src/main/java/com/example/demo/exception/BusinessException.java
package com.example.demo.exception;

public class BusinessException extends RuntimeException {

	private final String code;

	public BusinessException(String code, String message) {
		super(message);
		this.code = code;
	}

	public String getCode() {
		return code;
	}
}

エラーレスポンス用の DTO の作成

レスポンス用の DTO を作成します
エラーレスポンスとして返却したい情報を管理します

  • vim src/main/java/com/example/demo/exception/dto/ErrorResponse.java
package com.example.demo.exception.dto;

import java.time.LocalDateTime;

public class ErrorResponse {

	private String code;
	private String message;
	private LocalDateTime timestamp;

	public ErrorResponse(String code, String message, LocalDateTime timestamp) {
		this.code = code;
		this.message = message;
		this.timestamp = timestamp;
	}

	public String getCode() {
		return code;
	}

	public String getMessage() {
		return message;
	}

	public LocalDateTime getTimestamp() {
		return timestamp;
	}
}

エラーハンドラの作成

これがメイン部分です
SpringBoot では @RestControllerAdvice を基本的に使います
このアノテーションがあるクラスを自動的に読み込みエラーハンドリングしてくれます

  • vim src/main/java/com/example/demo/exception/GlobalExceptionHandler.java
package com.example.demo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.example.demo.exception.dto.ErrorResponse;

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.time.LocalDateTime;

@RestControllerAdvice
public class GlobalExceptionHandler {

	@ExceptionHandler(BusinessException.class)
	@ResponseStatus(HttpStatus.BAD_REQUEST)
	public ErrorResponse handleBusinessException(BusinessException ex) {
		return new ErrorResponse(ex.getCode(), ex.getMessage(), LocalDateTime.now());
	}

	@ExceptionHandler(Exception.class)
	@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
	public ErrorResponse handleException(Exception ex) {
		return new ErrorResponse("INTERNAL_SERVER_ERROR", "予期しないエラーが発生しました", LocalDateTime.now());
	}
}

サービス側修正

サービス側は作成したカスタム Exception を返却するようにします

  • vim src/main/java/com/example/demo/service/UserService.java
package com.example.demo.service;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import com.example.demo.dto.RedisMessageRequest;
import com.example.demo.dto.UserRequest;
import com.example.demo.dto.UserResponse;
import com.example.demo.entity.User;
import com.example.demo.exception.BusinessException;
import com.example.demo.repository.UserRepository;
import com.fasterxml.jackson.databind.ObjectMapper;

@Service
public class UserService {

	@Autowired
	private UserRepository userRepository;

	@Autowired
	private StringRedisTemplate redisTemplate;

	@Autowired
	private ObjectMapper objectMapper;

	public void createUser(UserRequest req) {

		// 例:ビジネスルール
		if (userRepository.existsByEmail(req.getEmail())) {
			throw new BusinessException("USER_ALREADY_EXISTS", "既に登録されています");
		}

		User user = new User();
		user.setName(req.getName());
		user.setEmail(req.getEmail());

		userRepository.save(user);

		// Redis キューにメッセージを JSON 形式で送信
		try {
			RedisMessageRequest message = new RedisMessageRequest();
			message.setName(req.getName());
			message.setEmail(req.getEmail());

			String jsonMessage = objectMapper.writeValueAsString(message);
			redisTemplate.convertAndSend("chat", jsonMessage);
		} catch (Exception e) {
			throw new RuntimeException("Failed to send Redis message", e);
		}
	}

	public List<UserResponse> getAllUsers() {
		List<User> users = new ArrayList<>();
		userRepository.findAll().forEach(users::add);

		return users.stream().map(this::toResponse).toList();
	}

	private UserResponse toResponse(User user) {
		UserResponse response = new UserResponse();
		response.setName(user.getName());
		return response;
	}
}

動作確認

  • ./gradlew clean && ./gradlew bootRun --args='--jasypt.encryptor.password=xxx'
  • curl -XPOST http://localhost:8080/demo/add -d '{"name": "First", "email": "someemail5@someemailprovider.com"}' -H 'content-type: application/json'
{"code":"USER_ALREADY_EXISTS","message":"既に登録されています","timestamp":"2026-05-25T13:32:16.13214"}

最後に

SpringBoot でエラーハンドリングする方法を紹介しました
@RestControllerAdvice を使いましょう