本文主要是介绍SpringBoot利用AOP记录系统日志,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
一、AOP的基本概念
利用Spring框架中aop
,我们可以实现业务代码与系统级服务进行解耦,例如日志记录、事务及其他安全业务等,可以使得我们的工程更加容易维护、优雅。
Advice(通知、切面): 某个连接点所采用的处理逻辑,也就是向连接点注入的代码, AOP在特定的切入点上执行的增强处理。
@Before:
标识一个前置增强方法,相当于BeforeAdvice的功能.
@After:
final增强,不管是抛出异常或者正常退出都会执行.
@AfterReturning:
后置增强,似于AfterReturningAdvice, 方法正常退出时执行.
@AfterThrowing:
异常抛出增强,相当于ThrowsAdvice.
@Around:
环绕增强,相当于MethodInterceptor.
JointPoint(连接点):
程序运行中的某个阶段点,比如方法的调用、异常的抛出等。
Pointcut(切入点):
JoinPoint的集合,是程序中需要注入Advice的位置的集合,指明Advice要在什么样的条件下才能被触发,在程序中主要体现为书写切入点表达式。
Advisor(增强):
是PointCut和Advice的综合体,完整描述了一个advice将会在pointcut所定义的位置被触发。
@Aspect(切面):
通常是一个类的注解,里面可以定义切入点和通知
AOP Proxy:
AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类。
二、什么是注解?
它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。为程序的元素(类、方法、成员变量)加上更直观、更明了的说明,这些说明信息是与程序的业务逻辑无关,并且供指定的工具或框架使用。Annontation像一种修饰符一样,应用于包、类型、构造方法、方法、成员变量、参数及本地变量的声明语句中。
Java注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。
三、注解的用处:
生成文档。这是最常见的,也是java 最早提供的注解。常用的有@param @return 等
跟踪代码依赖性,实现替代配置文件功能。比如Dagger 2 依赖注入,未来java 开发,将大量注解配置,具有很大用处;
在编译时进行格式检查。如@override 放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出。
四、注解的原理
注解本质是一个继承了Annotation 的特殊接口,其具体实现类是Java 运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java 运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler 的invoke 方法
。
4.1、元注解
所有元注解定义在java.lang.annotation包下面,其中Annotation是注解的基本接口,所有的注解都继承这个接口。
java.lang.annotation 提供了四种元注解,专门注解其他的注解(在自定义注解的时候,需要使用到元注解):
1、@Documented:
指定被标注的注解会包含在javadoc中。
2、@Retention:
指定注解的生命周期(源码、class文件、运行时),其参考值见类的定义:java.lang.annotation.RetentionPolicy
● RetentionPolicy.SOURCE :
(注解只保留在源文件(.java文件))在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。@Override, @SuppressWarnings都属于这类注解。
● RetentionPolicy.CLASS :
在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式。(注解被保留到class文件,但jvm加载class文件时候被遗弃,默认值(.class文件))
● RetentionPolicy.RUNTIME :
始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。我们自定义的注解通常使用这种方式。
(注解被保存到class文件中,jvm加载class文件之后仍然存在(内存中的字节码))
生命周期长度 SOURCE < CLASS < RUNTIME
3、@Target:
指定注解使用的目标范围(类、方法、字段等),其参考值见类的定义:java.lang.annotation.ElementType
● ElementType.CONSTRUCTOR :
用于描述构造器。
● ElementType.FIELD :
成员变量、对象、属性(包括enum实例)。
● ElementType.LOCAL_VARIABLE:
用于描述局部变量。
● ElementType.METHOD :
用于描述方法。
● ElementType.PACKAGE :
用于描述包。
● ElementType.PARAMETER :
用于描述参数。
● ElementType.ANNOTATION_TYPE:
用于描述参数
● ElementType.TYPE :
用于描述类、接口(包括注解类型) 或enum声明。
4、@Inherited:
指定子类可以继承父类的注解,只能是类上的注解,方法和字段的注解不能继承。即如果父类上的注解是@Inherited修饰的就能被子类继承。
jdk1.8又提供了以下两个元注解
5、@Native:
指定字段是一个常量,其值引用native code。
6、@Repeatable:
注解上可以使用重复注解,即可以在一个地方可以重复使用同一个注解,像spring中的包扫描注解就使用了这个。
7、使用@interface
关键词来定义注解。
4-2、自定义注解
自定义注解类编写的一些规则:
Annotation 类型定义为@interface, 所有的Annotation 会自动继承java.lang.Annotation这一接口,并且不能再去继承别的类或是接口。
参数成员只能用public 或默认(default) 这两个访问权修饰。语法:类型 属性名() [default 默认值]; default表示默认值 ,也可以不编写默认值的.
参数成员只能用基本类型byte、short、char、int、long、float、double、boolean八种基本数据类型和String、Enum、Class、annotations等数据类型,以及这一些类型的数组.
要获取类方法和字段的注解信息,必须通过Java的反射技术来获取 Annotation 对象,因为你除此之外没有别的获取注解对象的方法。
注解也可以没有定义成员,,不过这样注解就没啥用了。
注意: 自定义注解需要使用到元注解。
注解方法不能有参数。
注解方法的返回类型局限于原始类型,字符串,枚举,注解,或以上类型构成的数组。
注解方法可以包含默认值。
4-3、 自定义注解使用:
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedAuth {String value() default "";
}
五、通过AOP实现日志记录
了解@AspectJ切点函数之execution()
execution()是最常用的切点函数,语法如下:
execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)
其中:返回类型模式、方法名模式、参数模式是必选项
通过execution()定义切点的不同方式
1.通过方法签名定义切点
execution(public * *(..))
匹配所有目标类的public方法。 第一个代表返回类型,第二个代表方法名,而…代表任意入参的方法
execution(* *To(..))
匹配目标类所有以To为后缀的方法。 第一个代表返回类型,而To代表任意以To为后缀的方法,而…代表任意入参的方法。
2.通过类定义切点
execution(* com.aop.spring.advisor.aspectJ.function.execution.classpoint.Cleaner.*(..))
3.通过类包定义切点
execution(* com.jiuyue.service.impl..*.*(..))
符号 | 含义 |
---|---|
execution() | 表达式的主体 |
第一个”*“符号 | 表示返回值的类型任意 |
com.jiuyue.service.impl | AOP所切的服务的包名,即,我们的业务部分 |
包名后面的”…“ | 表示当前包及子包 |
第二个”*“ | 表示类名,*即所有类 |
.*(…) | 表示任何方法名,括号表示参数,两个点表示任何参数类型 |
在类名模式串中:
.* 表示包下的所有类。
..*表示包、子孙包下的所有类。
execution(* com.test.*(..))
匹配com.test包下所有类的所有方法
execution(* com.test..*(..))
匹配com.test包、子孙包下所有类的所有方法.比如 com.jiuyue.dao ,com.test.dao.impl,com.test.service,com.test.service.impl包下所有类的所有方法都匹配。 当 …出现在类名中时,必须后面跟*表示子孙包下的所有类。
execution(* com..*Dao.find*(..))
匹配包名前缀为com的任何包下类名后缀为Dao的方法,方法名必须以find为前缀, 比如com.test.UserDao#findUserById()方法都是匹配切点。
通过方法入参定义切点
切点表达式中方法入参部分比较复杂,可以使用“”和“…”通配符,其中“”表示任意类型的参数,而“…”表示任意类型参数且参数个数不限。
execution(* joke(String,int))
匹配joke(String,int)方法,且joke方法的第一个入参是String,第二个入参是int。 比如 匹配 SmartSeller#joke(String ,int)方法。 如果方法中的入参类型是java.lang包下的,这可以直接使用类名,否则必须使用全限定类名,比如 joke(java.util.List,int)
execution(* joke(String,*))
匹配目标类中的joke()方法,该方法第一个入参为String,第二个入参为任意类型。 比如 joke(String s1, String s2)和joke(String s1,double d)都匹配,但是 joke(String s1, String s2,double d3)不匹配。
execution(* joke(String,..))
匹配目标类中的joke方法,该方法的第一个入参为String,后面可以有任意个入参且入参类型不限。 比如 joke(String s1),joke(String s1,String s2)和joke(String s1,double d2,String s3)都匹配。
execution(* joke(Object+))
匹配目标类中的joke()方法,方法拥有一个入参,且入参是Object类型或该类的子类。 它匹配joke(String s1) 和joke(Client c) . 如果定义的切点是execution(* joke(Object)) ,则只匹配joke(Object object)而不匹配joke(String s1) 或者joke(Client c)。
六、具体代码实现
NeedAuth 自定义注解
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedAuth {String value() default "";
}
LogAspect.java
import groovy.util.logging.Slf4j;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;@Component
@Slf4j
@Aspect
public class LogAspect {/*** JointPoint(连接点):程序运行中的某个阶段点,比如方法的调用、异常的抛出等。* Pointcut(切入点): JoinPoint的集合,是程序中需要注入Advice的位置的集合,* 指明Advice要在什么样的条件下才能被触发,在程序中主要体现为书写切入点表达式。*/
// @Pointcut("@annotation(cn.test.dis.adapter.server.service.custom.annotation.NeedAuth)")@Pointcut("execution(* cn.test.dis.adapter.server.service.custom.test.service.SpringAopTestService.*(..))")public void logPoint(){}@Before("logPoint()")public void beforeNotify() {System.out.println("******** @Before我是前置通知MyAspect");}@After("logPoint()")public void afterNotify() {System.out.println("******** @After我是后置通知");}@AfterReturning("logPoint()")public void afterReturningNotify() {System.out.println("********@AfterReturning我是返回后通知");}@AfterThrowing("logPoint()")public void afterThrowingNotify() {System.out.println("********@AfterThrowing我是异常通知");}@Around("logPoint()")public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {Object retValue = null;System.out.println("我是环绕通知之前AAA");retValue = proceedingJoinPoint.proceed();MethodSignature sign = (MethodSignature) proceedingJoinPoint.getSignature();Method method = sign.getMethod();System.out.println("method:" + method.getName());System.out.println("我是环绕通知之后BBB");return retValue;}
}
SpringAopTestController.java
@RestController
public class SpringAopTestController{@AutowiredSpringAopTestService springAopTestService;public void aopTest(){springAopTestService.message();}public void anoMessage(){springAopTestService.anoMessage();}
}
import cn.test.dis.adapter.server.common.config.DouyinConfig;
import cn.test.dis.adapter.server.service.custom.annotation.NeedAuth;
import org.springframework.boot.SpringBootVersion;
import org.springframework.core.SpringVersion;
import org.springframework.stereotype.Service;import javax.annotation.Resource;@Service
public class SpringAopTestService {@Resourceprivate DouyinConfig douyinConfig;//增加配置类public void message(){System.out.println("执行方法message");}@NeedAuthpublic void anoMessage(){System.out.println("spring版本:" + SpringVersion.getVersion() + "\t" + "SpringBoot版本:" + SpringBootVersion.getVersion());System.out.println("执行方法needAuth");int a = 1/0;}
}
执行结果:
![]() | |
![]() | |
Spring版本不一样,通知执行顺序可能也会存在差异
spring4的几种通知的执行顺序:
程序执行正常:1、环绕通知前
2、@Before通知
3、程序逻辑
4、环绕通知后
5、@After通知
6、@AfterReturning通知程序执行异常:1、环绕通知前
2、@Before通知
3、@After通知
4、@AfterThrowing异常通知
异常日志
spring5的几种通知的执行顺序:
程序执行正常:1、环绕通知前
2、@Before通知
3、程序逻辑
4、@AfterReturning通知
5、@After通知
6、环绕通知后程序执行异常:1、环绕通知前
2、@Before通知
3、@AfterThrowing异常通知
4、@After通知
异常日志
这篇关于SpringBoot利用AOP记录系统日志的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!