# scaffold 项目之操作日志、访问日志、错误日志
# 简介
在一个系统中,我们需要知道哪些用户做了哪些操作,或者哪些用户访问了项目,还有系统运行的状态这些记录,方便我们定位排查问题或者溯源。
在本项目中提供了 2 大类 4 种系统日志:
- 审计日志:用户操作日志、登陆日志
- API 日志:访问日志、错误日志
# 操作日志
操作日志记录:【什么人】在【什么数据】对【什么对象】做了【什么事情】,可以在【系统管理 -> 审计日志 -> 操作日志】菜单中看到对应的列表,详情如图显示:

可以看到具体操作了什么,结果如何。
后续可以考虑记录更详细的情况,比如
记录变更前后的情况
# 实现方式:
主要是通过注解和切面的方式实现的。
创建记录日志的注解
package com.tz.scaffold.framework.operatelog.core.annotations;
import com.tz.scaffold.framework.operatelog.core.enums.OperateTypeEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/*** <p> Project: scaffold - OperateLog </p>
*
* 操作日志注解
* @author Tz
* @date 2024/01/09 23:45
* @version 1.0.0
* @since 1.0.0
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OperateLog {
// ========== 模块字段 ==========/*** 操作模块
*
* 为空时,会尝试读取 {@link Tag#name ()} 属性*/
String module() default "";
/*** 操作名
*
* 为空时,会尝试读取 {@link Operation#summary ()} 属性*/
String name() default "";
/*** 操作分类
*
* 实际并不是数组,因为枚举不能设置 null 作为默认值
*/
OperateTypeEnum[] type() default {};
// ========== 开关字段 ==========/*** 是否记录操作日志
*/
boolean enable() default true;
/*** 是否记录方法参数
*/
boolean logArgs() default true;
/*** 是否记录方法结果的数据
*/
boolean logResultData() default true;
}创建切面类来记录日志
package com.tz.scaffold.framework.operatelog.core.aop;
/*** <p> Project: scaffold - OperateLogAspect </p>
*
* 拦截使用 @OperateLog 注解,如果满足条件,则生成操作日志。
* <p>
* 满足如下任一条件,则会进行记录:
* <li>
* 1. 使用 @ApiOperation + 非 @GetMapping
* <li>
* 2. 使用 @OperateLog 注解
* <p>
* 但是,如果声明 @OperateLog 注解时,将 enable 属性设置为 false 时,强制不记录。
* @author Tz
* @date 2024/01/09 23:45
* @version 1.0.0
* @since 1.0.0
*/
@Aspect@Slf4jpublic class OperateLogAspect {
/*** 用于记录操作内容的上下文
*
* @see OperateLog#getContent ()
*/
private static final ThreadLocal<String> CONTENT = new ThreadLocal<>();
/*** 用于记录拓展字段的上下文
*
* @see OperateLog#getExts ()
*/
private static final ThreadLocal<Map<String, Object>> EXTS = new ThreadLocal<>();
@Resourceprivate OperateLogFrameworkService operateLogFrameworkService;
@Around("@annotation(operation)")
public Object around(ProceedingJoinPoint joinPoint, Operation operation) throws Throwable {
// 可能也添加了 @ApiOperation 注解com.tz.scaffold.framework.operatelog.core.annotations.OperateLog operateLog = getMethodAnnotation(joinPoint,
com.tz.scaffold.framework.operatelog.core.annotations.OperateLog.class);
return around0(joinPoint, operateLog, operation);
}@Around("!@annotation(io.swagger.v3.oas.annotations.Operation) && @annotation(operateLog)")
// 兼容处理,只添加 @OperateLog 注解的情况public Object around(ProceedingJoinPoint joinPoint,
com.tz.scaffold.framework.operatelog.core.annotations.OperateLog operateLog) throws Throwable {
return around0(joinPoint, operateLog, null);
}private Object around0(ProceedingJoinPoint joinPoint,
com.tz.scaffold.framework.operatelog.core.annotations.OperateLog operateLog,
Operation operation) throws Throwable {
// 目前,只有管理员,才记录操作日志!所以非管理员,直接调用,不进行记录Integer userType = WebFrameworkUtils.getLoginUserType();
if (!Objects.equals(userType, UserTypeEnum.ADMIN.getValue())) {
return joinPoint.proceed();
}// 记录开始时间LocalDateTime startTime = LocalDateTime.now();
try {
// 执行原有方法Object result = joinPoint.proceed();
// 记录正常执行时的操作日志this.log(joinPoint, operateLog, operation, startTime, result, null);
return result;
} catch (Throwable exception) {
this.log(joinPoint, operateLog, operation, startTime, null, exception);
throw exception;
} finally {
clearThreadLocal();
}}public static void setContent(String content) {
CONTENT.set(content);
}public static void addExt(String key, Object value) {
if (EXTS.get() == null) {
EXTS.set(new HashMap<>());
}EXTS.get().put(key, value);
}private static void clearThreadLocal() {
CONTENT.remove();
EXTS.remove();
}private void log(ProceedingJoinPoint joinPoint,
com.tz.scaffold.framework.operatelog.core.annotations.OperateLog operateLog,
Operation operation,
LocalDateTime startTime, Object result, Throwable exception) {
try {
// 判断不记录的情况if (!isLogEnable(joinPoint, operateLog)) {
return;
}// 真正记录操作日志this.log0(joinPoint, operateLog, operation, startTime, result, exception);
} catch (Throwable ex) {
log.error("[log][记录操作日志时,发生异常,其中参数是 joinPoint({}) operateLog({}) apiOperation({}) result({}) exception({}) ]",
joinPoint, operateLog, operation, result, exception, ex);
}}private void log0(ProceedingJoinPoint joinPoint,
com.tz.scaffold.framework.operatelog.core.annotations.OperateLog operateLog,
Operation operation,
LocalDateTime startTime, Object result, Throwable exception) {
OperateLog operateLogObj = new OperateLog();
// 补全通用字段operateLogObj.setTraceId(TracerUtils.getTraceId());
operateLogObj.setStartTime(startTime);
// 补充用户信息fillUserFields(operateLogObj);
// 补全模块信息fillModuleFields(operateLogObj, joinPoint, operateLog, operation);
// 补全请求信息fillRequestFields(operateLogObj);
// 补全方法信息fillMethodFields(operateLogObj, joinPoint, operateLog, startTime, result, exception);
// 异步记录日志operateLogFrameworkService.createOperateLog(operateLogObj);
}private static void fillUserFields(OperateLog operateLogObj) {
operateLogObj.setUserId(WebFrameworkUtils.getLoginUserId());
operateLogObj.setUserType(WebFrameworkUtils.getLoginUserType());
}private static void fillModuleFields(OperateLog operateLogObj,
ProceedingJoinPoint joinPoint,
com.tz.scaffold.framework.operatelog.core.annotations.OperateLog operateLog,
Operation operation) {
//module 属性if (operateLog != null) {
operateLogObj.setModule(operateLog.module());
}if (StrUtil.isEmpty(operateLogObj.getModule())) {
Tag tag = getClassAnnotation(joinPoint, Tag.class);
if (tag != null) {
// 优先读取 @Tag 的 name 属性if (StrUtil.isNotEmpty(tag.name())) {
operateLogObj.setModule(tag.name());
}// 没有的话,读取 @API 的 description 属性if (StrUtil.isEmpty(operateLogObj.getModule()) && ArrayUtil.isNotEmpty(tag.description())) {
operateLogObj.setModule(tag.description());
}}}//name 属性if (operateLog != null) {
operateLogObj.setName(operateLog.name());
}if (StrUtil.isEmpty(operateLogObj.getName()) && operation != null) {
operateLogObj.setName(operation.summary());
}//type 属性if (operateLog != null && ArrayUtil.isNotEmpty(operateLog.type())) {
operateLogObj.setType(operateLog.type()[0].getType());
}if (operateLogObj.getType() == null) {
RequestMethod requestMethod = obtainFirstMatchRequestMethod(obtainRequestMethod(joinPoint));
OperateTypeEnum operateLogType = convertOperateLogType(requestMethod);
operateLogObj.setType(operateLogType != null ? operateLogType.getType() : null);
}//content 和 exts 属性operateLogObj.setContent(CONTENT.get());
operateLogObj.setExts(EXTS.get());
}private static void fillRequestFields(OperateLog operateLogObj) {
// 获得 Request 对象HttpServletRequest request = ServletUtils.getRequest();
if (request == null) {
return;
}// 补全请求信息operateLogObj.setRequestMethod(request.getMethod());
operateLogObj.setRequestUrl(request.getRequestURI());
operateLogObj.setUserIp(ServletUtils.getClientIP(request));
operateLogObj.setUserAgent(ServletUtils.getUserAgent(request));
}private static void fillMethodFields(OperateLog operateLogObj,
ProceedingJoinPoint joinPoint,
com.tz.scaffold.framework.operatelog.core.annotations.OperateLog operateLog,
LocalDateTime startTime, Object result, Throwable exception) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
operateLogObj.setJavaMethod(methodSignature.toString());
if (operateLog == null || operateLog.logArgs()) {
operateLogObj.setJavaMethodArgs(obtainMethodArgs(joinPoint));
}if (operateLog == null || operateLog.logResultData()) {
operateLogObj.setResultData(obtainResultData(result));
}operateLogObj.setDuration((int) (LocalDateTimeUtil.between(startTime, LocalDateTime.now()).toMillis()));
// (正常)处理 resultCode 和 resultMsg 字段if (result instanceof CommonResult) {
CommonResult<?> commonResult = (CommonResult<?>) result;
operateLogObj.setResultCode(commonResult.getCode());
operateLogObj.setResultMsg(commonResult.getMsg());
} else {
operateLogObj.setResultCode(SUCCESS.getCode());
}// (异常)处理 resultCode 和 resultMsg 字段if (exception != null) {
operateLogObj.setResultCode(INTERNAL_SERVER_ERROR.getCode());
operateLogObj.setResultMsg(ExceptionUtil.getRootCauseMessage(exception));
}}private static boolean isLogEnable(ProceedingJoinPoint joinPoint,
com.tz.scaffold.framework.operatelog.core.annotations.OperateLog operateLog) {
// 有 @OperateLog 注解的情况下if (operateLog != null) {
return operateLog.enable();
}// 没有 @ApiOperation 注解的情况下,只记录 POST、PUT、DELETE 的情况return obtainFirstLogRequestMethod(obtainRequestMethod(joinPoint)) != null;
}private static RequestMethod obtainFirstLogRequestMethod(RequestMethod[] requestMethods) {
if (ArrayUtil.isEmpty(requestMethods)) {
return null;
}return Arrays.stream(requestMethods).filter(requestMethod ->
requestMethod == RequestMethod.POST
|| requestMethod == RequestMethod.PUT
|| requestMethod == RequestMethod.DELETE)
.findFirst().orElse(null);
}private static RequestMethod obtainFirstMatchRequestMethod(RequestMethod[] requestMethods) {
if (ArrayUtil.isEmpty(requestMethods)) {
return null;
}// 优先,匹配最优的 POST、PUT、DELETERequestMethod result = obtainFirstLogRequestMethod(requestMethods);
if (result != null) {
return result;
}// 然后,匹配次优的 GETresult = Arrays.stream(requestMethods).filter(requestMethod -> requestMethod == RequestMethod.GET)
.findFirst().orElse(null);
if (result != null) {
return result;
}// 兜底,获得第一个return requestMethods[0];
}private static OperateTypeEnum convertOperateLogType(RequestMethod requestMethod) {
if (requestMethod == null) {
return null;
}switch (requestMethod) {
case GET:
return OperateTypeEnum.GET;
case POST:
return OperateTypeEnum.CREATE;
case PUT:
return OperateTypeEnum.UPDATE;
case DELETE:
return OperateTypeEnum.DELETE;
default:
return OperateTypeEnum.OTHER;
}}private static RequestMethod[] obtainRequestMethod(ProceedingJoinPoint joinPoint) {
RequestMapping requestMapping = AnnotationUtils.getAnnotation( // 使用 Spring 的工具类,可以处理 @RequestMapping 别名注解
((MethodSignature) joinPoint.getSignature()).getMethod(), RequestMapping.class);
return requestMapping != null ? requestMapping.method() : new RequestMethod[]{};
}@SuppressWarnings("SameParameterValue")
private static <T extends Annotation> T getMethodAnnotation(ProceedingJoinPoint joinPoint, Class<T> annotationClass) {
return ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(annotationClass);
}@SuppressWarnings("SameParameterValue")
private static <T extends Annotation> T getClassAnnotation(ProceedingJoinPoint joinPoint, Class<T> annotationClass) {
return ((MethodSignature) joinPoint.getSignature()).getMethod().getDeclaringClass().getAnnotation(annotationClass);
}private static String obtainMethodArgs(ProceedingJoinPoint joinPoint) {
// TODO 提升:参数脱敏和忽略MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String[] argNames = methodSignature.getParameterNames();
Object[] argValues = joinPoint.getArgs();
// 拼接参数Map<String, Object> args = Maps.newHashMapWithExpectedSize(argValues.length);
for (int i = 0; i < argNames.length; i++) {
String argName = argNames[i];
Object argValue = argValues[i];
// 被忽略时,标记为 ignore 字符串,避免和 null 混在一起args.put(argName, !isIgnoreArgs(argValue) ? argValue : "[ignore]");
}return JsonUtils.toJsonString(args);
}private static String obtainResultData(Object result) {
// TODO 提升:结果脱敏和忽略if (result instanceof CommonResult) {
result = ((CommonResult<?>) result).getData();
}return JsonUtils.toJsonString(result);
}private static boolean isIgnoreArgs(Object object) {
Class<?> clazz = object.getClass();
// 处理数组的情况if (clazz.isArray()) {
return IntStream.range(0, Array.getLength(object))
.anyMatch(index -> isIgnoreArgs(Array.get(object, index)));
}// 递归,处理数组、Collection、Map 的情况if (Collection.class.isAssignableFrom(clazz)) {
return ((Collection<?>) object).stream()
.anyMatch((Predicate<Object>) OperateLogAspect::isIgnoreArgs);
}if (Map.class.isAssignableFrom(clazz)) {
return isIgnoreArgs(((Map<?, ?>) object).values());
}// objreturn object instanceof MultipartFile
|| object instanceof HttpServletRequest
|| object instanceof HttpServletResponse
|| object instanceof BindingResult;
}}创建自动配置类
package com.tz.scaffold.framework.operatelog.config;
import com.tz.scaffold.framework.operatelog.core.aop.OperateLogAspect;
import com.tz.scaffold.framework.operatelog.core.service.OperateLogFrameworkService;
import com.tz.scaffold.framework.operatelog.core.service.OperateLogFrameworkServiceImpl;
import com.tz.scaffold.module.system.api.logger.OperateLogApi;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
/*** <p> Project: scaffold - ScaffoldOperateLogAutoConfiguration </p>
*
* 操作日志配置类
* @author Tz
* @date 2024/01/09 23:45
* @version 1.0.0
* @since 1.0.0
*/
@AutoConfigurationpublic class ScaffoldOperateLogAutoConfiguration {
@Beanpublic OperateLogAspect operateLogAspect() {
return new OperateLogAspect();
}@Beanpublic OperateLogFrameworkService operateLogFrameworkService(OperateLogApi operateLogApi) {
return new OperateLogFrameworkServiceImpl(operateLogApi);
}}封装成组件,下次只需要引入这个 jar 包,直接在方法上加上对应的注解即可记录操作日志
# 登陆日志
登陆日志记录:记录了【哪个用户】在【什么时间】登陆或登出了系统,可以在【系统管理 -> 审计日志 -> 登陆日志】菜单中看到对应的列表。如图显示:

登陆日志的在系统模块下实现,每次用户登陆的时候就会记录
代码:
private void createLoginLog(Long userId, String username, | |
LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) { | |
// 插入登录日志 | |
LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); | |
reqDTO.setLogType(logTypeEnum.getType()); | |
reqDTO.setTraceId(TracerUtils.getTraceId()); | |
reqDTO.setUserId(userId); | |
reqDTO.setUserType(getUserType().getValue()); | |
reqDTO.setUsername(username); | |
reqDTO.setUserAgent(ServletUtils.getUserAgent()); | |
reqDTO.setUserIp(ServletUtils.getClientIP()); | |
reqDTO.setResult(loginResult.getResult()); | |
loginLogService.createLoginLog(reqDTO); | |
// 更新最后登录时间 | |
if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) { | |
userService.updateUserLogin(userId, ServletUtils.getClientIP()); | |
} | |
} |
# API 访问日志
# 数据库记录
api 访问日志,记录每次 api 调用的信息,包括:HTTP 请求、用户、开始时间、时长等等信息。
在【基础设施 -> API 日志 -> 访问日志】菜单,可以看到对应的情况。
# 访问日志记录的实现
通过过滤器实现,过滤 RESTful API 请求,异步记录日志(这样不会造成接口的堵塞)
添加一个
ApiAccessLogFilter过滤器,代码:/*** <p> Project: scaffold - ApiAccessLogFilter </p>
*
* API 访问日志 Filter
* @author Tz
* @date 2024/01/09 23:45
* @version 1.0.0
* @since 1.0.0
*/
@Slf4jpublic class ApiAccessLogFilter extends ApiRequestFilter {
private final String applicationName;
private final ApiAccessLogFrameworkService apiAccessLogFrameworkService;
public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) {
super(webProperties);
this.applicationName = applicationName;
this.apiAccessLogFrameworkService = apiAccessLogFrameworkService;
}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// 获得开始时间LocalDateTime beginTime = LocalDateTime.now();
// 提前获得参数,避免 XssFilter 过滤处理Map<String, String> queryString = ServletUtils.getParamMap(request);
String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
try {
// 继续过滤器filterChain.doFilter(request, response);
// 正常执行,记录日志createApiAccessLog(request, beginTime, queryString, requestBody, null);
} catch (Exception ex) {
// 异常执行,记录日志createApiAccessLog(request, beginTime, queryString, requestBody, ex);
throw ex;
}}private void createApiAccessLog(HttpServletRequest request, LocalDateTime beginTime,
Map<String, String> queryString, String requestBody, Exception ex) {
ApiAccessLog accessLog = new ApiAccessLog();
try {
this.buildApiAccessLogDTO(accessLog, request, beginTime, queryString, requestBody, ex);
apiAccessLogFrameworkService.createApiAccessLog(accessLog);
} catch (Throwable th) {
log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th);
}}private void buildApiAccessLogDTO(ApiAccessLog accessLog, HttpServletRequest request, LocalDateTime beginTime,
Map<String, String> queryString, String requestBody, Exception ex) {
// 处理用户信息accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request));
accessLog.setUserType(WebFrameworkUtils.getLoginUserType(request));
// 设置访问结果CommonResult<?> result = WebFrameworkUtils.getCommonResult(request);
if (result != null) {
accessLog.setResultCode(result.getCode());
accessLog.setResultMsg(result.getMsg());
} else if (ex != null) {
accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode());
accessLog.setResultMsg(ExceptionUtil.getRootCauseMessage(ex));
} else {
accessLog.setResultCode(0);
accessLog.setResultMsg("");
}// 设置其它字段accessLog.setTraceId(TracerUtils.getTraceId());
accessLog.setApplicationName(applicationName);
accessLog.setRequestUrl(request.getRequestURI());
Map<String, Object> requestParams = MapUtil.<String, Object>builder().put("query", queryString).put("body", requestBody).build();
accessLog.setRequestParams(toJsonString(requestParams));
accessLog.setRequestMethod(request.getMethod());
accessLog.setUserAgent(ServletUtils.getUserAgent(request));
accessLog.setUserIp(ServletUtils.getClientIP(request));
// 持续时间accessLog.setBeginTime(beginTime);
accessLog.setEndTime(LocalDateTime.now());
accessLog.setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS));
}}日志存储的实现
调用
infra模块的接口来实现保存日志package com.tz.scaffold.framework.apilog.core.service;
/*** <p> Project: scaffold - ApiAccessLogFrameworkService </p>
*
* API 访问日志 Framework Service 接口
* @author Tz
* @date 2024/01/09 23:45
* @version 1.0.0
* @since 1.0.0
*/
public interface ApiAccessLogFrameworkService {
/*** 创建 API 访问日志
*
* @param apiAccessLog API 访问日志
*/
void createApiAccessLog(ApiAccessLog apiAccessLog);
}实现类 实际就是调用的 infra 的模块的接口:
package com.tz.scaffold.framework.apilog.core.service;
import cn.hutool.core.bean.BeanUtil;
import com.tz.scaffold.module.infra.api.logger.ApiAccessLogApi;
import com.tz.scaffold.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Async;
/*** <p> Project: scaffold - ApiAccessLogFrameworkServiceImpl </p>
*
* API 访问日志 Framework Service 实现类
* <p>
* 基于 {@link ApiAccessLogApi} 服务,记录访问日志* @author Tz
* @date 2024/01/09 23:45
* @version 1.0.0
* @since 1.0.0
*/
@RequiredArgsConstructorpublic class ApiAccessLogFrameworkServiceImpl implements ApiAccessLogFrameworkService {
private final ApiAccessLogApi apiAccessLogApi;
@Override@Asyncpublic void createApiAccessLog(ApiAccessLog apiAccessLog) {
ApiAccessLogCreateReqDTO reqDTO = BeanUtil.copyProperties(apiAccessLog, ApiAccessLogCreateReqDTO.class);
apiAccessLogApi.createApiAccessLog(reqDTO);
}}
# API 错误日志
Api 错误日志,记录每次 Api 调用接口或异常的记录,包括 HTTP 请求、用户、异常的堆栈信息。
在【基础设施 -> API 日志 -> 错误日志】菜单,可以看到对应的情况。
# 错误日志记录实现
通过 GlobalExceptionHandler 拦截每次 RESTful API 异常进行异步记录
代码:
/** | |
* <p> Project: scaffold - GlobalExceptionHandler </p> | |
* | |
* 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@RestControllerAdvice | |
@AllArgsConstructor | |
@Slf4j | |
public class GlobalExceptionHandler { | |
private final String applicationName; | |
private final ApiErrorLogFrameworkService apiErrorLogFrameworkService; | |
/** | |
* 处理所有异常,主要是提供给 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 NoHandlerFoundException) { | |
return noHandlerFoundExceptionHandler(request, (NoHandlerFoundException) ex); | |
} | |
if (ex instanceof HttpRequestMethodNotSupportedException) { | |
return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) 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("[missingServletRequestParameterExceptionHandler]", 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); | |
FieldError fieldError = ex.getBindingResult().getFieldError(); | |
// 断言,避免告警 | |
assert fieldError != null; | |
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); | |
} | |
/** | |
* 处理 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())); | |
} | |
/** | |
* 处理 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); | |
} | |
/** | |
* 处理 SpringMVC 请求地址不存在 | |
* | |
* 注意,它需要设置如下两个配置项: | |
* 1. spring.mvc.throw-exception-if-no-handler-found 为 true | |
* 2. spring.mvc.static-path-pattern 为 /statics/** | |
*/ | |
@ExceptionHandler(NoHandlerFoundException.class) | |
public CommonResult<?> noHandlerFoundExceptionHandler(HttpServletRequest req, NoHandlerFoundException ex) { | |
log.warn("[noHandlerFoundExceptionHandler]", ex); | |
return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL())); | |
} | |
/** | |
* 处理 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())); | |
} | |
/** | |
* 处理 Resilience4j 限流抛出的异常 | |
*/ | |
public CommonResult<?> requestNotPermittedExceptionHandler(HttpServletRequest req, Throwable ex) { | |
log.warn("[requestNotPermittedExceptionHandler][url({}) 访问过于频繁]", req.getRequestURL(), ex); | |
return CommonResult.error(TOO_MANY_REQUESTS); | |
} | |
/** | |
* 处理 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); | |
} | |
/** | |
* 处理业务异常 ServiceException | |
* | |
* 例如说,商品库存不足,用户手机号已存在。 | |
*/ | |
@ExceptionHandler(value = ServiceException.class) | |
public CommonResult<?> serviceExceptionHandler(ServiceException ex) { | |
log.info("[serviceExceptionHandler]", ex); | |
return CommonResult.error(ex.getCode(), ex.getMessage()); | |
} | |
/** | |
* 处理系统异常,兜底处理所有的一切 | |
*/ | |
@ExceptionHandler(value = Exception.class) | |
public CommonResult<?> defaultExceptionHandler(HttpServletRequest req, Throwable ex) { | |
// 情况一:处理表不存在的异常 | |
CommonResult<?> tableNotExistsResult = handleTableNotExists(ex); | |
if (tableNotExistsResult != null) { | |
return tableNotExistsResult; | |
} | |
// 情况二:部分特殊的库的处理 | |
if (Objects.equals("io.github.resilience4j.ratelimiter.RequestNotPermitted", ex.getClass().getName())) { | |
return requestNotPermittedExceptionHandler(req, ex); | |
} | |
// 情况三:处理异常 | |
log.error("[defaultExceptionHandler]", ex); | |
// 插入异常日志 | |
this.createExceptionLog(req, ex); | |
// 返回 ERROR CommonResult | |
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); | |
} | |
private void createExceptionLog(HttpServletRequest req, Throwable e) { | |
// 插入错误日志 | |
ApiErrorLog errorLog = new ApiErrorLog(); | |
try { | |
// 初始化 errorLog | |
initExceptionLog(errorLog, req, e); | |
// 执行插入 errorLog | |
apiErrorLogFrameworkService.createApiErrorLog(errorLog); | |
} catch (Throwable th) { | |
log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th); | |
} | |
} | |
private void initExceptionLog(ApiErrorLog 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(ExceptionUtils.getStackTrace(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<String, Object> requestParams = MapUtil.<String, Object>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("[报表模块 scaffold-module-report - 表结构未导入][参考 https://www.tzzfj.cn/ 开启]"); | |
return CommonResult.error(NOT_IMPLEMENTED.getCode(), | |
"[报表模块 scaffold-module-report - 表结构未导入][参考 https://www.tzzfj.cn/ 开启]"); | |
} | |
// 2. 工作流 | |
if (message.contains("bpm_")) { | |
log.error("[工作流模块 scaffold-module-bpm - 表结构未导入][参考 https://www.tzzfj.cn/ 开启]"); | |
return CommonResult.error(NOT_IMPLEMENTED.getCode(), | |
"[工作流模块 scaffold-module-bpm - 表结构未导入][参考 https://www.tzzfj.cn/ 开启]"); | |
} | |
// 3. 微信公众号 | |
if (message.contains("mp_")) { | |
log.error("[微信公众号 scaffold-module-mp - 表结构未导入][参考 https://www.tzzfj.cn/ 开启]"); | |
return CommonResult.error(NOT_IMPLEMENTED.getCode(), | |
"[微信公众号 scaffold-module-mp - 表结构未导入][参考 https://www.tzzfj.cn/ 开启]"); | |
} | |
// 4. 商城系统 | |
if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) { | |
log.error("[商城系统 scaffold-module-mall - 已禁用][参考 https://www.tzzfj.cn/mall/ 开启]"); | |
return CommonResult.error(NOT_IMPLEMENTED.getCode(), | |
"[商城系统 scaffold-module-mall - 已禁用][参考 https://www.tzzfj.cn/ 开启]"); | |
} | |
// 5. 支付平台 | |
if (message.contains("pay_")) { | |
log.error("[支付模块 scaffold-module-pay - 表结构未导入][参考 https://www.tzzfj.cn/ 开启]"); | |
return CommonResult.error(NOT_IMPLEMENTED.getCode(), | |
"[支付模块 scaffold-module-pay - 表结构未导入][参考 https://www.tzzfj.cn/ 开启]"); | |
} | |
return null; | |
} | |
} |