Java代理模式及动态代理详解

Java的动态代理在实践中有着广泛的使用场景,比如最场景的Spring AOP、Java注解的获取、日志、用户鉴权等。本篇文章带大家了解一下代理模式、静态代理以及基于JDK原生动态代理。

代理模式

无论学习静态代理或动态代理,我们都要先了解一下代理模式。

先看百度百科的定义:

代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

直接看定义可能有些难以理解,我们就以生活中具体的实例来说明一下。

我们都去过超市购买过物品,超市从厂商那里购买货物之后出售给我们,我们通常并不知道货物从哪里经过多少流程才到超市。

在这个过程中,等于是厂商“委托”超市出售货物,对我们来说是厂商(真实对象)是不可见的。而超市(代理对象)呢,作为厂商的“代理者”来与我们进行交互。

同时,超市还可以根据具体的销售情况来进行折扣等处理,来丰富被代理对象的功能。

通过代理模式,我们可以做到两点:

1、隐藏委托类的具体实现。

2、实现客户与委托类的解耦,在不改变委托类代码的情况下添加一些额外的功能(日志、权限)等。

代理模式角色定义

在上述的过程中在编程的过程中我们可以定义为三类对象:

  • Subject(抽象主题角色):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法。比如:广告、出售等。
  • RealSubject(真实主题角色):真正实现业务逻辑的类。比如实现了广告、出售等方法的厂家(Vendor)。
  • Proxy(代理主题角色):用来代理和封装真实主题。比如,同样实现了广告、出售等方法的超时(Shop)。

以上三个角色对应的类图如下:

Java代理及动态代理详解

静态代理实例

静态代理是指代理类在程序运行前就已经存在,这种情况下的代理类通常都是我们在Java代码中定义的。

下面我们就以具体的实例来演示一下静态代理。

首先定义一组接口Sell,用来提供广告和销售等功能。然后提供Vendor类(厂商,被代理对象)和Shop(超市,代理类),它们分别实现了Sell接口。

Sell接口定义如下:

/**
 * 委托类和代理类都实现了Sell接口
 * @author sec
 * @version 1.0
 * @date 2020/3/21 9:30 AM
 **/
public interface Sell {

	/**
	 * 出售
	 */
	void sell();

	/**
	 * 广告
	 */
	void ad();
}

Vendor类定义如下:

/**
 * 供应商
 * @author sec
 * @version 1.0
 * @date 2020/3/21 9:30 AM
 **/
public class Vendor implements Sell{

	@Override
	public void sell() {
		System.out.println("Shop sell goods");
	}

	@Override
	public void ad() {
		System.out.println("Shop advert goods");
	}
}

Shop类定义如下:

/**
 * 超市,代理类
 * @author sec
 * @version 1.0
 * @date 2020/3/21 9:30 AM
 **/
public class Shop implements Sell{

	private Sell sell;

	public Shop(Sell sell){
		this.sell = sell;
	}

	@Override
	public void sell() {
	    System.out.println("代理类Shop,处理sell");
		sell.sell();
	}

	@Override
	public void ad() {
	    System.out.println("代理类Shop,处理ad");
		sell.ad();
	}
}

其中代理类Shop通过聚合的方式持有了被代理类Vendor的引用,并在对应的方法中调用Vendor对应的方法。在Shop类中我们可以新增一些额外的处理,比如筛选购买用户、记录日志等操作。

下面看看在客户端中如何使用代理类。

/**
 * 静态代理类测试方法
 * @author sec
 * @version 1.0
 * @date 2020/3/21 9:33 AM
 **/
public class StaticProxy {

	public static void main(String[] args) {

		// 供应商---被代理类
		Vendor vendor = new Vendor();

		// 创建供应商的代理类Shop
		Sell sell = new Shop(vendor);

		// 客户端使用时面向的是代理类Shop。
		sell.ad();
		sell.sell();
	}
}

在上述代码中,针对客户看到的是Sell接口提供了功能,而功能又是由Shop提供的。我们可以在Shop中修改或新增一些内容,而不影响被代理类Vendor。

静态代理的缺点

静态代理实现简单且不侵入原代码,但当场景复杂时,静态代理会有以下缺点:

1、当需要代理多个类时,代理对象要实现与目标对象一致的接口。要么,只维护一个代理类来实现多个接口,但这样会导致代理类过于庞大。要么,新建多个代理类,但这样会产生过多的代理类。

2、当接口需要增加、删除、修改方法时,目标对象与代理类都要同时修改,不易维护。

于是,动态代理便派上用场了。

动态代理

动态代理是指代理类在程序运行时进行创建的代理方式。这种情况下,代理类并不是在Java代码中定义的,而是在运行时根据Java代码中的“指示”动态生成的。

相比于静态代理,动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类的函数。

基于JDK原生动态代理实现

实现动态代理通常有两种方式:JDK原生动态代理和CGLIB动态代理。这里,我们以JDK原生动态代理为例来进行讲解。

JDK动态代理主要涉及两个类:java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler。

InvocationHandler接口定义了如下方法:

/**
 * 调用处理程序
 */
public interface InvocationHandler { 
    Object invoke(Object proxy, Method method, Object[] args); 
}

顾名思义,实现了该接口的中介类用做“调用处理器”。当调用代理类对象的方法时,这个“调用”会转送到invoke方法中,代理类对象作为proxy参数传入,参数method标识了具体调用的是代理类的哪个方法,args为该方法的参数。这样对代理类中的所有方法的调用都会变为对invoke的调用,可以在invoke方法中添加统一的处理逻辑(也可以根据method参数对不同的代理类方法做不同的处理)。

Proxy类用于获取指定代理对象所关联的调用处理器。

下面以添加日志为例来演示一下动态代理。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Date;

public class LogHandler implements InvocationHandler {
    Object target;  // 被代理的对象,实际的方法执行者

    public LogHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);  // 调用 target 的 method 方法
        after();
        return result;  // 返回方法的执行结果
    }
    // 调用invoke方法之前执行
    private void before() {
        System.out.println(String.format("log start time [%s] ", new Date()));
    }
    // 调用invoke方法之后执行
    private void after() {
        System.out.println(String.format("log end time [%s] ", new Date()));
    }
}

客户端编写程序使用动态代理代码如下:

import java.lang.reflect.Proxy;

/**
 * 动态代理测试
 *
 * @author sec
 * @version 1.0
 * @date 2020/3/21 10:40 AM
 **/
public class DynamicProxyMain {

	public static void main(String[] args) {
		// 创建中介类实例
		LogHandler logHandler = new LogHandler(new Vendor());
		// 设置该变量可以保存动态代理类,默认名称$Proxy0.class
		System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

		// 获取代理类实例Sell
		Sell sell = (Sell) (Proxy.newProxyInstance(Sell.class.getClassLoader(), new Class[]{Sell.class}, logHandler));

		// 通过代理类对象调用代理类方法,实际上会转到invoke方法调用
		sell.sell();
		sell.ad();
	}
}

执行之后,打印日志如下:

调用方法sell之【前】的日志处理
Shop sell goods
调用方法sell之【后】的日志处理
调用方法ad之【前】的日志处理
Shop advert goods
调用方法ad之【后】的日志处理

经过上述验证,我们发现已经成功为我们的被代理类统一添加了执行方法之前和执行方法之后的日志。

在上述实例中为了看一下生成的动态代理类的代码,我们添加了下面的属性设置(在生产环境中需要去掉该属性)。

// 设置该变量可以保存动态代理类,默认名称$Proxy0.class
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

那么,我们可以执行main方法之后,还生成了一个名字为$Proxy0.class类文件。通过反编译可看到如下的代码:

package com.sun.proxy;

import com.choupangxia.proxy.Sell;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Sell {
    private static Method m1;
    private static Method m2;
    private static Method m4;
    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 ad() throws  {
        try {
            super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void sell() 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");
            m4 = Class.forName("com.choupangxia.proxy.Sell").getMethod("ad");
            m3 = Class.forName("com.choupangxia.proxy.Sell").getMethod("sell");
            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(代理类)继承了Proxy类,并且实现了被代理的所有接口,以及equals、hashCode、toString等方法。

由于动态代理类继承了Proxy类,所以每个代理类都会关联一个InvocationHandler方法调用处理器。

类和所有方法都被public final修饰,所以代理类只可被使用,不可以再被继承。

每个方法都有一个Method对象来描述,Method对象在static静态代码块中创建,以“m+数字”的格式命名。

调用方法的时候通过super.h.invoke(this,m1,(Object[])null);调用。其中的super.h.invoke实际上是在创建代理的时候传递给Proxy.newProxyInstance的LogHandler对象,它继承InvocationHandler类,负责实际的调用处理逻辑。

小结

关于代理和动态代理相关的内容,我们就讲这么多。了解了代理模式可以让我们的系统设计的更加具有可扩展性。而动态代理的应用就更广了,各类框架及业务场景都在使用。有了两个基础,就能够更好的学习其他框架。

关于CGLIB动态代理的内容,我们下篇文章再来聊一聊。


---
程序新视界:精彩和成长都不容错过
![程序新视界-微信公众号](https://img2020.cnblogs.com/other/1742867/202003/1742867-20200321112843411-428493370.png)

posted on 2020-03-21 11:29  程序新视界  阅读(2685)  评论(0编辑  收藏  举报

导航