Compare commits

...

2 Commits

Author SHA1 Message Date
057a066404 修改注释 2025-05-13 12:41:51 +09:00
158a93d608 增加全局异常处理和业务异常处理 2025-05-13 12:41:41 +09:00
3 changed files with 171 additions and 7 deletions

View File

@ -2,7 +2,7 @@ package co.jp.app.common;
public enum ResultCode { public enum ResultCode {
SUCCESS(200, "Success"), // 通常与 HTTP 200 OK 对应 SUCCESS(200, "Success"),
// 客户端错误段 (1000 - 1999) // 客户端错误段 (1000 - 1999)
BAD_REQUEST(1000, "HTTP 400 Bad Request"), BAD_REQUEST(1000, "HTTP 400 Bad Request"),
@ -12,11 +12,11 @@ public enum ResultCode {
METHOD_NOT_ALLOWED(1004, "HTTP 405 Method Not Allowed"), METHOD_NOT_ALLOWED(1004, "HTTP 405 Method Not Allowed"),
REQUEST_TIMEOUT(1005, "HTTP 408 Request Timeout"), REQUEST_TIMEOUT(1005, "HTTP 408 Request Timeout"),
CONFLICT(1006, "HTTP 409 Conflict"), CONFLICT(1006, "HTTP 409 Conflict"),
UNSUPPORTED_MEDIA_TYPE(1007, "HTTP 415 Unsupported Media Type"), // 对应 UNSUPPORTED_MEDIA_TYPE(1007, "HTTP 415 Unsupported Media Type"),
TOO_MANY_REQUESTS(1008, "HTTP 429 Too Many Requests"), // 对应 TOO_MANY_REQUESTS(1008, "HTTP 429 Too Many Requests"),
VALIDATION_ERROR(1009, "Parameter validation failure"), // 通常由 @Valid 触发,可包含更详细的字段错误信息 VALIDATION_ERROR(1009, "Parameter validation failure"),
// 服务端错误段 (2000 - 2999) - 通常建议与 HTTP 5xx 状态码含义相似 // 服务端错误段 (2000 - 2999)
INTERNAL_SERVER_ERROR(2000, "HTTP 500 Internal Server Error"), INTERNAL_SERVER_ERROR(2000, "HTTP 500 Internal Server Error"),
SERVICE_UNAVAILABLE(2001, "HTTP 503 Service Unavailable"), SERVICE_UNAVAILABLE(2001, "HTTP 503 Service Unavailable"),
GATEWAY_TIMEOUT(2002, "HTTP 504 Gateway Timeout"), GATEWAY_TIMEOUT(2002, "HTTP 504 Gateway Timeout"),
@ -26,7 +26,7 @@ public enum ResultCode {
// ================================== 用户模块状态码 (3000 - 3999) ================================== // ================================== 用户模块状态码 (3000 - 3999) ==================================
// 注册相关 // 注册相关
// USER_REGISTRATION_SUCCESS(3000, "用户注册成功"), // 成功状态也可以用通用的 SUCCESS // USER_REGISTRATION_SUCCESS(3000, "用户注册成功"),
USER_EMAIL_ALREADY_EXISTS(3001, "Email already exists"), USER_EMAIL_ALREADY_EXISTS(3001, "Email already exists"),
// USER_USERNAME_ALREADY_EXISTS(3002, "Username"), // USER_USERNAME_ALREADY_EXISTS(3002, "Username"),
USER_PASSWORD_TOO_SHORT(3003, "password too short"), USER_PASSWORD_TOO_SHORT(3003, "password too short"),
@ -77,7 +77,6 @@ public enum ResultCode {
return message; return message;
} }
// (可选) 允许覆盖默认消息的方法,用于更具体的场景
public String getMessage(String customDetail) { public String getMessage(String customDetail) {
return this.message + (customDetail == null || customDetail.isEmpty() ? "" : " (" + customDetail + ")"); return this.message + (customDetail == null || customDetail.isEmpty() ? "" : " (" + customDetail + ")");
} }

View File

@ -0,0 +1,38 @@
package co.jp.app.exception;
import co.jp.app.common.ResultCode;
public class BusinessException extends RuntimeException {
private final ResultCode resultCode;
private final String detailMessage; // 附加详细信息
public BusinessException(ResultCode resultCode) {
super(resultCode.getMessage());
this.resultCode = resultCode;
this.detailMessage = null;
}
// 有详细信息的构造函数
public BusinessException(ResultCode resultCode, String detailMessage) {
super(detailMessage != null && !detailMessage.isEmpty() ? detailMessage : resultCode.getMessage());
this.resultCode = resultCode;
this.detailMessage = detailMessage;
}
// 无详细信息的构造函数
public BusinessException(ResultCode resultCode, Throwable cause) {
super(resultCode.getMessage(), cause);
this.resultCode = resultCode;
this.detailMessage = null;
}
public ResultCode getResultCode() {
return resultCode;
}
public String getDetailMessage() {
return detailMessage;
}
}

View File

@ -0,0 +1,127 @@
package co.jp.app.exception;
import co.jp.app.common.ApiResponse;
import co.jp.app.common.ResultCode;
import co.jp.app.exception.BusinessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.stream.Collectors;
@RestControllerAdvice
public class GlobalExceptionHandler {
// 日志记录器
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// 业务异常
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Object>> handleBusinessException(BusinessException ex) {
logger.warn("业务异常: Code={}, Message={}, Detail={}", ex.getResultCode().getCode(), ex.getResultCode().getMessage(), ex.getDetailMessage(), ex);
ApiResponse<Object> body;
if (ex.getDetailMessage() != null && !ex.getDetailMessage().isEmpty()) {
body = ApiResponse.fail(ex.getResultCode(), ex.getDetailMessage());
} else {
body = ApiResponse.fail(ex.getResultCode());
}
HttpStatus httpStatus = determineHttpStatusFromResultCode(ex.getResultCode());
return new ResponseEntity<>(body, httpStatus);
}
// 参数校验异常
@ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
public ResponseEntity<ApiResponse<Object>> handleValidationExceptions(BindException ex) { // BindException 是 MethodArgumentNotValidException 的父类
String errorMessage = ex.getBindingResult().getFieldErrors().stream()
.map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
.collect(Collectors.joining("; "));
logger.warn("参数校验失败: {}", errorMessage, ex);
return new ResponseEntity<>(ApiResponse.fail(ResultCode.VALIDATION_ERROR, errorMessage), HttpStatus.BAD_REQUEST);
}
// 参数缺失异常
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseEntity<ApiResponse<Object>> handleMissingServletRequestParameterException(MissingServletRequestParameterException ex) {
String message = "请求参数 '" + ex.getParameterName() + "' 不能为空";
logger.warn(message, ex);
return new ResponseEntity<>(ApiResponse.fail(ResultCode.BAD_REQUEST, message), HttpStatus.BAD_REQUEST);
}
// Spring Security 用户认证异常
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ApiResponse<Object>> handleAuthenticationException(AuthenticationException ex) {
logger.warn("认证失败: {}", ex.getMessage(), ex);
if (ex instanceof BadCredentialsException) {
return new ResponseEntity<>(ApiResponse.fail(ResultCode.USER_INVALID_CREDENTIALS), HttpStatus.UNAUTHORIZED);
} else if (ex instanceof UsernameNotFoundException) {
return new ResponseEntity<>(ApiResponse.fail(ResultCode.USER_INVALID_CREDENTIALS, "用户名或密码错误(用户不存在)"), HttpStatus.UNAUTHORIZED);
}
return new ResponseEntity<>(ApiResponse.fail(ResultCode.UNAUTHORIZED, ex.getMessage()), HttpStatus.UNAUTHORIZED);
}
// Spring Security 权限异常
// @ExceptionHandler(AccessDeniedException.class)
// public ResponseEntity<ApiResponse<Object>> handleAccessDeniedException(AccessDeniedException ex) {
// logger.warn("权限不足: {}", ex.getMessage(), ex);
// return new ResponseEntity<>(ApiResponse.fail(ResultCode.FORBIDDEN), HttpStatus.FORBIDDEN);
// }
// 其他异常
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Object>> handleAllUncaughtException(Exception ex) {
logger.error("发生未捕获的服务器内部错误!", ex);
return new ResponseEntity<>(ApiResponse.fail(ResultCode.INTERNAL_SERVER_ERROR), HttpStatus.INTERNAL_SERVER_ERROR);
}
private HttpStatus determineHttpStatusFromResultCode(ResultCode resultCode) {
if (resultCode == null) return HttpStatus.INTERNAL_SERVER_ERROR;
switch (resultCode) {
case SUCCESS:
return HttpStatus.OK;
case UNAUTHORIZED:
case USER_INVALID_CREDENTIALS:
case USER_TOKEN_INVALID:
case USER_TOKEN_EXPIRED:
return HttpStatus.UNAUTHORIZED;
case FORBIDDEN:
return HttpStatus.FORBIDDEN;
case NOT_FOUND:
case USER_ACCOUNT_NOT_FOUND:
case USER_PROFILE_NOT_FOUND:
return HttpStatus.NOT_FOUND;
case CONFLICT:
case USER_EMAIL_ALREADY_EXISTS:
return HttpStatus.CONFLICT;
case BAD_REQUEST:
case VALIDATION_ERROR:
case USER_PASSWORD_TOO_SHORT:
return HttpStatus.BAD_REQUEST;
default:
if (resultCode.getCode() >= 2000 && resultCode.getCode() < 3000) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
return HttpStatus.BAD_REQUEST;
}
}
}