【Spring】代理模式

文章目录

  • 代理模式
    • 对代理模式的理解
    • 静态代理
    • 动态代理
      • JDK动态代理
        • 原理
        • 源码
        • 优化
      • CGLIB动态代理
        • 使用
        • 原理
      • JDK与CGLIB的对比
  • 面试题
    • JDK动态代理和CGLIB有什么区别?
    • 既然有没有接口都可以用CGLIB,为什么Spring还要使用JDK动态代理?

代理模式

对代理模式的理解

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

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

代理模式的类图:

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

  • 静态代理
  • 动态代理

静态代理

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

/**
 * 订单接口
 **/
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源代码,在每个业务方法中添加统计逻辑,如下:


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,在子类中重写每个方法,代码如下:

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接口提供一个代理类。

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开闭原则,同时采用的是关联关系,所以程序的耦合度较低。所以这种方案是被推荐的。
编写客户端程序:

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动态代理

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


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代理类是可以动态生成的。这个类不需要写。我们直接写客户端程序即可:


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()方法中调用目标方法了。代码如下:

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程序:


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动态代理的实现
首先一个提供一个接口和一个目标类

interface Foo {
    void foo();
}

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

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

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 中的实现:

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

也就是说,“功能增强” 和 “调用目标” 这两部分的代码都是不确定的。
针对这种 “不确定” 的实现,可以提供一个抽象类,等到用户具体使用时才实现抽象类,重写抽象方法。

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();
}

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

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");
    }
}


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() 方法中添加两个入参,分别表示需要调用的目标方法和目标方法的参数:

interface InvocationHandler {
    void invoke(Method method, Object[] params) throws Throwable;
}

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

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 对象和参数信息反射调用目标方法:

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 实例,这些实例是可以复用的,因此可以将这些实例的创建移动到静态代码块中
  • 提供代理对象参数

最终的代码

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);
        }
    }
}

测试代码

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 反编译代理类字节码文件。

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,需要引入它的依赖:

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>

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


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

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

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接口实现类:

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()方法中调用目标以及添加增强:

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

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

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

执行结果:

原理

同样先模拟一下
目标类:

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 动态代理生成的代理类:

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);
        }
    }
}

测试一下

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 动态代理更合适。需要在性能和创建代理对象的时间开销之间进行权衡,选择合适的方案。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/370181.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

3 编辑器(Vim)

1.完成 vimtutor。备注&#xff1a;它在一个 80x24&#xff08;80 列&#xff0c;24 行&#xff09; 终端窗口看起来效果最好。 2.下载我们提供的 vimrc&#xff0c;然后把它保存到 ~/.vimrc。 通读这个注释详细的文件 &#xff08;用 Vim!&#xff09;&#xff0c; 然后观察 …

如何在Shopee平台上进行测款选品

在如今竞争激烈的电商市场&#xff0c;选择合适的产品成为卖家们提高销售业绩的重要一环。在Shopee平台上进行测款选品&#xff0c;可以帮助卖家找到符合市场需求的产品&#xff0c;提高销售业绩。本文将介绍一些策略和步骤&#xff0c;帮助卖家在Shopee平台上进行测款选品。 …

javaEE - 20( 18000字 Tomcat 和 HTTP 协议入门 -1)

一&#xff1a; HTTP 协议 1.1. HTTP 是什么 HTTP (全称为 “超文本传输协议”) 是一种应用非常广泛的 应用层协议. HTTP 诞生与1991年. 目前已经发展为最主流使用的一种应用层协议. 最新的 HTTP 3 版本也正在完善中, 目前 Google / Facebook 等公司的产品已经支持了. HTT…

【Python基础021】Python中的何如实现文件的读写

Python中文件的读写在程序运行过程中是一个非常重要的操作&#xff0c;我们通常会将一些大量的临时数据暂时存放到一个临时文件&#xff0c;这就需要用到文件的读取与写入功能。话不多说&#xff0c;我们直接上才艺。 1、文本文件和二进制文件 讲文件读写前&#xff0c;先说说…

sqli.labs靶场(29到40关)

29、第二十九关 id1 id1 尝试发现是单引号闭合&#xff0c; -1 union select 1,2,3-- -1 union select 1,2,database()-- -1 union select 1,2,(select group_concat(table_name) from information_schema.tables where table_schemasecurity)-- -1 union select 1,2,(select…

C#(C Sharp)学习笔记_前言及Visual Studio Code配置C#运行环境【一】

前言 这可以说是我第一次正式的踏入C#的学习道路&#xff0c;我真没想过我两年前是怎么跳过C#去学Unity3D游戏开发的&#xff08;当然了&#xff0c;游戏开发肯定是没有成功的&#xff0c;都是照搬代码&#xff09;。而现在&#xff0c;我真正地学习一下C#&#xff0c;就和去年…

STM32--揭秘中断(简易土货版)

抢占优先级响应优先级 视频学习--中断​​​​​​​

队列---数据结构

定义 队列&#xff08;Queue&#xff09;简称队&#xff0c;也是一种操作受限的线性表&#xff0c;只允许在表的一端进行插入&#xff0c;而在表的另一端进行删除。向队列中插入元素称为入队或进队&#xff1b;删除元素称为出队或离队。 队头&#xff08;Front&#xff09;&a…

单片机的50个电路

单片机 电源 声音模块 收音机 485 蓝牙 光耦 can 光敏电阻 单片机 矩阵 单片机电路 时钟 ADC 接口电路 红外发射 显示模块 红外接收 蜂鸣器驱动 流水灯 usb供电 烧录电路 数码管 EEPROM LCD1602电路 数码管 max485 红外开关 译码器 移位寄存器 步进电机控制 复位电路 下载电路 …

Python之列表的增删改查

列表的查 a ["klvchen", "tom", "jack", "james", "lily", "lucy"] print(a[1:4]) 结果&#xff1a; [tom, jack, james] 注意&#xff1a; 列表的切片的下标以 0 为开始。即&#xff1a; 下标0 --> klvchen…

基于相关向量机和特征选取的短期负荷预测MATLAB程序

微❤关注“电气仔推送”获得资料&#xff08;专享优惠&#xff09; 参考文献 相关向量机和特征选取技术在短期负荷预测中的应用——刘刚 运行环境——MATLAB 程序简介 通过改进传统的Relief算法&#xff0c;提出一种短期负荷预测特征输入量的选取方法&#xff0c;并使用相…

【webpack】技巧使用

webpack和TypeScript 安装webpack相关内容安装TS相关内容配置初始化数据初始化运行展示和目录展示报错解决&#xff08;缺失文件配置&#xff09; 安装前端必备神奇lodash测试一下entry配置index.html模板配置修改打包出来的index.html的titleinject注入chunks 属性多页面配置 …

C# 信号量(Semaphore)详细使用案例

文章目录 简介信号量的工作原理使用场景使用示例其他使用实例1. 数据库连接池管理2. 文件读写同步3. 生产者消费者问题4. 打印任务队列同步5. Web服务器并发请求限制 简介 在C#中&#xff0c;信号量&#xff08;Semaphore&#xff09;是.NET框架提供的一个同步类&#xff0c;位…

【C/Python】Gtk部件ListStore的使用

一、C语言 在GTK中&#xff0c;Gtk.ListStore是一个实现了Gtk.TreeModel接口的存储模型&#xff0c;用于在如Gtk.TreeView这样的控件中存储数据。以下是一个简单的使用Gtk.ListStore的C语言示例&#xff0c;该示例创建了一个列表&#xff0c;并在图形界面中显示&#xff1a; …

【Vue项目中使用videojs播放本地mp4的项目】

目录 以下是一个使用video.js播放本地mp4文件的Vue项目代码示例&#xff1a;1. 首先&#xff0c;在终端中使用以下命令安装video.js和video.js插件&#xff1a;2. 在Vue组件中&#xff0c;引入video.js和videojs-youtube插件&#xff1a;3. 配置video-js.css文件&#xff0c;可…

DS:时间复杂度和空间复杂度

创作不易&#xff0c;感谢三连&#xff01; 一、算法 1.1 什么是算法 算法(Algorithm):就是定义良好的计算过程&#xff0c;他取一个或一组的值为输入&#xff0c;并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤&#xff0c;用来将输入数据转化成输出结果。…

AI新工具(20240204)pot-desktop - 为用户提供便捷的文字翻译和识别功能;ChatALL - 能够同时向多个AI机器人发送提示

pot-desktop - 为用户提供便捷的文字翻译和识别功能 pot-desktop pot-desktop是一款备受欢迎的跨平台划词翻译和OCR软件&#xff0c;为用户提供便捷的文字翻译和识别功能。 功能点&#xff1a; 划词翻译&#xff1a;用户只需将鼠标光标悬停在需要翻译的文字上&#xff0c;po…

028 方法的重载

方法重载的定义 使用案例 public static void main(String[] args) {// 匹配到max(int a, int b)System.out.println(max(1, 3));// 匹配到max(double a, double b)System.out.println(max(1L, 3L));// 匹配到max(double a, double b, double c, double d)&#xff0c;int自动…

3D 转换

1&#xff0c;3D的特点&#xff1a; 近小远大 物体后面遮挡不可见 2&#xff0c;3D移动 translate3d 3D移动在2D移动的基础上多加了一个可以移动的方向&#xff0c;就是z轴方向 transform&#xff1a;translateX&#xff08;100px&#xff09;&#xff1a;仅仅是在x轴上移动…

MySQL查询缓存

MySQL查询缓存 MySQL在查询的时候首先会查询缓存&#xff0c;如果缓存命中的话就直接返回结果&#xff0c;不需要解析sql语句&#xff0c;也不会生成执行计划&#xff0c;更不会执行&#xff1b;如果没有命中缓存&#xff0c;则再进行SQL解析以及进行查询&#xff0c;并将结果返…
最新文章