一文总结代理:代理模式、代理服务器

概述

代理在计算机编程领域,是一个很通用的概念,包括:代理设计模式,代理服务器等。

代理类持有具体实现类的实例,将在代理类上的操作转化为实例上方法的调用。为某个对象提供一个代理,以控制对这个对象的访问。代理类和委托类有共同的父类或父接口,这样在任何使用委托类对象的地方都可以用代理对象替代。代理类负责请求的预处理、过滤、将请求分派给委托类处理、以及委托类执行完请求后的后续处理。为其他对象提供一种代理以控制对这个对象的访问。代理对象起到中介作用,可以去掉功能服务或者增加额外的功能。

角色

代理模式一般涉及到的角色有:

  • 对象:Client,请求客户端
  • 抽象角色:Subject,声明真实对象和代理对象的共同接口,对应代理接口;
  • 真实角色:RealSubject,代理角色所代表的真实对象,是最终要引用的对象,对应委托类;
  • 代理角色:Proxy,代理对象角色内部含有对真实对象的引用,从而可以操作真实对象,同时代理对象提供与真实对象相同的接口以便在任何时刻都能代替真实对象。同时,代理对象可以在执行真实对象操作时,附加其他的操作,相当于对真实对象进行封装,对应代理类,可以有未公开的方法。

UML类图如下:

调用顺序示意图:

分类

代理从类型来分类:

  • 虚代理:虚拟代理,Virtual Proxy,根据需要来创建开销很大的对象,该对象只有在需要时才会被真正创建,即所谓的延迟加载;
  • 远程代理:Remote Proxy,用来在不同的地址空间上代表同一个对象,这个不同的地址空间可以是在本机,也可以在其它机器上,在Java里面最典型的就是RMI技术;
  • copy-on-write代理:在客户端操作时,只有对象确实改变后,才会真的拷贝一个日标对象,算是虚代理的一个分支;
  • 保护代理:Protect Proxy,控制对原始对象的访问,如果有需要,可以给不同的用户提供不同的访问权限,以控制他们对原始对象的访问;
  • Cache代理:为那些昂贵操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果;
  • 防火墙代理:保护对象不被恶意用户访问和操作;
  • 同步代理:使多个用户能够同时访问目标对象而没有冲突;
  • 智能引用代理:Smart Reference Proxy,在访问对象时执行一些附加操作。比如,对指向实际对象的引用计数、第一次引用一个持久对象时,将它装入内存等

另外,根据代理类的生成时间的不同,可分为静态代理和动态代理。

静态代理

代理和被代理对象(目标对象)在代理之前是确定的,都实现相同的接口或者继承相同的抽象类。目标对象作为代理对象的一个属性,具体接口实现中,代理对象可以在调用目标对象相应方法前后加上其他业务处理逻辑。由程序员创建或工具生成代理类的源码,再编译代理类。所谓静态,在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定。

实现静态代理有继承、聚合方法。

优点:业务类只需要关注业务逻辑本身,保证业务类的重用性。

缺点:

  1. 需要大量硬编码
  2. 一个代理类只能代理一个业务类
  3. 如果业务类增加方法时,相应的代理类也要增加方法

动态代理

动态代理类的源码是在程序运行期间由JVM根据反射等机制动态的生成,不存在代理类的字节码文件。代理类和委托类的关系是在程序运行时确定。优势在于可以很方便的对代理类的函数进行统一的处理,添加方法调用次数、添加日志功能等,而不用修改每个代理类的函数。分为JDK动态代理和cglib动态代理。

实现

一般有JDK和cglib等实现方式。

JDK

基于反射,核心API包括:
java.lang.reflect.Proxy,Java动态代理机制生成的所有动态代理类的父类,提供一组静态方法来为一组接口动态地生成代理类及其对象,共4个static方法:

java 复制代码
public class Proxy implements java.io.Serializable {
	// 用于获取指定代理对象所关联的调用处理器
	public static InvocationHandler getInvocationHandler(Object proxy)
	// 用于获取关联于指定类装载器和一组接口的动态代理类的类对象
	public static class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)
	// 用于判断指定类对象是否是一个动态代理类
	public static boolean isProxyClass(Class<?> cl)
	// 用于为指定类装载器、一组接口及调用处理器生成动态代理类实例
	public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
}

InvocationHandler接口主要用来处理执行逻辑,源码:

java 复制代码
public interface InvocationHandler {
	// obj指代理类,method指被代理的方法,args为该参数的方法
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

使用JDK动态代理的步骤

  1. 创建被代理的类以及接口;
  2. 创建一个实现接口InvocationHandler,并重写invoke方法的类;
  3. 调用Proxy的静态方法,创建代理类newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)
  4. 通过代理调用方法。

实例

代表Subject的接口:

java 复制代码
public interface Movable {
	void move();
}

Car实现Movable接口,是要代理的实际对象,对应RealSubject:

java 复制代码
@Slf4j
public class Car implements Movable {
	@Override
	public void move() {
		// 实现开车
		try {
			Thread.sleep(new Random().nextInt(1000));
			System.out.println("汽车行驶中....");
		} catch (InterruptedException e) {
			log.error("thread fail", e);
		}
	}
}

对应于Proxy的TimeHandler类实现InvocationHandler接口,并在invoke()方法中添加额外的逻辑,用于在代理对象方法调用前后执行:

java 复制代码
public class TimeHandler implements InvocationHandler {
	private final Object target;
	
	TimeHandler(Object target) {
		super();
		this.target = target;
	}
	
	/**
	 * 参数:
	 * proxy:被代理对象
	 * method:被代理对象的方法
	 * args:方法的参数
	 * res:方法的返回值
	 */
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		long startTime = System.currentTimeMillis();
		System.out.println("汽车开始行驶....");
		Object res = method.invoke(target, args);
		long endTime = System.currentTimeMillis();
		System.out.println("汽车结束行驶....  汽车行驶时间:" + (endTime - startTime) + "毫秒!");
		return res;
	}
}

JDK动态代理测试类,也就是上图中的Client类:

java 复制代码
public class Test {
	public static void main(String[] args) {
		Car car = new Car();
		InvocationHandler h = new TimeHandler(car);
		Class<?> cls = car.getClass();
		/*
		 * loader:类加载器
		 * interfaces:实现接口
		 * h:InvocationHandler
		 */
		Movable m = (Movable) Proxy.newProxyInstance(cls.getClassLoader(), cls.getInterfaces(), h);
		m.move();
	}
}

使用Proxy.newProxyInstance()方法创建代理对象,指定Movable接口作为代理对象类型,并将TimeHandler对象作为代理对象的InvocationHandler。

缺点:

  1. 仍有硬编码
  2. 需要在对象初始化时,使用特定的方式进行初始化

源码分析

基于JDK-22,以Proxy.newProxyInstance()方法为切入点:

java 复制代码
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) {
	Objects.requireNonNull(h);
	@SuppressWarnings("removal")
	final Class<?> caller = System.getSecurityManager() == null ? null : Reflection.getCallerClass();
	// 查找或生成指定的代理类及其构造函数
	Constructor<?> cons = getProxyConstructor(caller, loader, interfaces);
	return newProxyInstance(caller, cons, h);
}

基于源码的注释,进一步查看getProxyConstructor代码:

java 复制代码
private static Constructor<?> getProxyConstructor(Class<?> caller, ClassLoader loader, Class<?>... interfaces) {
	// optimization for single interface
	if (interfaces.length == 1) {
		Class<?> intf = interfaces[0];
		if (caller != null) {
			checkProxyAccess(caller, loader, intf);
		}
		return proxyCache.sub(intf).computeIfAbsent(loader, (ld, clv) -> new ProxyBuilder(ld, clv.key()).build());
	} else {
	    // interfaces cloned
	    final Class<?>[] intfsArray = interfaces.clone();
	    if (caller != null) {
	        checkProxyAccess(caller, loader, intfsArray);
	    }
	    final List<Class<?>> intfs = Arrays.asList(intfsArray);
	    return proxyCache.sub(intfs).computeIfAbsent(loader, (ld, clv) -> new ProxyBuilder(ld, clv.key()).build());
	}
}

核心方法是构建ProxyBuilder实例,ProxyBuilder是java.lang.reflect.Proxy的静态内部类,利用构造器模式:

java 复制代码
ProxyBuilder(ClassLoader loader, List<Class<?>> interfaces) {
	// 在模块系统完全初始化之前不支持代理
	if (!VM.isModuleSystemInited()) {
	    throw new InternalError("Proxy is not supported until module system is fully initialized");
	}
	// 接口数不得超过65535个
	if (interfaces.size() > 65535) {
		throw new IllegalArgumentException("interface limit exceeded: " interfaces.size());
	}
	Set<Class<?>> refTypes = referencedTypes(loader, interfaces);
	// IAE if violates any restrictions specified in newProxyInstance
	validateProxyInterfaces(loader, interfaces, refTypes);
	this.interfaces = interfaces;
	this.context = proxyClassContext(loader, interfaces, refTypes);
	assert getLoader(context.module()) == loader;
}

核心方法之一,referencedTypes检查是否为static方法:

java 复制代码
private static Set<Class<?>> referencedTypes(ClassLoader loader, List<Class<?>> interfaces) {
	var types = new HashSet<Class<?>>();
	for (var intf : interfaces) {
		for (Method m : intf.getMethods()) {
			// 不能为static方法
			if (!Modifier.isStatic(m.getModifiers())) {
				addElementType(types, m.getReturnType());
				addElementTypes(types, m.getSharedParameterTypes());
				addElementTypes(types, m.getSharedExceptionTypes());
			}
		}
	}
	return types;
}

validateProxyInterfaces方法检查是否是接口,

java 复制代码
private static void validateProxyInterfaces(ClassLoader loader, List<Class<?>> interfaces, Set<Class<?>> refTypes) {
	Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.size());
	for (Class<?> intf : interfaces) {
	    // 检查是否为接口
	    if (!intf.isInterface()) {
	        throw new IllegalArgumentException(intf.getName() + " is not an interface");
	    }
		// 检查是否为隐藏类:JDK15引入新特性
	    if (intf.isHidden()) {
	        throw new IllegalArgumentException(intf.getName() + " is a hidden interface");
	    }
		// 检查是否是密封类:JDK17引入新特性
	    if (intf.isSealed()) {
	        throw new IllegalArgumentException(intf.getName() + " is a sealed interface");
	    }
	    // 验证类加载器是否将此接口的名称解析为同一个Class对象,下同
	    ensureVisible(loader, intf);
	    // 检查是否已经生成过此接口的代理类
	    if (interfaceSet.put(intf, Boolean.TRUE) != null) {
	        throw new IllegalArgumentException("repeated interface: " + intf.getName());
	    }
	}
	for (Class<?> type : refTypes) {
	    ensureVisible(loader, type);
	}
}

为什么JDK动态代理只能代理接口

面试时常见的问题之一,参考上面的源码分析,实际上还可以补充回答:JDK动态代理对密封类,隐藏类不生效,即不能代理密封类,隐藏类。

cglib

Code Generation Library,一款高性能Code生成类库的开源组件,可在运行期扩展Java类与实现Java接口,很多其他开源组件都在使用cglib

net.sf.cglib.proxy.MethodInterceptor接口是最通用的回调类型,经常被基于代理的AOP用来实现拦截方法的调用,接口只定义一个方法:

java 复制代码
public interface MethodInterceptor extends Callback {
	// object是代理对像,method是拦截方法,args是方法参数。原来的方法可通过使用Method对象的一般反射调用,或使用MethodProxy对象调用,后者更快
	Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}	

实例

创建代理类:

java 复制代码
public class CglibProxy implements MethodInterceptor {
	private final Enhancer enhancer = new Enhancer();
	
	Object getProxy(Class<?> clazz) {
	    // 设置创建子类的类
	    enhancer.setSuperclass(clazz);
	    enhancer.setCallback(this);
	    return enhancer.create();
	}
	
	/**
	 * 拦截所有目标类方法的调用
	 */
	@Override
	public Object intercept(Object obj, Method m, Object[] args, MethodProxy proxy) throws Throwable {
	    System.out.println("日志开始...");
	    // 代理类调用父类的方法
	    Object res = proxy.invokeSuper(obj, args);
	    System.out.println("日志结束...");
	    return res;
	}
}

测试类:

java 复制代码
public class Client {
	
	public static void main(String[] args) {
		CglibProxy proxy = new CglibProxy();
		Train t = (Train) proxy.getProxy(Train.class);
		t.move();
	}
	
	static class Train {
		void move() {
			System.out.println("火车行驶中...");
		}
	}
}

源码分析

基于cglib最新版3.3.0来分析,从enhancer.create()入手,调用createHelper方法:

java 复制代码
private Object createHelper() {
	// 校验callbackTypes、filter是否为空,及相应处理策略
	preValidate();
	Object key = KEY_FACTORY.newInstance((superclass != null) ? superclass.getName() : null,
	        ReflectUtils.getNames(interfaces),
	        filter == ALL_ZERO ? null : new WeakCacheKey<CallbackFilter>(filter),
	        callbackTypes,
	        useFactory,
	        interceptDuringConstruction,
	        serialVersionUID);
	this.currentKey = key;
	Object result = super.create(key);
	return result;
}

核心方法是create:

java 复制代码
protected Object create(Object key) {
	try {
		ClassLoader loader = getClassLoader();
		// 查询缓存
		Map<ClassLoader, ClassLoaderData> cache = CACHE;
		ClassLoaderData data = cache.get(loader);
		// DCL
		if (data == null) {
			synchronized (AbstractClassGenerator.class) {
				cache = CACHE;
				data = cache.get(loader);
				if (data == null) {
					// 构建缓存
					Map<ClassLoader, ClassLoaderData> newCache = new WeakHashMap<ClassLoader, ClassLoaderData>(cache);
					data = new ClassLoaderData(loader);
					newCache.put(loader, data);
					CACHE = newCache;
				}
			}
		}
		this.key = key;
		Object obj = data.get(this, getUseCache());
		if (obj instanceof Class) {
			// 用于向后兼容
			return firstInstance((Class) obj);
		}
		// 真正创建代理对象
		return nextInstance(obj);
	} catch (RuntimeException e) {
		throw e;
	} catch (Error e) {
		throw e;
	} catch (Exception e) {
		throw new CodeGenerationException(e);
	}
}

不管是firstInstance还是nextInstance,最后都是调用ReflectUtils.newInstance方法:

java 复制代码
public static Object newInstance(final Constructor cstruct, final Object[] args) {
	boolean flag = cstruct.isAccessible();
	try {
	    if (!flag) {
	        cstruct.setAccessible(true);
	    }
	    // 使用JDK
	    return cstruct.newInstance(args);
	} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
	    throw new CodeGenerationException(e);
	} finally {
	    if (!flag) {
	        cstruct.setAccessible(flag);
	    }
	}
}

最后使用JDK源码Constructor.newInstance(args);,因此cglib不能对声明为final的方法进行代理,因为cglib原理是动态生成被代理类的子类。

区别

主要区别:

  • JDK:利用拦截器(实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理;
  • cglib:利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理

具体来说:

JDK动态代理只能针对实现接口的类生成代理(实例化一个类)。此时代理对象和目标对象实现相同的接口,目标对象作为代理对象的一个属性,具体接口实现中,可以在调用目标对象相应方法前后加上其他业务处理逻辑。

cglib是针对类实现代理,主要是对指定的目标类生成一个子类(没有实例化一个类),覆盖其中的方法,通过方法拦截技术拦截所有父类方法的调用。

使用区别:

  • JDK不能用于非接口类、隐藏类、(未经允许扩展的)密封类(的子类)
  • cglib不能用于final方法、隐藏类、同上

Spring AOP

Spring AOP基于JDK Proxy和cglib来生成代理对象,具体使用哪种方式生成由AopProxyFactory根据AdvisedSupport对象的配置来决定。默认策略是如果目标类是接口,则使用JDK动态代理,如果目标对象没有实现接口,则默认会采用cglib代理。

代理模式将继承模式和关联模式结合在一起使用,是两者的综合体,不过这个综合体的作用倒不是解决对象注入的问题,而是为具体操作对象找到一个保姆或者是秘书,对外代表具体的实例对象,实例对象的入口和出口都是通过这个二号首长,具体的实例对象是一号首长,一号首长是要干大事的,所以一些事务性,重复性的工作例如泡茶,安排车子,这样的工作是不用劳烦一号首长的大驾,而是二号首长帮忙解决的,这就是AOP的思想。AOP解决程序开发里事务性,和核心业务无关的问题,但这些问题对于业务场景的实现是很有必要的,在实际开发里AOP也是节省代码的一种方式。

AOP将应用系统分为两部分,核心业务逻辑(Core business concerns)及横向的通用逻辑,也就是所谓的方面Crosscutting enterprise concerns,例如,大中型应用都会涉及到的持久化、事务、安全、日志和调试等。

实现AOP的技术,主要分为两大类:

  • 动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;
  • 静态织入的方式,引入特定的语法创建Aspect,从而使得编译器可以在编译期间织入有关Aspect代码。

拓展

代理服务器

一般有正向代理、反向代理。

正向代理

一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。客户端才能使用正向代理。如通过Chrome的SwitchSharp访问外网。

反向代理

反向代理:以代理服务器来接受Internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给Internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。这个服务器没有保存任何网页的真实数据,所有的静态网页或者CGI程序,都保存在内部的Web服务器上。因此对反向代理服务器的攻击并不会使得网页信息遭到破坏,这样就增强Web服务器的安全性。

反向代理经常和CDN一起工作,其基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置反向代理节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络,CDN系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。其目的是使用户可就近取得所需内容,解决Internet网络拥挤的状况,提高用户访问网站的响应速度。

区别

正向代理解决的是客户端访问互联网的问题,客户端知道目标的;

反向代理解决的是互联网收到客户端请求,如何把请求转到内网服务器的问题,不知道目标,代理的是服务端。

相关推荐
biubiubiu070617 小时前
代理模式(JDK,CGLIB动态代理,AOP切面编程)
代理模式
痞老板22 天前
【杂谈】虚拟机与EasyConnect运行巧设:Reqable助力指定应用流量专属化
运维·安全·fiddler·代理模式
西岭千秋雪_2 天前
设计模式の享元&模板&代理模式
java·设计模式·代理模式·享元模式·模板方法模式
越甲八千2 天前
重撸设计模式--代理模式
设计模式·代理模式
抓哇FullStack-Junior2 天前
设计模式——代理模式
java·开发语言·设计模式·代理模式
Adellle5 天前
判题机的开发(代码沙箱、三种模式、工厂模式、策略模式优化、代理模式)
java·后端·代理模式·策略模式
夏旭泽9 天前
设计模式-代理模式
设计模式·代理模式
努力学计算机的小白一枚10 天前
力扣1049.最后一块石头的重量(01背包)之理解篇
算法·leetcode·代理模式
lzz的编码时刻13 天前
深入理解代理模式(Proxy):静态代理、动态代理与AOP
java·设计模式·代理模式
huaqianzkh13 天前
代理模式的理解和实践
java·设计模式·代理模式