4. Spring之AOP

1. AOP基本概念

  1. 面向切面编程

​ 利用AOP可以对业务逻辑的各个部分进行隔离,从而使业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发效率。AOP主要意图为将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。

  1. 举例说明:

image-20201218135445960

有三个功能模块,现在要对三个功能模块都加入日志记录的功能,原有方法是在每一个模块的代码中加入相应的日志记录功能代码

通过AOP,则可以在不修改原有功能代码的基础上,加入日志记录的功能

image-20201218135652139

2. AOP底层原理

AOP底层使用动态代理

  1. 有两种情况动态代理

    (1)有接口情况,使用JDK动态代理

    ​ 创建接口实现类代理对象,增强类的方法

    image-20201218153709796

    (2)无接口情况,使用CGLIB动态代理

    ​ 创建子类代理对象,增强类的方法

    image-20201218155318714

3. JDK动态代理实现

使用JDK动态代理,需要使用Proxy类里面的方法创建代理对象

image-20201221101227577

image-20201221101624619

image-20201221101916383

  1. 调用Proxy类中的newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 方法

    ClassLoader loader:类加载器

    Class<?>[] interface:增强方法所在的类,这个类实现的接口,支持多个接口,只要是实现了指定接口的类的方法,都可以被增强

    InvocationHandler h:实现InvocationHandler接口,创建代理对象,编写增强的方法

  2. JDK动态代理代码

    (1)创建接口,定义方法

    public interface UserDao {
          
          
    
        int add(int a, int b);
    
        String update(String id);
    }
    

    (2)创建接口实现类,实现方法

    public class UserDaoImpl implements UserDao {
          
          
    
        @Override
        public int add(int a, int b) {
          
          
            System.out.println("执行了add方法");
            return a + b;
        }
    
        @Override
        public String update(String id) {
          
          
            System.out.println("执行了update方法");
            return id;
        }
    }
    

    (3)编写JDKProxy类,实现相应的增强逻辑

    public class JDKProxy implements InvocationHandler {
          
          
    
        // 1. 传递要被代理的对象 通过有参构造传递
        private Object object;
    
        public JDKProxy(Object object) {
          
          
            this.object = object;
        }
    
        // 增强的逻辑
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
          
          
            // 被增强的方法执行之前
            System.out.println(method.getName() + "方法执行前");
            System.out.println("方法执行前传递的参数" + Arrays.toString(args));
    
            // 执行原本对象的方法
            Object res = method.invoke(object, args);
    
            // 被增强的方法执行之后
            System.out.println(method.getName() + "方法执行后");
    
            return res;
        }
    }
    

    (4)使用Proxy类创建接口的代理对象

    public class Main {
          
          
    
        public static void main(String[] args) {
          
          
            // 创建要被代理的对象
            UserDao userDao = new UserDaoImpl();
            // 定义要被增强方法所属类实现的接口
            Class[] interfaces = {
          
          UserDao.class};
            // 创建代理对象
            JDKProxy jdkProxy = new JDKProxy(userDao);
            // 获取接口实现类的代理对象
            UserDao dao = (UserDao) Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, jdkProxy);
            dao.add(1, 2);
            System.out.println("###############");
            dao.update("aaa");
        }
    }
    

4. CGLib动态代理实现

利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理

  1. 需要引入cglib的依赖

    image-20201223155707483

    需要引入asm依赖,否则会抛出

    Exception in thread "main" java.lang.NoClassDefFoundError: org/objectweb/asm/Type异常

    还需要注意版本问题,否则会抛出

    class net.sf.cglib.core.DebuggingClassWriter has interface org.objectweb.asm.ClassVisitor as super class异常,经过测试,cglib 3.2.5和asm-6.0不会出现问题

    image-20201223164310453

  2. CGLib动态代理代码

    (1)创建一个类,不实现任何接口,类中定义一个普通方法,一个最终方法

    public class UserDaoImpl {
          
          
    
        public void update(String name) {
          
          
            System.out.println("调用了update方法" + name);
        }
    
        final public void check() {
          
          
            System.out.println("调用了check方法");
        }
    }
    
    

    (2)编写代理类,实现相应增强方法

    import net.sf.cglib.proxy.Enhancer;
    import net.sf.cglib.proxy.MethodInterceptor;
    import net.sf.cglib.proxy.MethodProxy;
    
    import java.lang.reflect.Method;
    import java.util.Arrays;
    
    public class CGLibProxy implements MethodInterceptor {
          
          
    
        // 1. 传递要被代理的对象 通过有参构造传递
        private Object object;
    
        public Object createCGLibProxyObject(Object object) {
          
          
            this.object = object;
            // 通过CGLIB动态代理获取代理对象的过程
            Enhancer enhancer = new Enhancer();
            // 设置enhancer对象的父类
            enhancer.setSuperclass(object.getClass());
            // 设置enhancer的回调对象
            enhancer.setCallback(this);
            // 返回创建的代理对象
            return enhancer.create();
        }
    
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
          
          
            // 被增强的方法执行之前
            System.out.println(method.getName() + "方法执行前");
            System.out.println("方法执行前传递的参数" + Arrays.toString(objects));
    
            // 执行原本对象的方法
            Object res = methodProxy.invoke(object, objects);
    
            System.out.println("方法执行后");
    
            return res;
        }
    }
    

    (3)通过代理对象执行相应方法

    public class Main {
          
          
    
        public static void main(String[] args) {
          
          
            // 创建要被代理的对象
            UserDaoImpl userDaoImpl = new UserDaoImpl();
            // 创建代理对象
            CGLibProxy proxy = new CGLibProxy();
            // 获取类的代理对象
            UserDaoImpl cgLibProxyObject = (UserDaoImpl) proxy.createCGLibProxyObject(userDaoImpl);
            // 通过代理对象调用目标方法
            cgLibProxyObject.update("张三");
            System.out.println("#############");
            // 由于check方法被final修饰,因此无法被cglib代理
            cgLibProxyObject.check();
        }
    }
    

5. AOP相关术语

  1. 连接点

    类里面哪些方法可以被增强,这些方法称为连接点

  2. 切入点

    实际被真正增强的方法,称为切入点

  3. 通知(增强)

    • 实际增强的逻辑部分称为通知(增强)
    • 通知有多种类型
      • 环绕通知
      • 前置通知
      • 方法返回通知
      • 异常通知
      • 后置通知(最终通知)
  4. 切面

    把通知应用到切入点的过程称为切面

6. AOP操作简单说明

Spring框架一般基于AspectJ实现AOP操作

  1. 什么是AspectJ

    AspectJ不是Spring组成部分,是一个独立的AOP框架,一般把AspectJ和Spring框架一起使用,进行AOP操作

  2. 基于AspectJ实现AOP操作

    (1)基于xml配置文件进行实现

    (2)基于注解方式进行实现

  3. 在项目中引入AOP相关依赖

    在已有依赖基础上引入以下相关依赖包

    image-20201223170709219

  4. 切入点表达式

    (1)切入点表达式的作用:知道对哪个类里面的哪个方法进行增强

    (2)语法结构:

    ​ execution( [权限修饰符] [返回值类型] [类的全路径] [方法名称] ([参数列表]) )

    ​ 举例1:对com.study.spring5.demo15.dao.UserDao类的add方法进行增强

    ​ execution(* com.study.spring5.demo15.dao.UserDao.add(…))

    ​ * 表示所有权限

    ​ … 表示参数

    ​ 举例2:对com.study.spring5.demo15.dao.UserDao类的所有方法进行增强

    ​ execution(* com.study.spring5.demo15.dao.UserDao.*(…))

    ​ 举例3:对com.study.spring5.demo15.dao包中的所有类,类的所有方法进行增强

    ​ execution(* com.study.spring5.demo15.dao.*.*(…))

7. 基于xml配置文件方式的AspectJ操作

日常使用注解方式,xml配置文件了解即可

  1. 创建两个类,增强类与被增强类,编写相应方法

    public class Car {
          
          
    
        public void buy() {
          
          
            System.out.println("buy()");
        }
    }
    
    public class CarProxy {
          
          
    
        public void before() {
          
          
            System.out.println("before()");
        }
    }
    
  2. 在spring配置文件中创建两个类对象

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                               http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                               http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    
        <!-- 创建car对象 -->
        <bean id="car" class="com.study.spring5.demo17.Car"></bean>
    	<!-- 创建carProxy对象 -->
        <bean id="carProxy" class="com.study.spring5.demo17.CarProxy"></bean>
    
    </beans>
    
  3. 在spring配置文件中配置AOP增强

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                               http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                               http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    
        <!-- 创建car对象 -->
        <bean id="car" class="com.study.spring5.demo17.Car"></bean>
        <!-- 创建carProxy对象 -->
        <bean id="carProxy" class="com.study.spring5.demo17.CarProxy"></bean>
    
        <!-- 配置AOP增强 -->
        <aop:config>
            <!-- 切入点 -->
            <aop:pointcut id="p" expression="execution(* com.study.spring5.demo17.Car.buy(..))"/>
    
            <!-- 配置切面 -->
            <aop:aspect ref="carProxy">
                <!-- 配置增强作用的具体方法 把before作用到bug方法上  -->
                <!-- 即把通知应用到切入点的过程称为切面 -->
                <aop:before method="before" pointcut-ref="p"/>
            </aop:aspect>
        </aop:config>
    
    </beans>
    
  4. 编写主方法

    public class Main {
          
          
    
        public static void main(String[] args) {
          
          
            ApplicationContext applicationContext =
                    new ClassPathXmlApplicationContext("bean17.xml");
            Car car = applicationContext.getBean("car", Car.class);
            car.buy();
        }
    }
    

8. 基于注解方式的AspectJ操作

  1. 各类通知实现过程

    (1)在spring配置文件中,开启注解扫描及开启生成代理对象

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xmlns:aop="http://www.springframework.org/schema/aop"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                               http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                               http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
    
        <!-- 开启注解扫描 -->
        <context:component-scan base-package="com.study.spring5.demo16"/>
    
        <!-- 开启Aspect生成代理对象 -->
        <aop:aspectj-autoproxy/>
    
    </beans>
    

    (2)使用注解创建User对象,在类中定义方法

    import org.springframework.stereotype.Component;
    
    @Component
    public class User {
          
          
    
        public void add() {
          
          
            System.out.println("调用了User::add方法");
    //        System.out.println(1 / 0);
        }
    }
    

    (3)使用注解创建增强类UserProxy,在作为通知方法上面添加通知类型的注解,并使用切入点表达式进行配置

    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    
    
    @Component
    // 生成代理对象
    @Aspect
    public class UserProxy {
          
          
    
        // @Before注解表示该方法作为前置通知
        @Before(value = "execution(* com.study.spring5.demo16.User.add(..))")
        public void before() {
          
          
            System.out.println("before");
        }
    
        // @After注解表示该方法作为后置通知(最终通知)
        // 在方法之后执行
        @After(value = "execution(* com.study.spring5.demo16.User.add(..))")
        public void after() {
          
          
            System.out.println("after");
        }
    
        // @AfterReturning注解表示该方法返回通知
        // 在方法返回值之后执行
        @AfterReturning(value = "execution(* com.study.spring5.demo16.User.add(..))")
        public void afterReturning() {
          
          
            System.out.println("after returning ");
        }
    
        // @Around注解表示该方法作为环绕通知
        @Around(value = "execution(* com.study.spring5.demo16.User.add(..))")
        public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
          
          
            System.out.println("around之前");
            // 执行被增强的方法
            proceedingJoinPoint.proceed();
            System.out.println("around之后");
        }
    
        // @AfterReturning注解表示该方法作为异常通知
        // 在方法抛出异常之后执行
        @AfterThrowing(value = "execution(* com.study.spring5.demo16.User.add(..))")
        public void afterThrowing() {
          
          
            System.out.println("after throwing ");
        }
    
    }
    

    (4)主方法

    public class Main {
          
          
    
     public static void main(String[] args) {
          
          
            ApplicationContext applicationContext =
                 new ClassPathXmlApplicationContext("bean16.xml");
            User user = applicationContext.getBean("user", User.class);
            user.add();
        }
    }
    

    不触发异常时候的通知结果

    image-20201224102033826

    触发异常时候的通知结果

    image-20201224102100216

    由此可以总结,当出现异常时,方法返回通知和环绕结束通知都不会执行,后置(最终)通知任何情况下都会被执行

    try {
          
          
        // 执行前置通知;
        // 执行目标方法;
        // 执行返回通知;
    } catch (Exception e) {
          
          
        // 执行异常通知;
    } finally {
          
          
        // 执行后置通知;
    }
    
  2. 对相同切入点进行抽取(重用切入点)

    可以使用@Pointcut注解对切入点进行抽取

    修改UserProxy类中代码,加入

    // 相同切入点抽取
    // addPoint()方法等价于execution(* com.study.spring5.demo16.User.add(..))
    @Pointcut(value = "execution(* com.study.spring5.demo16.User.add(..))")
    public void addPoint() {
          
          
    
    }
    

    而后修改原本before方法的注解

    // @Before注解表示该方法作为前置通知
    // @Before(value = "execution(* com.study.spring5.demo16.User.add(..))")
    @Before(value = "addPoint()")
    public void before() {
          
          
    	System.out.println("before");
    }
    

    由此就可以实现对相同切入点的抽取工作,方便后续维护工作

  3. 有多个增强类对同一个方法进行增强,可以设置增强类优先级

    创建另一个代理对象类

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.stereotype.Component;
    
    @Component
    @Aspect
    public class UserLogProxy {
          
          
    
        @Before(value = "execution(* com.study.spring5.demo16.User.add(..))")
        public void before() {
          
          
            System.out.println("UserLogProxy::before");
        }
    }
    

    如果想要调整增强类的顺序,可以在使用@AspectJ注解的类上使用@Order(数字类型)注解,数字越小优先级越高

    修改UserLogProxy类代码,在类上添加@Order()注解

    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;
    
    @Component
    @Aspect
    @Order(value = 2)
    public class UserLogProxy {
          
           ... }
    

    修改UserProxy的代码,在类上添加@Order()注解

    @Component
    // 生成代理对象
    @Aspect
    @Order(1)
    public class UserProxy {
          
           ... }
    

    通过修改@Order()的值,自行测试结果即可

9. 基于完全注解方式的AspectJ操作

  1. User类

    import org.springframework.stereotype.Component;
    
    @Component
    public class User {
          
          
    
        public void add() {
          
          
            System.out.println("调用了User::add方法");
        }
    }
    
  2. UserProxy类

    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    
    @Component
    @Aspect
    public class UserProxy {
          
          
    
        // @Before注解表示该方法作为前置通知
        @Before(value = "addPoint()")
        public void before() {
          
          
            System.out.println("before");
        }
    
        // 相同切入点抽取
        @Pointcut(value = "execution(* com.study.spring5.demo18.User.add(..))")
        public void addPoint() {
          
          
    
        }
    }
    
  3. 配置类(替换掉第8章节中的xml配置文件)

    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    
    @Configuration
    @ComponentScan(basePackages = {
          
          "com.study.spring5.demo18"})
    // 相当于在xml配置文件中使用 <aop:aspectj-autoproxy/> 开启Aspect生成代理对象
    @EnableAspectJAutoProxy
    public class ConfigAop {
          
          
    }
    
  4. 主方法

    public class Main {
          
          
    
        public static void main(String[] args) {
          
          
            ApplicationContext applicationContext =
                    new AnnotationConfigApplicationContext(ConfigAop.class);
            User user = applicationContext.getBean("user", User.class);
            user.add();
        }
    }
    
  5. 其他说明

    在声明@EnableAspectJAutoProxy注解时,可以指定参数proxyTargetClass = true,

    如 @EnableAspectJAutoProxy(proxyTargetClass = true)

    相当于xml配置文件的

    <aop:aspectj-autoproxy proxy-target-class=“true”/>

    这个值默认为false,proxy-target-class属性值决定是基于接口的还是基于类的代理被创建。如果proxy-target-class 属性值被设置为true,那么基于类的代理将起作用(这时需要cglib库)。如果proxy-target-class属值被设置为false或者这个属性被省略,那么标准的JDK 基于接口的代理将起作用
    需要注意的是,即使你未声明 proxy-target-class=“true” ,但运行类没有继承接口,spring也会自动使用CGLIB代理。高版本spring自动根据运行类选择 JDK 或 CGLIB 代理

猜你喜欢

转载自blog.csdn.net/guo_ridgepole/article/details/111871815
今日推荐