package com.baiyue.common.web; import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.core.api.ResultCode; import com.baiyue.common.core.exception.BizException; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolationException; import io.micrometer.core.instrument.MeterRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.ResponseEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.AuthenticationException; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.dao.CannotAcquireLockException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.servlet.resource.NoResourceFoundException; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.multipart.MaxUploadSizeExceededException; import org.springframework.web.multipart.support.MissingServletRequestPartException; import java.util.LinkedHashMap; import java.util.Map; /** * Servlet Web 全局异常处理器,将业务异常、参数校验异常和系统异常转换为统一 API 响应。 */ @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); @Autowired(required = false) private MeterRegistry meterRegistry; @ExceptionHandler(BizException.class) public ResponseEntity> handleBusiness(BizException exception, HttpServletRequest request) { return failure(exception.getResultCode(), exception.getMessage(), request); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity>> handleValidation( MethodArgumentNotValidException exception, HttpServletRequest request) { return validationResponse(fieldErrors(exception.getBindingResult().getFieldErrors()), request); } @ExceptionHandler(BindException.class) public ResponseEntity>> handleBinding( BindException exception, HttpServletRequest request) { return validationResponse(fieldErrors(exception.getBindingResult().getFieldErrors()), request); } @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity>> handleConstraint( ConstraintViolationException exception, HttpServletRequest request) { Map errors = new LinkedHashMap<>(); exception.getConstraintViolations().forEach(violation -> errors.put(violation.getPropertyPath().toString(), violation.getMessage())); return validationResponse(errors, request); } @ExceptionHandler({HttpMessageNotReadableException.class, MissingRequestHeaderException.class, MissingServletRequestParameterException.class, MissingServletRequestPartException.class, MethodArgumentTypeMismatchException.class}) public ResponseEntity> handleBadRequest(Exception exception, HttpServletRequest request) { return failure(ResultCode.BAD_REQUEST, ResultCode.BAD_REQUEST.message(), request); } @ExceptionHandler(AuthenticationException.class) public ResponseEntity> handleAuthentication( AuthenticationException exception, HttpServletRequest request) { return failure(ResultCode.UNAUTHORIZED, ResultCode.UNAUTHORIZED.message(), request); } @ExceptionHandler(AccessDeniedException.class) public ResponseEntity> handleAccessDenied( AccessDeniedException exception, HttpServletRequest request) { return failure(ResultCode.FORBIDDEN, ResultCode.FORBIDDEN.message(), request); } @ExceptionHandler(ResponseStatusException.class) public ResponseEntity> handleResponseStatus( ResponseStatusException exception, HttpServletRequest request) { ResultCode code = resultCode(exception.getStatusCode().value()); String message = exception.getReason() == null || exception.getReason().isBlank() ? code.message() : exception.getReason(); return failure(code, message, request); } @ExceptionHandler(NoResourceFoundException.class) public ResponseEntity> handleNoResourceFound( NoResourceFoundException exception, HttpServletRequest request) { return failure(ResultCode.NOT_FOUND, ResultCode.NOT_FOUND.message(), request); } @ExceptionHandler(CannotAcquireLockException.class) public ResponseEntity> handleCannotAcquireLock( CannotAcquireLockException exception, HttpServletRequest request) { log.warn("Database lock acquisition conflict requestId={} traceId={}", CorrelationContext.requestId(request), CorrelationContext.traceId(request)); return failure(ResultCode.CONFLICT, "资源正在处理中,请稍后重试", request); } @ExceptionHandler(MaxUploadSizeExceededException.class) public ResponseEntity> handleMaxUploadSize( MaxUploadSizeExceededException exception, HttpServletRequest request) { long maxBytes = exception.getMaxUploadSize(); String message = maxBytes > 0 ? "上传文件不能超过" + Math.max(1, (maxBytes + 1024 * 1024 - 1) / (1024 * 1024)) + "MB" : "上传文件超过服务允许大小"; return failure(ResultCode.BAD_REQUEST, message, request); } @ExceptionHandler(Exception.class) public ResponseEntity> handleUnexpected(Exception exception, HttpServletRequest request) { log.error("Unhandled request failure requestId={} traceId={}", CorrelationContext.requestId(request), CorrelationContext.traceId(request), exception); return failure(ResultCode.INTERNAL_ERROR, ResultCode.INTERNAL_ERROR.message(), request); } private Map fieldErrors(Iterable fieldErrors) { Map errors = new LinkedHashMap<>(); fieldErrors.forEach(error -> errors.putIfAbsent(error.getField(), error.getDefaultMessage())); return errors; } private ResponseEntity>> validationResponse( Map errors, HttpServletRequest request) { ResultCode code = ResultCode.VALIDATION_FAILED; ApiResponse> body = ApiResponse.of(code, validationMessage(errors), errors, CorrelationContext.requestId(request), CorrelationContext.traceId(request)); recordFailure(code); return ResponseEntity.ok(body); } private String validationMessage(Map errors) { if (errors.isEmpty()) { return ResultCode.VALIDATION_FAILED.message(); } return errors.entrySet().stream() .map(entry -> entry.getKey() + ": " + entry.getValue()) .collect(java.util.stream.Collectors.joining(";")); } private ResponseEntity> failure(ResultCode code, String message, HttpServletRequest request) { ApiResponse body = ApiResponse.failure(code, message, CorrelationContext.requestId(request), CorrelationContext.traceId(request)); recordFailure(code); return ResponseEntity.ok(body); } private void recordFailure(ResultCode code) { if (meterRegistry != null) { meterRegistry.counter("baiyue.api.business.failures", "code", code.code()).increment(); } } private ResultCode resultCode(int status) { return switch (status) { case 400 -> ResultCode.BAD_REQUEST; case 401 -> ResultCode.UNAUTHORIZED; case 403 -> ResultCode.FORBIDDEN; case 404 -> ResultCode.NOT_FOUND; case 409 -> ResultCode.CONFLICT; case 422 -> ResultCode.VALIDATION_FAILED; case 429 -> ResultCode.TOO_MANY_REQUESTS; case 503 -> ResultCode.SERVICE_UNAVAILABLE; default -> ResultCode.INTERNAL_ERROR; }; } }