一、什么是 AOP ?

1.1 AOP 解释

  • AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

1.2 AOP 解决了什么问题?

  • AOP (Aspect Oriented Programing) 及面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等待,Struts2的拦截器设计就是基于AOP的思想,是个比较经典的例子。
  • 在不改变原有逻辑的基础上,给一些方法增加额外的功能,代理也是这个功能,读写分离也是用 aop 来做


1.3 AOP 技术的实现

  • 一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;
  • 二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。


二、AOP 的使用场景

使用 AOP 来横切关注点,具体可以在以下方面使用

  • Authentication 权限
  • Caching 缓存
  • Context passing 内容传递
  • Error handling 错误处理
  • Lazy loading 懒加载
  • Debugging  调试
  • logging, tracing, profiling and monitoring 记录 跟踪 优化 校准
  • Performance optimization 性能优化
  • Persistence  持久化
  • Resource pooling 资源池
  • Synchronization 同步
  • Transactions 事务

三、静态代理 ? 动态代理?

3.1 使用静态代理

使用 静态代理 是使用硬编码,方法相互嵌套的一种方案。

说到方法嵌套,相必大家应该都不陌生,我在开发 Java EE 应用程序的时候,有 dao 层负责和数据库打交道,service 层负责调用持久层的方法,然后会有 controller 调用 service 的方法并返回的结果返回给前台。

这样结合起来就很像套娃,接下来我们就用一个简单的实例来演示经验代理的实现

3.1.1 静态代理实例

  1. 编写一个名为 Clinet 的接口 ```java package cn.gorit.start;

public interface Client { public void say(); }

  1. <a name="33OHe"></a>
  2. ###
  3. 2. 编写一个 People 类实现 Client 接口
  4. ```java
  5. package cn.gorit.start;
  6. public class People implements Client {
  7. private String name;
  8. public People(String name) {
  9. this.name = name;
  10. }
  11. public void say() {
  12. System.out.println(name+ "委托人说完了。。。");
  13. }
  14. }
  1. 编写 Lawer 类 ```java package cn.gorit.start;

public class Lawer { private Client wtr;

  1. public Lawer(Client wtr) {
  2. this.wtr = wtr;
  3. System.out.println("我是律师,接下来该我说话了");
  4. }
  5. public void say() {
  6. System.out.println("我是律师,我说完了,下面由我的委托人说话了");
  7. wtr.say();
  8. System.out.println("我是律师,我的委托人叙述完毕了");
  9. }

}


4. 编写 Test 类
```java
package cn.gorit.start;

public class Test {
    // 静态代理(硬编码) -- 方法的嵌套
    public static void main(String[] args) {
        Client wtr = new People("coco");
        Lawer ls = new Lawer(wtr); // 
        ls.say();
    }
}

image.png
代码的逻辑并不难,在 Lawer 类中 调用了 People 类中的 say 方法,实现了简单的静态代理

3.2 使用动态代理

动态代理的特点:

  • 字节码随用随创建,随用随加载

作用

  • 不修改源码的基础上对方法增强

动态代理的分类:

  • 基于接口的动态代理
  • 基于子类的动态代理

基于接口的动态代理:

  • 涉及的类:Proxy
  • 提供者:JDK 官方

如何创建代理对象?

  • 使用 Proxy 类中的 newProxyInstance方法


创建代理对象的要求:_

  • 被代理的类最少实现一个接口,如果没有则不能使用

这次我们以 JDK 自带的 Proxy 类实现动态代理

  • 被代理的类至少实现一个接口,如果没有则不能使用

newProxyInstance方法的参数:

  • ClassLoder:类加载器
    • 它是用于加载代理对象字节码的,和被代理对象使用相同的的类加载器(固定写法)
  • Class[]: 字节码数组
    • 它是用于让代理独享和被代理对象有相同的方法,固定写法
  • InvocationHandler:用于提供增强的代码
    • 它是让我们写如何代理,我们一般都是些一个该接口的实现类,通常情况都是匿名内部类,但不是必须的。此接口的实现类都是谁用谁写

      3.2.1 实例代码

  1. 创建一个 IProducer 实现生产者 ```java package com.gorit.proxy;

/**

  • 一个生产者
  • / public interface IProducer { /*

    • 销售
    • */ public void saleProduct(float money);

      /**

    • 售后
    • */ public void afterService(float money); } ```
  1. 创建 Producer 类实现 IProducer 接口 ```java package com.gorit.proxy;

/**

  • 一个生产者
  • / public class Producer implements IProducer{ /*

    • 销售
    • */ @Override public void saleProduct(float money) { System.out.println(“销售商品,并拿到钱:”+money); }

      /**

    • 售后
    • */ @Override public void afterService(float money) { System.out.println(“提供售后服务,并拿到钱:”+money); } } ```
  1. 实现 Client 类,实现动态代理 ```java package com.gorit.proxy;

import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy;

/**

  • 模拟一个消费者 (基于接口的动态代理)
  • */ public class Client { public static void main(String[] args) {

     final Producer producer = new Producer();
    
     IProducer proxyProducer = (IProducer)
                                 Proxy.
                                 newProxyInstance(producer.getClass().getClassLoader(), producer.getClass().getInterfaces(), new InvocationHandler() {
         /**
          * 作用:执行被代理对象的任何接口方法都会经过该方法
          * @param proxy 代理对象的引用
          * @param method 当前执行的方法
          * @param args  当前执行方法所需的参数
          * @return 和代理对象方法有相同的返回值
          * */
         @Override
         public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
             // 提供增强的代码
             Object returnValue = null;
             // 1. 获取方法执行的参数
             Float money = (Float)args[0];
             // 2. 判断当前方法是不是销售
             if ("saleProduct".equals(method.getName())) {
                 returnValue =  method.invoke(producer,money*0.8f);
             }
             return returnValue;
         }
     });
    
     proxyProducer.saleProduct(10000f);
    

    } } ``` image.png

四、使用 Spring AOP (XML)

4.1 Spring 基于 XML 的 AOP 配置

  1. 把通知 Bean 也交给 spring 来管理
  2. 使用 aop:config 标签表名开始 AOP 配置
  3. 使用 aop:aspect 标签表名配置切面
    1. id 属性是给切面一个唯一标志
    2. ref 属性时指定通知类bean 的Id
  4. 在aop:aspect 标签内部使用对应的标签来配置通知的类型
    1. 我们现在示例是让 printLog 方法在切入点执行之前,所以是前置通知
    2. aop:before 表示配置前置通知
      1. method属性: 用于指定Logger 哪个方法是前置通知
      2. pointcut属性:用于指定切入点表达式,该表达式的含义是指对业务层中哪些方法增强


4.2 切入点表达式的写法:

  • 关键字:execution(表达式)
  • 表达式:访问修饰符 返回值 包名.包名.报名…方法名(参数列表)
  • 标准表达式写法:
    • public void cn.gorit.service.impl.AccountServiceImpl.saveAccount()
    • 访问修饰符可以省略:
      • void cn.gorit.service.impl.AccountServiceImpl.saveAccount()
    • 返回值可以使用通配符,表示任意范沪指
        • cn.gorit.service.impl.AccountServiceImpl.saveAccount()
    • 报名可以用通配符表示任意包,但是有几级包就得写
      • *. ….
    • 全通配写法
        • ...*(..)
    • 实际开发中切入点表达式的通常写法:
    • 切到业务层实现类下的所有方法:
        • cn.gorit.service.impl..(..)

4.3 实例代码

本次实例以模拟日志打印进行 AOP 演示

  • pom.xml 依赖

      <dependencies>
          <dependency>
              <groupId>org.springframework</groupId>
              <artifactId>spring-context</artifactId>
              <version>5.2.3.RELEASE</version>
          </dependency>
    
          <dependency>
              <groupId>org.aspectj</groupId>
              <artifactId>aspectjweaver</artifactId>
              <version>1.8.7</version>
          </dependency>
      </dependencies>
    
  1. 编写工具类 Logger ,放在 util 包下 ```java package cn.gorit.utils;

import org.aspectj.lang.ProceedingJoinPoint;

/**

  • 用于记录日志的工具类,它里面提供了公共的代码
  • */ public class Logger {

    /**

    • 前置通知
    • */ public void beforePrintlog() { System.out.println(“前置通知:Logger类找那个的 printlog 方法开始记录日志了”); }

      /**

    • 后置通知
    • / public void afterReturninglog() { System.out.println(“后置通知:Logger类找那个的 afterReturnrintlog 方法开始记录日志了”); } /*
    • 异常通知
    • / public void afterThrowinglog() { System.out.println(“异常通知:Logger类找那个的 afterThrowingPrintlog 方法开始记录日志了”); } /*
    • 最终通知
    • */ public void lastPrintlog() { System.out.println(“最终通知:Logger类找那个的 lastPrintlog 方法开始记录日志了”); }

      /**

    • 环绕通知
    • 问题:
    • 当我们配置了环绕通知之后,切入点方法没哟执行,而通知方法执行了
    • 分析:
    • 通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码没有
    • 解决:
    • Spring框架为我们提供了一个接口: ProceedingJoinPoint。 该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
    • 该接口可以作为环绕通知的方法参数,在程序执行时,spring框架 会为我们提供该接口的实现类供我们使用。 *
    • spring 中的环绕通知:
    • 它是spring框架为我们提供的一种可以在代码中 手动控制增强方法何时执行的方式。
    • */ public Object aroundPrintlog(ProceedingJoinPoint pjd) { Object rtValue = null; try {

       Object[] args = pjd.getArgs();//得到方法执行所需要的参数
      
       System.out.println("环绕通知:Logger类找那个的 aroundPrintlog 方法开始记录日志了 【前置】");
      
       rtValue = pjd.proceed(args);//明确调用业务层方法
      
       System.out.println("环绕通知:Logger类找那个的 aroundPrintlog 方法开始记录日志了 【后置】");
       return rtValue;
      

      } catch (Throwable t) {

       System.out.println("环绕通知:Logger类找那个的 aroundPrintlog 方法开始记录日志了 【异常】");
           throw new RuntimeException(t);
      

      } finally {

       System.out.println("环绕通知:Logger类找那个的 aroundPrintlog 方法开始记录日志了 【最终】");
      

      } } } ```

  1. 编写 service 接口 IAccountService,模拟账户业务层接口 ```java package cn.gorit.service;

/**

  • 账户的业务层接口
  • */ public interface IAccountService {

    /**

    • 模拟保存账户
    • */ public void saveAccount();

      /**

    • 模拟更新账户
    • */ public void updateAccount(int i);

      /**

    • 删除账户
    • */ public int deleteAccount(); }

3. 模拟账户的业务层实现类:AccountServiceImpl
```java
package cn.gorit.service.imp;

import cn.gorit.service.IAccountService;

/**
 * 账户的业务层实现类
 * */
public class AccountServiceImpl implements IAccountService {


    public void saveAccount() {
        System.out.println("执行了保存");
    }

    public void updateAccount(int i) {
        System.out.println("执行了更新:"+i);
    }

    public int deleteAccount() {
        System.out.println("执行了删除");
        return 0;
    }
}
  1. 配置 bean.xml

这里只配置两个通知的类型,后面的注解配置会更加详细的配置

<?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-3.0.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

    <!-- Spring 的 IOC,把 service 配置进来 【要增强的方法】 -->
    <bean id="accountService" class="cn.gorit.service.imp.AccountServiceImpl"></bean>

    <!-- 配置Logger 类 -->
    <bean id="logger" class="cn.gorit.utils.Logger"></bean>

    <!-- 配置 AOP -->
    <aop:config>
        <!-- 配置切面 -->
        <!--配置切入点表达式, id 属性用于指定表达式的唯一标识。 expression 属性 用于指定表达式内容
            此标签写在 aop:aspect 标签内部职能当切面使用。
            它还可以写在 aop:aspect 外面(必须写在上面),此时编程了所有切面可用-->
        <aop:pointcut id="pt1" expression="execution( * cn.gorit.service.imp.*.*(..))"/>
        <aop:aspect id="logAdvice" ref="logger">

<!--            <aop:before method="printlog" pointcut="execution(public void cn.gorit.service.imp.AccountServiceImpl.saveAccount())"></aop:before>-->
            <!-- 配置前置通知:在切入点方法执行之前执行-->
            <aop:before method="beforePrintlog" pointcut-ref="pt1"></aop:before>
            <!-- 配置后置通知:在切入点方法正常执行之后-->
            <aop:after-returning method="afterReturninglog" pointcut-ref="pt1"></aop:after-returning>
            <!-- 配置异常通知-:在切入点方法产生异常之后执行-->
            <aop:after-throwing method="afterThrowinglog" pointcut="execution( * cn.gorit.service.imp.*.*(..))"></aop:after-throwing>
            <!-- 配置最终通知:无论切入点方法是否正常他都会在其后面执行-->
            <aop:after method="lastPrintlog" pointcut-ref="pt1"></aop:after>

            <!-- 配置环绕通知: 详细的注入请看 Logger类 -->
            <aop:around method="aroundPrintlog" pointcut-ref="pt1"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>
  1. 编写测试类 ```java package cn.gorit;

import cn.gorit.service.IAccountService; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test { public static void main(String[] args) { // 1. 获取容器 ApplicationContext ac = new ClassPathXmlApplicationContext(“bean.xml”); // 2. 获取对象 IAccountService as = (IAccountService) ac.getBean(“accountService”); // 3. 执行方法 as.saveAccount(); } }

![image.png](https://cdn.nlark.com/yuque/0/2020/png/473179/1591576983822-9e4ec8ff-29f0-4cf8-b940-47382da94556.png#align=left&display=inline&height=233&margin=%5Bobject%20Object%5D&name=image.png&originHeight=233&originWidth=601&size=119075&status=done&style=none&width=601)
<a name="RVdqn"></a>
# 五、使用 Spring AOP (注解)
<a name="e6IPG"></a>
### 5.1 配置文件

1. pom.xml
```xml
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.3.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.8.7</version>
        </dependency>
    </dependencies>
  1. bean.xml 编写 ```xml <?xml version=”1.0” encoding=”UTF-8”?>
<!-- 配置 spring 创建容器时要扫描的包 (使用注解) -->
<context:component-scan base-package="cn.gorit"></context:component-scan>

<!--配置spring开启注解AOP 的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

<a name="YiMHu"></a>
### 5.2 JavaBean 编写

1. 编写 IAccountService 接口
```java
package cn.gorit.service;

/**
 * 账户的业务层接口
 * */
public interface IAccountService {

    /**
     * 模拟保存账户
     * */
    public void saveAccount();

    /**
     * 模拟更新账户
     * */
    public void updateAccount(int i);

    /**
     * 删除账户
     * */
    public int deleteAccount();
}
  1. 接口对应的实现类 AccountService ```java package cn.gorit.service.imp;

import cn.gorit.service.IAccountService; import org.springframework.stereotype.Service;

/**

  • 账户的业务层实现类
  • */ @Service(“AccountService”) public class AccountServiceImpl implements IAccountService {
public void saveAccount() {
    System.out.println("执行了保存");
}

public void updateAccount(int i) {
    System.out.println("执行了更新:"+i);
}

public int deleteAccount() {
    System.out.println("执行了删除");
    return 0;
}

}


3. 工具类 Logger
```java
package cn.gorit.utils;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * 用于记录日志的工具类,它里面提供了公共的代码
 * */

@Component("logger")
@Aspect  //表示当前类是一个切面类
public class Logger {

    /**
    *    表示切入点
    */
    @Pointcut("execution( * cn.gorit.service.imp.*.*(..))")
    private void pt1() {}

    /**
     * 前置通知
     * */
    @Before("pt1()")
    public void beforePrintlog() {
        System.out.println("前置通知:Logger类找那个的 printlog 方法开始记录日志了");
    }

    /**
     * 后置通知
     * */
    @AfterReturning("pt1()")
    public void afterReturninglog() {
        System.out.println("后置通知:Logger类找那个的 afterReturnrintlog 方法开始记录日志了");
    }
    /**
     * 异常通知
     * */
    @AfterThrowing("pt1()")
    public void afterThrowinglog() {
        System.out.println("异常通知:Logger类找那个的 afterThrowingPrintlog 方法开始记录日志了");
    }
    /**
     * 最终通知
     * */

    @After("pt1()")
    public void lastPrintlog() {
        System.out.println("最终通知:Logger类找那个的 lastPrintlog 方法开始记录日志了");
    }

    /**
     * 环绕通知
     * 问题:
     *      当我们配置了环绕通知之后,切入点方法没有执行,而通知方法执行了
     *  分析:
     *      通过对比动态代理中的环绕通知代码,发现动态代理的环绕通知有明确的切入点方法调用,而我们的代码没有
     *  解决:
     *      Spring框架为我们提供了一个接口: ProceedingJoinPoint。 该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
     *      该接口可以作为环绕通知的方法参数,在程序执行时,spring框架 会为我们提供该接口的实现类供我们使用。
     *
     *  spring 中的环绕通知:
     *      它是spring框架为我们提供的一种可以在代码中 手动控制增强方法何时执行的方式。
     * */

    @Around("pt1()")
    public Object aroundPrintlog(ProceedingJoinPoint pjd) {
        Object rtValue = null;
        try {
            Object[] args = pjd.getArgs();//得到方法执行所需要的参数

            System.out.println("环绕通知:Logger类找那个的 aroundPrintlog 方法开始记录日志了 【前置】");

            rtValue = pjd.proceed(args);//明确调用业务层方法

            System.out.println("环绕通知:Logger类找那个的 aroundPrintlog 方法开始记录日志了 【后置】");
            return rtValue;
        } catch (Throwable t) {
            System.out.println("环绕通知:Logger类找那个的 aroundPrintlog 方法开始记录日志了 【异常】");
                throw new RuntimeException(t);
        } finally {
            System.out.println("环绕通知:Logger类找那个的 aroundPrintlog 方法开始记录日志了 【最终】");
        }
    }
}

5.3 测试运行

package cn.gorit;

import cn.gorit.service.IAccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
    public static void main(String[] args) {
        // 1. 获取容器
        ApplicationContext ac =  new ClassPathXmlApplicationContext("bean.xml");
        // 2. 获取对象
        IAccountService as = (IAccountService) ac.getBean("AccountService");
        // 3. 执行方法
        as.saveAccount();
    }
}

image.png

参考文章