提交 | 用户 | 时间
|
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) { |
325d2f
|
208 |
// 不包含的时候,才进行打印,避免 ex 堆栈过多 |
e7c126
|
209 |
if (!IGNORE_ERROR_MESSAGES.contains(ex.getMessage())) { |
325d2f
|
210 |
// 即使打印,也只打印第一层 StackTraceElement,并且使用 warn 在控制台输出,更容易看到 |
H |
211 |
StackTraceElement[] stackTrace = ex.getStackTrace(); |
|
212 |
log.warn("[serviceExceptionHandler]\n\t{}", stackTrace[0]); |
e7c126
|
213 |
} |
H |
214 |
return CommonResult.error(ex.getCode(), ex.getMessage()); |
|
215 |
} |
|
216 |
|
|
217 |
/** |
|
218 |
* 处理系统异常,兜底处理所有的一切 |
|
219 |
*/ |
|
220 |
@ExceptionHandler(value = Exception.class) |
|
221 |
public CommonResult<?> defaultExceptionHandler(HttpServletRequest req, Throwable ex) { |
|
222 |
// 情况一:处理表不存在的异常 |
|
223 |
CommonResult<?> tableNotExistsResult = handleTableNotExists(ex); |
|
224 |
if (tableNotExistsResult != null) { |
|
225 |
return tableNotExistsResult; |
|
226 |
} |
|
227 |
|
|
228 |
// 情况二:处理异常 |
|
229 |
log.error("[defaultExceptionHandler]", ex); |
|
230 |
// 插入异常日志 |
|
231 |
createExceptionLog(req, ex); |
|
232 |
// 返回 ERROR CommonResult |
|
233 |
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); |
|
234 |
} |
|
235 |
|
|
236 |
private void createExceptionLog(HttpServletRequest req, Throwable e) { |
|
237 |
// 插入错误日志 |
|
238 |
ApiErrorLogCreateReqDTO errorLog = new ApiErrorLogCreateReqDTO(); |
|
239 |
try { |
|
240 |
// 初始化 errorLog |
|
241 |
buildExceptionLog(errorLog, req, e); |
|
242 |
// 执行插入 errorLog |
|
243 |
apiErrorLogFrameworkService.createApiErrorLog(errorLog); |
|
244 |
} catch (Throwable th) { |
|
245 |
log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th); |
|
246 |
} |
|
247 |
} |
|
248 |
|
|
249 |
private void buildExceptionLog(ApiErrorLogCreateReqDTO errorLog, HttpServletRequest request, Throwable e) { |
|
250 |
// 处理用户信息 |
|
251 |
errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); |
|
252 |
errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request)); |
|
253 |
// 设置异常字段 |
|
254 |
errorLog.setExceptionName(e.getClass().getName()); |
|
255 |
errorLog.setExceptionMessage(ExceptionUtil.getMessage(e)); |
|
256 |
errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e)); |
|
257 |
errorLog.setExceptionStackTrace(ExceptionUtil.stacktraceToString(e)); |
|
258 |
StackTraceElement[] stackTraceElements = e.getStackTrace(); |
|
259 |
Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空"); |
|
260 |
StackTraceElement stackTraceElement = stackTraceElements[0]; |
|
261 |
errorLog.setExceptionClassName(stackTraceElement.getClassName()); |
|
262 |
errorLog.setExceptionFileName(stackTraceElement.getFileName()); |
|
263 |
errorLog.setExceptionMethodName(stackTraceElement.getMethodName()); |
|
264 |
errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber()); |
|
265 |
// 设置其它字段 |
|
266 |
errorLog.setTraceId(TracerUtils.getTraceId()); |
|
267 |
errorLog.setApplicationName(applicationName); |
|
268 |
errorLog.setRequestUrl(request.getRequestURI()); |
|
269 |
Map<String, Object> requestParams = MapUtil.<String, Object>builder() |
|
270 |
.put("query", ServletUtils.getParamMap(request)) |
|
271 |
.put("body", ServletUtils.getBody(request)).build(); |
|
272 |
errorLog.setRequestParams(JsonUtils.toJsonString(requestParams)); |
|
273 |
errorLog.setRequestMethod(request.getMethod()); |
|
274 |
errorLog.setUserAgent(ServletUtils.getUserAgent(request)); |
|
275 |
errorLog.setUserIp(ServletUtils.getClientIP(request)); |
|
276 |
errorLog.setExceptionTime(LocalDateTime.now()); |
|
277 |
} |
|
278 |
|
|
279 |
/** |
|
280 |
* 处理 Table 不存在的异常情况 |
|
281 |
* |
|
282 |
* @param ex 异常 |
|
283 |
* @return 如果是 Table 不存在的异常,则返回对应的 CommonResult |
|
284 |
*/ |
|
285 |
private CommonResult<?> handleTableNotExists(Throwable ex) { |
|
286 |
String message = ExceptionUtil.getRootCauseMessage(ex); |
|
287 |
if (!message.contains("doesn't exist")) { |
|
288 |
return null; |
|
289 |
} |
|
290 |
// 1. 数据报表 |
|
291 |
if (message.contains("report_")) { |
325d2f
|
292 |
log.error("[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]"); |
e7c126
|
293 |
return CommonResult.error(NOT_IMPLEMENTED.getCode(), |
325d2f
|
294 |
"[报表模块 yudao-module-report - 表结构未导入][参考 https://cloud.iocoder.cn/report/ 开启]"); |
e7c126
|
295 |
} |
H |
296 |
// 2. 工作流 |
|
297 |
if (message.contains("bpm_")) { |
325d2f
|
298 |
log.error("[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]"); |
e7c126
|
299 |
return CommonResult.error(NOT_IMPLEMENTED.getCode(), |
325d2f
|
300 |
"[工作流模块 yudao-module-bpm - 表结构未导入][参考 https://cloud.iocoder.cn/bpm/ 开启]"); |
e7c126
|
301 |
} |
H |
302 |
// 3. 微信公众号 |
|
303 |
if (message.contains("mp_")) { |
325d2f
|
304 |
log.error("[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]"); |
e7c126
|
305 |
return CommonResult.error(NOT_IMPLEMENTED.getCode(), |
325d2f
|
306 |
"[微信公众号 yudao-module-mp - 表结构未导入][参考 https://cloud.iocoder.cn/mp/build/ 开启]"); |
e7c126
|
307 |
} |
H |
308 |
// 4. 商城系统 |
|
309 |
if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) { |
325d2f
|
310 |
log.error("[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]"); |
e7c126
|
311 |
return CommonResult.error(NOT_IMPLEMENTED.getCode(), |
325d2f
|
312 |
"[商城系统 yudao-module-mall - 已禁用][参考 https://cloud.iocoder.cn/mall/build/ 开启]"); |
e7c126
|
313 |
} |
H |
314 |
// 5. ERP 系统 |
|
315 |
if (message.contains("erp_")) { |
325d2f
|
316 |
log.error("[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); |
e7c126
|
317 |
return CommonResult.error(NOT_IMPLEMENTED.getCode(), |
325d2f
|
318 |
"[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); |
e7c126
|
319 |
} |
H |
320 |
// 6. CRM 系统 |
|
321 |
if (message.contains("crm_")) { |
325d2f
|
322 |
log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); |
e7c126
|
323 |
return CommonResult.error(NOT_IMPLEMENTED.getCode(), |
325d2f
|
324 |
"[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); |
e7c126
|
325 |
} |
H |
326 |
// 7. 支付平台 |
|
327 |
if (message.contains("pay_")) { |
325d2f
|
328 |
log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); |
e7c126
|
329 |
return CommonResult.error(NOT_IMPLEMENTED.getCode(), |
325d2f
|
330 |
"[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); |
H |
331 |
} |
|
332 |
// 8. AI 大模型 |
|
333 |
if (message.contains("ai_")) { |
|
334 |
log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); |
|
335 |
return CommonResult.error(NOT_IMPLEMENTED.getCode(), |
|
336 |
"[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); |
e7c126
|
337 |
} |
H |
338 |
return null; |
|
339 |
} |
|
340 |
|
|
341 |
} |