Spring AOP 功能使用详解
Posted codingjav
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring AOP 功能使用详解相关的知识,希望对你有一定的参考价值。
前言
AOP 既熟悉又陌生,了解过 Spring 人的都知道 AOP 的概念,即面向切面编程,可以用来管理一些和主业务无关的周边业务,如日志记录,事务管理等;陌生是因为在工作中基本没有使用过,AOP 的相关概念也是云里雾里;最近在看 Spring 的相关源码,所以还是先来捋一捋 Spring 中 AOP 的一个用法。
相关概念
在学习 Spring AOP 的用法之前,先来看看 AOP 的相关概念,
Spring AOP 的详细介绍,请参考官网 https://docs.spring.io/spring/docs/2.5.x/reference/aop.html
-
Join point
:连接点,表示程序执行期间的一个点,在 Spring AOP 表示的就是一个方法,即一个方法可以看作是一个 Join point -
pointcut
:切点,就是与连接点匹配的谓词,什么意思呢,就是需要执行 Advice 的连接点就是切点 -
Advice
:增强,在连接点执行的操作,分为前置、后置、异常、最终、环绕增强五种 -
Aspect
:切面,由 pointcut 和 Advice 组成,可以简单的认为 @Aspect 注解的类就是一个切面 -
Target object
:目标对象,即 织入 advice 的目标对象 -
AOP proxy
:代理类,在 Spring AOP 中, 一个 AOP 代理是一个 JDK 动态代理对象或 CGLIB 代理对象 -
`Weaving` :织入,将 Aspect 应用到目标对象中去
注:上述几个概念中,比较容易混淆的是 Join point 和 pointcut,可以这么来理解,在 Spring AOP 中,所有的可执行方法都是 Join point,所有的 Join point 都可以植入 Advice;而 pointcut 可以看作是一种描述信息,它修饰的是 Join point,用来确认在哪些 Join point 上执行 Advice,
栗子
在了解了 AOP 的概念之后,接下来就来看看如何使用 Spring Aop
-
要想使用 Spring AOP ,首先先得在 Spring 配置文件中配置如下标签:
1<aop:aspectj-autoproxy expose-proxy="true" proxy-target-class="true"/>
该标签有两个属性, expose-proxy
和 proxy-target-class
,默认值都为 false
;
expose-proxy
: 是否需要将当前的代理对象使用 ThreadLocal
进行保存,这是什么意思呢,例如 Aop 需要对某个接口下的所有方法进行拦截,但是有些方法在内部进行自我调用,如下所示:
1 public void test_1()
2
3 this.test_2();
4
5 public void test_2()
6
7
调用 test_1,此时 test_2 将不会被拦截进行增强,因为调用的是 AOP 代理对象而不是当前对象,而 在 test_1 方法内部使用的是 this 进行调用,所以 test_2 将不会被拦截增强,所以该属性 expose-proxy 就是用来解决这个问题的,即 AOP 代理的获取。
proxy-target-class
:是否使用 CGLIB 进行代理,因为 Spring AOP 的底层技术就是使用的是动态代理,分为 JDK 代理 和 CGLIB 代理,该属性的默认值为 false,表示 AOP 底层默认使用的使用 JDK 代理,当需要代理的类没有实现任何接口的时候才会使用 CGLIB 进行代理,如果想都是用 CGLIB 进行代理,可以把该属性设置为 true 即可。
-
定义需要 aop 拦截的方法,模拟一个 User 的增删改操作:
接口:
1public interface IUserService
2 void add(User user);
3 User query(String name);
4 List<User> qyertAll();
5 void delete(String name);
6 void update(User user);
7
接口实现:
1@Service("userServiceImpl")
2public class UserServiceImpl implements IUserService
3
4 @Override
5 public void add(User user)
6 System.out.println("添加用户成功,user=" + user);
7
8
9 @Override
10 public User query(String name)
11 System.out.println("根据name查询用户成功");
12 User user = new User(name, 20, 1, 1000, "java");
13 return user;
14
15
16 @Override
17 public List<User> qyertAll()
18 List<User> users = new ArrayList<>(2);
19 users.add(new User("zhangsan", 20, 1, 1000, "java"));
20 users.add(new User("lisi", 25, 0, 2000, "Python"));
21 System.out.println("查询所有用户成功, users = " + users);
22 return users;
23
24
25 @Override
26 public void delete(String name)
27 System.out.println("根据name删除用户成功, name = " + name);
28
29
30 @Override
31 public void update(User user)
32 System.out.println("更新用户成功, user = " + user);
33
34
.
-
定义 AOP 切面
在 Spring AOP 中,使用 @Aspect
注解标识的类就是一个切面,然后在切面中定义切点(pointcut)和 增强(advice):
3.1 前置增强,@Before(),在目标方法执行之前执行
1@Component
2@Aspect
3public class UserAspectj
4
5 // 在方法执行之前执行
6 @Before("execution(* main.tsmyk.mybeans.inf.IUserService.add(..))")
7 public void before_1()
8 System.out.println("log: 在 add 方法之前执行....");
9
10
上述的方法 before_1() 是对接口的 add() 方法进行 前置增强,即在 add() 方法执行之前执行,
测试:
1@RunWith(SpringJUnit4ClassRunner.class)
2@ContextConfiguration("/resources/myspring.xml")
3public class TestBean
4
5 @Autowired
6 private IUserService userServiceImpl;
7
8 @Test
9 public void testAdd()
10 User user = new User("zhangsan", 20, 1, 1000, "java");
11 userServiceImpl.add(user);
12
13
14// 结果:
15// log: 在 add 方法之前执行....
16// 添加用户成功,user=Username='zhangsan', age=20, sex=1, money=1000.0, job='java'
如果想要获取目标方法执行的参数等信息呢,我们可在 切点的方法中添参数 JoinPoint
,通过它了获取目标对象的相关信息:
1 @Before("execution(* main.tsmyk.mybeans.inf.IUserService.add(..))")
2 public void before_2(JoinPoint joinPoint)
3 Object[] args = joinPoint.getArgs();
4 User user = null;
5 if(args[0].getClass() == User.class)
6 user = (User) args[0];
7
8 System.out.println("log: 在 add 方法之前执行, 方法参数 = " + user);
9
重新执行上述测试代码,结果如下:
1log: 在 add 方法之前执行, 方法参数 = Username='zhangsan', age=20, sex=1, money=1000.0, job='java'
2添加用户成功,user=Username='zhangsan', age=20, sex=1, money=1000.0, job='java'
3.2 后置增强,@After(),在目标方法执行之后执行,无论是正常退出还是抛异常,都会执行
1 // 在方法执行之后执行
2 @After("execution(* main.tsmyk.mybeans.inf.IUserService.add(..))")
3 public void after_1()
4 System.out.println("log: 在 add 方法之后执行....");
5
执行 3.1 的测试代码,结果如下:
1添加用户成功,user=Username='zhangsan', age=20, sex=1, money=1000.0, job='java'
2log: ==== 方法执行之后 =====
3.3 返回增强,@AfterReturning(),在目标方法正常返回后执行,出现异常则不会执行,可以获取到返回值:
1@AfterReturning(pointcut="execution(* main.tsmyk.mybeans.inf.IUserService.query(..))", returning="object")
2public void after_return(Object object)
3 System.out.println("在 query 方法返回后执行, 返回值= " + object);
4
测试:
1@Test
2public void testQuery()
3 userServiceImpl.query("zhangsan");
4
5// 结果:
6// 根据name查询用户成功
7// 在 query 方法返回后执行, 返回值= Username='zhangsan', age=20, sex=1, money=1000.0, job='java'
当一个方法同时被 @After() 和 @AfterReturning() 增强的时候,先执行哪一个呢?
1@AfterReturning(pointcut="execution(* main.tsmyk.mybeans.inf.IUserService.query(..))", returning="object")
2public void after_return(Object object)
3 System.out.println("===log: 在 query 方法返回后执行, 返回值= " + object);
4
5
6@After("execution(* main.tsmyk.mybeans.inf.IUserService.query(..))")
7public void after_2()
8 System.out.println("===log: 在 query 方法之后执行....");
9
测试:
1根据name查询用户成功
2===log: 在 query 方法之后执行....
3===log: 在 query 方法返回后执行, 返回值= Username='zhangsan', age=20, sex=1, money=1000.0, job='java'
可以看到,即使 @After() 放在 @AfterReturning() 的后面,它也先被执行,即 @After() 在 @AfterReturning() 之前执行。
3.4 异常增强,@AfterThrowing,在抛出异常的时候执行,不抛异常不执行。
1@AfterThrowing(pointcut="execution(* main.tsmyk.mybeans.inf.IUserService.query(..))", throwing = "ex")
2public void after_throw(Exception ex)
3 System.out.println("在 query 方法抛异常时执行, 异常= " + ex);
4
现在来修改一下它增强的 query() 方法,让它抛出异常:
1@Override
2public User query(String name)
3 System.out.println("根据name查询用户成功");
4 User user = new User(name, 20, 1, 1000, "java");
5 int a = 1/0;
6 return user;
7
测试:
1@Test
2public void testQuery()
3 userServiceImpl.query("zhangsan");
4
5// 结果:
6// 在 query 方法抛异常时执行, 异常= java.lang.ArithmeticException: / by zero
7// java.lang.ArithmeticException: / by zero ...........
3.5 环绕增强,@Around,在目标方法执行之前和之后执行
1@Around("execution(* main.tsmyk.mybeans.inf.IUserService.delete(..))")
2public void test_around(ProceedingJoinPoint joinPoint) throws Throwable
3 Object[] args = joinPoint.getArgs();
4 System.out.println("log : delete 方法执行之前, 参数 = " + args[0].toString());
5 joinPoint.proceed();
6 System.out.println("log : delete 方法执行之后");
7
测试:
1@Test
2public void test5()
3 userServiceImpl.delete("zhangsan");
4
5
6// 结果:
7// log : delete 方法执行之前, 参数 = zhangsan
8// 根据name删除用户成功, name = zhangsan
9// log : delete 方法执行之后
以上就是 Spring AOP 的几种增强。
上面的栗子中,在每个方法上方的切点表达式都需要写一遍,现在可以使用 @Pointcut
来声明一个可重用的切点表达式,之后在每个方法的上方引用这个切点表达式即可:
1// 声明 pointcut
2@Pointcut("execution(* main.tsmyk.mybeans.inf.IUserService.query(..))")
3public void pointcut()
4
5
6@Before("pointcut()")
7public void before_3()
8 System.out.println("log: 在 query 方法之前执行");
9
10@After("pointcut()")
11public void after_4()
12 System.out.println("log: 在 query 方法之后执行....");
13
指示符
在上面的栗子中,使用了 execution
指示符,它用来匹配方法执行的连接点,也是 Spring AOP 使用的主要指示符,在切点表达式中使用了通配符 () 和 (.. ),其中,( )可以表示任意方法,任意返回值,(..)表示方法的任意参数 ,接下来来看下其他的指示符。
1. within
匹配特定包下的所有类的所有 Joinpoint(方法),包括子包,注意是所有类,而不是接口,如果写的是接口,则不会生效,如 within(main.tsmyk.mybeans.impl.*
将会匹配 main.tsmyk.mybeans.impl
包下所有类的所有 Join point
;within(main.tsmyk.mybeans.impl..*
两个点将会匹配该包及其子包下的所有类的所有 Join point
。
栗子:
1@Pointcut("within(main.tsmyk.mybeans.impl.*)")
2public void testWithin()
3
4
5@Before("testWithin()")
6public void test_within()
7 System.out.println("test within 在方法执行之前执行.....");
8
执行该包下的类 UserServiceImpl 的 delete 方法,结果如下:
1@Test
2public void test5()
3 userServiceImpl.delete("zhangsan");
4
5
6// 结果:
7// test within 在方法执行之前执行.....
8// 根据name删除用户成功, name = zhangsan
2. @within
匹配所有持有指定注解类型的方法,如 @within(Secure)
,任何目标对象持有Secure注解的类方法;必须是在目标对象上声明这个注解,在接口上声明的对它不起作用。
3. target
匹配的是一个目标对象,target(main.tsmyk.mybeans.inf.IUserService)
匹配的是该接口下的所有 Join point
:
1@Pointcut("target(main.tsmyk.mybeans.inf.IUserService)")
2public void anyMethod()
3
4
5@Before("anyMethod()")
6public void beforeAnyMethod()
7 System.out.println("log: ==== 方法执行之前 =====");
8
9
10@After("anyMethod()")
11public void afterAnyMethod()
12 System.out.println("log: ==== 方法执行之后 =====");
13
之后,执行该接口下的任意方法,都会被增强。
4. @target
匹配一个目标对象,这个对象必须有特定的注解,如 @target(org.springframework.transaction.annotation.Transactional)
匹配任何 有 @Transactional
注解的方法
5. this
匹配当前AOP代理对象类型的执行方法,this(service.IPointcutService)
,当前AOP对象实现了 IPointcutService
接口的任何方法
6. arg
匹配参数,
1 // 匹配只有一个参数 name 的方法
2 @Before("execution(* main.tsmyk.mybeans.inf.IUserService.query(String)) && args(name)")
3 public void test_arg()
4
5
6
7 // 匹配第一个参数为 name 的方法
8 @Before("execution(* main.tsmyk.mybeans.inf.IUserService.query(String)) && args(name, ..)")
9 public void test_arg2()
10
11
12
13 // 匹配第二个参数为 name 的方法
14 @Before("execution(* main.tsmyk.mybeans.inf.IUserService.query(String)) && args(*, name, ..)")
15 public void test_arg3()
16
17
7. @arg
匹配参数,参数有特定的注解,@args(Anno)),方法参数标有Anno注解。
8. @annotation
匹配特定注解@annotation(org.springframework.transaction.annotation.Transactional)
匹配 任何带有 @Transactional
注解的方法。
9. bean
匹配特定的 bean 名称的方法
1 // 匹配 bean 的名称为 userServiceImpl 的所有方法
2 @Before("bean(userServiceImpl)")
3 public void test_bean()
4 System.out.println("===================");
5
6
7 // 匹配 bean 名称以 ServiceImpl 结尾的所有方法
8 @Before("bean(*ServiceImpl)")
9 public void test_bean2()
10 System.out.println("+++++++++++++++++++");
11
测试:
执行该bean下的方法:
1@Test
2public void test5()
3 userServiceImpl.delete("zhangsan");
4
5//结果:
6// ===================
7// +++++++++++++++++++
8// 根据name删除用户成功, name = zhangsan
以上就是 Spring AOP 所有的指示符的使用方法了。
Spring AOP 原理
Spring AOP 的底层使用的使用 动态代理;共有两种方式来实现动态代理,一个是 JDK 的动态代理,一种是 CGLIB 的动态代理,下面使用这两种方式来实现以上面的功能,即在调用 UserServiceImpl 类方法的时候,在方法执行之前和之后加上日志。
JDK 动态代理
实现 JDK 动态代理,必须要实现 InvocationHandler
接口,并重写 invoke
方法:
1public class UserServiceInvocationHandler implements InvocationHandler
2
3 // 代理的目标对象
4 private Object target;
5
6 public UserServiceInvocationHandler(Object target)
7 this.target = target;
8
9
10 @Override
11 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
12
13 System.out.println("log: 目标方法执行之前, 参数 = " + args);
14
15 // 执行目标方法
16 Object retVal = method.invoke(target, args);
17
18 System.out.println("log: 目标方法执行之后.....");
19
20 return retVal;
21
22
测试:
1public static void main(String[] args) throws IOException
2
3 // 需要代理的对象
4 IUserService userService = new UserServiceImpl();
5 InvocationHandler handler = new UserServiceInvocationHandler(userService);
6 ClassLoader classLoader = userService.getClass().getClassLoader();
7 Class[] interfaces = userService.getClass().getInterfaces();
8
9 // 代理对象
10 IUserService proxyUserService = (IUserService) Proxy.newProxyInstance(classLoader, interfaces, handler);
11
12 System.out.println("动态代理的类型 = " + proxyUserService.getClass().getName());
13 proxyUserService.query("zhangsan");
14
15 // 把字节码写到文件
16 byte[] bytes = ProxyGenerator.generateProxyClass("$Proxy", new Class[]UserServiceImpl.class);
17 FileOutputStream fos =new FileOutputStream(new File("D:/$Proxy.class"));
18 fos.write(bytes);
19 fos.flush();
20
21
结果:
1动态代理的类型 = com.sun.proxy.$Proxy0
2log: 目标方法执行之前, 参数 = [Ljava.lang.Object;@2ff4acd0
3根据name查询用户成功
4log: 目标方法执行之后.....
可以看到在执行目标方法的前后已经打印了日志;刚在上面的 main 方法中,我们把代理对象的字节码写到了文件里,现在来分析下:
反编译 &Proxy.class 文件如下:
可以看到它通过实现接口来实现的。
JDK 只能代理那些实现了接口的类,如果一个类没有实现接口,则无法为这些类创建代理。此时可以使用 CGLIB 来进行代理。
CGLIB 动态代理
接下来看下 CGLIB 是如何实现的。
首先新建一个需要代理的类,它没有实现任何接口:
1public class UserServiceImplCglib
2 public User query(String name)
3 System.out.println("根据name查询用户成功, name = " + name);
4 User user = new User(name, 20, 1, 1000, "java");
5 return user;
6
7
现在需要使用 CGLIB 来实现在方法 query 执行的前后加上日志:
使用 CGLIB 来实现动态代理,也需要实现接口 MethodInterceptor
,重写 intercept
方法:
1public class CglibMethodInterceptor implements MethodInterceptor
2
3 @Override
4 public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable
5
6 System.out.println("log: 目标方法执行之前, 参数 = " + args);
7
8 Object retVal = methodProxy.invokeSuper(obj, args);
9
10 System.out.println("log: 目标方法执行之后, 返回值 = " + retVal);
11 return retVal;
12
13
测试:
1public static void main(String[] args)
2
3 // 把代理类写入到文件
4 System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\\\");
5
6 Enhancer enhancer = new Enhancer();
7 enhancer.setSuperclass(UserServiceImplCglib.class);
8 enhancer.setCallback(new CglibMethodInterceptor());
9
10 // 创建代理对象
11 UserServiceImplCglib userService = (UserServiceImplCglib) enhancer.create();
12 System.out.println("动态代理的类型 = " + userService.getClass().getName());
13
14 userService.query("zhangsan");
15
结果:
1动态代理的类型 = main.tsmyk.mybeans.impl.UserServiceImplCglib$$EnhancerByCGLIB$$772edd85
2log: 目标方法执行之前, 参数 = [Ljava.lang.Object;@77556fd
3根据name查询用户成功, name = zhangsan
4log: 目标方法执行之后, 返回值 = Username='zhangsan', age=20, sex=1, money=1000.0, job='java'
可以看到,结果和使用 JDK 动态代理的一样,此外,可以看到代理类的类型为 main.tsmyk.mybeans.impl.UserServiceImplCglib$$EnhancerByCGLIB$$772edd85
,它是 UserServiceImplCglib 的一个子类,即 CGLIB 是通过 继承的方式来实现的。
总结
-
JDK 的动态代理是通过反射和拦截器的机制来实现的,它会为代理的接口生成一个代理类。
-
CGLIB 的动态代理则是通过继承的方式来实现的,把代理类的class文件加载进来,通过修改其字节码生成子类的方式来处理。
-
JDK 动态代理只能对实现了接口的类生成代理,而不能针对类。
-
CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,但是因为采用的是继承, 所以 final 类或方法无法被代理。
-
Spring AOP 中,如果实现了接口,默认使用的是 JDK 代理,也可以强制使用 CGLIB 代理,如果要代理的类没有实现任何接口,则会使用 CGLIB 进行代理,Spring 会进行自动的切换。
上述实现 Spring AOP 的栗子采用的是 注解的方法来实现的,此外,还可以通过配置文件的方式来实现 AOP 的功能。以上就是 Spring AOP 的一个详细的使用过程。
以上是关于Spring AOP 功能使用详解的主要内容,如果未能解决你的问题,请参考以下文章