Spring AOP 功能使用详解

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> 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 应用到目标对象中去
  • pointcut :切点,就是与连接点匹配的谓词,什么意思呢,就是需要执行 Advice 的连接点就是切点

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

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

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

    栗子

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

  • 要想使用 Spring  AOP ,首先先得在 Spring 配置文件中配置如下标签:
  • 
    1aop: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    ListUser 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 ListUser qyertAll() {
    18        ListUser 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=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// 声明 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 文件如下:

    Spring AOP 功能使用详解

    可以看到它通过实现接口来实现的。

    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: 目标方法执行之后, 返回值 = User{name='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 会进行自动的切换。
  • CGLIB 的动态代理则是通过继承的方式来实现的,把代理类的class文件加载进来,通过修改其字节码生成子类的方式来处理。

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

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

    原文始发于微信公众号(Java技术大杂烩):

    本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> Spring AOP 功能使用详解


     上一篇
    Spring 事务提交回滚源码解析 Spring 事务提交回滚源码解析
    前言在上篇文章   中分析了 Spring 事务初始化的一个过程,当初始化完成后,Spring 是如何去获取事务,当目标方法异常后,又是如何进行回滚的,又或是目标方法执行成功后,又是怎么提交的呢?此外,事务的提交和回滚由底层数据库进行控制,
    2021-04-05
    下一篇 
    Spring AOP 注解方式源码解析 Spring AOP 注解方式源码解析
    前言在上篇文章  中,知道了 Sprig AOP 的一个详细用法,现在的看看Spring 中是如何实现的。 aspectj-autoproxy我们知道,要想使用 Spring AOP ,必须首先在配置文件中添加该配置项 aop:aspec
    2021-04-05