JAVA的动态代理是一个非常重要的实现技术,在jdk以及spring等框架中大量应用。
为什么动态代理这么重要呢?首先,什么是动态,什么是代理?
动态是相对静态而言,如java是先编译后运行,那么其代码是静态方式被读取和编译的,也可以说java是静态的语言,如果你想在一个java程序在编译之后的运行时期再去临时编译某个类从而获得实例对象,通常情况下是不能实现的,例如:new Student(),首先你要编写好student这个类,否则编译出错,因此你不能够在执行这行代码的时候才去编译student这个类。那么,动态的就是让其能够在运行时能够去编译并且执行的操作。
而代理,也就是代理模式,这里就不做解释了,有静态代理和动态代理,如上面所述,动态代理相对于静态代理,就是能够在运行时动态编译执行的代理功能模式,由于是动态执行,因此可以根据程序策略或状态动态的生成对应的类和实例,从而不必事先要编写每一个一一对应的代理类,极大的减少了代码量,另外,对于那些只有在运行时才能确定的策略,也可以使用动态代理实现随机应变的能力。
参考API,JDK提供了实现动态代理的相关组件。包括如下:
接口 InvocationHandler;
类 Proxy;
它们都在java.lang.reflect包中。
InvocationHandler是代理实例的调用处理程序 实现的接口。 该接口只有一个方法:Objectinvoke(Object proxy,Method method,Object[] args)throwsThrowable。直接的理解为:当调用被代理对象的某个方法时,实际上会在该接口的实现类上调用invoke方法。也就是说,invoke方法就是代替原来执行的方法。
Proxy 提供用于创建动态代理类和实例的静态方法,创建出来的代理类实例都是Proxy的子类。它的这两个方法是比较重要的:getProxyClass动态生成代理类定义,n而ewProxyInstance整合了getProxyClass方法并通过构造方法反射获得代理类的实例。
实际上JDK的动态代理实现是比较容易的,例如下面的例子:
//一个接口
public interface UserDao {
void save(User user);
}
//接口的实现类
public class UserDaoImpl implements UserDao{
@Override
public void save(User user) {
System.out.println("save");
}
}
//InvocationHandler的实现类,也就是调用处理类,调用处理程序
public class Handler implements InvocationHandler{
private UserDao UserDao;
public Handler(UserDao ud) {
this.UserDao = ud;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("start");//前置处理
method.invoke(UserDao, args);
System.out.println("end");//后置处理
return "done";
}
}
//测试代理使用类
public class TestProxy {
public static void main(String [] args){
UserDao ud = new UserDaoImpl();
Handler handler = new Handler(ud);//创建调用处理类
//得到代理类实例
UserDao proxy = (UserDao) Proxy.newProxyInstance(UserDao.class.getClassLoader(), new Class[]{UserDao.class}, handler);
proxy.save(new User());//调用代理类的方法
}
}
创建handler这一步是动态逻辑的关键,实际中可根据业务流程或者配置,更改需要使用handler,从而达到运行时改变原有接口方法save前后的处理逻辑,但是不会修改原有方法save中的逻辑。而这些修改后的逻辑对于原来UserDao的调用方来说是透明的,只需要实现一个代理工厂类获取代理实例即可和原来的方式一样调用Dao。如:
public void saveUser(){
UserDao ud = DaoProxyFactory.getUserDao();//通用的逻辑被抽离出去了,只留下关键业务逻辑
ud.save(new User());
}
如果换成非动态代理的方式,如:
public void saveUser(){
UserDao ud = new UserDaoImpl();
System.out.println("前置处理,如通用格式校验等等");//业务代码前后充满了各种通用逻辑,臃肿且难维护
ud.save(new User());
System.out.println("后置处理,如发出通知等等");
}
通过上面的例子,已经可以基本的使用动态代理了,但是动态代理不是纯编码的技术实现,它涉及到了底层的东西,因此需要加深理解,尤其是动态代理类实例的产生过程,根据API和相关源码我画了一个流程图,如果单纯用文字描述,恐难以说清楚。
动态代理的使用步骤按照上图的1-4步来操作,图中还说明了代理类实例的产生过程,这部分通常需要解读源码和反编译生成的文件来分析,这里图中我给出的是经过整理的重点部分,这样比较容易理解逻辑顺序。
这里结合代理类反编译后的代码分析,有几点需要注意:
1、JDK方式产生的所有代理实例都是Proxy类的子类,然后实现代理类实现的接口的方法,如上面的UserDao,在代理类的内部通过对原始调用的方法命名,如m1,m2,m3,save命名为m3,然后在替代方法中调用这些原始方法。
至于为什么m0,m1,m2没排上,其实是被object的三个方法用了。下面是api的一段描述:
- 在代理实例上的
java.lang.Object
中声明的hashCode
、equals
或toString
方法的调用将按照与编码和指派接口方法调用相同的方式进行编码,并被指派到调用处理程序的invoke
方法,如上所述。传递到invoke
的Method
对象的声明类是java.lang.Object
。代理类不重写从java.lang.Object
继承的代理实例的其他公共方法,所以这些方法的调用行为与其对java.lang.Object
实例的操作一样。
jdk方式的代理都是继承proxy类,因此只能代理多个接口,而无法对类进行代理。
public final class $Proxy0 extends Proxy implements UserDao
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
static
{
try
{
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {
Class.forName("java.lang.Object")
});
m3 = Class.forName("Jdkproxy.UserDao").getMethod("save", new Class[0]);
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
}
catch(NoSuchMethodException nosuchmethodexception)
{
throw new NoSuchMethodError(nosuchmethodexception.getMessage());
}
catch(ClassNotFoundException classnotfoundexception)
{
throw new NoClassDefFoundError(classnotfoundexception.getMessage());
}
}
2、在invoke方法中,不能在该方法中调用传入的Object proxy对象的方法,包括toString,hashCode,equlas,这是什么原因?
invoke(Object proxy,Method mehtod,Object[]args){
proxy.toString();//造成死循环
}
代理类反编译后的代码可知其形式如下:
public final void save(User args) {
try {
super.h.invoke(this, m3, args);
return;
}catch(Error _ex) { }
catch(Throwable throwable) {
throw new UndeclaredThrowableException(throwable);
}
}
public final String toString()
{
try
{
return (String)super.h.invoke(this, m2, null);//回调父类中的handler实例对应的方法,也就是说,你的handler所实现的invoke方法不仅是用来回调接口的业务方法,还用来回调object的toString,equals,hascode方法
}
catch(Error _ex) { }
catch(Throwable throwable)
{
throw new UndeclaredThrowableException(throwable);
}
}
在proxy对象中实现的业务方法和toString,hashCode,equlas方法内部都是回调InvocationHandler的invoke方法,在回调的同时将自身作为入参,所以在handler的invoke方法中,使用proxy调用这些方法必定无穷递归。
同时还得知,在invoke方法中,method.invoke()是必需编写的的,否则调用代理类实例的toString,hashCode,equlas方法时不会被执行,并且method.invoke(this,args)还是method.invoke(userDao,args)也是有区别的,这会决定调用谁的toString,hashCode,equlas。
3、如第2点所述,既然存在无穷递归的问题,又为何invoke方法第一个参数需要传入一个object?
根据代码可知在代理类实例中将自身对象传入 super.h.invoke(this, m3, args),但是我们实现的invoke方法中却不得而知其用途,后来我翻看了java有关动态代理的应用,也不知其然,索性翻看java自带的远程过程调用rmi包中的RemoteObjectInvocationHandler类,这个类也实现了InvocationHandler接口,通过排查调用invoke的方法应该能看出一些缘由:
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
if (! Proxy.isProxyClass(proxy.getClass())) {
throw new IllegalArgumentException("not a proxy");
}
if (Proxy.getInvocationHandler(proxy) != this) {
throw new IllegalArgumentException("handler mismatch");
}
if (method.getDeclaringClass() == Object.class) {
return invokeObjectMethod(proxy, method, args);
} else if ("finalize".equals(method.getName()) && method.getParameterCount() == 0 &&
!allowFinalizeInvocation) {
return null; // ignore
} else {
return invokeRemoteMethod(proxy, method, args);
}
}
Proxy.isProxyClass和Proxy.getInvocationHandler这两个方法就利用了proxy,确定当前invoke方法的对象是否为一个代理类,并且校验该代理对象是否为jdk创建的缓存代理对象, 所以传入的Object proxy这个对象的可用于安全性的校验。
//判断传入的类型超类是否为Proxy,并且是否已经在弱引用缓存中
public static boolean isProxyClass(Class<?> cl) {
return Proxy.class.isAssignableFrom(cl) && proxyClassCache.containsValue(cl);
}
/**
* a cache of proxy classes
*/
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
再看一下invokeRemoteMethod这个方法里面传入proxy的作用:
/**
* Handles remote methods.
**/
private Object invokeRemoteMethod(Object proxy,
Method method,
Object[] args)
throws Exception
{
try {
if (!(proxy instanceof Remote)) {
throw new IllegalArgumentException(
"proxy not Remote instance");
}
return ref.invoke((Remote) proxy, method, args,
getMethodHash(method));
} catch (Exception e) {
if (!(e instanceof RuntimeException)) {
Class<?> cl = proxy.getClass();
try {
method = cl.getMethod(method.getName(),
method.getParameterTypes());
} catch (NoSuchMethodException nsme) {
throw (IllegalArgumentException)
new IllegalArgumentException().initCause(nsme);
}
Class<?> thrownType = e.getClass();
for (Class<?> declaredType : method.getExceptionTypes()) {
if (declaredType.isAssignableFrom(thrownType)) {
throw e;
}
}
e = new UnexpectedException("unexpected exception", e);
}
throw e;
}
}
可以看出proxy在该方法发生异常的时候,还可以用于获取method等方法相关信息,用于定位问题。
如果想实现类的代理,可以使用CGLIB代理,它的原理和JDK并不一样,但是理解了JDK代理的原理,CGLIB也就不难理解,这个在另一篇中讨论。