通过AOP和自定义注解实现请求日志收集功能

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/linzhiqiang0316/article/details/89638245

概述

今天给大家介绍一下:如何通过AOP和自定义注解实现全局请求日志收集功能。

一般线上程序都会遇到一个问题,如何排查线上bug,因为有时候这种bug是突发性,测试很难复现的出来。所以记录出错那一刻的请求信息,就异常的关键了。

那请求信息,我们该如何记录下来呢,总不能通过log日志一个个记录下来吧,这样工作量大,而且很难扩展。不用着急,今天就教给大家一招,轻轻松松实现日志收集功能。

接下来我们就来看看,我是如何通过:AOP和自定义注解来实现请求日志统一收集功能。

核心流程

流程图如下所示:

我们先在接口上面添加自定义注解,这样每次请求就都会走AOP的处理中心。在处理中心中,我们可以将请求信息存储到数据库中,方便后期排查问题。

自定义注解

我们先来看看自定义注解是如何实现的,代码如下所示:

/**
 * 自定义注解
 *
 * @author linzhiqiang
 * @date 2019/4/26
 */
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestLog {

    /**
     * 请求模块名称
     * @return
     */
    public String module() default "";

    /**
     * 接口详情描述
     * @return
     */
    public String operationDesc() default "";
}

这边我们定义了两个字段,一个是接口的模块名称,另外一个就是接口具体的功能描述。

接口注释

这两个参数需要在接口处传入,现在我们就来看看接口是如何添加注解和传值的,代码如下所示:

 /**
     * request测试专用
     * @return
     */
    @RequestLog(module = "requestTest", operationDesc = "request测试专用")
    @RequestMapping(value = "requestTest", method = RequestMethod.POST)
    public String requestTest(@RequestBody ArticleSubjectDto articleSubjectDto) {
        String result = null;
        try {
            System.out.println("我的是方法");
            result = "请求成功";
        }catch (Exception e){
            logger.error("requestTest查询失败", e);
            return JsonUtils.toJson(ResponseUtils.failInServer(result));
        }
        return JsonUtils.toJson(ResponseUtils.success(result));
    }

我们可以看到,在接口上面添加我们自定义的注解,然后写上对应的参数值就可以了。

AOP处理中心

最后我们来看看最核心的AOP处理中心是如何实现的,代码如下所示:

/**
 * 切面AOP
 * @author linzhiqiang
 */
@Aspect
@Component
public class SystemLogAspect {
    private static final Logger logger = LoggerFactory.getLogger(SystemLogAspect.class);
    private static final String UNKNOWN = "unknown";

    @Autowired
    private LogRepository logRepository;
    /**
     * 1,表示在哪个类的哪个方法进行切入。配置有切入点表达式。
     * 2,对有@SystemLog标记的方法,记录其执行参数及返回结果。
     */
    @Pointcut("execution(* com.minimal..controller..*.*(..))&&@annotation(com.minimal.common.sdk.log.RequestLog)")
    public void controllerAspect() {
    }

    /**
     * 配置controller环绕通知,使用在方法aspect()上注册的切入点
     */
    @Around("controllerAspect()")
    public Object aroundMethod(ProceedingJoinPoint point) throws Throwable {
        if (logger.isDebugEnabled()) {
            logger.info(">>>>>>>>>>>>>>>进入日志切面<<<<<<<<<<<<<<<<");
        }
        // 获取接口的路径地址
        String methodTarget = point.getTarget().getClass().getName() + "." + point.getSignature().getName() + "()";
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        OperateLogPO operLogPO = new OperateLogPO();
        operLogPO.setId(UUID.randomUUID().toString());
        operLogPO.setMethod(methodTarget);
        operLogPO.setCreateTime(new Date());
        operLogPO.setIp(getClientIpAddr(request));
        operLogPO.setBrownerNo(getBrownerNo(request));
        operLogPO.setOsNo(getOsNo(request));
        // 获取接口的请求参数
        operLogPO.setParams(JsonUtils.toJson(point.getArgs()));
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        RequestLog log = method.getAnnotation(RequestLog.class);
        String desc = log.operationDesc();
        String module = log.module();
        operLogPO.setModule(module);
        operLogPO.setOperationDesc("模块:" + module + ",操作行为:" + desc);
        logger.info("前置通知>>>>>>>>>>>>>>>操作模块:" + module + ",操作方法:" + methodTarget + ",操作行为:" + desc + "<<<<<<<<<<<<<<<<");
        Object result = null;
        try {
            result = point.proceed();
            // 设置请求结果
            operLogPO.setResult(JsonUtils.toJson(result));
            // 返回通知(操作成功:1,操作失败:2)
            operLogPO.setStatus("1");
        } catch (Throwable e) {
            operLogPO.setStatus("2");
            // 异常通知
            throw new RuntimeException(e);
        } finally {
            // 后置通知
            logger.info("后置通知>>>>>>>>>>>>>>>操作模块:" + module + ",操作方法:" + methodTarget + ",操作行为:" + desc + ",操作结果:" + operLogPO.getStatus() + "!(操作成功:1,操作失败:2)<<<<<<<<<<<<<<<<");
            logRepository.insert(operLogPO);
        }
        return result;
    }

    /**
     * 功能:获取IP地址
     *
     * @param request
     * @return
     */
    public static String getClientIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

    /**
     * 功能:获取浏览器版本
     *
     * @return
     */
    public String getBrownerNo(HttpServletRequest request) {
        return getNo(request, new String[]{"MSIE", "FIREFOX", "CHROME", "SAFARI", "OPERA"});
    }

    /**
     * 功能:获取操作系统版本
     *
     * @return
     */
    public String getOsNo(HttpServletRequest request) {
        return getNo(request, new String[]{"WINDOWS NT", "IOS"});
    }

    /**
     * 获取参数
     *
     * @param request
     * @param osNos
     * @return
     */
    public String getNo(HttpServletRequest request, String[] osNos) {
        String userAgent = request.getHeader("user-agent");
        String osNo = "";
        if (userAgent != null) {
            String str = userAgent.toUpperCase();
            for (int i = 0; i < osNos.length; i++) {
                if (str.indexOf(osNos[i]) > 0) {
                    String str1 = str.substring(str.indexOf(osNos[i]));
                    if (str1.indexOf(";") > 0) {
                        osNo = str1.substring(0, str1.indexOf(";"));
                    } else {
                        osNo = osNos[i];
                    }
                    break;
                }
            }
        }
        return osNo;
    }
}

最核心部分就是添加 @Around注解的方法了,这里是切面的处理中心,我们可以将获取的参数和接口的返回值插入到Mongodb中。

结果测试

最后我们来测试一下整个过程是否没正确,我们通过Postman进行post请求,观察Mongodb中是否有请求日志插入,如果数据插入成功且正确,就说明我们的代码是没问题的,下面我们来看测试结果。

Postman请求

Mongodb日志

我们可以看到,已经成功的将数据插入到Mongodb中了,而且数据完整无误。

到这边通过AOP和自定义注解实现请求日志收集功能就介绍完毕了,是不是超级简单呀~

注意点:

  1. 请求日志我们可以只留前面几个月,不然日志数量太大会影响查询性能。
  2. 核心的接口进行日志收集,其它简单的接口可以不用。
  3. 保存的请求信息要尽量详细,方便日后bug排查。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏「3306 Pai」社区

由MySQL复制延迟说起

杨奇龙,网名“北在南方”,7年DBA老兵,目前任职于杭州有赞科技DBA,主要负责数据库架构设计和运维平台开发工作,擅长数据库性能调优、故障诊断。

13110
来自专栏沃趣科技

DTCC 2019 | 沃趣科技邀您共同回首“数据库技术 十年变迁路”

作为国内备受关注的数据库及大数据领域技术盛会,第十届中国数据库技术大会(DTCC 2019)将于2019年5月8日-10日,在北京隆重召开。沃趣科技作为国内优秀...

17730
来自专栏KEN DO EVERTHING

深入学习MySQL 02 日志系统:bin log,redo log,undo log

上一篇文章中,我们了解了一条查询语句的执行过程,按理说这篇应该讲一条更新语句的执行过程,但这个过程比较复杂,涉及到了好几个日志与事物,所以先梳理一下3个重要的日...

25140
来自专栏Bug生活2048

[mini-blog]基于云开发的博客小程序诞生

对于完全依赖云开发的博客来说,文章的发布还是比较麻烦的,毕竟不能在小程序上直接写文章吧,效率太低,所以我利用公众号作为的文章数据源,利用云函数写了个定时同步的方...

23440
来自专栏Linyb极客之路

抛开复杂的架构设计,MySQL优化思想基本都在这了

数据库优化一方面是找出系统的瓶颈,提高MySQL数据库的整体性能,而另一方面需要合理的结构设计和参数调整,以提高用户的相应速度,同时还要尽可能的节约系统资源,以...

14140
来自专栏Linyb极客之路

数据库之架构:主备+分库?主从+读写分离?

1、高可用分析:高可用,主库挂了,keepalive(只是一种工具)会自动切换到备库。这个过程对业务层是透明的,无需修改代码或配置。

11320
来自专栏沉默王二

接口和抽象类,傻傻分不清楚?

兄弟们,你们怎么看,这段解释把我绕得晕乎乎的,好像喝过一斤二锅头。到底是解释抽象类呢还是接口呢?傻傻分不清楚。

11730
来自专栏EAWorld

微服务架构解析:API Fortress,一曲数字化交响乐

我最喜欢设计和构建的东西,就是作业编排。我乐于设想软件的每个组成部分是如何构成一幅宏大的图景,系统在高负载或者系统失败等各种不同的场景下如何产生反馈。

10120
来自专栏Java3y

通俗易懂讲解一条SQL是怎么执行的

额~~不是我不说啊,因为细说起来,我可以细分为DML(Update、Insert、Delete),DDL(表结构修改),DCL(权限操作),DQL(Sele...

11420

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励