SpringBoot架构实战:拦截器+全局异常+日志追踪一体化解决方案

图片
01
概述
图片
本文介绍企业级Java项目中常用的日志链路追踪、统一异常处理、权限拦截等核心功能的实现方案。通过AOP切面、自定义拦截器、线程上下文管理等技术手段,实现以下核心功能:
  • 全链路日志追踪
  • 统一异常处理机制
  • 接口权限验证
  • 请求耗时监控
  • 标准化日志格式
@PostMapping("/pullWxChatRecordScheduleTask")
public String dingshirenwu(){
    try{
        //拉取聊天记录
        wxCompanyChatService.pullWxChatRecord(null);
        
        //查询时间倒序,配置时间内的(例如:三分钟)内里未被大模型扫描的群聊id集合
        List<String> groupIdList = wxCompanyChatService.getGroupId();
        log.info(LogUtil.id() + "获取有问题反馈的群ID {}", groupIdList);
        
        //启动线程池任务
        ExecutorService threadPool = LLmAnalysisChatRecordThread.getLlmAnalySisChatRecordThreadPool();
        for (String groupId : groupIdList) {
            String logId = ThreadLocalUtil.get(Constants.THREAD_NO);
            Long seq = ThreadLocalUtil.get(Constants.THREAD_SEQ);
            threadPool.submit(() -> {
                //日志ID :使用主线程日志ID + 子线程ID
                ThreadLocalUtil.set(Constants.THREAD_NO, logId + "-" + Thread.currentThread().getId());
                ThreadLocalUtil.set(Constants.THREAD_SEQ, seq);
                wxGroupChatService.analysisGroupChatRecordByGroupId(groupId);
            });
        }
    }catch (Exception e){
        log.error(LogUtil.id() + "异常捕捉", e);
        return"失败";
    }
    return"成功";
}
图片
02
核心功能模块
图片
1.1 全局日志切面(WebControllerAop)
实现原理: 基于Spring AOP的环绕通知
核心功能:
  • 请求参数/响应结果格式化
  • 接口耗时计算(精确到秒)
  • 请求事件名称映射
  • 文件类型请求特殊处理
  • 线程上下文初始化
/**
 * @author: guohong
 */

@Slf4j
@Aspect
@Component
publicclass WebControllerAop {

    /**
     * 指定切点
     * 匹配 com.example.demo.controller包及其子包下的所有类的所有方法
     */
    @Pointcut("execution(public * ****.apis.controller.*.*(..)) || execution(public * ****.apis.*.controller.*.*(..))")
    publicvoidwebLog(){
    }

    /**
     * 环绕通知,环绕增强,相当于MethodInterceptor
     *
     * @param joinPoint
     * @return
     */
    @Around("webLog()")
    public Object arround(ProceedingJoinPoint joinPoint)throws Throwable {
        long startTime = System.currentTimeMillis();
        //获取目标方法的参数信息
        Signature signature = joinPoint.getSignature();
        String args;
        if (ReqUrlConstants.FILE_URL_MAP.containsKey(signature.getDeclaringTypeName())) {
            args = "【文件流类型】";
        } else {
            args = formatArgs(joinPoint);
        }

        //AOP代理类的类(class)信息
        signature.getDeclaringType();
        // 接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            Object o = joinPoint.proceed();
            //记录日志
            log.info("接收请求-{} \n 请求ID:{} \n 请求地址: {} \n token: {} \n 入参 : {}  \n 出参 : {} \n 请求时间:{} \n 响应时间:{} \n 耗时:{}秒",
                    "", LogUtil.idEnd(), "",  "", args, formatRet(o), DateUtil.date(startTime), DateUtil.now(), (System.currentTimeMillis() - startTime) / 1000d);
            ThreadLocalUtil.remove();
            return o;
        }
        HttpServletRequest req = attributes.getRequest();
        String logId = req.getHeader(Constants.PROCESS_ID);
        if (StringUtils.isBlank(logId)) {
            LogUtil.createId();
        } else {
            ThreadLocalUtil.set(Constants.THREAD_NO, logId);
        } 
        String token = req.getHeader(Constants.CAR_TOKEN);

        String eventName = getEventName(req.getRequestURL().toString());
        String reqName = "系统接口:" + eventName + " " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName() + "()";
        String reqUrl = req.getMethod() + " " + req.getRequestURL().toString();
        HashMap<String, String> map = Maps.newHashMap();
        map.put(Constants.THREAD_REQ_NAME, reqName);
        map.put(Constants.THREAD_REQ_URL, reqUrl);
        map.put(Constants.THREAD_REQ_PARAM, args);
        map.put(Constants.THREAD_REQ_TIME, startTime + "");
        ThreadLocalUtil.set(Constants.THREAD_REQ_PARAM, map);

        //执行目标方法
        Object o = joinPoint.proceed();

        //记录日志
        log.info("接收请求:{} \n 请求ID:{} \n 请求地址: {} \n token: {} \n 入参 : {}  \n 出参 : {} \n 请求时间:{} \n 响应时间:{} \n 耗时:{}秒",
                eventName, LogUtil.idEnd(), reqUrl, token, args, formatRet(o), DateUtil.date(startTime), DateUtil.now(), (System.currentTimeMillis() - startTime) / 1000d);

        //移除所有线程map,防止线程复用导致的变量错乱及内存溢出
        ThreadLocalUtil.remove();
        return o;
    }

    private String formatArgs(JoinPoint joinPoint){
        String args = Arrays.toString(joinPoint.getArgs());
        try {
            StringBuilder argsBuild = new StringBuilder();
            Object[] argsArray = joinPoint.getArgs();
            if (argsArray.length >= 1) {
                argsBuild.append(JSON.toJSONString(argsArray[0]));
            }
            if (StringUtils.length(argsBuild.toString()) > 0) {
                return argsBuild.toString();
            }
        } catch (Exception ignored) {
            return args;
        }
        return args;
    }

    private Object formatRet(Object ret){
        try {
            String retJson = JSON.toJSONString(ret);
            if (StringUtils.isNotBlank(retJson)) {
                return retJson;
            }
        } catch (Exception ignored) {
            return ret;
        }
        return ret;
    }

    /**
     * 根据请求url 获取对应的业务名称
     *
     * @param requestURL
     * @return
     */
    privatestatic String getEventName(String requestURL){
        if (StringUtils.isBlank(requestURL)) {
            return"未知URL";
        }
        for (Map.Entry<String, String> entry : ReqUrlConstants.MAP.entrySet()) {
            if (StringUtils.contains(requestURL, entry.getKey())) {
                return entry.getValue();
            }
        }
        return"未配置事件";
    }

}
1.2 统一异常处理(GlobalExceptionHandler)
异常分类处理:
  • 文件上传异常(MultipartException
  • 参数校验异常(MethodArgumentNotValidException
  • 业务异常(ApiException
  • 未捕获异常兜底处理
java代码:
@Slf4j
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE) // 设置最高优先级
publicclass GlobalExceptionHandler {
    @Value(value = "${spring.servlet.multipart.max-file-size}")
    String singleMaxFileSize;
    @Value(value = "${spring.servlet.multipart.max-request-size}")
    String maxRequestSize;


    /**
     *  文件上传错误
     * @param req
     * @param ex
     * @return
     */
    @ResponseBody
    @ExceptionHandler(value = MultipartException.class)
    @ResponseStatus(HttpStatus.OK)
    publicResult<Object> MultipartExceptionHandler(HttpServletRequestreq, MultipartExceptionex) {

        if (ex.getCause().getCause() instanceof FileSizeLimitExceededException) {
            return formatResult(ErrorResultCode.CLIENT_DATA_EXE.getErrorCode(), "单个文件上传大小不能超过" + singleMaxFileSize, ex);
        } elseif (ex.getCause().getCause() instanceof SizeLimitExceededException) {
            return formatResult(ErrorResultCode.CLIENT_DATA_EXE.getErrorCode(), "请求的总上传文件大小不能超过" + maxRequestSize, ex);
        } else {
            return formatResult(ErrorResultCode.CLIENT_DATA_EXE.getErrorCode(), "上传文件失败", ex);
        }
    }
    /**
     * 其他错误
     */
    @ResponseBody
    @ExceptionHandler({Exception.class})
    @ResponseStatus(HttpStatus.OK)
    publicResult<Object> exception(HttpServletRequestreq, Exceptionex) {
        return formatResult(ErrorResultCode.SYSTEM_ERROR.getErrorCode(), "系统异常:" + ex.getMessage(), ex);
    }
    //运行时异常
    @ResponseBody
    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.OK)
    publicResult<Object> runtimeExceptionHandler(HttpServletRequestreq, RuntimeExceptionex) {
        return formatResult(ErrorResultCode.SYSTEM_ERROR.getErrorCode(), "执行异常:" + ex.getMessage(), ex);
    }
    /**
     * 业务异常
     */
    @ResponseBody
    @ExceptionHandler({ApiException.class})
    @ResponseStatus(HttpStatus.OK)
    publicResult<Object> ApiExceptionHandler(ApiExceptione) {
        log.error("Message: {}", e.getMessage(), e);
        return formatResult(e.getResultCode().getErrorCode(), e.getResultCode().getError(), e);
    }

    @ResponseBody
    @ExceptionHandler({MissingServletRequestParameterException.class})
    @ResponseStatus(HttpStatus.OK)
    publicResult<Object> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterExceptione) {
        log.error("Message: {}", e.getMessage(), e);
        return formatResult(ErrorResultCode.PARAM_REQUIRED.getErrorCode(), String.format(ErrorResultCode.PARAM_REQUIRED.getError(), e.getParameterName()), e);
    }

    @ResponseBody
    @ExceptionHandler({MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.OK)
    publicResult<Object> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidExceptione) {
        log.error("Message: {}", e.getMessage(), e);
        return formatResult(ErrorResultCode.PARAM_REQUIRED.getErrorCode(), String.format(ErrorResultCode.PARAM_REQUIRED.getError(), this.getBindingResultErrors(e.getBindingResult())), e);
    }

    @ResponseBody
    @ExceptionHandler({HttpMediaTypeNotSupportedException.class})
    @ResponseStatus(HttpStatus.OK)
    publicResult<Object> httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedExceptione) {
        log.error("Message: {}", e.getMessage(), e);
        return formatResult(ErrorResultCode.REQUEST_TYPE_ERROR.getErrorCode(), ErrorResultCode.REQUEST_TYPE_ERROR.getError(), e);
    }
    

    private <T extends Throwable> Result<Object> formatResult(String errorCode, String errorMsg, T ex){

        Map<String, String> reqMap = ThreadLocalUtil.get(Constants.THREAD_REQ_PARAM);
        long startTime = System.currentTimeMillis();
        String reqName = "内部处理";
        String url = "";
        String param = "";
        if (!CollectionUtils.isEmpty(reqMap)){
            if (StringUtils.isNotBlank(reqMap.get(Constants.THREAD_REQ_TIME))) {
                startTime = Long.parseLong(reqMap.get(Constants.THREAD_REQ_TIME));
            }
            if (StringUtils.isNotBlank(reqMap.get(Constants.THREAD_REQ_NAME))) {
                reqName = reqMap.get(Constants.THREAD_REQ_NAME);
            }
            if (StringUtils.isNotBlank(reqMap.get(Constants.THREAD_REQ_URL))) {
                url = reqMap.get(Constants.THREAD_REQ_URL);
            }
            if (StringUtils.isNotBlank(reqMap.get(Constants.THREAD_REQ_PARAM))) {
                param = reqMap.get(Constants.THREAD_REQ_PARAM);
            }
        }
        
        //获取异常信息
        StringJoiner err = new StringJoiner("/n/r ");
        StackTraceElement[] stackTrace = ex.getStackTrace();
        if (null != stackTrace) {
            for (int i = 0; i < (Math.min(stackTrace.length, 100)); i++) {
                err.add(stackTrace[i].toString());
            }
        }
        //记录日志
        log.info("响应异常-{} \n 请求ID:{} \n 请求url: {} \n 入参 : {}  \n 出参 : {} \n 请求时间:{}  \n 响应时间: {} \n 耗时:{}秒",
                reqName, LogUtil.idEnd(),url, param,
                "异常代码:" + errorCode + " 异常信息:" + errorMsg + "-" + ex.getMessage()+"/n/r" + err, 
                startTime, DateUtil.now(), (System.currentTimeMillis() - startTime) / 1000);
        
        //移除所有线程map,防止线程复用导致的变量错乱及内存溢出
        ThreadLocalUtil.remove();
        returnnew Result(errorCode, errorMsg);
    }


    private String getBindingResultErrors(BindingResult bindingResult){
        StringBuilder sb = new StringBuilder();
        if (bindingResult.hasErrors()) {
            List<ObjectError> list = bindingResult.getAllErrors();
            Iterator var4 = list.iterator();

            while(var4.hasNext()) {
                ObjectError error = (ObjectError)var4.next();
                sb.append(error.getDefaultMessage() + ",");
            }
        }

        return sb.toString();
    }
}
  • 统一响应格式:
{"code":"ERROR_001","msg":"参数校验失败"}
1.3 权限拦截器(RequestInterceptor)
安全校验流程:
  • Token有效性验证
  • Redis缓存用户信息
  • 权限服务远程调用
  • 用户上下文传递
支持功能:
  • 外部用户权限标识
  • Token自动续期(60天)
  • 用户信息线程级缓存
  • 配置类:
@Configuration
publicclass InterceptorConfig implements WebMvcConfigurer {
    @Override
    publicvoidaddInterceptors(InterceptorRegistry registry){
        //连接所有
        registry.addInterceptor(SpringUtil.getBean(RequestInterceptor.class))
                .addPathPatterns("/**")
                .excludePathPatterns(
                  "/login" 
                        ,"/we-chat/**"
                );
    }

}
  • 拦截器:
@Component
publicclass RequestInterceptor implements HandlerInterceptor {

    @Autowired
    RedisUtil redisUtil;

    @Autowired
    BaseController baseController;

    @Autowired
    AuthService authService;

    @Override   
    publicbooleanpreHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {
        boolean result = false;
        String token = request.getHeader(Constants.TOKEN);
        if(ObjectUtil.isEmpty(token)){
            ApiException.newThrow(ErrorResultCode.TOKEN_IS_NULL);
        }
        //从缓存中查找此token有没有存在
        UserInfoDto userInfo = JSONObject.parseObject(String.valueOf(redisUtil.getCache(token)), UserInfoDto.class);
        //如果redis缓存为空,去查一次天权系统查看token,然后放入redis中
        if(ObjectUtil.isEmpty(userInfo)){
            R r = authService.userPermission(Constants.SKYAUTH_APPID, token, new UserAuthBaseDto());
            Integer code = (Integer)r.get("code");
            //feign调用成功
            if(****.infrastructure.general.constants.HttpStatus.SUCCESS==code){
                Object data = r.get("data");
                if(ObjectUtil.isNotEmpty(data)){
                    UserAuthDto userAuthDto = JSON.parseObject(JSON.toJSONString(data), UserAuthDto.class);
                    userInfo = new UserInfoDto();
                    //添加外部用户权限标识
                    ObjectMapper objectMapper = new ObjectMapper();
                    JsonNode jsonNode = objectMapper.readTree(userAuthDto.getDepts().toString());
                    if (JsonUtils.containsFuncNormalList(jsonNode, Constants.IS_EXT_USER_RIGHTS)) {
                        userInfo.setIsExternal("1");
                    }
                    BeanUtils.copyProperties(userAuthDto,userInfo);
                    //feign调用成功且token依然有效
                    redisUtil.setCacheWithExpiration(token,JSONObject.toJSONString(userInfo),60, TimeUnit.DAYS);
                    request.setAttribute(Constants.USER_NAME,userInfo.getUserName());
                    request.setAttribute(Constants.USER_ID,userInfo.getUserId());
                    request.setAttribute(Constants.IS_EXTERNAL, userInfo.getIsExternal());
                    result = true;
                }else {
                    //这里是调用feign成功但是token校验失败了 返回401
                    ApiException.newThrow(ErrorResultCode.LOGIN_FAILED,"登陆已超时,请重新登录");
                }
            }else {
                //feign调用失败
                ApiException.newThrow(ErrorResultCode.CLIENT_DATA_EXE,(String)r.get("msg"));
            }
        }else{
            request.setAttribute(Constants.USER_NAME, userInfo.getUserName());
            request.setAttribute(Constants.USER_ID, userInfo.getUserId());
            request.setAttribute(Constants.IS_EXTERNAL, userInfo.getIsExternal());
            result = true;
        }
        return result;
    }

    @Override
    publicvoidpostHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    publicvoidafterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }

}
1.4 线程上下文管理
ThreadLocalUtil:
  • 线程安全的数据存储
  • 支持嵌套调用上下文传递
  • 自动清理机制
/**
* @Description: 线程共享类
*/
publicclass ThreadLocalUtil {

    privatestatic ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal() {
        protected Map<String, Object> initialValue(){
            returnnew HashMap(4);
        }
    };

    publicstatic Map<String, Object> getThreadLocal(){
        return threadLocal.get();
    }

    publicstatic <T> T get(String key){
        Map map = threadLocal.get();
        return (T)map.get(key);
    }

    publicstatic <T> T get(String key,T defaultValue){
        Map map = threadLocal.get();
        return (T)map.get(key) == null ? defaultValue : (T)map.get(key);
    }

    publicstaticvoidset(String key, Object value){
        Map map = threadLocal.get();
        map.put(key, value);
    }

    publicstaticvoidset(Map<String, Object> keyValueMap){
        Map map = threadLocal.get();
        map.putAll(keyValueMap);
    }

    publicstaticvoidremove(){
        threadLocal.remove();
    }

    publicstatic <T> T remove(String key){
        Map map = threadLocal.get();
        return (T)map.remove(key);
    }
}
LogUtil:
  • 唯一日志ID生成(时间戳+UUID)
  • 多线程ID继承机制
  • 日志前缀自动追加
/*
* @Description: 线程日志处理类
*/
publicclass LogUtil {

    /**
     * 返回当前日志ID,如果为空则生成。拼接-和序号
     */
    publicstatic String id(){

        String logId = ThreadLocalUtil.get(Constants.THREAD_NO);
        if (StringUtils.isEmpty(logId)) {
            logId = DateUtil.format(new Date(), "yyyyMMddHHmmss") + UUID.randomUUID().toString().replaceAll("-", "").substring(0, 16);
        }
        Long seq = ThreadLocalUtil.get(Constants.THREAD_SEQ);
        if (seq == null) {
            seq = 1L;
            ThreadLocalUtil.set(Constants.THREAD_SEQ, seq);
        } else {
            seq = seq + 1;
            ThreadLocalUtil.set(Constants.THREAD_SEQ, seq);
        }
        return logId + "^" + seq +"-";
    }

    /**
     * 返回当前日志ID,如果为空则生成。拼接-和序号
     */
    publicstatic String idEnd(){
        return StringUtils.removeEnd(id(), "-");
    }
    
    /**
     * 返回当前日志ID,如果为空则生成。拼接-和序号
     */
    publicstaticvoidcreateId(){
        String logId = ThreadLocalUtil.get(Constants.THREAD_NO);
        if (StringUtils.isEmpty(logId)) {
            logId = DateUtil.format(new Date(), "yyyyMMddHHmmss") + UUID.randomUUID().toString().replaceAll("-", "").substring(0, 16);
            ThreadLocalUtil.set(Constants.THREAD_NO, logId);
        }
    }
}
图片
03
核心设计亮点
图片
2.1 日志链路追踪方案

  • 唯一TraceID生成规则:yyyyMMddHHmmss+16位随机数
  • 多线程ID继承:主线程ID + 子线程ID
  • 日志要素包含:[请求时间] [序号][响应时间][耗时][入参][出参][异常堆栈]
2.2 性能优化策略
  • 文件类型请求参数特殊处理
  • Redis缓存用户信息(60天)
  • 异常堆栈智能截取(保留前100行)
2.3 安全控制
  • 白名单路径配置
  • 双重Token验证机制(本地缓存+远程验证)
  • 外部用户权限标识隔离
图片
04
使用示例
图片
3.1 基础日志记录
log.info(LogUtil.id() + "获取有问题反馈的群ID {}", groupIdList);
3.2 多线程场景
threadPool.submit(() -> {
    ThreadLocalUtil.set(Constants.THREAD_NO, logId + "-" + Thread.currentThread().getId());
    // 业务逻辑
});
图片
05
部署注意事项
图片
  • 需要配置Redis连接信息
  • 权限服务地址配置
  • 文件上传大小限制参数:
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=100MB
图片
06
总结
图片
本方案实现了企业级项目的三个核心需求:
  • 可追踪性: 全链路日志追踪能力
  • 稳定性: 统一异常处理机制
  • 安全性: 完善的权限验证体系
通过合理的线程上下文管理、AOP切面设计、异常处理规范,显著提升了系统的可维护性和问题排查效率。实际使用中可根据业务需求扩展日志采集维度,集成APM系统实现更完善的监控体系。
来源:juejin.cn/post/7488657770628939830
SpringBoot架构实战:拦截器+全局异常+日志追踪一体化解决方案
posted @ 2025-12-06 13:02  CharyGao  阅读(3)  评论(0)    收藏  举报