前言

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 {34    @Override5    public void add(User user) {6        System.out.println("添加用户成功,user=" + user);7    }89    @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@Component2@Aspect3public class UserAspectj {45    // 在方法执行之前执行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 {45    @Autowired6    private IUserService userServiceImpl;78    @Test9    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=User{name='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 方法之前执行, 方法参数 = User{name='zhangsan', age=20, sex=1, money=1000.0, job='java'}
2添加用户成功,user=User{name='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=User{name='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 方法返回后执行, 返回值= User{name='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 方法返回后执行, 返回值= User{name='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// 声明 pointcut2@Pointcut("execution(* main.tsmyk.mybeans.inf.IUserService.query(..))")3public void pointcut(){4}56@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}45@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(){45    }67    // 匹配第一个参数为 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    }67    // 匹配 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 {23    // 代理的目标对象4    private Object target;56    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 {23    // 需要代理的对象4    IUserService userService = new UserServiceImpl();5    InvocationHandler handler = new UserServiceInvocationHandler(userService);6    ClassLoader classLoader = userService.getClass().getClassLoader();7    Class[] interfaces = userService.getClass().getInterfaces();89    // 代理对象
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 {23    @Override4    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {56        System.out.println("log: 目标方法执行之前, 参数 = " + args);78        Object retVal = methodProxy.invokeSuper(obj, args);9
10        System.out.println("log: 目标方法执行之后, 返回值 = " + retVal);
11        return retVal;
12    }
13}

测试:

 1public static void main(String[] args) {23    // 把代理类写入到文件4    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\");56    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: 目标方法执行之后, 返回值 = User{name='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 功能使用详解相关推荐

  1. 跟着小马哥学系列之 Spring AOP(Advisor 详解)

    学好路更宽,钱多少加班. --小马哥 简介 大家好,我是小马哥成千上万粉丝中的一员!2019年8月有幸在叩丁狼教育举办的猿圈活动中知道有这么一位大咖,从此结下了不解之缘!此系列在多次学习极客时间< ...

  2. Spring AOP实现原理详解之Cglib代理实现

    引入 我们在前文中已经介绍了SpringAOP的切面实现和创建动态代理的过程,那么动态代理是如何工作的呢?本文主要介绍Cglib动态代理的案例和SpringAOP实现的原理. 要了解动态代理是如何工作 ...

  3. Spring AOP切点表达式详解

    1. 简介 面向对象编程,也称为OOP(即Object Oriented Programming)最大的优点在于能够将业务模块进行封装,从而达到功能复用的目的.通过面向对象编程,不同的模板可以相互组装 ...

  4. 跟着小马哥学系列之 Spring AOP(AbstractAutoProxyCreator 详解)

    学成路更宽,吊打面试官. --小马哥 版本修订 2021.5.19:去除目录 简介 大家好,我是小马哥成千上万粉丝中的一员!2019年8月有幸在叩丁狼教育举办的猿圈活动中知道有这么一位大咖,从此结下了 ...

  5. Spring AOP 与代理详解

    SpringBoot 系列教程 - 源码地址:https://github.com/laolunsi/spring-boot-examples 大家知道我现在还是一个 CRUD 崽,平时用 AOP 也 ...

  6. Spring AOP之PointCut详解

    一.PointCut接口 /** Copyright 2002-2012 the original author or authors.** Licensed under the Apache Lic ...

  7. Spring AOP 的proxy详解

    spring 提供了多种不同的方案实现对 bean 的 aop proxy, 包括 ProxyFactoryBean, 便利的 TransactionProxyFactoryBean 以及 AutoP ...

  8. Spring之Joinpoint类详解

    说明 Joinpoint是AOP的连接点.一个连接点代表一个被代理的方法.我们从源码角度看连接点有哪些属性和功能. 源码 /** Copyright 2002-2016 the original au ...

  9. 【Spring AOP】静态代理设计模式、Spring 动态代理开发详解、切入点详解(切入点表达式、切入点函数)

    AOP 编程 静态代理设计模式 1. 为什么需要代理设计模式 2. 代理设计模式 名词解释 代理开发的核心要素 静态代理编码 静态代理存在的问题 Spring 动态代理开发 搭建开发环境 Spring ...

最新文章

  1. Kali Linux安全渗透教程1.1Linux安全渗透简介
  2. 皮一皮:直男表示,有钱女人的世界完全不懂...
  3. 计算机的磁盘地址格式,如何查看电脑硬盘是GPT分区还是MBR分区
  4. openjdk-alpine镜像无法打印线程堆栈和内存堆栈问题
  5. java utf 8 转unicode_java 在Unicode和UTF-8之间转换
  6. shell的if和else
  7. java第一句_Java如何开始第一个项目?
  8. C# 使用Quartz简单实例以及备忘
  9. 保护地球生命力,从寻找“百兽之王”开始
  10. y700支持m2硬盘_两块硬盘一起读写?奥睿科M.2 NVMe双盘位固态硬盘盒使用
  11. bzoj 4293: [PA2015]Siano(线段树)
  12. 【Python】Tanimoto相似度算法实现
  13. jenkins 编译java_Jenkins 通过 maven 构建编译 JAVA 项目环境
  14. 【CarMaker学习笔记】CarMaker Python API 接口使用方法
  15. C++写的12306抢票软件
  16. 手机便签软件哪个好用?哪种手机便签软件好使用
  17. OpenCV 文字检测与识别模块
  18. PCA为什么要进行中心化
  19. 用一条SQL 语句 查询出每门课都大于80 分的学生姓名
  20. 利用先序遍历输入法建立二叉树

热门文章

  1. .net runtime占用cpu_追踪将服务器CPU耗光的凶手!
  2. nslookup查询结果详解
  3. 使用控制结构——循环语句——基本循环
  4. 高性能消息中间件——NATS
  5. 如何在 Java 中正确使用 wait, notify 和 notifyAll?
  6. Python天天美味(21) - httplib,smtplib
  7. 【转】WCF与Web API 区别(应用场景)
  8. WCF中几个容易忽略的知识点
  9. winserver 服务开机启动
  10. 21种代码的“坏味道”