利用AOP及Interceptor封装链路id、接口统一返回结果Result对象

链路id封装实现及使用

现有模块在返回结果中统一封装了响应码code及响应信息msg, 遇到报错时通过报错信息无法快速定位到报错相关的日志信息,查看日志不是很方便

{code: “-1”, msg: “系统繁忙”, content: null}

封装链路id后,可根据此id作为关键字查找日志,日志信息中会包含整个请求中的相关日志,定位问题会方便很多

{
“code”: “1”,
“msg”: “success”,
“content”: {
“page”: 1,
“pageSize”: 10,
“total”: 0,
“data”: [],
“extend”: null
},
"requestId": “880637”,
“costTime”: 17
}

使用方式

获取接口返回的数据,找到返回数据中的requestId, 登陆服务器,切换到日志路径,使用命令 grep ‘requestId’ xxx.log

也可在grep 后加参数 -B | -A rows, 日志会输出关键字前后对应行数相关的日志, 其余参数可使用 grep --help查看。

某些情况下可能拿不到requestId, 无法直接使用。此情况下可根据请求参数中的业务标识id或者返回结果中的业务数据作为关键字先查询,找到对应的requestId后再查看整个请求的日志。

实现方式

代码示例:

定义接口统一返回结果及常量

  1. 返回结果中一般会定义状态码、返回消息、接口数据、链路id、请求耗时等字段,创建Result对象

    ​ 可在result对象中封装一些返回成功、失败的构造方法

@Data
@ApiModel(description = "响应对象")
public class ResultEntity<T> implements Serializable {
    private static final long serialVersionUID = 1L;
    /**
     * 应答码
     */
    @NonNull
    @ApiModelProperty(value = "应答码")
    private String code;
    /**
     * 应答码描述
     */
    private String msg;
    /**
     * 返回的数据
     */
    private T content;

    @ApiModelProperty(value = "链路id(方便查询日志 通过 grep ‘xxxx’ xxx.log 定位整个请求的日志)", example = "56895623")
    private String requestId;//链路id 方便查询日志 通过 grep ‘xxxx’ xxx.log 定位整个请求的日志

    //@ApiModelProperty(value = "服务器ip", example = "127.0.0.1")
    //private String ip;//服务器ip 多机器部署时通过服务器ip直接去对应机器找对应日志 容易暴露地址 不适合在生产放开

    @ApiModelProperty(value = "请求耗时(单位 毫秒)", example = "253" ,  hidden = true)
    private long costTime;//请求耗时(单位 毫秒)
}
  1. 定义常量 MdCConstants

    public class MdCConstants {
        public static  final String REQUEST_ID = "requestId"; //链路id
        public static  final String COST_TIME = "costTime"; //请求耗时
        public static  final String START_TIME = "startTime"; //请求开始时间
    }
    

创建拦截器拦截相关请求

  1. 创建拦截器RequestAdapterInterceptor, 封装对应逻辑

    拦截器中需重写前置处理方法,首先从http header中获取链路id, 没有则生成6位随机数字,以requestId作为key放入MDC中.

​ 要从header中获取链路id是因为接口可能被其它服务调用,沿用上层服务的id方便整个请求链的追踪

​ 在拦截器链路走完后可在afterCompletion方法中清空MDC, 避免没有及时清空导致内存泄露问题

​ MDC的介绍及使用:

​ https://www.jianshu.com/p/1dea7479eb07

​ https://segmentfault.com/a/1190000008315137#articleHeader12

​ 拦截器介绍:

​ https://juejin.cn/post/6844904020675559432#h

@Component
@Slf4j
public class RequestAdapterInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取请求头中是否有requestId 没有则生成
        String requestId = request.getHeader(MdCConstants.REQUEST_ID);

        if (StringUtils.isBlank(requestId)) {
            requestId = RandomStringUtils.randomNumeric(6);
            log.info("generate requestId: {}", requestId);
        }
        MDC.put(MdCConstants.REQUEST_ID, requestId);
        //MDC.put(MdCConstants.START_TIME, String.valueOf(System.currentTimeMillis())); 通过AOP封装接口耗时

        return true;
    }

   /** @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        String startTime = MDC.get(MdCConstants.START_TIME);

        if (StringUtils.isNotBlank(startTime)) {
            long costTime = System.currentTimeMillis() - Long.valueOf(startTime);
            log.info("current api cost time: {}", costTime);
            MDC.put(MdCConstants.COST_TIME, String.valueOf(costTime));
        }
    }*/

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        MDC.clear();//清空 避免内存泄露
    }
}

  1. 注册拦截器

    拦截器声明后需注册进容器中,可在注册时设置拦截器需拦截的请求和排除不拦截的请求,具体提供的方法可自行查看

/**
* 注册项目中的Interceptor
* */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {

    @Resource
    private RequestAdapterInterceptor requestIdInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(requestIdInterceptor)
                .addPathPatterns("/**/**");//设置拦截的请求
    }
}

  1. feign接口中传递requestId

    创建FeignRequestInterceptor类,在发起远程调用前会先进拦截器,通过此拦截器在header中添加链路id, 传递给被调用方

/**
* 用于feign调用中传递链路id
* */
@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        //在请求头中添加链路id
        template.header(MdCConstants.REQUEST_ID, MDC.get(MdCConstants.REQUEST_ID));
    }

}

日志增强

要在日志中打印链路id, 需要利用MDC在日志配置文件中修改日志打印格式,加入链路id参数即可。

放入MDC的参数在日志中使用 %X{参数名}

找到项目的日志配置文件logback,在layout标签中修改即可。示例如下:

        <!-- 日志输出的文件的格式  -->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %5p %X{requestId} [%t] \(%F:%L\) - %m%n</pattern>
        </layout>

封装接口返回结果

利用AOP对controller层进行增强处理,打印接口出入参,将controller层返回的数据统一封装成Result对象。

​ 后续进行接口开发时无需将接口的返回值声明为ResultEntity,可直接返回对应的实体类, 无数据返回时可直接声明为void.

/**
 * 2021.11.02改动
 * 打印接口出入参 封装请求结果为ResultEntity,后续无需再controller层声明方法返回值为ResultEntity,可返回空对象或者对应的数据结构,由此处统一封装
 * */
@Aspect
@Component
@Slf4j
public class WebLogAspect {
    /**
     * 作用于com.ab.dh.datahouse.controller包下的所有public方法
     * 两个..代表所有子目录,最后括号里的两个..代表所有参数
     */
    @Pointcut("execution(public * com.ab.dh.datahouse.controller.*.*(..))")
    public void logPointCut() {}

    @Around("logPointCut()")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        long startTime = System.currentTimeMillis();
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        //打印请求路径和入参
        log.info("request URL: {}", request.getRequestURI());
        log.info("request params: {}", JSON.toJSONString(getArgs(pjp.getArgs())));

        //执行真正的方法调用
        Object ob = pjp.proceed();
        log.info("id:{}, request uri:{}, handle request time: {}", logId, request.getRequestURI(), System.currentTimeMillis() - startTime);

        ResultEntity result = null;
        //封装结果 将对象封装成result
        if (ob instanceof ResultEntity) {
            //nothing 已经是Result 无需再封装
            result = (ResultEntity) ob;
        } else {
            result = ResultEntity.success();
            result.setContent(ob);
        }

        //计算请求耗时 封装进Result中
        result.setCostTime(System.currentTimeMillis() - startTime);
        result.setRequestId(MDC.get(MdCConstants.REQUEST_ID));//封装链路id
        //打印出参
        log.info("request end, result info: {}", JSON.toJSONString(result));

        return result;
    }
    
    /**
     * 获取请求入参,排除参数中request response对象
     * */
    private Object[] getArgs(Object[] args) {
        if (args == null || args.length == 0) {
            return null;
        }

        Object[] result = new Object[args.length];
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof ServletResponse || args[i] instanceof ServletRequest) {
                continue;
            }
            result[i] = args[i];
        }

        return result;
    }
}

打印接口入参时需过滤掉request、response对象,部分接口会将这俩个对象作为入参实现某些操作,如不过滤这俩个对象,在打印这俩个参数进行json解析时会遇到如下报错:

java.lang.IllegalStateException: getOutputStream() has already been called for this response
at org.apache.catalina.connector.Response.getWriter(Response.java:581)
at org.apache.catalina.connector.ResponseFacade.getWriter(ResponseFacade.java:227)
at com.alibaba.fastjson.serializer.ASMSerializer_2_ResponseFacade.write(Unknown Source)
at com.alibaba.fastjson.serializer.ObjectArrayCodec.write(ObjectArrayCodec.java:103)
at com.alibaba.fastjson.serializer.JSONSerializer.write(JSONSerializer.java:281)
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:673)
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:611)
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:576)
at com.dh.manager.interceptor.WebLogAspect.doAround(WebLogAspect.java:52)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)

全局异常处理

当前项目中进行了全局的异常处理,会拦截项目内抛出的异常,将异常信息也封装成ResultEntity对象返回,在此处只需将链路requestId赋值给result对象即可。

/**
 * Controller全局异常控制
 * @Description
 */
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 在Controller方法执行之前,校验一些参数不匹配,Get post方法等异常
     *
     * @param
     * @return
     * @Function handleExceptionInternal
     * @Description
     * @version v1.0
     * @author hzx
     * @see ResponseEntityExceptionHandler#handleExceptionInternal(Exception,
     * Object, HttpHeaders,
     * HttpStatus,
     * WebRequest)
     */
    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers,
                                                             HttpStatus status, WebRequest request) {
        ResultEntity<Object> resultEntity = ResultEntity.ofStatus(ResultCode.SYS_PARAM_ERROR);

        if (ex instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) ex;
            List<FieldError> fieldErrors = methodArgumentNotValidException.getBindingResult().getFieldErrors();
            List<String> messages = new ArrayList<>();
            for (FieldError fieldError : fieldErrors) {
                messages.add(fieldError.getDefaultMessage());
            }
            resultEntity.setMsg("arguments not valid: \n" + JSON.toJSONString(messages));
        } else if ((ex instanceof IllegalArgumentException || ex instanceof MissingServletRequestParameterException)) {
            resultEntity.setCode(ResultCode.SYS_PARAM_ERROR.getCode());
            resultEntity.setMsg(ex.getMessage());
        } else if ((ex instanceof BusinessException)) {
            BusinessException businessException = (BusinessException) ex;
            resultEntity.setCode(businessException.getCode());
            resultEntity.setMsg(businessException.getMessage());
        } else {
            if (ex.getMessage() != null && ex.getMessage().length() < 100) {
                resultEntity = ResultEntity
                        .error("wrong request to match controller definition: \n" + processExMsg(ex));
            }
        }
        LOGGER.error("全局异常控制:", ex);
        //封装链路id
        resultEntity.setRequestId(MDC.get(MdCConstants.REQUEST_ID));

        return new ResponseEntity<Object>(resultEntity, HttpStatus.OK);
    }

    /**
     * Controller内部异常控制
     *
     * @param
     * @return
     * @throws Exception
     * @Function handlerException
     * @Description
     * @version v1.0
     * @author hzx
     */
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Object handlerException(HttpServletRequest request, HttpServletResponse response, Exception e) {
    	//e.printStackTrace();
        LOGGER.error("global exception :", e);
        // 统一返回200 http status code
        response.setStatus(HttpStatus.OK.value());
        ResultEntity<?> resultEntity = new ResultEntity<>();
        //封装链路id
        resultEntity.setRequestId(MDC.get(MdCConstants.REQUEST_ID));

        if (e instanceof IllegalArgumentException) {
            resultEntity.setCode(ResultCode.SYS_PARAM_ERROR.getCode());
            resultEntity.setMsg(processExMsg(e));
            return resultEntity;
        }
        if (e instanceof AuthenticationException) {
            resultEntity.setCode(ResultCode.USER_ILLEGAL_TOKEN.getCode());
            resultEntity.setMsg(ResultCode.USER_ILLEGAL_TOKEN.getMsg());
            return resultEntity;
        }
        if (e instanceof BusinessException) {
            BusinessException businessException = (BusinessException) e;
            resultEntity.setCode(businessException.getCode());
            resultEntity.setMsg(businessException.getMessage());
            return resultEntity;
        } else {
            resultEntity.setCode(ResultCode.ERROR.getCode());
            resultEntity.setMsg(ResultCode.ERROR.getMsg());
            return resultEntity;
        }
    }

    private String processExMsg(Exception e) {
        if (e.getMessage() != null && e.getMessage().length() < 100) {
            return e.getMessage();
        }
        
        return "";
    }
}

可扩展功能点

  1. 返回服务器ip地址(生产慎用

    ​ 后续服务集群部署时,可在接口返回结果中返回服务器ip地址,避免查日志时需去多台机器上查找

  2. 线程池或者异步事件中传递链路id

    后续项目中如有使用到线程池或者异步事件,可传递此链路id, 定义异步线程的日志将会方便很多

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值