🎧 羽森 / Kent

spring中的AOP2020-02-24

什么是AOP

看到陌生的词,习惯性搜一搜解释

面向切面的程序设计(Aspect-oriented programming,AOP,又译作面向方面的程序设计、剖面导向程序设计)是计算机科学中的一种程序设计范型,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为「切点(Pointcut)」的代码块进行统一管理与装饰,如「对所有方法名以‘set*’开头的方法添加后台日志」。该思想使得开发人员能够将与代码核心业务逻辑关系不那么密切的功能(如日志功能)添加至程序中,同时又不降低业务代码的可读性。面向切面的程序设计思想也是面向切面软件开发的基础。

面向切面的程序设计将代码逻辑切分为不同的模块(即关注点(Concern),一段特定的逻辑功能)。几乎所有的编程思想都涉及代码功能的分类,将各个关注点封装成独立的抽象模块(如函数、过程、模块、类以及方法等),后者又可供进一步实现、封装和重写。部分关注点「横切」程序代码中的数个模块,即在多个模块中都有出现,它们即被称作「横切关注点(Cross-cutting concerns, Horizontal concerns)」。

日志功能即是横切关注点的一个典型案例,因为日志功能往往横跨系统中的每个业务模块,即「横切」所有有日志需求的类及方法体。而对于一个信用卡应用程序来说,存款、取款、帐单管理是它的核心关注点,日志和持久化将成为横切整个对象结构的横切关注点。 切面的概念源于对面向对象的程序设计的改进,但并不只限于此,它还可以用来改进传统的函数。与切面相关的编程概念还包括元对象协议、主题(Subject)、混入(Mixin)和委托(Delegate)。

emmmm,大概...对,大概理解了

动态代理

看完维基百科对面向切面编程的解释,先放一边,让我们先来学一下java中的动态代理,新建一个项目,现模拟一个简单商品出售流程,先创建包com.karasawa.proxy,新建一个实体类接口IProducer和一个对应的实现类Producer充当业务层,再来模拟一个表现层的类Client

IProducer中有出售方法:

  // 对生产家要求的接口
 public interface IProducer {
  // 出售
  void saleProduct(float money);
}

实现类我们做模拟用,就不写复杂了,直接来个打印:

public class Producer implements IProducer {

  public void saleProduct(float money) {
    System.out.println("卖出装备,赚" + money);
  }
}

我们在Client里充当客户,调用Producer的出售方法,很简单的一个小代码片段:

public class Client {

  public static void main(String[] args) {
    Producer producer = new Producer();
    producer.saleProduct(100);
  }
}

所以什么是动态代理呢?动态代理,即不修改源码的基础上,对原有方法进行增强,分为基于子类的动态代理和基于接口的动态代理,两种不管是哪个,代理对象一旦创建,执行被代理对象的所有方法都会经过该该方法

这么说吧,耐克厂家自己出售商品,自己搞售后,订单少还行,订单一多,就忙不过来了,此时有了淘宝网店,线下实体店,帮他分担,由这些渠道去搞销售,他厂家只负责售后(当然包括生产),这些店去拿货一双鞋80块,卖出去100块,中间赚了20%的钱,这些店就是代理,作用和原来的一样(原来厂家自己销售,现在由店来销售,功能一样),只不过多了抽取20%的费用,这一操作是在代理商这里进行的,厂家并不知道你拿多少。这就是动态代理!

不修改源码的基础上(保证了功能一样),对原有方法进行增强(同样是买,卖出去的同时抽取20%的钱)

行,让我们基于上面的想法,两种动态代理的实现方法都试试

基于接口的动态代理

涉及的类是Proxy,由java官方提供(JDK),代理对象可以通过newProxyInstance方法来创建,创建的要求是被代理类至少有实现一个接口,没有则不能用,newProxyInstance有三个参数,

  1. classLoader:用于加载代理对象的字节码,和代理对象使用相同加载器
  2. Class[]:字节码数组,让代理对象和被代理对象有相同的方法
  3. invocationHandle:用于提供增强的代码,它是让我们写如何代理。我们一般都是些一个该接囗的实现类,通常情况下都是匿名内部类,但不是必须的

blabla一大堆,先修改例子看看proxy长啥样

package com.karasawa.proxy;

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

/**
 * @program: SpringDynamicProxy
 * @description: 模拟客户
 * @author: karasawa
 * @create: 2020-02-24 22:43
 **/
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() {
          @Override
          public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            float money = (float) args[0];
            return method.invoke(producer,money*0.8f);
          }
        });
    proxyProducer.saleProduct(100);
  }
}

运行后只会打印 「卖出装备,赚80.0」

newProxyInstance的第一第二个参数基本是固定的(并不是和我的一模一样,而是方式一样,吧Producer换成对应的要被代理的类),你需要做的就是在第三个参数InvocationHandle方法重写invoke方法,该方法返回一个Object对象,方法的内容就是增强原有方法,按我们的思路,这里应该吧收入抽取20%,method.invoke()会返回一个Object对象,method.invoke的意思是调用了什么方法,(即职能,我们的思路中是销售)第一个参数是被代理对象,第二个参数是所执行方法所带的参数(执行方法是saleProduct,参数是money,在这里做处理)

这样一个动态代理对象就可以了,创建该对象,调用saleProduct方法,参数100,控制台只输出80,可以看到动态代理对象最重要的还是invoke方法的重写

上面也提过,创建一个动态代理对象,不管用了该对象的什么方法,都会经过上面我们增强过的内容(厂家并不是只把销售让店家去做,同样的,Producer里肯定不止一个saleProduct方法),这样问题就很大了,所以我们优化一下,吧invoke方法改成这样:

Object returnValue = null;
float money = (float) args[0];
if("saleProduct".equals(method.getName())){
    returnValue = method.invoke(producer,money*0.8f);
}else{
    returnValue = method.invoke(producer,args);
}
return returnValue;

method.getName()能获取进入该代理的方法名,判断是销售后,则进行抽取20%,否则原封不动返回出去

接下来让我们试试用「基于子类的动态代理」

基于子类的动态代理

涉及的类:Enhancer(由第三方库cglib提供)

由create方法创建代理对象,被代理类不能是最终类。

create方法的参数有2个

  1. class:指定被代理对象的字节码
  2. Callback: 用于增加代码

不过我们一般会用Callback的子接口实现类MethodInterceptor(相当于InvocationHandle)

在karasawa包下新建包命cglib,proxy包中的Client和Product复制到cglib包下,用Maven导入cglib的依赖

  <dependencies>
    <dependency>
      <groupId>cglib</groupId>
      <artifactId>cglib</artifactId>
      <version>2.1_3</version>
    </dependency>
  </dependencies>

将Client修改成这样:

package com.karasawa.cglib;

import java.lang.reflect.Method;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

/**
 * @program: SpringDynamicProxy
 * @description: 模拟客户
 * @author: 唐育森
 * @create: 2020-02-24 22:43
 **/
public class Client {

  public static void main(String[] args) {
    final Producer producer = new Producer();
    Producer cglibProducer = (Producer) Enhancer
        .create(producer.getClass(), new MethodInterceptor() {

          @Override
          public Object intercept(Object o, Method method, Object[] objects,
              MethodProxy methodProxy)
              throws Throwable {
            Object returnValue;
            Float money = (Float) objects[0];
            if ("saleProduct".equals(method.getName())) {
              returnValue = method.invoke(producer, money * 0.8f);
            } else {
              returnValue = method.invoke(producer, objects);
            }
            return returnValue;
          }
        });
    cglibProducer.saleProduct(100f);
  }
}

运行得到的一样的效果,到这我们都能感觉到动态代理有拦截的功能了。

cglib的方法和基于接口的动态代理很相似,就不过多讲述了,想知道更详细自行搜

spring中的AOP

学习spring的AOP之前,先让我们了解一下几个专业词汇

专业词汇

Joinpoint(连接点)

在基于接口的动态代理中,IProducer中的saleProduct就是一个连接点(IProducer下的所有方法都是连接点),连接着Producer和动态代理

pointCut(切点)

支持增强代码的那些方法,比如上面的例子有这样的代码:

if ("saleProduct".equals(method.getName())) {
    returnValue = method.invoke(producer, money * 0.8f);
}

判断到方法是saleProduct时才会执行增强的功能,此时saleProduct就是一个切点,非saleProduct的其他方法不支持,对应着不是切入点,而是连接点

Advice(通知)

分为四种,前置/后置/异常/最终/环绕通知,在上面的invoke方法里,我们给他加上try/catch/finally

@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Object returnValue = null;
    float money = (float) args[0];
    try{
        if("saleProduct".equals(method.getName())){
            // -----------写在这,或者在这之前是前置通知    
            returnValue = method.invoke(producer,money*0.8f);
            // -----------写在这,或者在这之后是后置通知
        }else{
            returnValue = method.invoke(producer,args);
        }
            return returnValue;
        }
    } catch(Exception e) {
        // ------------写着这里的是异常通知
        throw new RuntimeException(e);
    } finally {
        // ------------ 写在这里的是最终通知
    }

}

Target(目标)

代理的目标对象,即被代理对象,上面的例子Target是IProducer

weaving(织入)

指吧增强应用添加到目标对象的过程,对应上面添加了money*0.8这个过程,上面只是小例子,在开发中,往往不会这么简单,可能是添加了事务,加入事务的这个过程,就叫织入

Proxy(代理)

被增强后,产生的一个结果代理类,上面是proxyProducer

Aspect(切面)

切面是切点和通知的结合,切点是一个方法,通知也是方法,我们需要先配置哪个通知为前置通知,哪个为后置通知,哪个为异常通知等等,而这一个过程,则叫切面

xlIcPg

理解不了可以先放着,自己用XML方式去配置一下AOP,就能明白了

使用AOP

想要用Spring的AOP,还需要导入一个依赖

  <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>

spring我就用注解吧,xml配置有些许麻烦 = - =

我们让IProducer多添加些方法

public interface IProducer {
  // 销售
  void saleProduct(float money);

  // 产品售后
  void serviceProduct();

  // 制造产品
  void makeProduct();
}

然后实现类

package com.karasawa.proxy;

import org.springframework.stereotype.Service;
public class Producer implements IProducer {

  @Override
  public void saleProduct(float money) {
    System.out.println("卖出装备,赚" + money);
  }

  @Override
  public void serviceProduct() {
    System.out.println("进行售后");
  }

  @Override
  public void makeProduct() {
    System.out.println("进行制造");
  }
}
public class Client {
  public static void main(String[] args) {
    Producer producer = new Producer();
    producer.saleProduct(100);
    producer.serviceProduct();
    producer.makeProduct();
  }
}

简单运行没问题。在karasawa包下创建一个工具包,创建一个工具类,用于打印日志,作为前置通知

开始配置AOP


@Component("logger")
@Aspect
public class Logger {

  //配置该方法为切点,execution()的意思是在这个包下面所有的方法都是切点
  @Pointcut("execution(* com.karasawa.proxy.*.*(..))")
  private void pointCut() {}

  //配置改方法为前置通知,并把切点传进来
  @Before("pointCut()")
  public void printLog(){
    System.out.println("这是日志");
  }

  //配置改方法为后置通知,并把切点传进来
  @AfterReturning("pointCut()")
  public void backNoice(){
    System.out.println("这是后置通知");
  }

  //配置改方法为异常通知,并把切点传进来
  @AfterThrowing("pointCut()")
  public void ExceptionNice(){
    System.out.println("这是异常通知");
  }

  //配置改方法为最终通知,并把切点传进来
  @After("pointCut()")
  public void finallyNoice(){
    System.out.println("这是最终通知");
  }
}

这样AOP就配置好了,差一个开启AOP,创建Spring的配置类,SringConfiguration

//表示这是一个Spring配置类
@Configuration
//表示扫描哪个包下的类
@ComponentScan("com.karasawa.*")
//表示开启AOP
@EnableAspectJAutoProxy
public class SpringConfiguration {}

让Producer类注解@Service,IProducer注解@Component,最后修改Client运行:

public class Client extends HttpServlet {

  public static void main(String[] args) {
    ApplicationContext springBean = new AnnotationConfigApplicationContext(SpringConfiguration.class);
    IProducer producer = (IProducer) springBean.getBean("producer");
    producer.serviceProduct();
  }
}
这是日志
进行售后
这是最终通知
这是后置通知

Process finished with exit code 0

发现了没,本应该后置在最终之前的,这是Spring的锅,不是我!Spring用纯注解或者注解来配置AOP的时候,后置通知的执行会在最终通知之前,有必要可以用环绕通知来解决,这个通知是我上面没讲的,总的来说,这个通知的内容就是我们最顶上写的动态代理中的invoke函数内容,没错!自己写!这样顺序就不会乱!

这里顺便写一下XML的AOP配置,方便以后自己翻一翻,放在< beans >标签里,需要导一些支持AOP的链接,这就不写了

<aop:config>
    <!-- 配置切入点表达式id属性用于指定表达式的唯一标识。expression属性用于指定表达式内容此标签写在aop:aspect标签内部只能当前切面使用。它还可以写在aop:aspect外面,此时就变成了所有切面可用 -->
    <acp:pointcut id="pt1"expression="execution(f com.itheima.service.impl.*.*(..))"/>
    <!-- 配置切面 -->
    <aop:aspect id="logAdvice"ref="logger">
        <!-- 配置前置通知:在切入点方法执行之前执行 -->
        <aop:before method="beforePrintLog"pointcut-ref="pt1"/>
        <!-- 配置后置通知:在切入点方法正常执行之后。它和异常通知永远只能执行一个-->
        <aop:after-returning method="afterReturningPrintLog"pointcut-ref="pt1"/>
        <!-- 配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个 -->
        <aop:after-throwing method="afterThrowingPrintLog"pointcut-ref="pt1"/>
        <!-- 配置最终通知:无论切入点方法是否正常执行它都会在其后面执行 -->
        <aop:after method="afterPrintLog"pointcut-ref="pt1"/>
        <!-- 配置环绕通知详细的注释请看Logger类中-->
        <aop:around method="aroundPringLog"pointcut-ref="pt1"/>
    </aop;aspect>
</aop:config〉

好了,以上就是本次学习AOP的记录,记得很粗糙,质量不是很好,本来不想记录的,细节、内容实在是太多了,想了想还是记下来让自己熟悉一会吧