美文网首页
九 Spring-AOP切面编程(1)

九 Spring-AOP切面编程(1)

作者: 唯老 | 来源:发表于2019-02-17 18:09 被阅读3次

    目录

    1. Web MVC发展史历程
    2.Spring概要
    3.Spring-依赖注入概要(IOC)
    4.属性注入的三种实现方式
    5.Spring-IoC XML装配
    6.Spring-XML设置Bean的值
    7.Spring-IoC 注解(1)
    8.Spring-IoC 注解(2)
    9.Spring-AOP切面编程(1)
    10.Spring-AOP切面编程(2)
    未完待续...

    一、概要

    软件开发一直在寻求一种高效、护展、维护的方式。

    面向对象的特点是继承、多态和封装。而封装的核心就是将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类的复用性增加。但是新的问题又来了,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。

    也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程,我们把切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。

    参考资料

    二、面向切面编程

    1、什么切面编程

    面向切面编程(也叫面向方面):Aspect Oriented Programming(AOP)。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。AOP是OOP的补充和完善

    2、主要解决的问题

    1. 解决代码复用的问题

    2. 解决关注点分离

    • 水平分离: 例如 控制层—服务层—数据库

    • 垂直分离: 例如 用户模块—商品模块

    • 关注点分离(功能): 业务和非业务

    3、主要的应用场景

    1. 日志记录
    2. 性能统计
    3. 安全控制
    4. 事务处理
    5. 异常处理
    6. 权限处理等等

    三、实例讲解

    1、原始代码

    1. 需求
      比如我们要写一个UserService,在调用保存用户和删除用户的时候需要去打开数据库和开启事务
    2. 示例代码


      image
    3. 说明
      通过上面的代码我们发现打开数据,开启事务,提交事务,关闭数据库是重复的代码,有重复的代码我们怎么办?
      通过类封装成方法提取重复的代码啊!(抽取成类的方式我们称之为:纵向抽取)

    2、提取类封装成方法

    1. 说明
      这个时候我们新建一个DBManager类 定义四个方法
    2. 示例代码
      public class DBManager {
          public void open() {
              System.out.println("打开数据库...");
          }
          public void colse() {
              System.out.println("关闭数据库...");
          }
          public void begin() {
              System.out.println("开启事务");
          }
          public void commit() {
              System.out.println("提交事务");
          }
      }
      
      public class UserService {
          private DBManager manager = new DBManager();
          public void save() {
              manager.open();
              manager.begin();
              System.out.println("保存用户信息");
              manager.commit();
              manager.colse();
          }
          public void delete() {
              manager.open();
              manager.begin();
              System.out.println("删除用户");
              manager.commit();
              manager.colse();
          }
      }
      
    3. 说明
      通过上面的案例解决了代码重复性的问题,但同时也会带来另外两个问题:
      • 耦合度:会造成封装类和业务类的耦合,
      • 侵入性强:被我们提取的逻辑代码还是融合到业务逻辑中
        但是,这种做法相比最原始的代码写法,已经有了一些的改进,那么有没有一种方案能解决耦合度,侵入性强的问题,
        答案就是代理模式

    四、代理模式

    1、代理模式概要

    代理模式是一种非常好理解的一种设计模式,举几个简单的代理的例子

    • 比如玩游戏升级太麻烦了,这个是时候我们可以去请代练帮我们升级,那这个代练其实就是一个代理
    • 比如我们回家过年买不到火车票,通常加价请第三方的一些黄牛帮我们去买
    • 歌星或者明星都有一个自己的经纪人,这个经纪人就是他们的代理人,当我们需要找明星表演时,不能直接找到该明星,只能是找明星的经纪人

    让代练帮我们升级,我们可能就想下副本,让黄牛帮我们买票,我们就的目的就是想回家,明星让经纪人接拍电影,明星只是需要去拍电影就行了。无论是游戏代练,黄牛,还是经纪人其它他们其实都是在帮我们在做事(做一些我们不想做,或者做不来的事情),但并不能把所有的事情都帮我们做了,比如黄牛帮我们买点票了,回家你还的自己回吧,

    2、分类

    1. 静态
      由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。
    2. 动态
      在程序运行时运用反射机制动态创建而成。

    3、作用

    代理解决的问题当两个类需要通信时,引入第三方代理类,将两个类的关系解耦,让我们只了解代理类即可,而且代理的出现还可以让我们完成与另一个类之间的关系的统一管理,但必须,代理类和委托类要实现相同的接口,因为代理真正调用的还是委托类的方法

    五、静态代理

    1、说明

    2、示例代码

    1. 业务代码
      /**
       * 简单业务层接口
       */
      public interface UserService{
          public void saveUser();
      }
      /**
       * 业务层实现类,实现save方法
       */
      public class UserServiceImpl implements UserService{
          @Override
          public void saveUser() {
              System.out.println("2:保存用户信息");
          }
      }
      
    2. 代理类
      /**
       * 代理类
       */
      public class UserServiceProxy implements UserService{
          private UserService userService;
          public UserServiceProxy(UserService userService) {
              super();
              this.userService = userService;
          }
          public void open(){
              System.out.println("1:打开数据库连接");
          }
          public void close(){
              System.out.println("3:关闭数据库连接");
          }
          @Override
          public void saveUser() {
              this.open();
              userService.saveUser();
              this.close();
          }
      }
      
    3. 测试代码
      /**
       * 测试类
       */
      public class TestProxy {
          public static void main(String[] args) {
              UserService userService =new UserServiceProxy(new UserServiceImpl());
              userService.saveUser();
          }
      }
      

    3、分析

    1、优点

    1. 代理使客户端不需要知道实现类是什么,怎么做的,而客户端只需知道代理即可(解耦合)

    2、缺点

    1. 代理类和委托类实现了相同的接口,代理类通过委托类实现了相同的方法。这样就出现了大量的代码重复。如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度
    2. 代理对象只服务于一种类型的对象,如果要服务多类型的对象。势必要为每一种对象都进行代理,静态代理在程序规模稍大时就无法胜任了
      由于每个代理类只能为一个接口服务,这样程序开发中必然会产生许多的代理类
      所以我们就会想能不能通过一个代理类完成全部的代理功能?,那么我们就需要用动态代理

    六、JDK动态代理

    1、概念

    JDK动态代理所用到的代理类在程序调用到代理类对象时才由JVM真正创建,JVM根据传进来的业务实现类对象 以及 方法名 ,动态地创建了一个代理类并执行,然后通过该代理类对象进行方法调用。我们需要做的,只需指定代理类的预处理、调用后操作即可

    目前Java开发包中包含了对动态代理的支持,但是其实现只支持对接口的的实现。 其实现主要通过java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。

    • Proxy类主要用来获取动态代理对象,

    • InvocationHandler接口用来约束调用者实现

    2、重要类介绍

    2.1、Proxy

    1. 说明
      这是 Java 动态代理机制的主类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象
    2. 核心方法
      返回值 方法 说明
      static InvocationHandler getInvocationHandler(Object proxy) 方法返回指定接口的代理类的实例,这些接口将调用方法调用到指定的调用处理程序。
      static Class getProxyClass(ClassLoader loader, Class[] interfaces) 用于获取关联于指定类装载器和一组接口的动态代理类的类对象
      static boolean isProxyClass(Class cls) 该方法用于判断指定类对象是否是一个动态代理类
      staticObject newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) 用于为指定类装载器、一组接口及调用处理器生成动态代理类实例

    2.2、InvocationHandler

    1. 说明
      这是调用处理器接口,它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问
    2. 语法
      // 该方法负责集中处理动态代理类上的所有方法调用。,第二个参数是被调用的方法对象
      // 第三个方法是调用参数。调用处理器根据这三个参数进行预处理或分派到委托类实例上发射执行
      Object invoke(Object proxy, Method method, Object[] args)
      
    3. 参数说明
      • proxy
        第一个参数既是代理类实例
      • method
        被调用的方法对象
      • args
        调用参数。调用处理器根据这三个参数进行预处理或分派到委托类实例上发射执行

    3、示例代码

    3.1、声明代理类

    1. 说明
      动态代理类只能代理接口(不支持抽象类),代理类都需要实现InvocationHandler类,实现invoke方法。该invoke方法就是调用被代理接口的所有方法时需要调用的,该invoke方法返回的值是被代理接口的一个实现类
    2. 示例代码
      // 第一步  定义代理类,实现InvocationHandler接口
       public class DynamicProxy implements InvocationHandler {
          //代理目标对象
          private Object target;
          public Object newProxyInstance(Object object) {
           this.target = object;
          return Proxy.newProxyInstance(object.getClass().getClassLoader()
                  , object.getClass().getInterfaces(),
                  this);
       }
          /**
           * 关联的这个实现类的方法被调用时将被执行
           * @param proxy  定义代理类的类的实例
           * @param method 代理类要实现的接口列表
           * @param args   指派方法调用的调用处理程序
           * @return 返回执行的目标对象
           * @throws Throwable
           */
          @Override
          public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
              System.out.println("====start====");
              /**
               * 打印方法的参数
               */
              if (args != null && 0 != args.length) {
                  for (Object arg : args) {
                      System.out.println(arg);
                  }
              }
              System.out.println("====方法被执行前====");
              Object invoke = null;
              try {
                  String name = method.getName();
                  if (name.equals("add") || name.equals("delete")) {
                      // 核心方法被执行
                      invoke = method.invoke(target, args);
                      System.out.println("====方法被执行后====");
                  }
              } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                  System.out.println("====方法出错误了====");
              }
              System.out.println("====end====");
              return invoke;
          }
       }
      

    3.2 声明代理接口与代理类

    1. 说明
      jdk动态代理只能代理接口,所以第一步先声明接口,然后在定义个实现类
    2. 示例代码
       //代理接口类
       public interface UserDao {
         void add(String name);
         void delete(String uid);
       }
      public interface ShopDao {
          boolean addShop();
      }
      
       //代理接口的实现类
       public class UserDaoImpl implements UserDao {
          @Override
          public void add(String name) {
              System.out.println("add:===" + name);
          }
          @Override
          public void delete(String uid) {
              System.out.println("delete" + uid);
       }
      public class ShopDaoImpl implements ShopDao {
          @Override
          public boolean addShop() {
              System.out.println("核心方法====>添加商品信息");
              return false;
          }
      }     
      
    3. 测试代码
      public class TestProxy {
          public static void main(String[] args) {
              DynamicProxy proxy = new DynamicProxy();
              UserDao userDao = (UserDao) proxy.newProxyInstance(new UserDaoImpl());
              userDao.add();
              userDao.delete();
              ShopDao shopDao = (ShopDao) proxy.newProxyInstance(new ShopDaoImpl());
              shopDao.addShop();
          }
      }
      

    3.3、总结

    可以看到,我们可以通过DynamicProxy代理不同类型的对象,如果我们把对外的接口都通过动态代理来实现,那么所有的函数调用最终都会经过invoke函数的转发,因此我们就可以在这里做一些自己想做的操作

    七、CGLib动态代理

    1、说明

    JDK实现动态代理需要实现类通过接口定义业务方法,对于没有接口的类,如何实现动态代理呢,这就需要CGLib了。CGLib采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。JDK动态代理与CGLib动态代理均是实现Spring AOP的基础。

    因为是第三方的所有需要导入第三方的jar包

    <!-- https://mvnrepository.com/artifact/cglib/cglib -->
    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.2.10</version>
    </dependency> 
    

    2、示例代码

    1. 定义代理类
      //代理对象
      public class  AccountDaoImpl{  
          public void add(String name) {
                 System.out.println("add:===" + name);
             }
      }  
      
    2. 定义动态代理类代理类
      import net.sf.cglib.proxy.Enhancer;
      import net.sf.cglib.proxy.MethodInterceptor;
      import net.sf.cglib.proxy.MethodProxy;
      import java.lang.reflect.Method;
      /**
       * 实现了一个方法拦截器接口
       */
      public class CglibProxy implements MethodInterceptor {
          // 实例化动态代码生成器
          private Enhancer enhancer = new Enhancer();
          /**
           * 动态生成一个新的类,使用父类的无参构造方法创建一个指定了特定回调的代理实例
           *
           * @param clazz 被代理对象
           * @return
           */
          public Object getProxy(Class clazz) {
              //设置需要创建子类的类
              enhancer.setSuperclass(clazz);
              //设置回调方法
              enhancer.setCallback(this);
              //通过字节码技术动态创建子类实例
              return enhancer.create();
          }
          /**
           * 实现MethodInterceptor接口方法
           * 拦截被代理对象的方法
           *
           * @param obj    代理类实例
           * @param method 被代理类所调用的被代理的方法
           * @param args   参数值列表
           * @param proxy  生成的代理类对方法的代理引用
           * @return
           * @throws Throwable
           */
          @Override
          public Object intercept(Object obj, Method method, Object[] args,
                                  MethodProxy proxy) throws Throwable {
              System.out.println(obj.getClass().getName());
              System.out.println("前置代理");
              //通过代理类调用父类中的方法
              Object result = proxy.invokeSuper(obj, args);
              System.out.println("后置代理");
              System.out.println(proxy.getClass().getName());
              return result;
          }
      }
      
    3. 测试
      public class TestCglib {
          public static void main(String[] args) {
              CglibProxy proxy = new CglibProxy();
              //通过生成子类的方式创建代理类
              UserDaoImpl dao = (UserDaoImpl) proxy.getProxy(UserDaoImpl.class);
              dao.add();
          }
      }
      

    八、总结

    1、动态代理和静态代理相比较,

    最大的好处就是接口中声明的所有的方法都被转移到一个集中的方法中去处理,就是invocke()方法.这样在接口中声明的方法比较多的情况下我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。

    2、CGLib与JDK动态代理的区别

    1. 原理
      • java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
      • 而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理
    2. JDK动态代理和CGLIB字节码生成的区别
      • JDK动态代理只能对实现了接口的类生成代理,而不能针对类
      • CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,因为是继承,所以该类或方法最好不要声明成final
    3. 速度上
      • 在jdk6以后sun公司对JDK动态代理进行优化,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率
      • 只有当进行大量调用的时候,jdk6和jdk7比CGLIB代理效率低一点,但是到jdk8的时候,jdk代理效率高于CGLIB代理

    3、实现原理

    1. 通过实现 InvocationHandler 接口创建自己的调用处理器;
    2. 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;
    3. 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
    4. 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
      // InvocationHandlerImpl 实现了 InvocationHandler 接口,并能实现方法调用从代理类到委托类的分派转发
      // 其内部通常包含指向委托类实例的引用,用于真正执行分派转发过来的方法调用
      InvocationHandler handler = new InvocationHandlerImpl(..); 
      // 通过 Proxy 为包括 Interface 接口在内的一组接口动态创建代理类的类对象
      Class clazz = Proxy.getProxyClass(classLoader, new Class[] { Interface.class, ... }); 
      // 通过反射从生成的类对象获得构造函数对象
      Constructor constructor = clazz.getConstructor(new Class[] { InvocationHandler.class }); 
      // 通过构造函数对象创建动态代理类实例
      Interface Proxy = (Interface)constructor.newInstance(new Object[] { handler });
      

    4、注意事项

    1. 包:如果所代理的接口都是 public 的,那么它将被定义在顶层包(即包路径为空),如果所代理的接口中有非 public 的接口(因为接口不能被定义为 protect 或 private,所以除 public 之外就是默认的 package 访问级别),那么它将被定义在该接口所在包(假设代理了 com.xxx.xxx 包中的某非 public 接口 A,那么新生成的代理类所在的包就是 com.xxx.xxx),这样设计的目的是为了最大程度的保证动态代理类不会因为包管理的问题而无法被成功定义并访问;
    2. 类修饰符:该代理类具有 final 和 public 修饰符,意味着它可以被所有的类访问,但是不能被再度继承;
    3. 类名:格式是“proxyN”,其中 N 是一个逐一递增的阿拉伯数字,代表 Proxy 类第 N 次生成的动态代理类,值得注意的一点是,并不是每次调用 Proxy 的静态方法创建动态代理类都会使得 N 值增加,原因是如果对同一组接口(包括接口排列的顺序相同)试图重复创建动态代理类,它会很聪明地返回先前已经创建好的代理类的类对象,而不会再尝试去创建一个全新的代理类,这样可以节省不必要的代码重复生成,提高了代理类的创建效率。

    三、实现原理

    实现AOP的技术,主要分为两大类:

    一是采用动态代理技术,代理能干嘛?代理可以帮我们增强对象的行为!使用动态代理实质上就是调用时拦截对象方法,对方法进行改造、增强!用通俗的话来讲就是在核心方法执行前后,执行特定的代码

    二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。

    相关文章

      网友评论

          本文标题:九 Spring-AOP切面编程(1)

          本文链接:https://www.haomeiwen.com/subject/uqaseqtx.html