目录
一、AOP的概念
1.1 术语
AOP就是将那些横切性的问题抽象出来形成一个切面,然后面对这些切面去编程。
- join point:连接点,在Spring AOP中连接点往往就是指一个方法,也就是目标对象中的方法
- pointcut: 切点,在spring中,切点就是连接点的集合,就可以将切点类比成一张表,连接点是这张表里面的记录。
- target object:目标对象(原始对象),通过AOP进行代码增强前的对象
- AOP proxy:代理对象,通过AOP将目标对象进行增强之后的对象,包含了原始对象的代码和增强代码。
- weaving:织入,其实就是代码增强的过程,将解决横切性问题的代码插入到目标对象的方法,进而生成代理对象的方法,这个过程叫做织入。要注意区分,目标对象变为代理对象的过程叫代理,目标对象的方法变成代理对象的方法的过程叫织入。
- advice:通知,它表示两个含义,一个是表示解决横切性问题的代码内容,也就是织入进目标对象的方法中的代码内容是什么,另一个是表示将这个代码织入到目标对象中方法中的什么位置(方法前/方法后/方法前后),总结来说就是在方法执行的什么时机(when:方法前/方法后/方法前后)做什么(what:增强的功能)
- aspect:切面,就是切点和advice的载体,我们要将advice织入到对应的连接点中,切点是连接点的集合,所以在编写代码的时候我们要进行织入的代码(advice)和要将代码插入到哪个连接点位置,都是要先定义到aspect(切面)中的。切面就是切点和advice的载体,切面 = 切入点 + 通知,通俗点就是:在什么时机,什么地方,做什么增强!在aspectJ框架中切面用一个类来表示,在spring的XML中以一个配置标签来表示。
1.2 AspectJ
AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。也就是说AspectJ是一种AOP技术。AspectJ是一种静态代理的框架。AspectJ 是静态代理的增强,所谓的静态代理就是 AOP 框架会在编译阶段生成 AOP 代理类,因此也称为编译时增强。
1.3 AOP和spring AOP有什么关系
AOP是一种思想,spring AOP是对AOP思想的实现
AspectJ也是对AOP思想的实现。
在之前spring AOP是有自己的实现AOP的语法的,但是很难记,后来spring AOP就引入了AspectJ的语法,但是实现方法还是spring AOP自己的实现方法。spring AOP是一个动态代理的框架。
二、spring AOP的使用
0、首先要引用AspectJ语法依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.0</version>
</dependency>
1、spring AOP开启AspectJ语法支持
JavaConfig方式:
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
XML方式:
<aop:aspectj-autoproxy/>
2、声明一个切面(Aspect)
声明一个切面就是创建一个切面bean交给spring容器去管理
用注解的话就是先用@Aspect注解将一个类定义为切面类,然后这个切面的bean交给spring容器去管理,将bean装配给spring容器使用注解或者XML都可以
JavaConfig:
/**
* 这个类就是一个切面
* 切面就是切点+通知
* 所在在这个类里面还需要定义切点和通知
*/
@Component
@Aspect
public class AppAspect {
}
2.1、声明一个Pointcut切点
/**
* 这个类就是一个切面
* 切面就是切点+通知
* 所在在这个类里面还需要定义切点和通知
*/
@Component
@Aspect
public class AppAspect {
/**
* 将@Pointcut注解作用于一个方法,这就定义了一个切点,注解中表示的是priv.cy.dao包下面的所有类的所有任意修饰符的方法都是连接点(方法括号里面的..表示的是任意参数)
* 将这些连接点集中到一起的集合就是切点
*
* 方法名任意,以后这个方法名就代表这个切点
*/
@Pointcut("execution(* priv.cy.dao.*.*(..))")
public void pointCut() {
}
}
2.2 声明一个advice通知
package priv.cy.app;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.aspectj.lang.annotation.Aspect;
/**
* 这个类就是一个切面
* 切面就是切点+通知
* 所在在这个类里面还需要定义切点和通知
*/
@Component
@Aspect
public class AppAspect {
/**
* 将@Pointcut注解作用于一个方法,这就定义了一个切点,注解中表示的是priv.cy.dao包下面的所有类的所有任意修饰符的方法都是连接点(方法括号里面的..表示的是任意参数)
* 将这些连接点集中到一起的集合就是切点
*
* 方法名任意,以后这个方法名就代表这个切点
*/
@Pointcut("execution(* priv.cy.dao.*.*(..))")
public void pointCut() {
}
/**
* 下面来定义advice(通知)
* 通知包括 通知的代码逻辑以及要将通知织入到什么位置
*/
/**
* 这就是一个advice
* advice=代码逻辑+织入位置
* 织入位置就是通过开头的注解表示的,将上面定义的代表切点的方法作为参数传入注解,下面这个注解表示,插入到上面pointCut()方法所代表的切点中所有的连接点的前面
* 连接点就是一个方法,也就是说要插入到方法前
*
* 要插入的内容(代码逻辑)就是写道这个注解标识的方法中,方法名任意
*/
@Before("pointCut()")
public void before() {
System.out.println("before");
}
}
dao:
@Repository
public class IndexDao {
public void query() {
System.out.println("query");
}
}
测试类:
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext annotationConfigApplicationContext
= new AnnotationConfigApplicationContext(AppConfig.class);
IndexDao dao = annotationConfigApplicationContext.getBean("indexDao", IndexDao.class);
dao.query();
}
}
执行结果:
官方文档例子
advice的注解还支持使用表达式:
@Component
@Aspect
public class AppAspect {
@Pointcut("execution(* priv.cy.dao.*.*(..))")
public void pointCutExecution() {
}
@Pointcut("within(priv.cy.dao.*)")
public void pointCutWithin() {
}
@Pointcut("args(java.lang.String)")
public void pointCutArgs() {
}
@Before("pointCutWithin()&&!pointCutArgs()")
public void before() {
System.out.println("before");
}
}
含义就是织入dao包下面参数不是String类型的方法,很好理解,就是普通的与逻辑表达式。
四、advice通知的位置
@Before
在方法执行前执行
@AfterReturning
在方法返回结果之后执行
@AfterThrowing
在方法抛出异常之后执行
@After
只要是方法结束,就会执行
@Around
使用环绕通知,就需要引入一个连接点类ProceedingJoinPoint,这个类继承的JoinPoint这个类。
连接点类表示的就是一个连接点,也就是一个方法。我们可以通过JoinPoint获得连接点的所在类,目标对象,代理对象,方法参数,方法返回值类型等。当然所有的通知方法都可以将这个类的对象作为参数传入。
ProceedingJoinPoint与JoinPoint最大的不同就是它提供了proceed()方法,可以用来执行连接点方法。
通过这个方法就可以实现环绕通知(around)
@Component
@Aspect
public class AppAspect {
@Pointcut("target(priv.cy.dao.IndexDao)")
public void pointCutTarget() {
}
@Around("pointCutTarget()")
public void around(ProceedingJoinPoint joinPoint) {
System.out.println("a");
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("b");
}
}
这样就在连接点方法的前后输出了a和b
执行结果
proceed()除了无参构造方法之外,还实现了一个有参构造方法
public Object proceed(Object[]args) throws Throwable;
这个有参方法可以让你设置传入连接点方法的参数
例子:
dao:
@Repository
public class IndexDao implements Dao{
@Override
public void query(String str) {
System.out.println("query 1");
System.out.println(str);
}
@Override
public void query() {
System.out.println("query 2");
}
}
aspect:
@Component
@Aspect
public class AppAspect {
@Pointcut("target(priv.cy.dao.IndexDao)")
public void pointCutTarget() {
}
@Around("pointCutTarget()")
public void around(ProceedingJoinPoint joinPoint) {
Object args[] = joinPoint.getArgs();
if (args != null && args.length > 0) {
for (int i = 0; i < args.length; i++) {
args[i] += " hahaha";
}
}
System.out.println("a");
try {
joinPoint.proceed(args);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("b");
}
}@Component
@Aspect
public class AppAspect {
@Pointcut("target(priv.cy.dao.IndexDao)")
public void pointCutTarget() {
}
@Around("pointCutTarget()")
public void around(ProceedingJoinPoint joinPoint) {
Object args[] = joinPoint.getArgs();
if (args != null && args.length > 0) {
for (int i = 0; i < args.length; i++) {
args[i] += " hahaha";
}
}
System.out.println("a");
try {
joinPoint.proceed(args);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
System.out.println("b");
}
}
Test:
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext annotationConfigApplicationContext
= new AnnotationConfigApplicationContext(AppConfig.class);
Dao dao = (Dao) annotationConfigApplicationContext.getBean("indexDao");
System.out.println(dao instanceof IndexDao);
dao.query("test");
System.out.println("=========================");
dao.query();
}
}
执行结果:
由上面的代码可以看出,传入的参数是test,但是因为JoinPoint提供了可以获取连接点方法参数的方法getArgs(),ProceedingJoinPoint对象就通过了这个方法获取到传入连接点方法的参数,然后对参数进行了修改,再将修改后的参数传入proceed(Object[]args)也就实现了对方法参数的修改,最终的执行结果是test hahaha。
args[]数组中的每一个元素是表示方法中的一个参数。
上面讲解的aspect都是基于注解来讲解的,使用XML的用法可以去看官方文档。使用XML时,aspect就是一个标签。XML的好处就是如果源码你是不能修改的,也就无法使用注解,这个时候就需要使用XML了