自定义注解实现打印系统日志

1 注解(Annotation)

  • Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。
  • Java 语言中的类、方法、变量、参数和包等都可以被标注。注解通过反射来在运行时获取标注内容(在编译器生成类文件时,标注可以被嵌入到字节码中,Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容。)

1.1 常见注解

  • Java内置注解
    • java.lang包下
      • @Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
      • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
      • @SuppressWarnings - 指示编译器去忽略注解中声明的警告。
      • @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
      • @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口
    • java.lang.annotation包下(元注解:标注注解的注解)
      • @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
      • @Documented - 标记这些注解是否包含在用户文档中。
      • @Target - 标记这个注解应该是哪种 Java 成员。
      • @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
      • @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
  • 自定义注解

1.2 自定义注解

1.2.1 注解声明方式

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SysLog {
    //注解的参数 参数类型+参数名+();
    //default 代表默认值, 正常注解有参数不传会报错,如果提供了默认值则不会报错
    //默认值为-1代表不存在找不到
    //参数名为value时 使用注解时可以不用写 value=XXX

    /**
     * All: 全部通知都执行
     * BEFORE: 前置通知,主要打印入参
     * AFTER: 后置通知
     * NO_THROW: 异常通知,只有数组里面有NO_THROW时,发生异常时才不会通知,其他情况,发生异常一律通知
     * AROUND: 环绕通知,主要打印方法执行时间
     * RETURN: 返回通知:主要打印执行结果
     */
    String[] value() default {"BEFORE","AROUND","RETURN"};
}

1.2.2 常用元注解使用说明

  • @Target:指明了修饰的这个注解的使用范围,即被描述的注解可以用在哪里。

    • TYPE:类,接口或者枚举
    • FIELD:域,包含枚举常量
    • METHOD:方法
    • PARAMETER:参数
    • CONSTRUCTOR:构造方法
    • LOCAL_VARIABLE:局部变量
    • ANNOTATION_TYPE:注解类型
    • PACKAGE:包
  • @Retention:指明修饰的注解的生存周期,即会保留到哪个阶段。(RUNTIME>CLASS>SOURCE)

    • SOURCE:源码级别保留,编译后即丢弃
    • CLASS:编译级别保留,编译后的class文件中存在,在jvm运行时丢弃,这是默认值
    • RUNTIME: 运行级别保留,编译后的class文件中存在,在jvm运行时保留,可以被反射调用
  • @Documented:指明修饰的注解,可以被例如javadoc此类的工具文档化,只负责标记,没有成员取值。

  • @Inherited 表明子类可以继承父类注解

2 AOP 面向切面编程

2.1 基本概念

在程序运行过程中,动态的将代码插入到原有的指定方法、指定位置上的思想被称之为面向切面编程。

  • advice: 增强、通知,在特定连接点执行的动作(其实就是你要插入到的代码)。
  • pointcut: 切点,一组连接点的总称,用于指定某个增强应该在何时被调用(像是对一组连接点的抽象声明,通常是execution表达式,即符合表达式的方法被称为切点)。
  • joinpoint: 连接点,在应用执行过程中能够插入切面的一个点。
  • aspect: 切面,即通知(增强)和切点的结合。

2.2 aspectj实现面向切面编程

2.2.1 定义切面

  • 切面是切点和增强的结合,使用aspectj定义切面时,需要准备一个类,来定义各种通知方法及实现。类上需要加上@Aspect注解。

2.2.2 定义切点

定义一个切点需要两部分组成:Pointcut表示式和Point签名。
Pointcut表示式通常有两种类型,一种是execution表达式,一种就是注解。
@Around("logPointCut()") 等价于@Around("@annotation(com.sler.springcloud.utils.SysLog)")

例:execution表达式:public * com.sler.springcloud.controller...(..)
public 代表访问权限
* 代表任意返回值
controller.. 代表当前包及子包
* 代表所有类
.*(..) 代表类下面所有方法,(..)代表允许任何形式的入参

    @Pointcut("@annotation(com.sler.springcloud.utils.SysLog)")  //Pointcut表示式
    public void logPointCut() {}                                 //Point签名

2.2.3 五种通知(增强)

  • @Before: 前置通知, 在方法执行之前执行
  • @After: 后置通知, 在方法执行之后执行 。后置方法在连接点方法完成之后执行,无论连接点方法执行成功还是出现异常,都将执行后置方法。
  • @AfterRunning: 返回通知, 在方法返回结果之后执行 当连接点方法成功执行后,返回通知方法才会执行,如果连接点方法出现异常,则返回通知方法不执行。
  • @AfterThrowing: 异常通知, 在方法抛出异常之后 异常通知方法只在连接点方法出现异常后才会执行,否则不执行。
  • @Around: 环绕通知, 围绕着方法执行
  • 执行顺序 环绕前 -> 前置 -> 环绕后(异常时无) -> 后置 -> 返回通知(或异常通知,二者存其一)

3 开发系统日志注解

3.1 代码实现

3.1.1 自定义注解类

package com.sler.springcloud.utils;

import java.lang.annotation.*;

/**
 * 类功能:系统日志注解
 * 作者: sler
 * 创建时间: 2021/10/20 18:38
 * 描述:元注解 :@Target @Retention @Documented @Inherited
 */

/** Target:指明了修饰的这个注解的使用范围,即被描述的注解可以用在哪里。
 *
 * TYPE:类,接口或者枚举
 * FIELD:域,包含枚举常量
 * METHOD:方法
 * PARAMETER:参数
 * CONSTRUCTOR:构造方法
 * LOCAL_VARIABLE:局部变量
 * ANNOTATION_TYPE:注解类型
 * PACKAGE:包
 */
@Target(ElementType.METHOD)

/** Retention:指明修饰的注解的生存周期,即会保留到哪个阶段。 RUNTIME>CLASS>SOURCE
 *
 * SOURCE:源码级别保留,编译后即丢弃
 * CLASS:编译级别保留,编译后的class文件中存在,在jvm运行时丢弃,这是默认值
 * RUNTIME: 运行级别保留,编译后的class文件中存在,在jvm运行时保留,可以被反射调用
 */
@Retention(RetentionPolicy.RUNTIME)

/**
 * Documented:指明修饰的注解,可以被例如javadoc此类的工具文档化,只负责标记,没有成员取值。
 */
@Documented

/**
 * 子类可以继承父类注解
 */
@Inherited
public @interface SysLog {
    //注解的参数 参数类型+参数名+();
    //default 代表默认值, 正常注解有参数不传会报错,如果提供了默认值则不会报错
    //默认值为-1代表不存在找不到
    //参数名为value时 使用注解时可以不用写 value=XXX

    /**
     * All: 全部通知都执行
     * BEFORE: 前置通知,主要打印入参
     * AFTER: 后置通知
     * NO_THROW: 异常通知,只有数组里面有NO_THROW时,发生异常时才不会通知,其他情况,发生异常一律通知
     * AROUND: 环绕通知,主要打印方法执行时间
     * RETURN: 返回通知:主要打印执行结果
     */
    String[] value() default {"BEFORE","AROUND","RETURN"};
}

3.1.2 切面类

package com.sler.springcloud.utils;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

/**
 * 类功能:系统日志切面
 * 作者: sler
 * 创建时间: 2021/10/20 18:41
 * 描述:
 */

/**
 * @Before: 前置通知, 在方法执行之前执行
 * @After: 后置通知, 在方法执行之后执行 。后置方法在连接点方法完成之后执行,无论连接点方法执行成功还是出现异常,都将执行后置方法。
 * @AfterRunning: 返回通知, 在方法返回结果之后执行 当连接点方法成功执行后,返回通知方法才会执行,如果连接点方法出现异常,则返回通知方法不执行。
 * @AfterThrowing: 异常通知, 在方法抛出异常之后 异常通知方法只在连接点方法出现异常后才会执行,否则不执行。
 * @Around: 环绕通知, 围绕着方法执行
 * <p>
 * 正常执行顺序 环绕前 -> 前置 -> 环绕后(异常时无) -> 后置 -> 返回通知(或异常通知,二者存其一)
 */

@Aspect
@Component
@Slf4j
public class SysLogAspect {

    /**
     * 定义切点
     * Pointcut切点包括
     *      Pointcut表示式:@Pointcut("@annotation(com.sler.springcloud.utils.SysLog)")
     *      Point签名:public void logPointCut(){}
     * 对定义好的切点进行增强时,可以使用表达式也可以使用签名
     */
    @Pointcut("@annotation(com.sler.springcloud.utils.SysLog)")
    public void logPointCut() {}

    /**
     * 环绕通知
     * @param joinPoint
     * @return
     * @throws Throwable
     */
//    @Around("logPointCut()")
    @Around("@annotation(com.sler.springcloud.utils.SysLog)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取切点方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        //获取自定义注解
        SysLog sysLog = method.getAnnotation(SysLog.class);
        //获取注解值
        String[] value = sysLog.value();
        Object result = null;

        if (this.check("AROUND",value)){
            long beginTime = System.currentTimeMillis();    //开始计时
            //执行方法
            result = joinPoint.proceed();
            long time = System.currentTimeMillis() - beginTime; //执行时长
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = signature.getName();
            log.info("系统环绕通知:方法" + className + "." + methodName + "(),执行时间:" + time + "ms");
            return result;
        }else {
            result = joinPoint.proceed();
            return result;
        }
    }

    /**
     * 异常日志 (可以做统一返回处理)
     *
     * @param joinPoint
     * @param e
     */
    @AfterThrowing(value = "logPointCut()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, Exception e) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        //获取自定义注解
        SysLog sysLog = method.getAnnotation(SysLog.class);
        //获取注解值
        String[] value = sysLog.value();
        if (!this.check("NO_THROW",value)){
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = signature.getName();

            log.error("系统异常通知:执行" + className + "." + methodName + "()方法发生异常,异常信息:" + e.toString());
        }
    }

    /**
     * 前置通知
     *
     * @param point
     */
    @Before("logPointCut()")
    public void beforMethod(JoinPoint point) {
        //获取切点方法
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        //获取自定义注解
        SysLog sysLog = method.getAnnotation(SysLog.class);
        //获取注解值
        String[] value = sysLog.value();
        if (this.check("BEFORE",value)){
            String className = point.getTarget().getClass().getName();
            String methodName = point.getSignature().getName();
            List<Object> args = Arrays.asList(point.getArgs());

            log.info("系统前置通知:待执行方法" + className + "." + methodName + "(),入参:" + args);
        }
    }

    /**
     * 后置通知
     *
     * @param point
     */
    @After("logPointCut()")
    public void afterMethod(JoinPoint point) {
        //获取切点方法
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        //获取自定义注解
        SysLog sysLog = method.getAnnotation(SysLog.class);
        //获取注解值
        String[] value = sysLog.value();
        if (this.check("AFTER",value)){
            String className = point.getTarget().getClass().getName();
            String methodName = point.getSignature().getName();

            log.info("系统后置通知:已执行方法" + className + "." + methodName+"()");
        }
    }

    /*通过returning属性指定连接点方法返回的结果放置在result变量中,在返回通知方法中可以从result变量中获取连接点方法的返回结果了。*/

    /**
     * 返回通知
     * @param point
     * @param result
     */
    @AfterReturning(value = "logPointCut()", returning = "result")
    public void afterReturning(JoinPoint point, Object result) {
        //获取切点方法
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        //获取自定义注解
        SysLog sysLog = method.getAnnotation(SysLog.class);
        //获取注解值
        String[] value = sysLog.value();
        if (this.check("RETURN",value)){
            String className = point.getTarget().getClass().getName();
            String methodName = point.getSignature().getName();

            log.info("系统返回通知:已执行方法" + className + "." + methodName + "(),执行结果:" + result);
        }
    }

    private boolean check(String type,String info[]){
        boolean flag = false;
        for (String s : info) {
            if("ALL".equals(s) || type.equals(s)){
                return true;
            }
        }
        return flag;
    }

}

3.1.3 测试方法

    @SysLog
    @RequestMapping("/phone/{length}")
    public Object createData( @PathVariable int length) throws Exception {
        List<String> name = phoneUtils.getPhones(length);
        Object o = JSONArray.toJSON(name);
        return o;
    }

3.2 测试结果

执行结果.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容