乐趣区

Spring笔记03AOP

1. AOP

1.1 AOP 介绍

1.1.1 什么是 AOP

  • 在软件业,AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程 ,通过 预编译方式 运行期动态代理 实现程序功能的统一维护的一种技术。AOP 是 OOP(面向对象编程)的延续,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,是 函数式编程 的一种衍生范型。利用 AOP 可以对业务逻辑的各个部分进行 隔离 ,从而使得业务逻辑各部分之间的 耦合度降低 ,提高程序的 可重用性,同时提高了开发的效率。
  • AOP 采取 横向抽取 机制,取代了传统 纵向继承 体系重复性代码。如下图所示:
  • 经典应用:事务管理、性能监视、安全检查、缓存、日志等。
  • Spring AOP 使用纯 Java 实现,不需要专门的编译过程和类加载器,在运行期通过 代理方式 向目标类织入增强代码。
  • AspectJ 是一个基于 Java 语言的 AOP 框架,从 Spring2.0 开始,Spring AOP 引入对 Aspect 的支持,AspectJ 扩展了 Java 语言,提供了一个专门的编译器,在编译时提供横向代码的织入。

1.1.2 AOP 实现原理

  • aop 底层将采用代理机制进行实现
  • 接口 + 实现类时:Spring 采用 JDK 的动态代理 Proxy
  • 只有实现类时:Spring 采用 cglib 字节码增强。这种底层属于继承增强。

1.1.3 AOP 术语【掌握】

  1. Target:目标类,需要被代理的类。本例中如:UserDao
  2. Joinpoint(连接点):所谓连接点是指那些可能被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。本例中如:UserDao 的所有的方法
  3. PointCut 切入点:所谓切入点是指我们要对哪些 Joinpoint 进行拦截,即已经被增强的连接点。例如:save()
  4. Advice:通知 / 增强,增强的代码。例如:checkPri()
    所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知,通知分为前置通知、后置通知、异常通知、最终通知、环绕通知(即切面要完成的功能)。
  5. Weaving(织入):是指把通知 / 增强 advice 应用到目标对象 target 来创建新的代理对象 proxy 的过程。
    spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装在期织入。
  6. Proxy:代理类,一个类被 AOP 织入增强后,就产生一个结果代理类。
  7. Aspect(切面):是切入点 Pointcut 和通知 Advice(引介)的结合。
  8. Introduction(引介):引介是一种 特殊的通知,在不修改类代码的前提下,Introduction 可以在运行期为类动态地添加一些方法或 Field。

  • 小结:

    一个线是一个特殊的面。
    一个切入点和一个通知,组成成一个特殊的面。
    详解如图 01:

    详解如图 02:

1.2 AOP 的底层实现(了解)

  • 动态代理

    • JDK 动态代理:只能对实现了接口的类产生代理
    • Cglib 动态代理(第三方代理技术):对没有实现接口的类产生代理对象,生成子类对象。
    • 代理知识点参考:https://www.cnblogs.com/itzho…

1.2.1 JDK 动态代理

  • 准备工作:新建 web 项目,不需要导包
  • 代理对象 UserDao

    package com.itzhouq.spring.demo1;
    
    public interface UserDao {public void save();
        public void update();
        public void find();
        public void delete();}
  • 实现类

    package com.itzhouq.spring.demo1;
    
    public class UserDaoImpl implements UserDao {
    
        @Override
        public void save() {System.out.println("保存用户");
        }
    
        @Override
        public void update() {System.out.println("更新用户");
        }
    
        @Override
        public void find() {System.out.println("查找用户");
        }
    
        @Override
        public void delete() {System.out.println("删除用户");
        }
    
    }
  • 使用 JDK 产生 UserDao 的代理类
  package com.itzhouq.spring.demo1;
  
  import java.lang.reflect.InvocationHandler;
  import java.lang.reflect.Method;
  import java.lang.reflect.Proxy;
  
  /*
   * 使用 JDK 动态代理对 UserDao 产生代理
   */
  public class JDKProxy implements InvocationHandler {
      // 将被增强的对象传递到代理总
      private UserDao userDao;
      public JDKProxy(UserDao userDao) {this.userDao = userDao;}
      /*
       * 产生 UserDao 代理的方法
       */
      public UserDao createProxy() {UserDao userDaoProxy = (UserDao) Proxy.newProxyInstance(userDao.getClass().getClassLoader(),
                  userDao.getClass().getInterfaces(),
                  this);
          return userDaoProxy;
      }
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
          // 判断方法名是不是 save
          if("save".equals(method.getName())) {
              // 增强
              System.out.println("权限校验 ==============");
              return method.invoke(userDao, args);
              
          }
          return method.invoke(userDao, args);
      }
  }
  • 测试类

    package com.itzhouq.spring.demo1;
    
    import org.junit.Test;
    
    public class SpringDemo1 {
        
        @Test
        //JDK 动态代理
        public void test1() {UserDao userDao = new UserDaoImpl();
            // 创建代理
            UserDao proxy = new JDKProxy(userDao).createProxy();
            proxy.save();
            proxy.find();
            proxy.delete();
            proxy.update();
            // 权限校验 ==============
            // 保存用户
            // 查找用户
            // 删除用户
            // 更新用户
        }
    }

1.2.2 Cglib 动态代理

  • Cglib 是第三方开源代码生成类库,动态添加类的属性和方法
  • cglib 的运行原理:在运行时创建目标类的子类从而对目标类进行增强。
  • 因为 Spring 核心包中包含了 cglib 的包,所以引入 Spring 的 4 + 2 必备包就可以使用 Cglib 了

  • 目标类:没有实现接口

    package com.itzhouq.spring.demo2;
    
    public class CustomerDao {public void save() {System.out.println("保存客户");
        }
        public void update() {System.out.println("更新客户");
        }
        public void find() {System.out.println("查找客户");
        }
        public void delete() {System.out.println("删除客户");
        }
    }
  • Cglib 代理类
 package com.itzhouq.spring.demo2;
 
 import java.lang.reflect.Method;
 
 import org.springframework.cglib.proxy.Enhancer;
 import org.springframework.cglib.proxy.MethodInterceptor;
 import org.springframework.cglib.proxy.MethodProxy;
 
 /*
  * Cglib 动态代理
  */
 public class CglibProxy implements MethodInterceptor {
     
     private CustomerDao customerDao;
 
     public CglibProxy(CustomerDao customerDao) {this.customerDao = customerDao;}
     
     // 使用 Cglib 产生代理的方法
     public CustomerDao createProxy() {
         // 1. 创建 cglib 的核心类对象
         Enhancer enhancer = new Enhancer();
         // 2. 设置父类
         enhancer.setSuperclass(customerDao.getClass());
         // 3. 设置回调(类似于 InvocationHandler 对象)enhancer.setCallback(this);
         // 4. 创建代理对象
         CustomerDao proxy = (CustomerDao) enhancer.create();
         return proxy;
     }
 
     @Override
     public Object intercept(Object proxy, Method method, Object[] arg, MethodProxy methodProxy) 
             throws Throwable {
         // 判断方法是否为 save
         if("save".equals(method.getName())) {
             // 增强
             System.out.println("权限校验 ========");
             return methodProxy.invokeSuper(proxy, arg);
         }
         return methodProxy.invokeSuper(proxy, arg);
     }
 }
  • 测试
  package com.itzhouq.spring.demo2;
  
  import org.junit.Test;
  
  public class SpringDemo2 {
      /*
       * cglib 的测试
       */
      
      @Test
      public void test1() {CustomerDao customerDao = new CustomerDao();
          CustomerDao proxy = new CglibProxy(customerDao).createProxy();
          proxy.save();
          proxy.find();
          proxy.update();
          proxy.delete();
          // 权限校验 ========
          // 保存客户
          // 查找客户
          // 更新客户
          // 删除客户
      }
  }

1.2.3 总结

  • Spring 在运行期,生成动态代理对象,不需要特殊的编译器。
  • Spring AOP 的底层是通过 JDK 动态代理或者 Cglib 动态代理技术为目标 bean 执行横向织入的。

    • Spring 会优先使用 Spring 使用 JDK 代理方式进行代理
    • 若目标对象没有实现任何接口,Spring 容器会使用 Cglib 动态代理
  • 标记为 final 的方法不能被代理,因为无法进行覆盖
  • Cglib 动态代理,是针对的目标类产生子类,所以目标类不能被 final 修饰。
  • Spring 只支持方法连接点,不提供属性连接。

2. Spring 的 AOP 的开发(AspectJ 的 XML 方式)

2.1 Spring 的 AOP 简介

  • AOP 思想最早是由 AOP 联盟组织提出。Spring 是使用这种思想最好的框架。
  • Spring 的 AOP 有自己的实现方式(非常繁琐)。AspectJ 是一个 AOP 框架,Spring 引入 AspectJ 作为自身 AOP 的开发
  • Spring 两种开发方式

    • Spring 传统方式(弃用)。
    • Spring 基于 AspectJ 的 AOP 开发方式(使用)。

2.2 AOP 入门开发

2.2.1 准备工程和 jar 包

  • 除去基本的 6 个包,在 web 项目中添加 aop 开发的相关 jar 包

    • AOP 联盟规范包:..spring 相关依赖包 spring-framework-3.0.2.RELEASE-dependenciesorg.aopalliancecom.springsource.org.aopalliance1.0.0com.springsource.org.aopalliance-1.0.0.jar
    • AOP 包:..spring-framework-4.2.4.RELEASE-distspring-framework-4.2.4.RELEASElibsspring-aop-4.2.4.RELEASE.jar
    • AspectJ 包:..spring 相关依赖包 spring-framework-3.0.2.RELEASE-dependenciesorg.aspectjcom.springsource.org.aspectj.weaver1.6.8.RELEASEcom.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
    • Spring 和 ASpectJ 整合包:..spring-framework-4.2.4.RELEASE-distspring-framework-4.2.4.RELEASElibsspring-aspects-4.2.4.RELEASE.jar
    • Spring 整合 JUnit 单元测试包:..spring-framework-4.2.4.RELEASE-distspring-framework-4.2.4.RELEASElibsspring-test-4.2.4.RELEASE.jar

2.2.2 引入 Spring 的配置文件

  • 引入 aop 的约束
  • 约束的位置:../spring-framework-4.2.4.RELEASE-dist/spring-framework-4.2.4.RELEASE/docs/spring-framework-reference/html/xsd-configuration.html 40.2.7 the aop schema
<?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/aop 
        http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>

2.2.3 编写目标类并完成配置

  • 目标接口

    package com.itzhouq.spring.demo3;
    
    public interface ProductDao {public void save();
        public void update();
        public void find();
        public void delete();}
  • 目标类

    package com.itzhouq.spring.demo3;
    
    public class ProductDaoImpl implements ProductDao {
    
        @Override
        public void save() {System.out.println("保存商品");
        }
    
        @Override
        public void update() {System.out.println("更新商品");
        }
    
        @Override
        public void find() {System.out.println("查找商品");
        }
    
        @Override
        public void delete() {System.out.println("删除商品");
        }
    
    }
  • 配置目标对象

    <!-- 配置目标对象:被增强的对象 -->
    <bean id="productDao" class="com.itzhouq.spring.demo3.ProductDaoImpl"></bean>

2.2.4 测试类

package com.itzhouq.spring.demo3;

import javax.annotation.Resource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/*
 * AOP 入门
 */
@RunWith(SpringJUnit4ClassRunner.class)    // 这两个注释就是 Spring 整合了 JUnit 单元测试
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringDemo3 {
    
    // 注入 productDao
    @Resource(name="productDao")
    private ProductDao productDao;
    
    @Test
    public void test1() {productDao.save();
        productDao.update();
        productDao.find();
        productDao.delete();
//        保存商品
//        更新商品
//        查找商品
//        删除商品
    }
}
  • 下面需要对 save()方法增强

2.2.5 编写一个切面类

  • 切面:多个通知和多个切入点的组合。
 package com.itzhouq.spring.demo3;
          /*
           * 切面类
           */
          public class MyAspectXML {public void checkPri() {System.out.println("权限校验。。。");
              }
          }

2.2.6 将切面类交给 Spring

  •     <!-- 将切面类交给 Spring 管理 -->
          <bean id="myAspect" class="com.itzhouq.spring.demo3.MyAspectXML"></bean>

2.2.7 通过 AOP 的配置实现动态代理

<!-- 通过 AOP 的配置完成对目标类产生代理 -->
    <aop:config>
        <!-- 表达式配置哪些类的哪些方法需要进行增强 -->
        <aop:pointcut expression="execution(* com.itzhouq.spring.demo3.ProductDaoImpl.save(..))" id="pointcut1"/>
        <!-- 配置切面 -->
        <aop:aspect ref="myAspect">
            <aop:before method="checkPri" pointcut-ref="pointcust1"/>
        </aop:aspect>
    </aop:config>

2.2.8 测试类中运行结果,save 实现了增强

@RunWith(SpringJUnit4ClassRunner.class)    // 这两个注释就是 Spring 整合了 JUnit 单元测试
@ContextConfiguration("classpath:applicationContext.xml")
public class SpringDemo3 {
    
    // 注入 productDao
    @Resource(name="productDao")
    private ProductDao productDao;
    
    @Test
    public void test1() {productDao.save();
        productDao.update();
        productDao.find();
        productDao.delete();
//        权限校验。。。//        保存商品
//        更新商品
//        查找商品
//        删除商品

    }
}

2.3 Spring 中的通知类型

2.3.1 前置通知

  • 配置

    <!-- 配置切面 -->
    <aop:aspect ref="myAspect">
        <aop:before method="checkPri" pointcut-ref="pointcut1"/>
    </aop:aspect>
  • 在目标方法执行之前进行操作
  • 可以获得切入点的信息
  • 比如在切面类中加入参数 JoinPoint

    public class MyAspectXML {public void checkPri(JoinPoint joinPoint) {System.out.println("权限校验。。。"+joinPoint);
        }
    }

    测试打印的效果

    //        权限校验。。。execution(void com.itzhouq.spring.demo3.ProductDao.save())
    //        保存商品
    //        更新商品
    //        查找商品
    //        删除商品

2.3.2 后置通知:

  • 在目标方法之后操作,可以获得返回值
  • 修改接口 ProductDao 和实现类 ProductDaoImpl 类的 delete 方法的返回值为 String
  • 配置后置通知

    <!-- 通过 AOP 的配置完成对目标类产生代理 -->
        <aop:config>
            <!-- 表达式配置哪些类的哪些方法需要进行增强 -->
            <aop:pointcut expression="execution(* com.itzhouq.spring.demo3.ProductDaoImpl.save(..))" id="pointcut1"/>
            <aop:pointcut expression="execution(* com.itzhouq.spring.demo3.ProductDaoImpl.delete(..))" id="pointcut2"/>
            <!-- 配置切面 -->
            <aop:aspect ref="myAspect">
                <!-- 前置通知 ============= -->
                <aop:before method="checkPri" pointcut-ref="pointcut1"/>
                <!-- 后置通知 ============= -->
                <aop:after-returning method="writeLog" pointcut-ref="pointcut2" returning="result"/>
            </aop:aspect>
        </aop:config>
  • 在后置切面类中添加记录日志的方法
 /*
       * 后置通知演示
       */
      public void writeLog(Object result) {System.out.println("日志记录 ======="+result);
      }
  • 测试

    权限校验。。。execution(void com.itzhouq.spring.demo3.ProductDao.save())
    //        保存商品
    //        更新商品
    //        查找商品
    //        删除商品
    //        日志记录 =======kkk

2.3.3 环绕通知:

  • 在目标方法执行之前 和之后进行操作
  • 环绕通知可以组织目标方法的执行
  • 举例:需求 — 在修改方法 update 前后添加性能监控
  • 在切面类中添加一个方法用来测试环绕通知
/**
       * 性能监控
       * @throws Throwable 
       */
      public Object around(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println("环绕前通知 ===========");
          Object obj = joinPoint.proceed();// 这一步相当于执行目标程序
          System.out.println("环绕后通知 =========");
          return obj;
      }
  • 配置环绕通知

    <aop:config>
            <!-- 表达式配置哪些类的哪些方法需要进行增强 -->
            <aop:pointcut expression="execution(* com.itzhouq.spring.demo3.ProductDaoImpl.save(..))" id="pointcut1"/>
            <aop:pointcut expression="execution(* com.itzhouq.spring.demo3.ProductDaoImpl.delete(..))" id="pointcut2"/>
            <aop:pointcut expression="execution(* com.itzhouq.spring.demo3.ProductDaoImpl.update(..))" id="pointcut3"/>
            <!-- 配置切面 -->
            <aop:aspect ref="myAspect">
                <!-- 前置通知 ============= -->
                <aop:before method="checkPri" pointcut-ref="pointcut1"/>
                <!-- 后置通知 ============= -->
                <aop:after-returning method="writeLog" pointcut-ref="pointcut2" returning="result"/>
                <!-- 环绕通知 -->
                <aop:around method="around" pointcut-ref="pointcut3"/>
            </aop:aspect>
        </aop:config>
  • 测试

    //        权限校验。。。execution(void com.itzhouq.spring.demo3.ProductDao.save())
    //        保存商品
    //        环绕前通知 ===========
    //        更新商品
    //        环绕后通知 =========
    //        查找商品
    //        删除商品
    //        日志记录 =======kkk

2.3.4 异常抛出通知:

  • 在程序抛出异常时候进行操作,可以得到异常信息
  • 在 find 方法上模拟一个异常

    @Override
        public void find() {System.out.println("查找商品");
            int i = 1 / 0;
        }
  • 切面类中添加一个方法用于测试异常抛出通知
 /*
       * 异常抛出通知
       */
      public void afterThrowing(Throwable ex) {System.out.println("异常抛出通知 =======" + ex);
      }
  • 配置异常通知

    <aop:config>
            <!-- 表达式配置哪些类的哪些方法需要进行增强 -->
            <aop:pointcut expression="execution(* com.itzhouq.spring.demo3.ProductDaoImpl.save(..))" id="pointcut1"/>
            <aop:pointcut expression="execution(* com.itzhouq.spring.demo3.ProductDaoImpl.delete(..))" id="pointcut2"/>
            <aop:pointcut expression="execution(* com.itzhouq.spring.demo3.ProductDaoImpl.update(..))" id="pointcut3"/>
            <aop:pointcut expression="execution(* com.itzhouq.spring.demo3.ProductDaoImpl.find(..))" id="pointcut4"/>
            <!-- 配置切面 -->
            <aop:aspect ref="myAspect">
                <!-- 前置通知 ============= -->
                <aop:before method="checkPri" pointcut-ref="pointcut1"/>
                <!-- 后置通知 ============= -->
                <aop:after-returning method="writeLog" pointcut-ref="pointcut2" returning="result"/>
                <!-- 环绕通知 -->
                <aop:around method="around" pointcut-ref="pointcut3"/>
                <!-- 异常抛出通知 -->
                <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut4" throwing="ex"/>
            </aop:aspect>
        </aop:config>
  • 测试

    //        权限校验。。。execution(void com.itzhouq.spring.demo3.ProductDao.save())
    //        保存商品
    //        环绕前通知 ===========
    //        更新商品
    //        环绕后通知 =========
    //        查找商品
    //        异常抛出通知 =======java.lang.ArithmeticException: / by zero

2.3.5 最终通知:

  • 在切面类中添加方法测试最终通知
/*
       * 最终通知: 相当于 finally 代码块中的内容
       */
      public void after() {System.out.println("最终通知 =========");
      }
  • 配置最终通知

    <!-- 配置最终通知 -->
                <aop:after method="after" pointcut-ref="pointcut4"/>
  • 测试
  • 无论是否有异常,最终通知都会执行。

2.3.6 引介通知(不用会)

2.4 切入点表达式【掌握】

1.execution()  用于描述方法【掌握】语法:execution(修饰符  返回值  包. 类. 方法名(参数) throws 异常)
        修饰符,一般省略
            public      公共方法
            *           任意
        返回值,不能省略
            void        返回没有值
            String      返回值字符串
            *           任意
        包,[可以省略]
            com.itheima.crm                 固定的包
            com.itheima.crm.*.service       crm 包下面的任意子包,固定目录 service(例如:com.itheima.crm.staff.service)com.itheima.crm..               crm 包下面的所有子包(含自己)com.itheima.crm.*.service..     crm 包下面的任意子包,固定目录 service,service 目录任意包(含自己)类,[可以省略]
            UserServiceImpl                 指定的类
            *Impl                           以 Impl 结尾的类
            User*                           以 User 开头的类
            *                               任意的类
        方法名,不能省略
            addUser                         固定的方法名
            add*                            以 add 开头的方法名
            *Do                             以 Do 结尾的方法名
            *                               任意的方法名
        (参数)
            ()                              无参
            (int)                           一个整型
            (int, int)                      两个整型
            (..)                            参数任意
        throws,[可以省略],一般省略。综合案例 1:execution(* com.itheima.crm.*.service..*.*(..))
    综合案例 2:<aop:pointcut expression="execution(* com.itheima.*WithCommit.*(..)) || 
                                  execution(* com.itheima.*Service.*(..))"id="myPointCut"/>

2.within: 匹配包或子包中的方法(了解)
    within(com.itheima.aop..*)
3.this: 匹配实现了接口的代理对象中的方法(了解)
    this(com.itheima.aop.user.UserDAO)
4.target: 匹配实现了接口的目标对象中的方法(了解)
    target(com.itheima.aop.user.UserDAO)
5.args: 匹配参数格式符合标准的方法(了解)
    args(int, int)
6.bean(id): 对指定的 bean 所有的方法(了解)
    bean('userServiceId')
退出移动版