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