Springboot通过AOP方式(@Aspect)和Javassist优雅地进行日志输出管理详细示例

前言

在日常工作中,我们免不了要打印很多log。而大部分需要输出的log又是重复的(例如传入参数,返回值)。
因此,通过AOP方式来进行日志管理可以减少很多代码量,也更加优雅。

主要使用技术:Aspect,Javassist
(本文旨在提供实现示例,因此不做过多的原理说明。)


本文由作者三汪首发于简书。

代码示例

在Sringboot环境下,将本文提供的代码及依赖复制到你的项目中,可直接使用。

Maven依赖
(不包含slf4j部分依赖)

        <!-- aspectj -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
        </dependency>

        <!-- javassist -->
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
        </dependency>

        <!-- fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.39</version>
        </dependency>

代码部分

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.alibaba.fastjson.JSON;

import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.Modifier;
import javassist.NotFoundException;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.LocalVariableAttribute;
import javassist.bytecode.MethodInfo;

@Aspect
@Component
public class MethodLogAop {

    private static final Logger LOGGER = LoggerFactory.getLogger(MethodLogAop.class);

    /**
     * 切点
     * 配置需要添加切面通知的包路径
     */
    @Pointcut("(execution(* com.wolfgy.demo.service..*.*(..)))")//||(execution(* com.wolfgy.demo.web..*.*(..)))
    public void webLog(){}

    /**
     * 前置通知
     * @param joinPoint 切点
     * @throws Throwable 异常
     */
    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        String classType = joinPoint.getTarget().getClass().getName();
        Class<?> clazz = Class.forName(classType);
        String clazzName = clazz.getName();
        LOGGER.info("类名:" + clazzName);
        String methodName = joinPoint.getSignature().getName();
        LOGGER.info("方法名:" + methodName);
        String[] paramNames = getFieldsName(this.getClass(), clazzName, methodName);
        Object[] args = joinPoint.getArgs();
        for(int k=0; k<args.length; k++){
            LOGGER.info("参数名:" + paramNames[k] + ",参数值:" + JSON.toJSONString(args[k]));
        }
    }


    /**
     * 得到方法参数的名称
     * @param cls 类
     * @param clazzName 类名
     * @param methodName 方法名
     * @return 参数名数组
     * @throws NotFoundException 异常
     */
    private static String[] getFieldsName(Class<?> cls, String clazzName, String methodName) throws NotFoundException {
        ClassPool pool = ClassPool.getDefault();
        ClassClassPath classPath = new ClassClassPath(cls);
        pool.insertClassPath(classPath);

        CtClass cc = pool.get(clazzName);
        CtMethod cm = cc.getDeclaredMethod(methodName);
        MethodInfo methodInfo = cm.getMethodInfo();
        CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
        LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
        String[] paramNames = new String[cm.getParameterTypes().length];
        int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;
        for (int i = 0; i < paramNames.length; i++){
            paramNames[i] = attr.variableName(i + pos); //paramNames即参数名
        }
        return paramNames;
    }

    /**
     * 后置通知
     * 打印返回值日志
     * @param ret 返回值
     * @throws Throwable 异常
     */
    @AfterReturning(returning = "ret", pointcut = "webLog()")
    public void doAfterReturning(JoinPoint joinPoint, Object ret) throws Throwable {
        String classType = joinPoint.getTarget().getClass().getName();
        Class<?> clazz = Class.forName(classType);
        String clazzName = clazz.getName();
        LOGGER.info("类名:" + clazzName);
        String methodName = joinPoint.getSignature().getName();
        LOGGER.info("方法名:" + methodName);
        LOGGER.info("返回值 : " + JSON.toJSONString(ret));
    }
}

代码效果

2017-11-23 13:17:28.865  INFO 5056 --- [nio-8081-exec-1] com.wolfgy.demo.aspectj.MethodLogAop     : 类名:com.wolfgy.demo.service.DemoService
2017-11-23 13:17:28.867  INFO 5056 --- [nio-8081-exec-1] com.wolfgy.demo.aspectj.MethodLogAop     : 方法名:save
2017-11-23 13:17:29.025  INFO 5056 --- [nio-8081-exec-1] com.wolfgy.demo.aspectj.MethodLogAop     : 参数名:project,参数值:{"name":"demo"}

2017-11-23 13:17:29.273  INFO 5056 --- [nio-8081-exec-1] com.wolfgy.demo.aspectj.MethodLogAop     : 类名:com.wolfgy.demo.service.DemoService
2017-11-23 13:17:29.273  INFO 5056 --- [nio-8081-exec-1] com.wolfgy.demo.aspectj.MethodLogAop     : 方法名:save
2017-11-23 13:17:29.273  INFO 5056 --- [nio-8081-exec-1] com.wolfgy.demo.aspectj.MethodLogAop     : 返回值 : null

补充说明

execution语法:

语法表达式:
execution(<修饰符> <返回类型> <类路径> <方法名>(<参数列表>) <异常模式> )
其中,修饰符和异常是可选的,如果不加类路径,则默认对所有的类生效。
常用实例:
1.通过方法签名、返回值定义切点:
execution(public * *Service(..)):定位于所有类下返回值任意、方法入参类型、数量任意,public类型的方法
execution(public String *Service(..)):定位于所有类下返回值为String、方法入参类型、数量任意,public类型的方法
2.通过类包定义切点:
execution(* com.yc.controller.BaseController+.*(..)):匹配任意返回类型,对应包下BaseController类及其子类等任意方法。
execution(* com.*.(..)):匹配任意返回类型,com包下所有类的所有方法
execution(* com..*.(..)):匹配任意返回类型,com包、子包下所有类的所有方法
(注意:.表示该包下所有类,..则涵括其子包。)
3.通过方法入参定义切点
(这里*表示任意类型的一个参数,..表示任意类型任意数量的参数)
execution(* speak(Integer,*)):匹配任意返回类型,所有类中只有两个入参,第一个入参为Integer,第二个入参任意的方法
execution(* speak(..,Integer,..)):匹配任意返回类型,所有类中至少有一个Integer入参,但位置任意的方法。


以上。
希望我的文章对你能有所帮助。
我不能保证文中所有说法的百分百正确,
但我能保证它们都是我的理解和感悟以及拒绝直接复制黏贴(确实需要引用的部分我会附上源地址)。
有什么意见、见解或疑惑,欢迎留言讨论。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 136,120评论 19 139
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 14,029评论 6 13
  • 1. 简介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的...
    笨鸟慢飞阅读 11,207评论 0 4
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 33,368评论 18 399
  • 聚会同学,从未推却,即便刮风下雪。 时光未老鬓发白,偶尔相逢人雀跃。 欢天喜地,兴高采烈,友谊长存勿缺。 容颜易老...
    高原_4697阅读 1,693评论 2 4

友情链接更多精彩内容