栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

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

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

利用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 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 afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        MDC.clear();//清空 避免内存泄露
    }
}

  1. 注册拦截器

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

@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, 传递给被调用方

@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标签中修改即可。示例如下:

        
        
            %d{yyyy-MM-dd HH:mm:ss.SSS} %5p %X{requestId} [%t] (%F:%L) - %m%n
        
封装接口返回结果

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

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

@Aspect
@Component
@Slf4j
public class WebLogAspect {
    
    @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;
    }
    
    
    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对象即可。

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    
    @Override
    protected ResponseEntity handleExceptionInternal(Exception ex, Object body, HttpHeaders headers,
                                                             HttpStatus status, WebRequest request) {
        ResultEntity resultEntity = ResultEntity.ofStatus(ResultCode.SYS_PARAM_ERROR);

        if (ex instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) ex;
            List fieldErrors = methodArgumentNotValidException.getBindingResult().getFieldErrors();
            List 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(resultEntity, HttpStatus.OK);
    }

    
    @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, 定义异步线程的日志将会方便很多

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号