潘志宝
2024-12-31 c903e9db81839c62dc3a046cfe3434d1a58726ce
提交 | 用户 | 时间
e7c126 1 package com.iailab.framework.web.core.handler;
H 2
3 import cn.hutool.core.exceptions.ExceptionUtil;
4 import cn.hutool.core.map.MapUtil;
5 import cn.hutool.core.util.StrUtil;
6 import com.iailab.framework.apilog.core.service.ApiErrorLogFrameworkService;
7 import com.iailab.framework.common.exception.ServiceException;
8 import com.iailab.framework.common.pojo.CommonResult;
9 import com.iailab.framework.common.util.collection.SetUtils;
10 import com.iailab.framework.common.util.json.JsonUtils;
11 import com.iailab.framework.common.util.monitor.TracerUtils;
12 import com.iailab.framework.common.util.servlet.ServletUtils;
13 import com.iailab.framework.web.core.util.WebFrameworkUtils;
14 import com.iailab.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
15 import lombok.AllArgsConstructor;
16 import lombok.extern.slf4j.Slf4j;
17 import org.springframework.security.access.AccessDeniedException;
18 import org.springframework.util.Assert;
19 import org.springframework.validation.BindException;
20 import org.springframework.validation.FieldError;
21 import org.springframework.web.HttpRequestMethodNotSupportedException;
22 import org.springframework.web.bind.MethodArgumentNotValidException;
23 import org.springframework.web.bind.MissingServletRequestParameterException;
24 import org.springframework.web.bind.annotation.ExceptionHandler;
25 import org.springframework.web.bind.annotation.RestControllerAdvice;
26 import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
27 import org.springframework.web.servlet.NoHandlerFoundException;
449017 28 import org.springframework.dao.DuplicateKeyException;
e7c126 29
H 30 import javax.servlet.http.HttpServletRequest;
31 import javax.validation.ConstraintViolation;
32 import javax.validation.ConstraintViolationException;
33 import javax.validation.ValidationException;
34 import java.time.LocalDateTime;
35 import java.util.Map;
36 import java.util.Set;
37
38 import static com.iailab.framework.common.exception.enums.GlobalErrorCodeConstants.*;
39
40 /**
41  * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号
42  *
43  * @author iailab
44  */
45 @RestControllerAdvice
46 @AllArgsConstructor
47 @Slf4j
48 public class GlobalExceptionHandler {
49
50     /**
51      * 忽略的 ServiceException 错误提示,避免打印过多 logger
52      */
53     public static final Set<String> IGNORE_ERROR_MESSAGES = SetUtils.asSet("无效的刷新令牌");
54
55     @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
56     private final String applicationName;
57
58     private final ApiErrorLogFrameworkService apiErrorLogFrameworkService;
59
60     /**
61      * 处理所有异常,主要是提供给 Filter 使用
62      * 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。
63      *
64      * @param request 请求
65      * @param ex 异常
66      * @return 通用返回
67      */
68     public CommonResult<?> allExceptionHandler(HttpServletRequest request, Throwable ex) {
69         if (ex instanceof MissingServletRequestParameterException) {
70             return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex);
71         }
72         if (ex instanceof MethodArgumentTypeMismatchException) {
73             return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex);
74         }
75         if (ex instanceof MethodArgumentNotValidException) {
76             return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex);
77         }
78         if (ex instanceof BindException) {
79             return bindExceptionHandler((BindException) ex);
80         }
81         if (ex instanceof ConstraintViolationException) {
82             return constraintViolationExceptionHandler((ConstraintViolationException) ex);
83         }
84         if (ex instanceof ValidationException) {
85             return validationException((ValidationException) ex);
86         }
87         if (ex instanceof NoHandlerFoundException) {
88             return noHandlerFoundExceptionHandler((NoHandlerFoundException) ex);
89         }
90         if (ex instanceof HttpRequestMethodNotSupportedException) {
91             return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex);
92         }
93         if (ex instanceof ServiceException) {
94             return serviceExceptionHandler((ServiceException) ex);
95         }
96         if (ex instanceof AccessDeniedException) {
97             return accessDeniedExceptionHandler(request, (AccessDeniedException) ex);
449017 98         }
D 99         if (ex instanceof DuplicateKeyException) {
100             return duplicateKeyExceptionHandler((DuplicateKeyException) ex);
e7c126 101         }
H 102         return defaultExceptionHandler(request, ex);
103     }
104
105     /**
106      * 处理 SpringMVC 请求参数缺失
107      *
108      * 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数
109      */
110     @ExceptionHandler(value = MissingServletRequestParameterException.class)
111     public CommonResult<?> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) {
112         log.warn("[missingServletRequestParameterExceptionHandler]", ex);
113         return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName()));
114     }
115
116     /**
117      * 处理 SpringMVC 请求参数类型错误
118      *
119      * 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String
120      */
121     @ExceptionHandler(MethodArgumentTypeMismatchException.class)
122     public CommonResult<?> methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) {
123         log.warn("[missingServletRequestParameterExceptionHandler]", ex);
124         return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage()));
125     }
126
127     /**
128      * 处理 SpringMVC 参数校验不正确
129      */
130     @ExceptionHandler(MethodArgumentNotValidException.class)
131     public CommonResult<?> methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) {
132         log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex);
133         FieldError fieldError = ex.getBindingResult().getFieldError();
134         assert fieldError != null; // 断言,避免告警
135         return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
136     }
137
138     /**
139      * 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验
140      */
141     @ExceptionHandler(BindException.class)
142     public CommonResult<?> bindExceptionHandler(BindException ex) {
143         log.warn("[handleBindException]", ex);
144         FieldError fieldError = ex.getFieldError();
145         assert fieldError != null; // 断言,避免告警
146         return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage()));
147     }
148
149     /**
150      * 处理 Validator 校验不通过产生的异常
151      */
152     @ExceptionHandler(value = ConstraintViolationException.class)
153     public CommonResult<?> constraintViolationExceptionHandler(ConstraintViolationException ex) {
154         log.warn("[constraintViolationExceptionHandler]", ex);
155         ConstraintViolation<?> constraintViolation = ex.getConstraintViolations().iterator().next();
156         return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage()));
157     }
158
159     /**
160      * 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常
161      */
162     @ExceptionHandler(value = ValidationException.class)
163     public CommonResult<?> validationException(ValidationException ex) {
164         log.warn("[constraintViolationExceptionHandler]", ex);
165         // 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读
166         return CommonResult.error(BAD_REQUEST);
167     }
168
169     /**
170      * 处理 SpringMVC 请求地址不存在
171      *
172      * 注意,它需要设置如下两个配置项:
173      * 1. spring.mvc.throw-exception-if-no-handler-found 为 true
174      * 2. spring.mvc.static-path-pattern 为 /statics/**
175      */
176     @ExceptionHandler(NoHandlerFoundException.class)
177     public CommonResult<?> noHandlerFoundExceptionHandler(NoHandlerFoundException ex) {
178         log.warn("[noHandlerFoundExceptionHandler]", ex);
179         return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL()));
180     }
181
182     /**
183      * 处理 SpringMVC 请求方法不正确
184      *
185      * 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配
186      */
187     @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
188     public CommonResult<?> httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) {
189         log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex);
190         return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage()));
191     }
192
193     /**
194      * 处理 Spring Security 权限不足的异常
195      *
196      * 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截
197      */
198     @ExceptionHandler(value = AccessDeniedException.class)
199     public CommonResult<?> accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) {
200         log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", WebFrameworkUtils.getLoginUserId(req),
201                 req.getRequestURL(), ex);
202         return CommonResult.error(FORBIDDEN);
203     }
204
205     /**
449017 206      * 处理业务异常 SQLIntegrityConstraintViolationException
D 207      *
208      * 数据库存在重复数据
209      */
210     @ExceptionHandler(value = DuplicateKeyException.class)
211     public CommonResult<?> duplicateKeyExceptionHandler(DuplicateKeyException ex) {
212         log.warn("[duplicateKeyExceptionHandler]", ex);
213         return CommonResult.error(DATA_REPETITION.getCode(), DATA_REPETITION.getMsg());
214     }
215
216     /**
e7c126 217      * 处理业务异常 ServiceException
H 218      *
219      * 例如说,商品库存不足,用户手机号已存在。
220      */
221     @ExceptionHandler(value = ServiceException.class)
222     public CommonResult<?> serviceExceptionHandler(ServiceException ex) {
325d2f 223         // 不包含的时候,才进行打印,避免 ex 堆栈过多
e7c126 224         if (!IGNORE_ERROR_MESSAGES.contains(ex.getMessage())) {
325d2f 225             // 即使打印,也只打印第一层 StackTraceElement,并且使用 warn 在控制台输出,更容易看到
H 226             StackTraceElement[] stackTrace = ex.getStackTrace();
227             log.warn("[serviceExceptionHandler]\n\t{}", stackTrace[0]);
e7c126 228         }
H 229         return CommonResult.error(ex.getCode(), ex.getMessage());
230     }
231
232     /**
233      * 处理系统异常,兜底处理所有的一切
234      */
235     @ExceptionHandler(value = Exception.class)
236     public CommonResult<?> defaultExceptionHandler(HttpServletRequest req, Throwable ex) {
237         // 情况一:处理表不存在的异常
238         CommonResult<?> tableNotExistsResult = handleTableNotExists(ex);
239         if (tableNotExistsResult != null) {
240             return tableNotExistsResult;
241         }
242
243         // 情况二:处理异常
244         log.error("[defaultExceptionHandler]", ex);
245         // 插入异常日志
246         createExceptionLog(req, ex);
247         // 返回 ERROR CommonResult
248         return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
249     }
250
251     private void createExceptionLog(HttpServletRequest req, Throwable e) {
252         // 插入错误日志
253         ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO();
254         try {
255             // 初始化 errorLog
256             buildExceptionLog(errorLog, req, e);
257             // 执行插入 errorLog
258             apiErrorLogFrameworkService.createApiErrorLog(errorLog);
259         } catch (Throwable th) {
260             log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(),  JsonUtils.toJsonString(errorLog), th);
261         }
262     }
263
264     private void buildExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) {
265         // 处理用户信息
266         errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
267         errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request));
268         // 设置异常字段
269         errorLog.setExceptionName(e.getClass().getName());
270         errorLog.setExceptionMessage(ExceptionUtil.getMessage(e));
271         errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e));
272         errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e));
273         StackTraceElement[] stackTraceElements = e.getStackTrace();
274         Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空");
275         StackTraceElement stackTraceElement = stackTraceElements[0];
276         errorLog.setExceptionClassName(stackTraceElement.getClassName());
277         errorLog.setExceptionFileName(stackTraceElement.getFileName());
278         errorLog.setExceptionMethodName(stackTraceElement.getMethodName());
279         errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber());
280         // 设置其它字段
281         errorLog.setTraceId(TracerUtils.getTraceId());
282         errorLog.setApplicationName(applicationName);
283         errorLog.setRequestUrl(request.getRequestURI());
284         Map<String, Object> requestParams = MapUtil.<String, Object>builder()
285                 .put("query", ServletUtils.getParamMap(request))
286                 .put("body", ServletUtils.getBody(request)).build();
287         errorLog.setRequestParams(JsonUtils.toJsonString(requestParams));
288         errorLog.setRequestMethod(request.getMethod());
289         errorLog.setUserAgent(ServletUtils.getUserAgent(request));
290         errorLog.setUserIp(ServletUtils.getClientIP(request));
291         errorLog.setExceptionTime(LocalDateTime.now());
292     }
293
294     /**
295      * 处理 Table 不存在的异常情况
296      *
297      * @param ex 异常
298      * @return 如果是 Table 不存在的异常,则返回对应的 CommonResult
299      */
300     private CommonResult<?> handleTableNotExists(Throwable ex) {
301         String message = ExceptionUtil.getRootCauseMessage(ex);
302         if (!message.contains("doesn't exist")) {
303             return null;
304         }
305         // 1. 数据报表
306         if (message.contains("report_")) {
325d2f 307             log.error("[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]");
e7c126 308             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
325d2f 309                     "[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]");
e7c126 310         }
H 311         // 2. 工作流
312         if (message.contains("bpm_")) {
325d2f 313             log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]");
e7c126 314             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
325d2f 315                     "[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]");
e7c126 316         }
H 317         // 3. 微信公众号
318         if (message.contains("mp_")) {
325d2f 319             log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
e7c126 320             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
325d2f 321                     "[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
e7c126 322         }
H 323         // 4. 商城系统
324         if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) {
325d2f 325             log.error("[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
e7c126 326             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
325d2f 327                     "[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
e7c126 328         }
H 329         // 5. ERP 系统
330         if (message.contains("erp_")) {
325d2f 331             log.error("[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
e7c126 332             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
325d2f 333                     "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
e7c126 334         }
H 335         // 6. CRM 系统
336         if (message.contains("crm_")) {
325d2f 337             log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
e7c126 338             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
325d2f 339                     "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
e7c126 340         }
H 341         // 7. 支付平台
342         if (message.contains("pay_")) {
325d2f 343             log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
e7c126 344             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
325d2f 345                     "[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
H 346         }
347         // 8. AI 大模型
348         if (message.contains("ai_")) {
349             log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
350             return CommonResult.error(NOT_IMPLEMENTED.getCode(),
351                     "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
e7c126 352         }
H 353         return null;
354     }
355
356 }