package cn.iocoder.yudao.framework.web.core.handler; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.exceptions.ExceptionUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi; import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.collection.SetUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; import com.google.common.util.concurrent.UncheckedExecutionException; import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import jakarta.validation.ValidationException; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.security.access.AccessDeniedException; import org.springframework.util.Assert; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; 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 org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.multipart.MaxUploadSizeExceededException; import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.resource.NoResourceFoundException; import tools.jackson.databind.exc.InvalidFormatException; import java.time.LocalDateTime; import java.util.List; import java.util.Map; import java.util.Set; import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.*; /** * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 * * @author 芋道源码 */ @RestControllerAdvice @AllArgsConstructor @Slf4j public class GlobalExceptionHandler { /** * 忽略的 ServiceException 错误提示,避免打印过多 logger */ public static final Set IGNORE_ERROR_MESSAGES = SetUtils.asSet("无效的刷新令牌"); @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") private final String applicationName; private final ApiErrorLogCommonApi apiErrorLogApi; /** * 处理所有异常,主要是提供给 Filter 使用 * 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。 * * @param request 请求 * @param ex 异常 * @return 通用返回 */ public CommonResult allExceptionHandler(HttpServletRequest request, Throwable ex) { if (ex instanceof MissingServletRequestParameterException) { return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex); } if (ex instanceof MethodArgumentTypeMismatchException) { return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex); } if (ex instanceof MethodArgumentNotValidException) { return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex); } if (ex instanceof BindException) { return bindExceptionHandler((BindException) ex); } if (ex instanceof ConstraintViolationException) { return constraintViolationExceptionHandler((ConstraintViolationException) ex); } if (ex instanceof ValidationException) { return validationException((ValidationException) ex); } if (ex instanceof MaxUploadSizeExceededException) { return maxUploadSizeExceededExceptionHandler((MaxUploadSizeExceededException) ex); } if (ex instanceof NoHandlerFoundException) { return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex); } if (ex instanceof NoResourceFoundException) { return noResourceFoundExceptionHandler(request, (NoResourceFoundException) ex); } if (ex instanceof HttpRequestMethodNotSupportedException) { return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex); } if (ex instanceof HttpMediaTypeNotSupportedException) { return httpMediaTypeNotSupportedExceptionHandler((HttpMediaTypeNotSupportedException) ex); } if (ex instanceof ServiceException) { return serviceExceptionHandler((ServiceException) ex); } if (ex instanceof AccessDeniedException) { return accessDeniedExceptionHandler(request, (AccessDeniedException) ex); } return defaultExceptionHandler(request, ex); } /** * 处理 SpringMVC 请求参数缺失 * * 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数 */ @ExceptionHandler(value = MissingServletRequestParameterException.class) public CommonResult missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) { log.warn("[missingServletRequestParameterExceptionHandler]", ex); return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName())); } /** * 处理 SpringMVC 请求参数类型错误 * * 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) public CommonResult methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) { log.warn("[methodArgumentTypeMismatchExceptionHandler]", ex); return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage())); } /** * 处理 SpringMVC 参数校验不正确 */ @ExceptionHandler(MethodArgumentNotValidException.class) public CommonResult methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) { log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex); // 获取 errorMessage String errorMessage = null; FieldError fieldError = ex.getBindingResult().getFieldError(); if (fieldError == null) { // 组合校验,参考自 https://t.zsxq.com/3HVTx List allErrors = ex.getBindingResult().getAllErrors(); if (CollUtil.isNotEmpty(allErrors)) { errorMessage = allErrors.get(0).getDefaultMessage(); } } else { errorMessage = fieldError.getDefaultMessage(); } // 转换 CommonResult if (StrUtil.isEmpty(errorMessage)) { return CommonResult.error(BAD_REQUEST); } return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", errorMessage)); } /** * 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验 */ @ExceptionHandler(BindException.class) public CommonResult bindExceptionHandler(BindException ex) { log.warn("[handleBindException]", ex); FieldError fieldError = ex.getFieldError(); assert fieldError != null; // 断言,避免告警 return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); } /** * 处理 SpringMVC 请求参数类型错误 * * 例如说,接口上设置了 @RequestBody 实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String */ @ExceptionHandler(HttpMessageNotReadableException.class) @SuppressWarnings("PatternVariableCanBeUsed") public CommonResult methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) { log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex); if (ex.getCause() instanceof InvalidFormatException) { InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause(); return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue())); } if (StrUtil.startWith(ex.getMessage(), "Required request body is missing")) { return CommonResult.error(BAD_REQUEST.getCode(), "请求参数类型错误: request body 缺失"); } return defaultExceptionHandler(ServletUtils.getRequest(), ex); } /** * 处理 Validator 校验不通过产生的异常 */ @ExceptionHandler(value = ConstraintViolationException.class) public CommonResult constraintViolationExceptionHandler(ConstraintViolationException ex) { log.warn("[constraintViolationExceptionHandler]", ex); ConstraintViolation constraintViolation = ex.getConstraintViolations().iterator().next(); return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage())); } /** * 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常 */ @ExceptionHandler(value = ValidationException.class) public CommonResult validationException(ValidationException ex) { log.warn("[constraintViolationExceptionHandler]", ex); // 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读 return CommonResult.error(BAD_REQUEST); } /** * 处理上传文件过大异常 */ @ExceptionHandler(MaxUploadSizeExceededException.class) public CommonResult maxUploadSizeExceededExceptionHandler(MaxUploadSizeExceededException ex) { return CommonResult.error(BAD_REQUEST.getCode(), "上传文件过大,请调整后重试"); } /** * 处理 SpringMVC 请求地址不存在 * * 注意,它需要设置如下两个配置项: * 1. spring.mvc.throw-exception-if-no-handler-found 为 true * 2. spring.mvc.static-path-pattern 为 /statics/** */ @ExceptionHandler(NoHandlerFoundException.class) public CommonResult noHandlerFoundExceptionHandler(NoHandlerFoundException ex) { log.warn("[noHandlerFoundExceptionHandler]", ex); return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL())); } /** * 处理 SpringMVC 请求地址不存在 */ @ExceptionHandler(NoResourceFoundException.class) private CommonResult noResourceFoundExceptionHandler(HttpServletRequest req, NoResourceFoundException ex) { log.warn("[noResourceFoundExceptionHandler]", ex); return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getResourcePath())); } /** * 处理 SpringMVC 请求方法不正确 * * 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配 */ @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public CommonResult httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) { log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex); return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage())); } /** * 处理 SpringMVC 请求的 Content-Type 不正确 * * 例如说,A 接口的 Content-Type 为 application/json,结果请求的 Content-Type 为 application/octet-stream,导致不匹配 */ @ExceptionHandler(HttpMediaTypeNotSupportedException.class) public CommonResult httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedException ex) { log.warn("[httpMediaTypeNotSupportedExceptionHandler]", ex); return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求类型不正确:%s", ex.getMessage())); } /** * 处理 Spring Security 权限不足的异常 * * 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截 */ @ExceptionHandler(value = AccessDeniedException.class) public CommonResult accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) { log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", WebFrameworkUtils.getLoginUserId(req), req.getRequestURL(), ex); return CommonResult.error(FORBIDDEN); } /** * 处理 Guava UncheckedExecutionException * * 例如说,缓存加载报错,可见 https://t.zsxq.com/UszdH */ @ExceptionHandler(value = UncheckedExecutionException.class) public CommonResult uncheckedExecutionExceptionHandler(HttpServletRequest req, UncheckedExecutionException ex) { return allExceptionHandler(req, ex.getCause()); } /** * 处理业务异常 ServiceException * * 例如说,商品库存不足,用户手机号已存在。 */ @ExceptionHandler(value = ServiceException.class) public CommonResult serviceExceptionHandler(ServiceException ex) { // 不包含的时候,才进行打印,避免 ex 堆栈过多 if (!IGNORE_ERROR_MESSAGES.contains(ex.getMessage())) { // 即使打印,也只打印第一层 StackTraceElement,并且使用 warn 在控制台输出,更容易看到 try { StackTraceElement[] stackTraces = ex.getStackTrace(); for (StackTraceElement stackTrace : stackTraces) { if (ObjUtil.notEqual(stackTrace.getClassName(), ServiceExceptionUtil.class.getName())) { log.warn("[serviceExceptionHandler]\n\t{}", stackTrace); break; } } } catch (Exception ignored) { // 忽略日志,避免影响主流程 } } return CommonResult.error(ex.getCode(), ex.getMessage()); } /** * 处理系统异常,兜底处理所有的一切 */ @ExceptionHandler(value = Exception.class) public CommonResult defaultExceptionHandler(HttpServletRequest req, Throwable ex) { // 特殊:如果是 ServiceException 的异常,则直接返回 // 例如说:https://gitee.com/zhijiantianya/yudao-cloud/issues/ICSSRM、https://gitee.com/zhijiantianya/yudao-cloud/issues/ICT6FM if (ex.getCause() != null && ex.getCause() instanceof ServiceException) { return serviceExceptionHandler((ServiceException) ex.getCause()); } // 情况一:处理表不存在的异常 CommonResult tableNotExistsResult = handleTableNotExists(ex); if (tableNotExistsResult != null) { return tableNotExistsResult; } // 情况二:处理异常 log.error("[defaultExceptionHandler]", ex); // 插入异常日志 createExceptionLog(req, ex); // 返回 ERROR CommonResult return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); } private void createExceptionLog(HttpServletRequest req, Throwable e) { // 插入错误日志 ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO(); try { // 初始化 errorLog buildExceptionLog(errorLog, req, e); // 执行插入 errorLog apiErrorLogApi.createApiErrorLogAsync(errorLog); } catch (Throwable th) { log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th); } } private void buildExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) { // 处理用户信息 errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request)); // 设置异常字段 errorLog.setExceptionName(e.getClass().getName()); errorLog.setExceptionMessage(ExceptionUtil.getMessage(e)); errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e)); errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e)); StackTraceElement[] stackTraceElements = e.getStackTrace(); Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空"); StackTraceElement stackTraceElement = stackTraceElements[0]; errorLog.setExceptionClassName(stackTraceElement.getClassName()); errorLog.setExceptionFileName(stackTraceElement.getFileName()); errorLog.setExceptionMethodName(stackTraceElement.getMethodName()); errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber()); // 设置其它字段 errorLog.setTraceId(TracerUtils.getTraceId()); errorLog.setApplicationName(applicationName); errorLog.setRequestUrl(request.getRequestURI()); Map requestParams = MapUtil.builder() .put("query", ServletUtils.getParamMap(request)) .put("body", ServletUtils.getBody(request)).build(); errorLog.setRequestParams(JsonUtils.toJsonString(requestParams)); errorLog.setRequestMethod(request.getMethod()); errorLog.setUserAgent(ServletUtils.getUserAgent(request)); errorLog.setUserIp(ServletUtils.getClientIP(request)); errorLog.setExceptionTime(LocalDateTime.now()); } /** * 处理 Table 不存在的异常情况 * * @param ex 异常 * @return 如果是 Table 不存在的异常,则返回对应的 CommonResult */ private CommonResult handleTableNotExists(Throwable ex) { String message = ExceptionUtil.getRootCauseMessage(ex); if (!message.contains("doesn't exist")) { return null; } // 1. 数据报表 if (message.contains("report_")) { log.error("[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]"); } // 2. 工作流 if (message.contains("bpm_")) { log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]"); } // 3. 微信公众号 if (message.contains("mp_")) { log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]"); } // 4. 商城系统 if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) { log.error("[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]"); } // 5. ERP 系统 if (message.contains("erp_")) { log.error("[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); } // 6. WMS 仓库管理系统 if (message.contains("wms_")) { log.error("[WMS 仓库管理系统 yudao-module-wms - 表结构未导入][参考 https://doc.iocoder.cn/wms/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[WMS 仓库管理系统 yudao-module-wms - 表结构未导入][参考 https://doc.iocoder.cn/wms/build/ 开启]"); } // 7. CRM 系统 if (message.contains("crm_")) { log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); } // 8. MES 系统 if (message.contains("mes_")) { log.error("[MES 系统 yudao-module-mes - 表结构未导入][参考 https://doc.iocoder.cn/mes/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[MES 系统 yudao-module-mes - 表结构未导入][参考 https://doc.iocoder.cn/mes/build/ 开启]"); } // 9. IM 即时通讯 if (message.contains("im_")) { log.error("[IM 即时通讯 yudao-module-im - 表结构未导入][参考 https://doc.iocoder.cn/im/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[IM 即时通讯 yudao-module-im - 表结构未导入][参考 https://doc.iocoder.cn/im/build/ 开启]"); } // 10. 支付平台 if (message.contains("pay_")) { log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); } // 11. AI 大模型 if (message.contains("ai_")) { log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); } // 12. IoT 物联网 if (message.contains("iot_")) { log.error("[IoT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]"); return CommonResult.error(NOT_IMPLEMENTED.getCode(), "[IoT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]"); } return null; } }