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

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

概述

本文介绍企业级Java项目中常用的日志链路追踪、统一异常处理、权限拦截等核心功能的实现方案。通过AOP切面、自定义拦截器、线程上下文管理等技术手段,实现以下核心功能:

  • 全链路日志追踪
  • 统一异常处理机制
  • 接口权限验证
  • 请求耗时监控
  • 标准化日志格式

一、核心功能模块

1.1 全局日志切面(WebControllerAop)

  • 实现原理:基于Spring AOP的环绕通知
  • 核心功能:
    • 请求参数/响应结果格式化
    • 接口耗时计算(精确到秒)
    • 请求事件名称映射
    • 文件类型请求特殊处理
    • 线程上下文初始化
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import ****.infrastructure.general.constants.Constants;
import ****.infrastructure.general.constants.ReqUrlConstants;
import ****.infrastructure.general.util.LogUtil;
import ****.infrastructure.general.util.ThreadLocalUtil;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

/**
 * @author: guohong
 */

@Slf4j
@Aspect
@Component
public class WebControllerAop {

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

    /**
     * 环绕通知,环绕增强,相当于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
     */
    private static 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代码:

import cn.hutool.core.date.DateUtil;
import ****.infrastructure.general.constants.Constants;
import ****.infrastructure.general.util.LogUtil;
import ****.infrastructure.general.util.ThreadLocalUtil;
import ****commons.base.Result;
import ****commons.exception.ApiException;
import ****commons.exception.ErrorResultCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException;
import org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MultipartException;

import javax.servlet.http.HttpServletRequest;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;

@Slf4j
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE) // 设置最高优先级
public class 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)
    public Result<Object> MultipartExceptionHandler(HttpServletRequest req, MultipartException ex) {

        if (ex.getCause().getCause() instanceof FileSizeLimitExceededException) {
            return formatResult(ErrorResultCode.CLIENT_DATA_EXE.getErrorCode(), "单个文件上传大小不能超过" + singleMaxFileSize, ex);
        } else if (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)
    public Result<Object> exception(HttpServletRequest req, Exception ex) {
        return formatResult(ErrorResultCode.SYSTEM_ERROR.getErrorCode(), "系统异常:" + ex.getMessage(), ex);
    }
    //运行时异常
    @ResponseBody
    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.OK)
    public Result<Object> runtimeExceptionHandler(HttpServletRequest req, RuntimeException ex) {
        return formatResult(ErrorResultCode.SYSTEM_ERROR.getErrorCode(), "执行异常:" + ex.getMessage(), ex);
    }
    /**
     * 业务异常
     */
    @ResponseBody
    @ExceptionHandler({ApiException.class})
    @ResponseStatus(HttpStatus.OK)
    public Result<Object> ApiExceptionHandler(ApiException e) {
        log.error("Message: {}", e.getMessage(), e);
        return formatResult(e.getResultCode().getErrorCode(), e.getResultCode().getError(), e);
    }

    @ResponseBody
    @ExceptionHandler({MissingServletRequestParameterException.class})
    @ResponseStatus(HttpStatus.OK)
    public Result<Object> missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException e) {
        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)
    public Result<Object> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
        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)
    public Result<Object> httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedException e) {
        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();
        return new 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)

  • 安全校验流程
    1. Token有效性验证
    2. Redis缓存用户信息
    3. 权限服务远程调用
    4. 用户上下文传递
  • 支持功能:
    • 外部用户权限标识
    • Token自动续期(60天)
    • 用户信息线程级缓存
  • **配置类:

import cn.hutool.extra.spring.SpringUtil;
import ****.aop.RequestInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //连接所有
        registry.addInterceptor(SpringUtil.getBean(RequestInterceptor.class))
                .addPathPatterns("/**")
                .excludePathPatterns(
                		"/login" 
                        ,"/we-chat/**"
                );
    }

}

拦截器:


import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import ****.apis.controller.BaseController;
import ****.client.model.UserAuthDto;
import ****.client.model.UserInfoDto;
import ****.infrastructure.general.constants.Constants;
import ****.infrastructure.redis.RedisUtil;
import ****.util.JsonUtils;
import ****commons.exception.ApiException;
import ****commons.exception.ErrorResultCode;
import ****skyauth.dto.R;
import ****skyauth.dto.UserAuthBaseDto;
import ****skyauth.service.AuthService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.concurrent.TimeUnit;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

@Component
public class RequestInterceptor implements HandlerInterceptor {

    @Autowired
    RedisUtil redisUtil;

    @Autowired
    BaseController baseController;

    @Autowired
    AuthService authService;

    @Override   
    public boolean preHandle(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
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

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

}

1.4 线程上下文管理

  • ThreadLocalUtil
    • 线程安全的数据存储
    • 支持嵌套调用上下文传递
    • 自动清理机制

import java.util.HashMap;
import java.util.Map;

/**
* @Description: 线程共享类
*/
public class ThreadLocalUtil {

    private static ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal() {
        protected Map<String, Object> initialValue() {
            return new HashMap(4);
        }
    };

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

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

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

    public static void set(String key, Object value) {
        Map map = threadLocal.get();
        map.put(key, value);
    }

    public static void set(Map<String, Object> keyValueMap) {
        Map map = threadLocal.get();
        map.putAll(keyValueMap);
    }

    public static void remove() {
        threadLocal.remove();
    }

    public static <T> T remove(String key) {
        Map map = threadLocal.get();
        return (T)map.remove(key);
    }
}
  • LogUtil
    • 唯一日志ID生成(时间戳+UUID)
    • 多线程ID继承机制
    • 日志前缀自动追加

import cn.hutool.core.date.DateUtil;
import ****.infrastructure.general.constants.Constants;
import org.apache.commons.lang3.StringUtils;

import java.util.Date;
import java.util.UUID;

/**
* @Description: 线程日志处理类
*/
public class LogUtil {

    /**
     * 返回当前日志ID,如果为空则生成。拼接-和序号
     */
    public static 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,如果为空则生成。拼接-和序号
     */
    public static String idEnd() {
        return StringUtils.removeEnd(id(), "-");
    }
    
    /**
     * 返回当前日志ID,如果为空则生成。拼接-和序号
     */
    public static void createId() {
        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);
        }
    }
}

调用示例:

@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 "成功";
    }

二、核心设计亮点

2.1 日志链路追踪方案

  • 唯一TraceID生成规则:yyyyMMddHHmmss+16位随机数
  • 多线程ID继承:主线程ID + 子线程ID
  • 日志要素包含:
    [请求时间] [序号][响应时间][耗时][入参][出参][异常堆栈]
    

2.2 性能优化策略

  • 文件类型请求参数特殊处理
  • Redis缓存用户信息(60天)
  • 异常堆栈智能截取(保留前100行)

2.3 安全控制

  • 白名单路径配置
  • 双重Token验证机制(本地缓存+远程验证)
  • 外部用户权限标识隔离

三、使用示例

3.1 基础日志记录

log.info(LogUtil.id() + "获取有问题反馈的群ID {}", groupIdList);

3.2 多线程场景

threadPool.submit(() -> {
    ThreadLocalUtil.set(Constants.THREAD_NO, logId + "-" + Thread.currentThread().getId());
    // 业务逻辑
});

四、部署注意事项

  1. 需要配置Redis连接信息
  2. 权限服务地址配置
  3. 文件上传大小限制参数:
    spring.servlet.multipart.max-file-size=10MB
    spring.servlet.multipart.max-request-size=100MB
    

五、总结

本方案实现了企业级项目的三个核心需求:

  1. 可追踪性:全链路日志追踪能力
  2. 稳定性:统一异常处理机制
  3. 安全性:完善的权限验证体系

通过合理的线程上下文管理、AOP切面设计、异常处理规范,显著提升了系统的可维护性和问题排查效率。实际使用中可根据业务需求扩展日志采集维度,集成APM系统实现更完善的监控体系。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值