1、理解AOP

1.1、什么是AOP

AOP(Aspect Oriented Programming),面向切面思想,是Spring的三大核心思想之一(两外两个:IOC-控制反转、DI-依赖注入)。

那么AOP为何那么重要呢?在我们的程序中,经常存在一些系统性的需求,比如权限校验、日志记录、统计等,这些代码会散落穿插在各个业务逻辑中,非常冗余且不利于维护。例如下面这个示意图:
在这里插入图片描述
有多少业务操作,就要写多少重复的校验和日志记录代码,这显然是无法接受的。当然,用面向对象的思想,我们可以把这些重复的代码抽离出来,写成公共方法,就是下面这样:
在这里插入图片描述
这样,代码冗余和可维护性的问题得到了解决,但每个业务方法中依然要依次手动调用这些公共方法,也是略显繁琐。有没有更好的方式呢?有的,那就是AOP,AOP将权限校验、日志记录等非业务代码完全提取出来,与业务代码分离,并寻找节点切入业务代码中:
在这里插入图片描述

1.2、AOP体系与概念

简单地去理解,其实AOP要做三类事:

  • 在哪里切入,也就是权限校验等非业务操作在哪些业务代码中执行。
  • 在什么时候切入,是业务代码执行前还是执行后。
  • 切入后做什么事,比如做权限校验、日志记录等。

因此,AOP的体系可以梳理为下图:
在这里插入图片描述
一些概念详解

  • Aspect:切面,即PointcutAdvice
  • Pointcut:切点,决定处理如权限校验、日志记录等在何处切入业务代码中(即织入切面)。切点分为execution方式和annotation方式。前者可以用路径表达式指定哪些类织入切面,后者可以指定被哪些注解修饰的代码织入切面。
  • Advice:处理,包括处理时机和处理内容。处理内容就是要做什么事,比如校验权限和记录日志。处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。
  • Joint point:连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。
  • Weaving:织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。

1.3、Spring AOP 通知的执行顺序

1、正常情况
在这里插入图片描述

2、异常情况
在这里插入图片描述
3、多个切面的情况
在这里插入图片描述


2、代理模式

  • 1、代理模式
  • 2、静态代理
  • 3、动态代理
    • 3.1、JDK动态代理机制
    • 3.2、CGLIB动态代理机制
    • 3.3、JDK动态代理和CGLIB动态代理对比
  • 4、静态代理和动态代理的对比
  • 5、总结

1、为什么要存在代理呢?

存在一个常见的需求:怎样在不修改类A代码的情况下,在调用类A的方法时进行一些功能的附加与增强呢?
先不考虑什么代理不代理的,我们设计一个简单的实现方案:

新创建一个类B,类B组合类A,在类B中创建一个方法b,方法b中调用类A中的方法a,在调用前和调用后都可以添加一些自定义的附加与增强代码。当有需求需要调用类A的方法a并且想要添加一个附加功能时,就去调用类B的方法b即可实现上述需求;

代理存在的意义:使用代理模式可以在不修改代理对象代码的基础上,通过扩展代理类,进行一些功能的附加与增强

2、代理模式的介绍

代理模式是一种比较好的理解的设计模式。简单来说就是 我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。

代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。

代理模式(Proxy),为其他对象提供一种代理以控制对这个对象的访问。他的特征是代理类与委托类实现相同的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类以及事后处理消息等。代理类与委托类质检通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。简单来说就是,我们访问实际对象时,是通过代理对象来访问的,代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,使我们可以附加多种用途。

代理模式结构图
在这里插入图片描述


2.1、静态代理

静态代理:由程序员创建或特定工具自动生成源代码,也就是在编译时就已经将接口,被代理类,代理类等确定下来。在程序运行之前,代理类的.class文件就已经生成。

静态代理中,我们对目标对象的每个方法的增强都是手动完成的(后面会具体演示代码),非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。 实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。

上面我们是从实现和应用角度来说的静态代理,从 JVM 层面来说, 静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。

静态代理实现步骤

  • 1、定义一个接口及其实现类;
  • 2、创建一个代理类同样实现这个接口
  • 3、将目标对象注注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。

下面通过代码展示!

案例一


1.定义发送短信的接口

public interface SmsService {
    String send(String message);
}

2.实现发送短信的接口

public class SmsServiceImpl implements SmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}

3.创建代理类并同样实现发送短信的接口

public class SmsProxy implements SmsService {

    private final SmsService smsService;

    public SmsProxy(SmsService smsService) {
        this.smsService = smsService;
    }

    @Override
    public String send(String message) {
        //调用方法之前,我们可以添加自己的操作
        System.out.println("before method send()");
        smsService.send(message);
        //调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method send()");
        return null;
    }
}

4.实际使用

public class Main {
    public static void main(String[] args) {
        SmsService smsService = new SmsServiceImpl();
        SmsProxy smsProxy = new SmsProxy(smsService);
        smsProxy.send("java");
    }
}

运行上述代码之后,控制台打印出:

before method send()
send message:java
after method send()

可以输出结果看出,我们已经增加了 SmsServiceImpl 的send()方法。


案例二

1、被代理类实现的接口

public interface HelloService {
    void hello();
}

2、被代理类

public class HelloServiceImpl implements HelloService {
    public void hello() {
        System.out.println("你吃了嘛?");
    }
}

3、代理类

public class HelloServiceStaticProxy implements HelloService {
    private HelloService helloService;

    public HelloServiceStaticProxy(HelloService helloService) {
        this.helloService = helloService;
    }

    public void hello() {
        System.out.println("你好,我是小王!");
        this.helloService.hello();
        System.out.println("好的,下次家里聊!");
    }
}

4、测试

@Test
public void staticProxy() {
    HelloServiceStaticProxy helloServiceStaticProxy = new HelloServiceStaticProxy(new HelloServiceImpl());
    helloServiceStaticProxy.hello();
}

结果

你好,我是小王!
你吃了嘛?
好的,下次家里聊!

小结
静态代理要求代理类和被代理类实现同一个接口,代理对象需要持有被代理的目标对象,在代理对象实现接口方法前后添加增强逻辑并调用目标对象方法。


案例三
静态代理是代理类在编译期就创建好了,不是编译器生成的代理类,而是我们手动创建的类。在编译时就已经将接口、本代理类和代理类确定下来。软件设计模式中所指的代理一般就是说的静态代理。
1、接口
Subject类,定义了RealSubject和Proxy的共用接口,这样就在任何使用RealSubject的地方都可以使用Proxy。

public interface Subject {
    /**
     * doSomething()
     */
    void doSomething();
}

2、RealSubject类,定义Proxy所代表的真实实体。

public class RealSubject implements Subject {
    @Override
    public void doSomething() {
        // 委托类执行操作
        System.out.println("RealSubject.doSomething()");
    }
}

3、ProxySubject类,保存一个引用使得代理可以访问实体,并提供一个与Subject的接口相同的接口,这样代理就可以用来替代实体。

public class ProxySubject implements Subject {

    private RealSubject realSubject;

    /**
     * 向代理类中注入委托类对象
     *
     * @param realSubject 委托类对象
     */
    public ProxySubject(RealSubject realSubject){
        this.realSubject = realSubject;
    }

    /**
     * 代理类执行操作
     */
    @Override
    public void doSomething() {
        System.out.println("代理类调用委托类方法之前");
        realSubject.doSomething();
        System.out.println("代理类调用委托类方法之后");
    }
}

测试第一种方式,不使用代理类,直接使用简单委托类执行。

public static void main(String[] args) {
        RealSubject realSubject = new RealSubject();
        
        realSubject.doSomething();
    }

输出:

RealSubject.doSomething()

测试第二种方式,使用代理类,执行增强逻辑。

public static void main(String[] args) {
        RealSubject realSubject = new RealSubject();

        ProxySubject proxySubject = new ProxySubject(realSubject);
        
        proxySubject.doSomething();
    }

输出:

代理类调用委托类方法之前
RealSubject.doSomething()
代理类调用委托类方法之后

我们在创建代理对象时,通过构造器塞入一个目标对象,然后在代理对象的方法内部调用目标对象同名方法,并在调用前后做增强逻辑。也就是说,代理对象 = 增强代码 + 目标对象。有了代理对象后,就不用原对象了。

静态代理的缺陷
开发者需要手动为目标类编写对应的代理类,而且要对类中的每个方法都编写增强逻辑的代码,如果当前系统中已经存在成百上千个类,工作量太大了,且重复代码过多。所以,有没有什么方法能让我们少写或者不写代理类,却能完成代理功能?


案例四
根据上面代理模式的类图,来写一个简单的静态代理的例子。举一个比较粗糙的例子,假如一个班的同学要向老师交班费,但是都是通过班长把自己的钱转交给老师。这里,班长就是代理学生上交班费,

班长就是学生的代理。

首先,我们创建一个Person接口。这个接口就是学生(被代理类),和班长(代理类)的公共接口,他们都有上交班费的行为。这样,学生上交班费就可以让班长来代理执行。
1、接口

* 创建Person接口
 * @author Gonjan
 */
public interface Person {
    //上交班费
    void giveMoney();
}

2、被代理对象——Student类实现Person接口。Student可以具体实施上交班费的动作。

public class Student implements Person {
    private String name;
    public Student(String name) {
        this.name = name;
    }
    
    @Override
    public void giveMoney() {
       System.out.println(name + "上交班费50元");
    }
}

3、代理类——StudentsProxy类,这个类也实现了Person接口,但是还另外持有一个学生类对象,由于实现了Peson接口,同时持有一个学生对象,那么他可以代理学生类对象执行上交班费(执行giveMoney()方法)行为。



/**
 * 学生代理类,也实现了Person接口,保存一个学生实体,这样既可以代理学生产生行为
 * @author Gonjan
 *
 */
public class StudentsProxy implements Person{
    //被代理的学生
    Student stu;
    
    public StudentsProxy(Person stu) {
        // 只代理学生对象
        if(stu.getClass() == Student.class) {
            this.stu = (Student)stu;
        }
    }
    
    //代理上交班费,调用被代理学生的上交班费行为
    public void giveMoney() {
        stu.giveMoney();
    }
}

下面测试一下,看如何使用代理模式:
public class StaticProxyTest {
    public static void main(String[] args) {
        //被代理的学生张三,他的班费上交有代理对象monitor(班长)完成
        Person zhangsan = new Student("张三");
        
        //生成代理对象,并将张三传给代理对象
        Person monitor = new StudentsProxy(zhangsan);
        
        //班长代理上交班费
        monitor.giveMoney();
    }
}

运行结果:
在这里插入图片描述
这里并没有直接通过张三(被代理对象)来执行上交班费的行为,而是通过班长(代理对象)来代理执行了。这就是代理模式。

代理模式最主要的就是有一个公共接口(Person),一个具体的类(Student),一个代理类(StudentsProxy),代理类持有具体类的实例,代为执行具体类实例方法。上面说到,代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。这里的间接性就是指不直接调用实际对象的方法,那么我们在代理过程中就可以加上一些其他用途。就这个例子来说,加入班长在帮张三上交班费之前想要先反映一下张三最近学习有很大进步,通过代理模式很轻松就能办到:

public class StudentsProxy implements Person{
    //被代理的学生
    Student stu;
    
    public StudentsProxy(Person stu) {
        // 只代理学生对象
        if(stu.getClass() == Student.class) {
            this.stu = (Student)stu;
        }
    }
    
    //代理上交班费,调用被代理学生的上交班费行为
    public void giveMoney() {
        System.out.println("张三最近学习有进步!");
        stu.giveMoney();
    }
}

运行结果:
在这里插入图片描述
可以看到,只需要在代理类中帮张三上交班费之前,执行其他操作就可以了。这种操作,也是使用代理模式的一个很大的优点。最直白的就是在Spring中的面向切面编程(AOP),我们能在一个切点之前执行一些操作,在一个切点之后执行一些操作,这个切点就是一个个方法。这些方法所在类肯定就是被代理了,在代理过程中切入了一些其他操作。


2.2、静态代理的缺点

所谓静态代理也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。

上面的代码就是实现了一个静态代理;其实静态代理就已经能够满足上述需求了,为什么还需要动态代理呢?这里就涉及到静态代理的两个缺点了

  • 1、代理对象的一个接口只服务于一种类型的对象,如果要代理的方法很多,势必要为每一种方法都进行代理,在程序规模稍大时静态代理代理类就会过多会造成代码混乱
  • 2、如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法,增加了代码维护的复杂度。

基于上述两个问题,动态代理诞生了~


3、动态代理

相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类( CGLIB 动态代理机制)。

从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

JDK 动态代理机制——目标对象实现了接口


1、jdk动态代理原理

在java动态代理机制中,InvocationHandler接口和Proxy类是核心
Proxy 类中使用频率最高的方法是:newProxyInstance() ,这个方法主要用来生成一个代理对象。

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        ......
    }

这个方法一共有 3 个参数

  • 1、loader :类加载器,用于加载代理对象。
  • 2、interfaces : 被代理类实现的一些接口;
  • 3、h : 实现了 InvocationHandler 接口的对象;

要实现动态代理的话,还必须需要实现InvocationHandler 来自定义处理逻辑。当我们的动态代理对象调用一个方法时候,这个方法的调用就会被转发到实现InvocationHandler 接口类的 invoke 方法来调用。

public interface InvocationHandler {
    /**
     * 当你使用代理对象调用方法的时候实际会调用到这个方法
     */
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

invoke() 方法有下面三个参数:

  • 1、proxy :动态生成的代理类
  • 2、method : 与代理类对象调用的方法相对应
  • 3、args : 当前 method 方法的参数

也就是说:你通过Proxy 类的 newProxyInstance() 创建的代理对象在调用方法的时候,实际会调用到实现InvocationHandler 接口的类的 invoke()方法。 你可以在 invoke() 方法中自定义处理逻辑,比如在方法执行前后做什么事情。

2、JDK 动态代理类使用步骤

  • 1、定义一个接口及其实现类;
  • 2、自定义 InvocationHandler 并重写invoke方法,在 invoke 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;
  • 3、通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;

案例一


1、定义发送短信的接口

public interface SmsService {
    String send(String message);
}

2、实现发送短信的接口

public class SmsServiceImpl implements SmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}

3、定义一个 JDK 动态代理类

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

/**
 * @author shuang.kou
 * @createTime 2020年05月11日 11:23:00
 */
public class DebugInvocationHandler implements InvocationHandler {
    /**
     * 代理类中的真实对象
     */
    private final Object target;

    public DebugInvocationHandler(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException {
        //调用方法之前,我们可以添加自己的操作
        System.out.println("before method " + method.getName());
        Object result = method.invoke(target, args);
        //调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method " + method.getName());
        return result;
    }
}

invoke() 方法: 当我们的动态代理对象调用原生方法的时候,最终实际上调用到的是 invoke() 方法,然后 invoke() 方法代替我们去调用了被代理对象的原生方法。
4、获取代理对象的工厂类

public class JdkProxyFactory {
    public static Object getProxy(Object target) {
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(), // 目标类的类加载
                target.getClass().getInterfaces(),  // 代理需要实现的接口,可指定多个
                new DebugInvocationHandler(target)   // 代理对象对应的自定义 InvocationHandler
        );
    }
}

getProxy() :主要通过Proxy.newProxyInstance()方法获取某个类的代理对象

5、实际使用

SmsService smsService = (SmsService) JdkProxyFactory.getProxy(new SmsServiceImpl());
smsService.send("java");

运行上述代码之后,控制台打印出:

before method send
send message:java
after method send

CGLIB 动态代理机制——目标对象没有实现接口

CGLIB 动态代理类使用步骤

  • 1、定义一个类;
  • 2、自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;
  • 3、通过 Enhancer 类的 create()创建代理类;

4、JDK 动态代理和 CGLIB 动态代理对比

1、JDK 动态代理只能只能代理实现了接口的类,而 CGLIB 可以代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。

2、就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。


5、静态代理和动态代理的对比

1、灵活性 :动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!

2、JVM 层面 :静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐