java 动态代理和静态代理浅析

声明:本文只针对静态代理和动态代理的具体使用方法进行分析对比,并总结其中的优劣势。如果想透彻理解动态代理和静态代理的完整设计原理,建议参阅尚学堂马士兵老师在2010年推出的设计模式之动态代理视频讲解,也可辅助其他资料自行研究。


一、代理

1.1 什么是代理

代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。

根据代理类的生成时间不同可以将代理分为静态代理动态代理两种。


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

1. 主题接口:定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;
2. 真实主题:真正实现业务逻辑的类;
3. 代理类:用来代理和封装真实主题;
4. 客户端:使用代理类和主题接口完成一些工作。

在代理模式中真实主题角色对于客户端角色来说的透明的,也就是客户端不知道也无需知道真实主题的存在。为了保持行为的一致性,代理类和委托类通常会实现相同的接口,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。


从上图可以看出:

Client 客户端不管怎么样,要求给我一个类,我能调用 DoAction() 并且能得到我想要的结果就行,

而在“后端”这一块,不管是 RealSubject 还是 Proxy 都需要一个 DoAction() 方法,本来客户端 Client 好好的调用 RealSubject 就行了,干嘛要来个代理呢?原因是,有时候要在执行(注意是运行时)DoAction() 之前或者之后干点什么(比如记录日志,程序运行时间等)。那有人就说了,可以直接在 Client 调用DoAction() 方法的时候,前后加操作逻辑代码呀,当程序代码量少可以这样做,但是要是有成百上千都是要记录日志呢,都手动复制一遍?当复制完之后,需求有变,那又来一次成百上千的修改复制?

因此,通过单纯的修改 Client 中的代码实现超灵活的调用 DoAction() 方法(指方法前后做一些业务逻辑程序)显然是不科学的,这就引出了代理,怎么代理呢?我们可以设计一个 Proxy 类(称为代理类),里面也有一个 DoAction() 方法(不单纯是方法名字一样,方法块里面的代码一致才能实现 RealSubject 的 DoAction() 方法),而在这个 Proxy 类里的DoAction()方法要想有它一样的代码块,不可能再复制一遍(那样的话和前者有啥区别),因此可以必须在 Proxy 类是能有有一个 RealSubject 对象作为 Proxy 类的属性值,在 DoAction() 方法里创建 RealSubject 类,并调用它的 DoAction() 方法,最后在这个 DoAction() 方法前后就可以增加业务逻辑代码,然后由 Client 客户端来调用这个 Proxy 类就可以了。

这时候 Client 可以创建 Proxy 类并调用它的 DoAction() 方法来实现执行了 RealSubject 类的 DoAction() 方法并有前后业务逻辑处理。这时,又来了一个问题,需求变了,就得再创建另一个 Proxy 类,Client 类里面创建的第一个 Proxy 类救得修改,什么都架不住量大里面的重复,需求一旦变化,需要修改两处代码,因此接口诞生了, Proxy 类和 RealSubject 类都有一个 DoAction() 方法,那设计他们都实现这个 Subject 接口,在 Client 类创建的时候,引用类似于 Proxy 类(代理类)的时候使用接口作为引用指针(多态),即使需求变了,只需要修改 Proxy 类即可。这时候的设计模式就是静态设计模式。

根据代理类的生成时间不同可以将代理分为静态代理和动态代理两种。 

1.2 代理的优点

隐藏委托类的实现,调用者只需要和代理类进行交互即可。
解耦,在不改变委托类代码情况下做一些额外处理,比如添加初始判断及其他公共操作。

1.3 代理模式的应用场景

代理的使用场景很多,struts2中的 Action 调用,Hibernate的懒加载, Spring的 AOP无一不用到代理。

总结起来可分为以下几类:

1. 在原方法执行之前和之后做一些操作,可以用代理来实现(比如记录Log,做事务控制等)。
2. 封装真实的主题类,将真实的业务逻辑隐藏,只暴露给调用者公共的主题接口。
3. 在延迟加载上的应用。

二、静态代理

所谓静态代理也就是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。

代码演示:

创建一个 Tank 原始类,实现了 Moveable 接口:

package com.test.proxy;

import java.util.Random;

public class Tank implements Moveable {

	@Override
	public void move() {
		Long start = System.currentTimeMillis();
		System.out.println("Tank moving...");
		// 睡眠时间:产生1000以内的随机毫秒值,表示坦克在移动
		try {
			Thread.sleep(new Random().nextInt(1000));
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		Long end = System.currentTimeMillis();
		System.out.println("time:" + (end-start));
	}

	@Override
	public void stop() {
		System.out.println("坦克停止了...");
	}
	
}
代理和被代理的共同接口:
package com.test.proxy;

public interface Moveable {
	void move();
	void stop();
}
静态代理 MyProxy 类代码实现:

package com.test.proxy;

public class MyProxy implements Moveable{

	private Moveable myProxy;
	
	public MyProxy(Moveable myProxy) {
		this.myProxy = myProxy;
	}
	
	@Override
	public void move() {
		System.out.println("静态代理开始...");
		myProxy.move();
		System.out.println("静态代理结束...");
	}

	@Override
	public void stop() {
		System.out.println("坦克停止了...");
	}

}

静态代理类优缺点

优点:业务类只需要关注业务逻辑本身,保证了业务类的重用性。这是代理的共有优点。

缺点:
1. 代理对象的一个接口只服务于一种类型的对象,如果要代理的方法很多,势必要为每一种方法都进行代理,静态代理在程序规模稍大时就无法胜任了。
2. 如果接口增加一个方法,除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。

另外,如果要按照上述的方法使用代理模式,那么真实角色(委托类)必须是事先已经存在的,并将其作为代理对象的内部属性。但是实际使用时,一个真实角色必须对应一个代理角色,如果大量使用会导致类的急剧膨胀;此外,如果事先并不知道真实角色(委托类),该如何使用代理呢?这个问题可以通过Java的动态代理类来解决。

三、动态代理

动态代理类的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以不存在代理类的字节码文件。代理类和委托类的关系是在程序运行时确定。

在java的动态代理API中,有两个重要的类和接口,一个是 InvocationHandler(Interface)、另一个则是 Proxy(Class),这一个类和接口是实现我们动态代理所必须用到的。

InvocationHandler(Interface) InvocationHandler是负责连接代理类和委托类的中间类必须实现的接口,

它自定义了一个 invoke 方法,用于集中处理在动态代理类对象上的方法调用,通常在该方法中实现对委托类的代理访问。
InvocationHandler 的核心方法
Object invoke(Object proxy, Method method, Object[] args)
参数说明:
proxy 该参数为代理类的实例
method 被调用的方法对象
args 调用method对象的方法参数
该方法也是InvocationHandler接口所定义的唯一的一个方法,该方法负责集中处理动态代理类上的所有方法的调用。

调用处理器根据这三个参数进行预处理或分派到委托类实例上执行。

Proxy(Class)
Proxy是 Java 动态代理机制的主类,它提供了一组静态方法来为一组接口动态地生成代理类及其对象。
Proxy 的静态方法
static InvocationHandler getInvocationHandler(Object proxy)
该方法用于获取指定代理对象所关联的调用处理器
static Class getProxyClass(ClassLoader loader, Class[] interfaces)
该方法用于获取关联于指定类装载器和一组接口的动态代理类的类对象
static boolean isProxyClass(Class cl)
该方法用于判断指定类对象是否是一个动态代理类

static Object newProxyInstance(ClassLoader loader, Class[] interfaces,InvocationHandler h)

参数说明:
loader 指定代理类的ClassLoader加载器
interfaces 指定代理类要实现的接口
h: 表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上
该方法用于为指定类装载器、一组接口及调用处理器生成动态代理类实例

使用Java 动态代理的两个重要步骤

      1. 通过实现 InvocationHandler 接口创建自己的调用处理器;

      2. 通过为Proxy类的newProxyInstance方法指定代理类的ClassLoader 对象

和代理要实现的interface以及调用处理器InvocationHandler对象 来创建动态代理类的对象;

代码实现第一步:

创建自己定义的动态调用处理器 MyInvocation,实现 InvocationHandler 接口,在重写的 invoke() 方法里写自己要控制的业务逻辑代码,

并使用调用 method.invoke(target,ags) (target 是创建 MyInvocation 的时候传进去的,类似于静态代码里的 Proxy 类)

就能在代码运行的时候执行原始的被代理对象的XXX方法了。

package com.test.proxy;

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

public class MyInvocation implements InvocationHandler {

	private Object target;
	
	public MyInvocation(Object target) {
		this.target = target;
	}
	
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		
		if("move".equals(method.getName())) {
			System.out.println("动态代理开始...");
			method.invoke(target, args);
			System.out.println("动态代理结束...");
		}else {
			System.out.println("动态代理开始...");
			System.out.println("非 move() 方法执行了");
			method.invoke(target, args);
			System.out.println("动态代理结束...");
		}
		
		return null;
	}

}
测试执行(静态代理和动态代理):

从代码执行结果可以看出,动态代理随便调用 Moveable 接口中的方法(代理对象已经动态的实现了被代理对象的所有方法),

而在静态代理中如果接口中多增加一个方法(即原始的被代理对象再增加方法),

代理类要想代理这个新方法的时候就得再重写接口里的新方法,这有劣势明显体现出来了。

package com.test.proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class Test {
	public static void main(String[] args) {
		Tank tank = new Tank();
		
		// 动态代理
		InvocationHandler myInvocation = new MyInvocation(tank);
		Moveable m = (Moveable)Proxy.newProxyInstance(tank.getClass().getClassLoader(), tank.getClass().getInterfaces(), myInvocation);
		m.move();
		m.stop();
		
		System.out.println("----分割线----");
		
		// 静态代理
		MyProxy myProxy = new MyProxy(tank);
		myProxy.move();
		myProxy.stop();
		
	}
	
}
动态代理类优缺点
优点
1.
动态代理类的字节码在程序运行时由Java反射机制动态生成,无需程序员手工编写它的源代码。
2. 动态代理类不仅简化了编程工作,而且提高了软件系统的可扩展性,因为Java 反射机制可以生成任意类型的动态代理类。
缺点

JDK的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能实现JDK的动态代理,

cglib是针对类来实现代理的,他的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,

但因为采用的是继承,所以不能对final修饰的类进行代理。


博文参考:

Java代理和动态代理机制分析和应用

https://github.com/crazycodeboy/Java_Advanced/tree/master/Java_Proxy_Test

java静态代理和动态代理

http://layznet.iteye.com/blog/1182924









  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值