【Spring】代理模式

文章目录

代理模式

对代理模式的理解

代理模式作用:保护对象,功能增强

代理模式的作用是:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为"代理"的第三者来实现间接引用。代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过**代理对象去掉客户不应该看到的内容和服务或者添加客户需要的额外服务。 **通过引入一个新的对象来实现对真实对象的操作或者将新的对象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一个对象,这就是代理模式的模式动机。

代理模式中的角色:

  • 代理类(代理主题)
  • 目标类(真实主题)
  • 代理类和目标类的公共接口(抽象主题):客户端在使用代理类时就像在使用目标类,不被客户端所察觉,所以代理类和目标类要有共同的行为,也就是实现共同的接口。

代理模式的类图:

代理模式在代码实现上,包括两种形式:

  • 静态代理
  • 动态代理

静态代理

现在有这样一个接口和实现类:

java 复制代码
/**
 * 订单接口
 **/
public interface OrderService {
    void generate();
    void detail();
    void modify();
}
public class OrderServiceImpl implements OrderService {
    @Override
    public void generate() {
        try {
            Thread.sleep(1234);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已生成");
    }

    @Override
    public void detail() {
        try {
            Thread.sleep(2541);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单信息如下:******");
    }
    @Override
    public void modify() {
        try {
            Thread.sleep(1010);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已修改");
    }
}

其中Thread.sleep()方法的调用是为了模拟操作耗时。

项目已上线,并且运行正常,只是客户反馈系统有一些地方运行较慢,要求项目组对系统进行优化。于是项目负责人就下达了这个需求。首先需要搞清楚是哪些业务方法耗时较长,于是让我们统计每个业务方法所耗费的时长。如果是你,你该怎么做呢?
第一种方案:直接修改Java源代码,在每个业务方法中添加统计逻辑,如下:

java 复制代码
public class OrderServiceImpl implements OrderService {
    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(1234);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已生成");
        long end = System.currentTimeMillis();
        System.out.println("耗费时长"+(end - begin)+"毫秒");
    }

    @Override
    public void detail() {
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(2541);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单信息如下:******");
        long end = System.currentTimeMillis();
        System.out.println("耗费时长"+(end - begin)+"毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        try {
            Thread.sleep(1010);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已修改");
        long end = System.currentTimeMillis();
        System.out.println("耗费时长"+(end - begin)+"毫秒");
    }
}

需求可以满足,但显然是违背了OCP开闭原则。这种方案不可取。
第二种方案:编写一个子类继承OrderServiceImpl,在子类中重写每个方法,代码如下:

java 复制代码
public class OrderServiceImplSub extends OrderServiceImpl{
    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        super.generate();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void detail() {
        long begin = System.currentTimeMillis();
        super.detail();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }

    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        super.modify();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }
}

这种方式可以解决,但是存在两个问题:

  • 第一个问题:假设系统中有100个这样的业务类,需要提供100个子类,并且之前写好的创建Service对象的代码,都要修改为创建子类对象。
  • 第二个问题:由于采用了继承的方式,导致代码之间的耦合度较高。

这种方案也不可取。
第三种方案 :使用代理模式(这里采用静态代理):静态代理采用的是装饰器模式

可以为OrderService接口提供一个代理类。

java 复制代码
public class OrderServiceProxy implements OrderService{ // 代理对象

    // 目标对象
    private OrderService orderService;

    // 通过构造方法将目标对象传递给代理对象
    public OrderServiceProxy(OrderService orderService) {
        this.orderService = orderService;
    }
    @Override
    public void generate() {
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.generate();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }
    @Override
    public void detail() {
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.detail();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }
    @Override
    public void modify() {
        long begin = System.currentTimeMillis();
        // 执行目标对象的目标方法
        orderService.modify();
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
    }
}

这种方式的优点:符合OCP开闭原则,同时采用的是关联关系,所以程序的耦合度较低。所以这种方案是被推荐的。

编写客户端程序:

java 复制代码
public class Client {
    public static void main(String[] args) {
        // 创建目标对象
        OrderService target = new OrderServiceImpl();
        // 创建代理对象
        OrderService proxy = new OrderServiceProxy(target);
        // 调用代理对象的代理方法
        proxy.generate();
        proxy.modify();
        proxy.detail();
    }
}

以上就是代理模式中的静态代理,其中OrderService接口是代理类和目标类的共同接口。OrderServiceImpl是目标类。OrderServiceProxy是代理类。

大家思考一下:如果系统中业务接口很多,一个接口对应一个代理类,显然也是不合理的,会导致类爆炸。怎么解决这个问题?动态代理可以解决。因为在动态代理中可以在内存中动态的为我们生成代理类的字节码。代理类不需要我们写了。类爆炸解决了,而且代码只需要写一次,代码也会得到复用。

动态代理

静态代理和动态代理的区别:

  • 静态代理是在编译时就已经将接口、代理类、被代理类的字节码文件确定下来。
  • 动态代理是程序在运行后通过反射创建字节码文件交由 JVM 加载。

在程序运行阶段,在内存中动态生成代理类,被称为动态代理,目的是为了减少代理类的数量。解决代码复用的问题。

在内存当中动态生成类的技术常见的包括:

  • JDK动态代理技术:只能代理接口。
  • CGLIB动态代理技术:CGLIB(Code Generation Library)是一个开源项目。是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。它既可以代理接口,又可以代理类,底层是通过继承的方式实现的 。性能比JDK动态代理要好。(底层有一个小而快的字节码处理框架ASM。)
  • Javassist动态代理技术:Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。

JDK动态代理

我们还是使用静态代理中的例子:一个接口和一个实现类。

java 复制代码
public interface OrderService {
    void generate();
    void detail();
    void modify();
}
public class OrderServiceImpl implements OrderService {
    @Override
    public void generate() {
        try {
            Thread.sleep(1234);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已生成");
    }

    @Override
    public void detail() {
        try {
            Thread.sleep(2541);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单信息如下:******");
    }

    @Override
    public void modify() {
        try {
            Thread.sleep(1010);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已修改");
    }
}

我们在静态代理的时候,除了以上一个接口和一个实现类之外,还要写一个代理类UserServiceProxy呀!在动态代理中UserServiceProxy代理类是可以动态生成的。这个类不需要写。我们直接写客户端程序即可:

java 复制代码
public class Client {
    public static void main(String[] args) {
        // 第一步:创建目标对象
        OrderService target = new OrderServiceImpl();
        // 第二步:创建代理对象
        OrderService orderServiceProxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), 调用处理器对象);
        // 第三步:调用代理对象的代理方法
        orderServiceProxy.detail();
        orderServiceProxy.modify();
        orderServiceProxy.generate();
    }
}

OrderService orderServiceProxy = Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(), 调用处理器对象);

这行代码做了两件事:

  • 第一件事:在内存中生成了代理类的字节码
  • 第二件事:创建代理对象

Proxy类全名:java.lang.reflect.Proxy。这是JDK提供的一个类(所以称为JDK动态代理)。主要是通过这个类在内存中生成代理类的字节码。

其中newProxyInstance()方法有三个参数:

  • 第一个参数:类加载器。在内存中生成了字节码,要想执行这个字节码,也是需要先把这个字节码加载到内存当中的。所以要指定使用哪个类加载器加载。
  • 第二个参数:接口类型。代理类和目标类实现相同的接口,所以要通过这个参数告诉JDK动态代理生成的类要实现哪些接口。
  • 第三个参数:调用处理器。这是一个JDK动态代理规定的接口,接口全名:java.lang.reflect.InvocationHandler。显然这是一个回调接口,也就是说调用这个接口中方法的程序已经写好了,就差这个接口的实现类了。(可以在这里面写增强代码)

所以接下来我们要写一下java.lang.reflect.InvocationHandler接口的实现类,并且实现接口中的方法,代码如下:

InvocationHandler接口中有一个方法invoke,这个invoke方法上有三个参数:

  • 第一个参数:Object proxy。代理对象。设计这个参数只是为了后期的方便,如果想在invoke方法中使用代理对象的话,尽管通过这个参数来使用。
  • 第二个参数:Method method。目标方法。
  • 第三个参数:Object[] args。目标方法调用时要传的参数。

我们将来肯定是要调用"目标方法"的,但要调用目标方法的话,需要"目标对象"的存在,"目标对象"从哪儿来呢?我们可以给TimerInvocationHandler提供一个构造方法,可以通过这个构造方法传过来"目标对象",代码如下:有了目标对象我们就可以在invoke()方法中调用目标方法了。代码如下:

java 复制代码
public class TimerInvocationHandler implements InvocationHandler {
    // 目标对象
    private Object target;

    // 通过构造方法来传目标对象
    public TimerInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 目标执行之前增强。
        long begin = System.currentTimeMillis();
        // 调用目标对象的目标方法
        Object retValue = method.invoke(target, args);
        // 目标执行之后增强。
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");
        // 一定要记得返回哦。
        return retValue;
    }
}

到此为止,调用处理器就完成了。接下来,应该继续完善Client程序:

java 复制代码
public class Client {
    public static void main(String[] args) {
        // 创建目标对象
        OrderService target = new OrderServiceImpl();
        // 创建代理对象
        OrderService orderServiceProxy = (OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                                                                                target.getClass().getInterfaces(),
                                                                                new TimerInvocationHandler(target));
        // 调用代理对象的代理方法
        orderServiceProxy.detail();
        orderServiceProxy.modify();
        orderServiceProxy.generate();
    }
}

大家可能会比较好奇:那个InvocationHandler接口中的invoke()方法没看见在哪里调用呀?

注意:当你调用代理对象的代理方法的时候,注册在InvocationHandler接口中的invoke()方法会被调用。也就是上面代码第24 25 26行,这三行代码中任意一行代码执行,注册在InvocationHandler接口中的invoke()方法都会被调用。

学到这里可能会感觉有点懵,折腾半天,到最后这不是还得写一个接口的实现类吗?没省劲儿呀?

你要这样想就错了!!!

我们可以看到,不管你有多少个Service接口,多少个业务类,这个TimerInvocationHandler接口是不是只需要写一次就行了,代码是不是得到复用了!!!!

而且最重要的是,以后程序员只需要关注核心业务的编写了,像这种统计时间的代码根本不需要关注。因为这种统计时间的代码只需要在调用处理器中编写一次即可。

到这里,JDK动态代理的原理就结束了。

原理

我们先来模拟以下JDK动态代理的实现

首先一个提供一个接口和一个目标类

java 复制代码
interface Foo {
    void foo();
}

public class Target implements Foo {
    @Override
    public void foo() {
        System.out.println("target foo");
    }
}
java 复制代码
OrderService orderServiceProxy = Proxy.newProxyInstance(
    target.getClass().getClassLoader(), 
    target.getClass().getInterfaces(), 
    调用处理器对象);

根据上面代码可知,我们将接口Class传递过去,故JDK就生成一个类并实现了Foo接口,并对其功能进行增强。

java 复制代码
public class $Proxy0 implements Foo{
    @Override
    public void foo() {
        // 1. 功能增强
        System.out.println("before...");
        // 2. 调用目标
        new Target().foo();
    }
}
//测试一下
public static void main(String[] args) {
    $Proxy0 proxy = new $Proxy0();
    proxy.foo();
}

代码的实现很简单,但仔细想一下,如果是 JDK 中的实现:

  • "功能增强" 的代码实现会直接硬编码吗?直接打印?
  • "调用目标" 的代码就这样直接写上去?存不存在满足某些条件才调用目标的场景呢?

也就是说,"功能增强" 和 "调用目标" 这两部分的代码都是不确定的。

针对这种 "不确定" 的实现,可以提供一个抽象类,等到用户具体使用时才实现抽象类,重写抽象方法。

java 复制代码
interface InvocationHandler {
    void invoke();
}

public class $Proxy0 implements Foo{
    private final InvocationHandler h;
    public $Proxy0(InvocationHandler h) {
        this.h = h;
    }

    @Override
    public void foo() {
        h.invoke();
    }
}

public static void main(String[] args) {
    $Proxy0 proxy = new $Proxy0(new InvocationHandler() {
        @Override
        public void invoke() {
            // 1. 功能增强
            System.out.println("before...");
            // 2. 调用目标
            new A13.Target().foo();
        }
    });
    proxy.foo();
}

这样的实现依旧有问题,如果接口中提供了两个抽象方法呢?比如:

java 复制代码
interface Foo {
    void foo();
    void bar();
}

public class Target implements Foo {
    @Override
    public void foo() {
        System.out.println("target foo");
    }

    @Override
    public void bar() {
        System.out.println("target bar");
    }
}
java 复制代码
public class $Proxy0 implements A13.Foo{

    private final A13.InvocationHandler h;

    public $Proxy0(A13.InvocationHandler h) {
        this.h = h;
    }

    @Override
    public void foo() {
        h.invoke();
    }

    @Override
    public void bar() {
        h.invoke();
    }
}

public static void main(String[] args) {
    $Proxy0 proxy = new $Proxy0(new InvocationHandler() {
        @Override
        public void invoke() {
            // 1. 功能增强
            System.out.println("before...");
            // 2. 调用目标
            new A13.Target().foo();
        }
    });
    proxy.foo();
    // 调用另一个方法
    proxy.bar();
}

结果是调用了两次目标对象的foo()方法。

如何解决这个问题呢?

可以在 invoke() 方法中添加两个入参,分别表示需要调用的目标方法和目标方法的参数:

java 复制代码
interface InvocationHandler {
    void invoke(Method method, Object[] params) throws Throwable;
}

增加参数之后需要修改代理类,并将实现的抽象方法的 Method 对象与参数传递给 invoke() 方法:

java 复制代码
public class $Proxy0 implements A13.Foo{

    private final A13.InvocationHandler h;

    public $Proxy0(A13.InvocationHandler h) {
        this.h = h;
    }

    @Override
    @SneakyThrows
    public void foo() {
        Method method = A13.Foo.class.getMethod("foo");
        h.invoke(method, new Object[0]);
    }

    @Override
    @SneakyThrows
    public void bar() {
        Method method = A13.Foo.class.getMethod("bar");
        h.invoke(method, new Object[0]);
    }
}

还需要修改下 main() 方法中 InvocationHandler 的实现,利用传递的 Method 对象和参数信息反射调用目标方法:

java 复制代码
public static void main(String[] args) {
    $Proxy0 proxy = new $Proxy0(new InvocationHandler() {
        @Override
        public void invoke(Method method, Object[] params) throws Throwable {
            // 1. 功能增强
            System.out.println("before...");
            // 2. 调用目标
            Object invoke = method.invoke(new Target(), params);
        }
    });
    proxy.foo();
    // 调用另一个方法
    proxy.bar();
}

继续做以下优化

  • 提供返回值
  • 每调用一次代理对象中的方法都会创建一个 Method 实例,这些实例是可以复用的,因此可以将这些实例的创建移动到静态代码块中
  • 提供代理对象参数

最终的代码

java 复制代码
interface InvocationHandler {
    Object invoke(Object proxy, Method method, Object[] params) throws Throwable;
}

public class $Proxy0 implements Foo {

    private final InvocationHandler h;
    
    static Method foo;
    static Method bar;

    static {
        try {
            foo = A13.Foo.class.getMethod("foo");
            bar = A13.Foo.class.getMethod("bar");
        } catch (NoSuchMethodException e) {
            throw new NoSuchMethodError(e.getMessage());
        }
    }

    public $Proxy0(InvocationHandler h) {
        this.h = h;
    }

    @Override
    public void foo() {
        try {
            h.invoke(this,foo, new Object[0]);
        } catch (RuntimeException | Error e) {
            throw e;
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }

    @Override
    public int bar() {
        try {
            return (int) h.invoke(this,bar, new Object[0]);
        } catch (RuntimeException | Error e) {
            throw e;
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }
}

测试代码

java 复制代码
public static void main(String[] args) {
    $Proxy0 proxy = new $Proxy0(new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
            // 1. 功能增强
            System.out.println("before...");
            // 2. 调用目标
            return method.invoke(new Target(), params);
        }
    });
    proxy.foo();
}

Proxy 类中有一个 InvocationHandler 对象的成员变量。因此还可以使代理类 $Proxy0 继承 Proxy 来进一步减少代码。

源码

JDK 动态代理生成的代理类是以字节码的形式存在的,并不存在所谓的 .java 文件。而字节码文件是在程序运行的时候生成的。我们这里使用 Arthas 反编译代理类字节码文件。

java 复制代码
final class $Proxy0 extends Proxy implements Foo {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void foo() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("indi.mofan.a12.JdkProxyDemo$Foo").getMethod("foo");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

其内容与自定义的 $Proxy0 几乎无异,只不过 JDK 生成的代理类信息还生成 equals()、toString() 和 hashCode() 三个方法对应的 Method 对象,并对它们也进行了相同的增强。

优化

使用 JDK 的动态代理时,会使用反射调用方法,相比于直接调用方法,性能稍微低一些。那么JDK有进行优化吗?

有:当通过反射调用同一个方法达到一个阈值时(17),JDK会自动为我们生成一GeneratedMethodAccessor2代理类,当再次调用这个方法。就不走反射,而是通过代理类直接调用目标对象的方法。因此性能得到了提升,但这样的提升也是有一定代价的:为优化 一个 方法的反射调用,生成了一个 GeneratedMethodAccessor2 代理类。

CGLIB动态代理

使用

CGLIB既可以代理接口,又可以代理类。底层采用继承的方式实现。所以被代理的目标类不能使用final修饰。

使用CGLIB,需要引入它的依赖:

xml 复制代码
<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>

我们准备一个没有实现接口的类,如下:

java 复制代码
public class UserService {
    public void login(){
        System.out.println("用户正在登录系统....");
    }
    public void logout(){
        System.out.println("用户正在退出系统....");
    }
}

使用CGLIB在内存中为UserService类生成代理类,并创建对象:

java 复制代码
public class Client {
    public static void main(String[] args) {
        // 创建字节码增强器
        Enhancer enhancer = new Enhancer();
        // 告诉cglib要继承哪个类
        enhancer.setSuperclass(UserService.class);
        // 设置回调接口
        enhancer.setCallback(方法拦截器对象);
        // 生成源码,编译class,加载到JVM,并创建代理对象
        UserService userServiceProxy = (UserService)enhancer.create();

        userServiceProxy.login();
        userServiceProxy.logout();

    }
}

和JDK动态代理原理差不多,在CGLIB中需要提供的不是InvocationHandler,而是:net.sf.cglib.proxy.MethodInterceptor

编写MethodInterceptor接口实现类:

java 复制代码
public class TimerMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        return null;
    }
}

MethodInterceptor接口中有一个方法intercept(),该方法有4个参数:

第一个参数:目标对象

第二个参数:目标方法

第三个参数:目标方法调用时的实参

第四个参数:代理方法

在MethodInterceptor的intercept()方法中调用目标以及添加增强:

java 复制代码
public class TimerMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 前增强
        long begin = System.currentTimeMillis();
        // 调用目标
        Object retValue = methodProxy.invokeSuper(target, objects);
        // 后增强
        long end = System.currentTimeMillis();
        System.out.println("耗时" + (end - begin) + "毫秒");
        // 一定要返回
        return retValue;
    }
}

回调已经写完了,可以修改客户端程序了:

java 复制代码
public class Client {
    public static void main(String[] args) {
        // 创建字节码增强器
        Enhancer enhancer = new Enhancer();
        // 告诉cglib要继承哪个类
        enhancer.setSuperclass(UserService.class);
        // 设置回调接口
        enhancer.setCallback(new TimerMethodInterceptor());
        // 生成源码,编译class,加载到JVM,并创建代理对象
        UserService userServiceProxy = (UserService)enhancer.create();

        userServiceProxy.login();
        userServiceProxy.logout();

    }
}

对于高版本的JDK,如果使用CGLIB,需要在启动项中添加两个启动参数:

  • --add-opens java.base/java.lang=ALL-UNNAMED
  • --add-opens java.base/sun.net.util=ALL-UNNAMED

执行结果:

原理

同样先模拟一下

目标类:

java 复制代码
public class Target {
    public void save() {
        System.out.println("save()");
    }

    public void save(int i) {
        System.out.println("save(int)");
    }

    public void save(long i) {
        System.out.println("save(long)");
    }
}

CGLib 动态代理生成的代理类:

java 复制代码
public class Proxy extends Target {

    private MethodInterceptor methodInterceptor;

    public void setMethodInterceptor(MethodInterceptor methodInterceptor) {
        this.methodInterceptor = methodInterceptor;
    }

    static Method save0;
    static Method save1;
    static Method save2;
    static MethodProxy save0Proxy;
    static MethodProxy save1Proxy;
    static MethodProxy save2Proxy;

    static {
        try {
            save0 = Target.class.getMethod("save");
            save1 = Target.class.getMethod("save", int.class);
            save2 = Target.class.getMethod("save", long.class);

            save0Proxy = MethodProxy.create(Target.class, Proxy.class, "()V", "save", "saveSuper");
            save1Proxy = MethodProxy.create(Target.class, Proxy.class, "(I)V", "save", "saveSuper");
            save2Proxy = MethodProxy.create(Target.class, Proxy.class, "(J)V", "save", "saveSuper");
        } catch (NoSuchMethodException e) {
            throw new NoSuchMethodError(e.getMessage());
        }
    }

    // >>>>>>>>>>>>>>>>>>>>>>>> 带原始功能的方法
    public void saveSuper() {
        super.save();
    }

    public void saveSuper(int i) {
        super.save(i);
    }

    public void saveSuper(long i) {
        super.save(i);
    }


    // >>>>>>>>>>>>>>>>>>>>>>>> 带增强功能的方法
    @Override
    public void save() {
        try {
            methodInterceptor.intercept(this, save0, new Object[0], save0Proxy);
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }

    @Override
    public void save(int i) {
        try {
            methodInterceptor.intercept(this, save1, new Object[]{i}, save1Proxy);
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }

    @Override
    public void save(long i) {
        try {
            methodInterceptor.intercept(this, save2, new Object[]{i}, save2Proxy);
        } catch (Throwable e) {
            throw new UndeclaredThrowableException(e);
        }
    }
}

测试一下

java 复制代码
public static void main(String[] args) {
    Target target = new Target();

    Proxy proxy = new Proxy();
    proxy.setMethodInterceptor(new MethodInterceptor() {
        @Override
        public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            System.out.println("before");
            // 反射调用
            return method.invoke(target, args);
            // 内部没有反射调用,但需要结合目标对象使用
            return methodProxy.invoke(target, args);
            // 内部没有反射调用,但需要结合代理对象使用
            return methodProxy.invokeSuper(o, args);

    });

    proxy.save();
    proxy.save(1);
    proxy.save(2L);
}
  • 调用 methodProxy.invoke() 方法时,会额外使用一个代理类,该代理类配合目标对象使用。
  • 调用 methodProxy.invokeSuper() 方法时,也会额外使用一个代理类,该代理类配合代理对象使用。
  • 当调用 MethodProxy 对象的 invoke() 方法或 invokeSuper() 方法时,就会生成这两个代理类,它们都继承至 FastClass。

调用 methodProxy.invoke() 方法时,就相当于调用 TargetFastClass 中的 invoke() 方法,并在这个 invoke() 方法中正常调用目标对象方法(Spring 底层的选择)。

调用 methodProxy.invokeSuper() 方法时,就相当于调用 ProxyFastClass 中的 invoke() 方法,并在这个 invoke() 方法中正常调用代理对象中带原始功能的方法。

JDK与CGLIB的对比

JDK 动态代理和 CGLIB 动态代理是 Java 中常用的两种动态代理方式,它们之间有一些比较。

  1. 适用范围:
    • JDK 动态代理只能代理实现了接口的类,对于没有实现接口的类无法进行代理。
    • CGLIB 动态代理可以代理没有实现接口的类,因此更加灵活。
  1. 性能:
    • JDK 动态代理生成的代理类的性能相对较低,因为每次调用代理方法都需要通过反射来调用目标方法。
    • CGLIB 动态代理相对于 JDK 动态代理,生成的代理类的性能相对较高,因为代理类是目标类的子类,不存在通过反射调用的开销。
  1. 生成方式:
    • JDK 动态代理是基于接口实现的,通过实现 InvocationHandler 接口,通过 Proxy.newProxyInstance() 方法创建代理对象。
    • CGLIB 动态代理通过生成目标类的子类来实现动态代理。
  1. 对 final 方法的支持:
    • JDK 动态代理无法代理接口中的 final 方法。
    • CGLIB 动态代理无法直接代理目标类中的 final 方法。

基于以上对比,选择使用 JDK 动态代理还是 CGLIB 动态代理取决于具体的需求。如果要代理的是接口,并且对性能有较高要求,可以选择 JDK 动态代理;如果要代理的是类,并且对性能和灵活性有较高的要求,可以选CGLIB 动态代理。需要注意的是,对于包含 final 方法的类,无论是 JDK 动态代理还是 CGLIB 动态代理都无法直接进行代理。

IoC使软件组件松耦合。AOP让你能够捕捉系统中经常使用的功能,把它转化成组件。

AOP(Aspect Oriented Programming):面向切面编程,面向方面编程。(AOP是一种编程技术)

AOP底层使用的就是动态代理来实现的。

Spring的AOP使用的动态代理是:JDK动态代理 + CGLIB动态代理技术。Spring在这两种动态代理中灵活切换,如果是代理接口,会默认使用JDK动态代理,如果要代理某个类,这个类没有实现接口,就会切换使用CGLIB。当然,你也可以强制通过一些配置让Spring只使用CGLIB。

面试题

JDK动态代理和CGLIB有什么区别?

JDK动态代理

这是Java提供的动态代理技术,可以在运行时创建接口的代理实例。Spring AOP默认采用这种方式,在接口的代理实例中织入代码。

CGLib动态代理

采用底层的字节码技术,在运行时创建子类代理的实例。当目标对象不存在接口时,Spring AOP就会采用这种方式,在子类实例中织入代码。

既然有没有接口都可以用CGLIB,为什么Spring还要使用JDK动态代理?

在性能方面,CGLib创建的代理对象比JDK动态代理创建的代理对象高很多。但是,CGLib在创建代理对象时所花费的时间比JDK动态代理多很多。所以,对于单例的对象因为无需频繁创建代理对象,采用CGLib动态代理比较合适。反之,对于多例的对象因为需要频繁的创建代理对象,则JDK动态代理更合 适。
扩展阅读

确实,在性能方面,CGLIB 创建的代理对象通常比 JDK 动态代理创建的代理对象性能更高。这是因为 CGLIB 动态代理生成的代理类是目标类的子类,可以直接调用父类的方法,避免了通过反射调用的开销。

然而,CGLIB 在创建代理对象时所花费的时间相对较多。CGLIB 动态代理生成代理类需要通过字节码技术对目标类进行增强,并创建新的子类。这个过程相对于 JDK 动态代理的创建代理对象过程更复杂。

考虑到以上性能和创建代理对象的时间因素,可以根据具体情况选择使用 JDK 动态代理还是 CGLIB 动态代理。

对于单例对象,由于只需要创建一次代理对象并重复使用,CGLIB 动态代理的性能优势可以得到充分发挥。通过 CGLIB 动态代理可以获得更高的性能

而对于多例对象,由于需要频繁创建代理对象,这时候 JDK 动态代理更适合。尽管 JDK 动态代理的性能相对较低,但是创建代理对象的时间较少,可以更好地适应多例对象的创建频率。

总结而言,对于单例的对象,CGLIB 动态代理通常更适合;而对于多例的对象,JDK 动态代理更合适。需要在性能和创建代理对象的时间开销之间进行权衡,选择合适的方案。

相关推荐
武子康10 分钟前
大数据-230 离线数仓 - ODS层的构建 Hive处理 UDF 与 SerDe 处理 与 当前总结
java·大数据·数据仓库·hive·hadoop·sql·hdfs
武子康12 分钟前
大数据-231 离线数仓 - DWS 层、ADS 层的创建 Hive 执行脚本
java·大数据·数据仓库·hive·hadoop·mysql
苏-言18 分钟前
Spring IOC实战指南:从零到一的构建过程
java·数据库·spring
界面开发小八哥26 分钟前
更高效的Java 23开发,IntelliJ IDEA助力全面升级
java·开发语言·ide·intellij-idea·开发工具
草莓base39 分钟前
【手写一个spring】spring源码的简单实现--容器启动
java·后端·spring
Allen Bright1 小时前
maven概述
java·maven
编程重生之路1 小时前
Springboot启动异常 错误: 找不到或无法加载主类 xxx.Application异常
java·spring boot·后端
薯条不要番茄酱1 小时前
数据结构-8.Java. 七大排序算法(中篇)
java·开发语言·数据结构·后端·算法·排序算法·intellij-idea
努力进修1 小时前
“探索Java List的无限可能:从基础到高级应用“
java·开发语言·list
politeboy1 小时前
k8s启动springboot容器的时候,显示找不到application.yml文件
java·spring boot·kubernetes