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

  1. Join point :连接点,表示程序执行期间的一个点,在 Spring AOP 表示的就是一个方法,即一个方法可以看作是一个 Join point

  2. pointcut :切点,就是与连接点匹配的谓词,什么意思呢,就是需要执行 Advice 的连接点就是切点

  3. Advice :增强,在连接点执行的操作,分为前置、后置、异常、最终、环绕增强五种

  4. Aspect :切面,由 pointcut 和 Advice 组成,可以简单的认为 @Aspect 注解的类就是一个切面

  5. Target object :目标对象,即 织入 advice 的目标对象

  6. AOP proxy :代理类,在 Spring AOP 中, 一个 AOP 代理是一个 JDK 动态代理对象或 CGLIB 代理对象

  7. `Weaving` :织入,将 Aspect 应用到目标对象中去

注:上述几个概念中,比较容易混淆的是 Join point   和  pointcut,可以这么来理解,在 Spring AOP 中,所有的可执行方法都是 Join point,所有的 Join point 都可以植入 Advice;而 pointcut 可以看作是一种描述信息,它修饰的是 Join point,用来确认在哪些 Join point 上执行 Advice,

栗子

在了解了 AOP 的概念之后,接下来就来看看如何使用  Spring Aop

  1. 要想使用 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 即可。

  1. 定义需要 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

.

  1. 定义 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 pointwithin(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 是通过 继承的方式来实现的。

总结

  1. JDK 的动态代理是通过反射和拦截器的机制来实现的,它会为代理的接口生成一个代理类。

  2. CGLIB 的动态代理则是通过继承的方式来实现的,把代理类的class文件加载进来,通过修改其字节码生成子类的方式来处理。

  3. JDK 动态代理只能对实现了接口的类生成代理,而不能针对类。

  4. CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,但是因为采用的是继承, 所以 final 类或方法无法被代理。

  5. Spring AOP 中,如果实现了接口,默认使用的是 JDK 代理,也可以强制使用 CGLIB 代理,如果要代理的类没有实现任何接口,则会使用 CGLIB 进行代理,Spring 会进行自动的切换。

上述实现 Spring AOP 的栗子采用的是 注解的方法来实现的,此外,还可以通过配置文件的方式来实现 AOP 的功能。以上就是 Spring AOP 的一个详细的使用过程。

以上是关于Spring AOP 功能使用详解的主要内容,如果未能解决你的问题,请参考以下文章

#yyds干货盘点# Spring AOP详解

Spring AOP 功能使用详解

Spring AOP 功能使用详解

Spring AOP 功能使用详解

Spring AOP 功能使用详解

Spring boot中使用aop详解