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

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: 环绕通知, 围绕着方法执行
 * 

* 正常执行顺序 环绕前 -> 前置 -> 环绕后(异常时无) -> 后置 -> 返回通知(或异常通知,二者存其一) */ @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 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 name = phoneUtils.getPhones(length);
        Object o = JSONArray.toJSON(name);
        return o;
    }

3.2 测试结果

执行结果.png

你可能感兴趣的:(自定义注解实现打印系统日志)