Spring MVC 异常处理与国际化
国际化异常消息能够为不同语言用户返回友好的错误提示,提升用户体验。
错误码枚举设计
Java
public enum ErrorCode {
// 系统错误 1xxx
SYSTEM_ERROR(1000, "system.error"),
PARAM_INVALID(1001, "param.invalid"),
PARAM_MISSING(1002, "param.missing"),
// 用户错误 2xxx
USER_NOT_FOUND(2001, "user.not.found"),
USER_EXISTS(2002, "user.exists"),
LOGIN_FAILED(2003, "login.failed"),
PASSWORD_ERROR(2004, "password.error"),
// 文件错误 3xxx
FILE_TOO_LARGE(3001, "file.too.large"),
FILE_TYPE_INVALID(3002, "file.type.invalid"),
FILE_UPLOAD_FAILED(3003, "file.upload.failed");
private final int code;
private final String messageKey;
ErrorCode(int code, String messageKey) {
this.code = code;
this.messageKey = messageKey;
}
public int getCode() { return code; }
public String getMessageKey() { return messageKey; }
}
国际化消息资源文件
properties
# messages.properties(默认)
system.error=系统错误
param.invalid=参数无效
param.missing=参数缺失
user.not.found=用户不存在
user.exists=用户已存在
login.failed=登录失败
password.error=密码错误
file.too.large=文件大小超过限制,最大{0}MB
file.type.invalid=不允许的文件类型,允许的类型:{0}
file.upload.failed=文件上传失败
# messages_en.properties(英文)
system.error=System error
param.invalid=Invalid parameter
param.missing=Missing parameter
user.not.found=User not found
user.exists=User already exists
login.failed=Login failed
password.error=Password error
file.too.large=File size exceeds limit, max {0}MB
file.type.invalid=Invalid file type, allowed types: {0}
file.upload.failed=File upload failed
# messages_zh_CN.properties(简体中文)
system.error=系统错误,请稍后重试
param.invalid=参数格式不正确
param.missing=缺少必填参数
user.not.found=用户不存在,请检查用户ID
user.exists=用户已存在,请使用其他用户名
login.failed=登录失败,请检查账号密码
password.error=密码错误
file.too.large=文件大小超过限制,最大允许{0}MB
file.type.invalid=不允许的文件类型,允许的文件类型:{0}
file.upload.failed=文件上传失败,请重试
业务异常类
Java
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
private final Object[] args;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessageKey());
this.errorCode = errorCode;
this.args = null;
}
public BusinessException(ErrorCode errorCode, Object... args) {
super(errorCode.getMessageKey());
this.errorCode = errorCode;
this.args = args;
}
public ErrorCode getErrorCode() { return errorCode; }
public Object[] getArgs() { return args; }
}
国际化异常处理器
Java
@RestControllerAdvice
public class GlobalExceptionHandler {
@Autowired
private MessageSource messageSource;
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResult<Void>> handleBusinessException(
BusinessException e,
HttpServletRequest request) {
Locale locale = LocaleContextHolder.getLocale();
// 获取国际化消息
String message = getMessage(e.getErrorCode(), e.getArgs(), locale);
ApiResult<Void> result = ApiResult.error(e.getErrorCode().getCode(), message);
return ResponseEntity.status(e.getErrorCode().getCode() < 500 ? 400 : 500)
.body(result);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResult<Void>> handleException(Exception e) {
Locale locale = LocaleContextHolder.getLocale();
String message = messageSource.getMessage(
ErrorCode.SYSTEM_ERROR.getMessageKey(),
null,
locale
);
ApiResult<Void> result = ApiResult.error(
ErrorCode.SYSTEM_ERROR.getCode(),
message
);
return ResponseEntity.internalServerError().body(result);
}
private String getMessage(ErrorCode errorCode, Object[] args, Locale locale) {
return messageSource.getMessage(
errorCode.getMessageKey(),
args,
errorCode.getMessageKey(),
locale
);
}
}
统一响应封装
Java
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResult<T> {
private int code;
private String message;
private T data;
private Long timestamp;
public static <T> ApiResult<T> success(T data) {
return new ApiResult<>(200, "success", data, System.currentTimeMillis());
}
public static <T> ApiResult<T> success() {
return success(null);
}
public static <T> ApiResult<T> error(int code, String message) {
return new ApiResult<>(code, message, null, System.currentTimeMillis());
}
public static <T> ApiResult<T> error(ErrorCode errorCode, Locale locale, MessageSource messageSource) {
String message = messageSource.getMessage(
errorCode.getMessageKey(),
null,
locale
);
return error(errorCode.getCode(), message);
}
public static <T> ApiResult<T> error(ErrorCode errorCode, Object[] args, Locale locale, MessageSource messageSource) {
String message = messageSource.getMessage(
errorCode.getMessageKey(),
args,
locale
);
return error(errorCode.getCode(), message);
}
}
业务中使用
Java
@Service
public class UserService {
public User findById(Long id) {
User user = userRepository.findById(id);
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
}
return user;
}
public void register(UserDTO dto) {
if (userRepository.existsByUsername(dto.getUsername())) {
throw new BusinessException(ErrorCode.USER_EXISTS);
}
// 创建用户...
}
public void login(String username, String password) {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new BusinessException(ErrorCode.LOGIN_FAILED);
}
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BusinessException(ErrorCode.PASSWORD_ERROR);
}
}
}
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ApiResult<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ApiResult.success(user);
}
@PostMapping
public ApiResult<User> register(@RequestBody UserDTO dto) {
userService.register(dto);
return ApiResult.success();
}
}
文件上传国际化异常
Java
@RestControllerAdvice
public class FileUploadExceptionHandler {
@Autowired
private MessageSource messageSource;
@ExceptionHandler(MaxUploadSizeExceededException.class)
public ResponseEntity<ApiResult<Void>> handleMaxSizeException(
MaxUploadSizeExceededException e) {
Locale locale = LocaleContextHolder.getLocale();
long maxSizeMB = e.getMaxFileSize() / 1024 / 1024;
String message = messageSource.getMessage(
ErrorCode.FILE_TOO_LARGE.getMessageKey(),
new Object[]{maxSizeMB},
locale
);
return ResponseEntity.badRequest()
.body(ApiResult.error(ErrorCode.FILE_TOO_LARGE.getCode(), message));
}
}
参数校验国际化
Java
@RestControllerAdvice
public class ValidationExceptionHandler {
@Autowired
private MessageSource messageSource;
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResult<List<FieldError>>> handleValidation(
MethodArgumentNotValidException e) {
Locale locale = LocaleContextHolder.getLocale();
List<FieldError> errors = e.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> new FieldError(
error.getField(),
messageSource.getMessage(error.getDefaultMessage(), null, locale)
))
.collect(Collectors.toList());
String message = messageSource.getMessage(
ErrorCode.PARAM_INVALID.getMessageKey(),
null,
locale
);
return ResponseEntity.badRequest()
.body(ApiResult.error(ErrorCode.PARAM_INVALID.getCode(), message, errors));
}
}
消息服务封装
Java
@Service
public class MessageService {
@Autowired
private MessageSource messageSource;
public String getMessage(String key) {
return getMessage(key, null);
}
public String getMessage(String key, Object[] args) {
Locale locale = LocaleContextHolder.getLocale();
return messageSource.getMessage(key, args, locale);
}
public String getMessage(ErrorCode errorCode) {
return getMessage(errorCode.getMessageKey());
}
public String getMessage(ErrorCode errorCode, Object[] args) {
Locale locale = LocaleContextHolder.getLocale();
return messageSource.getMessage(
errorCode.getMessageKey(),
args,
locale
);
}
}
LocaleContextHolder获取Locale
Java
// Spring自动从请求头Accept-Language解析Locale
@GetMapping("/test")
public String testLocale() {
Locale locale = LocaleContextHolder.getLocale();
String language = locale.getLanguage();
String country = locale.getCountry();
return "Language: " + language + ", Country: " + country;
}
响应示例
中文用户响应:
JSON
{
"code": 2001,
"message": "用户不存在,请检查用户ID",
"data": null,
"timestamp": 1716000000000
}
英文用户响应:
JSON
{
"code": 2001,
"message": "User not found",
"data": null,
"timestamp": 1716000000000
}
要点总结
- 错误码枚举包含code和messageKey
- messageKey对应国际化资源文件中的key
- BusinessException携带错误码和参数
- GlobalExceptionHandler通过MessageSource获取国际化消息
- LocaleContextHolder自动获取请求Locale
- 参数校验消息同样支持国际化
📝 发现内容有误?点击此处直接编辑