自定义注解实现打印系统日志
2021-11-09 本文已影响0人
小疏林er
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 开始支持,标识某注解可以在同一个声明上使用多次。
- java.lang包下
- 自定义注解
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;
}