Java 代理使用详解

代理模式简介

代理模式(Proxy)是通过代理对象访问目标对象,这样可以在目标对象基础上增强额外的功能,如添加权限,访问控制和审计等功能。

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

我们根据加载被代理类的时机不同,将代理分为静态代理和动态代理。如果我们在代码编译时就确定了被代理的类是哪一个,那么就可以直接使用静态代理;如果不能确定,那么可以使用类的动态加载机制,在代码运行期间加载被代理的类这就是动态代理,比如 RPC 框架和 Spring AOP 机制。动态代理又可以分为 JDK的动态代理 和 Cglib 的动态代理。

一、静态代理

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

不足:静态代理实现了目标对象的所有方法,一旦目标接口增加方法,代理对象和目标对象都要进行相应的修改,增加维护成本。
在这里插入图片描述

静态代理示例

(1)接口类 Shopping.java 接口

interface Shopping {
    void buy();
}

(2)实现类 Client.java

class Client implements Shopping {
	private String name;
	
    public Client(String name) {
        this.name = name;
    }
    
    public void buy() {
        System.out.println("我想买这件商品");
    }
}

(3)代理类 StaticProxy.java

class StaticProxy implements Shopping {

	// 用接口统一接收被代理的顾客
    private Shopping shopping;

    public StaticProxy(Shopping shopping) {
        this.shopping = shopping;
    }

    public void buy() {
        System.out.println("降价促销,疯狂大甩卖了!");
        shopping.buy();
    }
}

(4)测试类

public class StaticProxyTest {

    public static void main(String[] args) {
    	// 被代理的顾客张三,他的购买活动由代理对象 service 完成
        Client client = new Client("张三");
        // 生成代理对象,并将顾客张三传给代理对象
        StaticProxy service = new StaticProxy(client);
        // 代理对象代理顾客张三完成购买
        service.buy();
    }
}

输出结果:

降价促销,疯狂大甩卖了!
我想买这件商品

静态代理最主要的就是有一个公共接口(Person),一个具体的类(Student),一个代理类(StudentsProxy),代理类持有具体类的实例,代为执行具体类实例方法。上面说到,代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。这里的间接性就是指不直接调用实际对象的方法,那么我们在代理过程中就可以加上一些其他用途。

二、动态代理

代理类在程序运行时创建的代理方式被成为动态代理。动态代理中的代理类并不是在 Java 代码中定义的,而是在运行时根据我们在 Java 代码中的“指示”动态生成的。相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。

1、JDK 动态代理

在 Java 的 java.lang.reflect 包下提供了一个 Proxy 类和一个 InvocationHandler 接口来生成 JDK 动态代理类和动态代理对象,所有对动态代理对象的方法调用都会转发到 InvocationHandler 中的 invoke() 方法中实现
JDK 代理方式必须要有接口
在这里插入图片描述

JDK 为我们的生成了一个叫 $Proxy0(这个名字后面的0是编号,有多个代理类会一次递增)的代理类,这个类文件时放在内存中的,我们在创建代理对象时,就是通过反射获得这个类的构造方法,然后创建的代理实例。
我们可以将 InvocationHandler 看做一个中介类,中介类持有一个被代理对象,在 invoke 方法中调用被代理对象的相应方法。通过聚合方式持有被代理对象的引用,把外部对 invoke 的调用最终都转为对被代理对象的调用。
代理类调用被代理对象的方法时,通过自身持有的中介类对象来调用中介类对象的 invoke 方法,然后 invoke 方法再调用被代理对象的相应方法,从而达到代理执行被代理对象的方法。也就是说,动态代理通过中介类实现了具体的代理功能。

优点

● JDK动态代理是JDK原生的,不需要任何依赖即可使用;
● 通过反射机制生成代理类的速度要比CGLib操作字节码生成代理类的速度更快;

缺点

● 如果要使用JDK动态代理,被代理的类必须实现了接口,否则无法代理;
● JDK动态代理无法为没有在接口中定义的方法实现代理,假设我们有一个实现了接口的类,我们为它的一个不属于接口中的方法配置了切面,Spring仍然会使用JDK的动态代理,但是由于配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入。
● JDK动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低;

(1)InvocationHandler 接口

每一个动态代理类都必须要实现 InvocationHandler 这个接口,并且每个代理类的实例都关联到了一个handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由 InvocationHandler这个接口的 invoke 方法来进行调用,并不是自己来真实调用,而是通过代理的方式来调用的。 invoke 方法是 InvocationHandler 接口的唯一一个方法:

Object invoke(Object proxy, Method method, Object[] args) throws Throwable 

在这里插入图片描述

(2)Proxy 类

Proxy 这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但是我们用的最多的就是 newProxyInstance
这个方法:

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

这个方法的作用就是得到一个动态的代理对象,接收三个参数,三个参数所代表的含义如下:
在这里插入图片描述
通过 Proxy.newProxyInstance 创建的代理对象是在 jvm 运行时动态生成的一个对象,它并不是我们的InvocationHandler类型,也不是我们定义的那组接口的类型,而是在运行是动态生成的一个对象,并且命名方式都是这样的形式:$proxy和一个表示对象标号的数字,例如 $Proxy0, $Proxy1。

(3)JDK 动态代理简单实现:

① 通过实现 InvocationHandler 接口来自定义自己的InvocationHandler;
② 通过 Proxy.getProxyClass 获得动态代理类的 class 对象,必须使用接口的class为参数;
③ 通过反射机制获得代理类的构造方法,方法签名为 getConstructor(InvocationHandler.class);
④ 通过构造函数获得代理对象,并将自定义的 InvocationHandler 实例对象传为参数传入,代理对象强转为目标对象的接口类型;
⑤ 通过代理对象调用目标方法;

Shopping.java 接口

interface Shopping {
    void buy();
}

实现类 Client.java

class Client implements Shopping {
	private String name;
	
    public Client(String name) {
        this.name = name;
    }
    
    public void buy() {
        System.out.println("我想买这件商品");
    }
}

ClientInvocationHandler.java 类

该类实现 InvocationHandler 接口,这个类中持有一个被代理接口实现类的实例对象 target。InvocationHandler 中有一个 invoke() 方法,所有执行代理对象的方法都会被替换成执行 invoke() 方法,然后 invoke() 方法中执行被代理对象target的相应方法。当然,在代理过程中,我们在真正执行被代理对象的方法前加入自己其他处理。

public class ClientInvocationHandler implements InvocationHandler {
   // InvocationHandler 持有的被代理对象
    private Shopping target;

    public ClientInvocationHandler(Shopping target) {
       this.target = target;
    }

    /**
     * proxy:代表动态代理对象
     * method:代表正在执行的方法
     * args:代表调用目标方法时传入的实参
     */   
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("JDK 动态代理执行" + method.getName() + "方法");
        Object result = method.invoke(target, args);
        return result;
    }
}

测试类

public class ProxyTest {
    public static void main(String[] args)throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
    
        // =========================第一种方式=========================
        // 获取动态代理类的class
        Class proxyClass = Proxy.getProxyClass(Shopping.class.getClassLoader(),Shopping.class);  // 必须使用接口的class为参数
        // 获得代理类的构造函数,并传入参数类型 InvocationHandler.class
        Constructor constructor = proxyClass.getConstructor(InvocationHandler.class);
        // 通过构造函数来创建动态代理对象,将自定义的InvocationHandler实例传入
        Shopping proxy = (Shopping) constructor.newInstance(new ClientInvocationHandler(new Client("张三")));  // 强转为目标对象的接口类型
        // 通过代理对象调用目标方法
        proxy.buy();


		// =========================第二种简便方式==========================
    	// 创建一个实例对象,这个对象是被代理的对象
        Client target = new Client("张三");
        // 创建一个与代理对象相关联的 InvocationHandler
        ClientInvocationHandler handler = new ClientInvocationHandler(target);
        // 创建一个接口类型的代理对象 proxy 来代理 target,代理对象的每个执行方法都会替换执行 Invocation 中的 invoke() 方法
        // 第一个参数 handler.getClass().getClassLoader() ,我们这里使用 handler 这个类的 ClassLoader 对象来加载我们的代理对象
        // 第二个参数 target.getClass().getInterfaces(),我们这里为代理对象提供的接口数组存放的是真实对象所实现的接口,表示我要代理的是该真实对象,这样我就能调用这组接口中的方法了
        // 第三个参数 handler, 我们这里将这个代理对象关联到了上方的 InvocationHandler 这个对象上
        Shopping proxy = (Shopping) Proxy.newProxyInstance(handler.getClass().getClassLoader(), target.getClass().getInterfaces(), handler);
        // 代理执行购买的方法
        proxy.buy();
}

输出结果:

JDK 代理执行buy方法
我想买这件商品

动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。是因为所有被代理执行的方法,都是通过在 InvocationHandler 中的 invoke() 方法调用的,所以我们只要在 invoke() 方法中统一处理,就可以对所有被代理的方法进行相同的操作了。

2、Cglib 动态代理

JDK 动态代理要求target对象是一个接口的实现对象,假如 target 对象并没有实现任何接口,只是一个单独的对象,这时候就会用到 Cglib 代理(Code Generation Library),即通过构建一个子类对象,从而实现对 target 对象的代理。
CGLib 实现动态代理的原理是,底层采用了 ASM 字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,将我们定义的额外的逻辑(简单理解为Spring中的切面)织入到方法中,对方法进行了增强。因此目标对象不能是 final 类(报错),且目标对象的方法不能是 final 或 static(不执行代理功能)
而通过字节码操作生成的代理类,和我们自己编写并编译后的类没有太大区别。

优点

● 使用CGLib代理的类,不需要实现接口,因为CGLib生成的代理类是直接继承自需要被代理的类;
● CGLib生成的代理类是原来那个类的子类,这就意味着这个代理类可以为原来那个类中所有能够被子类重写的方法进行代理;
● CGLib生成的代理类,和我们自己编写并编译的类没有太大区别,对方法的调用和直接调用普通类的方式一致,所以CGLib执行代理方法的效率要高于JDK的动态代理;

缺点

● 由于CGLib的代理类使用的是继承,这也就意味着如果需要被代理的类是一个final类则无法使用CGLib代理;
● 由于CGLib实现代理方法的方式是重写父类的方法,所以无法对final方法或者private方法进行代理,因为子类无法重写这些方法;
● CGLib生成代理类的方式是通过操作字节码,这种方式生成代理类的速度要比JDK通过反射生成代理类的速度更慢;

Cglib 动态代理简单实现:

① 引入相关依赖;
② 代理类实现 MethodInterceptor 接口,实现 intercept 方法;
③ 创建代理对象

(1)添加 Cglib 依赖的 jar 包

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

(2)被代理对象类 Client1.java

public class Client1 {

    private String name;

	// 无参构造函数
    public Client1() {
    }

    public Client1(String name) {
        this.name = name;
    }

    public void buy() {
        System.out.println("我想买这件商品");
    }
}

(3)代理类 CglibProxy.java

public class CglibProxy implements MethodInterceptor {

    private Object target;

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

    // 给目标对象创建一个代理对象
    public Object getProxyInstance() {
        // 工具类
        Enhancer en = new Enhancer();
        // 设置父类,被代理类必须要有无参构造函数
        en.setSuperclass(target.getClass());
        //设置回调函数
        en.setCallback(this);
        //创建子类代理对象
        return en.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("Cglib 动态代理执行" + method.getName() + "方法");
        Object obj = method.invoke(target);
        return obj;
    }
}

(4)测试类

public class test {
    public static void main(String[] args) {
    
        Client1 target = new Client1("zj");
        // 创建一个与代理对象相关联的 proxyFactory 
        CglibProxy proxyFactory = new CglibProxy(target);
         // 给目标对象创建一个代理对象
        Client1 proxy = (Client1) proxyFactory.getProxyInstance();
        proxy.buy();
    }
}

输出结果:

Cglib 动态代理执行buy方法
我想买这件商品

3、JDK 代理和 Cglib 代理的区别

(1)实现方式

① JDK 动态代理利用拦截器(拦截器必须实现 InvocationHanlder )和反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用 InvokeHandler 来处理。
② CGLIB 动态代理利用 ASM 开源包,将被代理对象类的 class 文件加载进来,通过修改其字节码生成子类来处理

(2)何时使用 JDK 或者 CGLIB

● 如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理实现 AOP,也可以强制使用 CGLIB 实现AOP。
● 如果目标对象没有实现了接口,必须采用 CGLIB 库,Spring 会自动在 JDK 动态代理和 CGLIB 之间转换。
● JDK 代理是不需要第三方库支持,只需要 JDK 环境就可以进行代理;Cglib 必须依赖于 CGLib 的类库。

(3)JDK 动态代理和 CGLIB 字节码生成的区别

1)JDK 动态代理只能对实现了接口的类生成代理,而不能针对类。

2)CGLIB 是针对类实现代理,为指定的被代理的类生成一个子类,覆盖其中的方法,并对覆盖的方法实现增强,但是因为采用的是继承,所以该类或方法最好不要声明成final,对于final类或方法,是无法继承的

(4)CGlib 与 JDK 的快慢

1)使用 CGLib 实现动态代理,CGLib 底层采用 ASM 字节码生成框架,使用字节码技术生成代理类,在 jdk6 之前比使用 Java 反射效率要高。唯一需要注意的是,CGLib 不能对声明为final的方法进行代理,因为 CGLib 原理是动态生成被代理类的子类。
2)在 jdk6、jdk7、jdk8 逐步对 JDK 动态代理优化之后,在调用次数较少的情况下, JDK 代理效率高于 CGLIB 代理效率,只有当进行大量调用的时候,jdk6 和 jdk7 比 CGLIB 代理效率低一点,但是到 jdk8 的时候,jdk 代理效率高于 CGLIB 代理。

总之,每一次jdk版本升级,jdk代理效率都得到提升,而CGLIB代理消息确有点跟不上步伐。

(5)Spring 如何选择用 JDK 还是 CGLIB?

1)当 Bean 实现接口时,Spring 就会用 JDK 的动态代理。

2)当 Bean 没有实现接口时,Spring 使用 CGlib 的动态代理。

3)可以强制使用CGlib(在spring配置中加入<aop:aspectj-autoproxy proxy-target-class=“true”/>)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值