来个一个简单的需求。界面有个简单的按钮,实现对按钮的监听。这个需求太简单了。
首先我们定义一个OnClickListener接口。
public interface OnClickListener {
void onClick();
}
再写一个OnClickListenerImpl实现OnClickListener接口
public class OnClickListenerImpl implements OnClickListener {
@Override
public void onClick() {
System.out.println("onClick");
}
}
现在需要加功能了。为了有日志的回溯,提出了需要记录点击前和点击后的时间戳。这里的时间记录,就通过简单的System.out.println()实现好了。首先想到的方法肯定是重写OnClickListenerImpl。变成这样
public class OnClickListenerImpl implements OnClickListener {
@Override
public void onClick() {
System.out.println(System.currentTimeMillis());
System.out.println("onClick");
System.out.println(System.currentTimeMillis());
}
}
但是,这样的修改并不符合开闭原则——即对新增功能开放,对修改功能关闭。我们这里直接改动了原来的代码,这是非常不合理的。这里又想到了一个办法,我们可以将OnClickListener再包装到一个类里面,通过那个类去实现时间的记录。代码就变成了这样:
public class OnClickListenerProxy implements OnClickListener {
private OnClickListener listener;
public OnClickListenerProxy(OnClickListener listener) {
this.listener = listener;
}
@Override
public void onClick() {
System.out.println(System.currentTimeMillis());
if (null != listener) {
listener.onClick();
}
System.out.println(System.currentTimeMillis());
}
}
这里,我们通过新建一个类,没有对原有的代码进行修改,达到了记录日志的效果。符合开闭原则。
上面的思想就是静态代理的思想,定义一个代理类,而具体的实现交给代理类的成员实现。上面的方案已经能解决很多问题了。
但是,假设原有实现OnClickListener接口的类有100个,要为这100个类都增加日志效果,可能就需要新建100个代理类去实现了。
这就涉及到动态代理。所谓的动态代理其实就是把真正的调用转交给一个其他的类,我们可以在这次调用前和调用后执行我们想要的代码,达到记录日志的目的。
在JDK中有原生的动态代理类——InvocationHandler。我们先来简单的了解一下这个类的使用。
class Handler implements InvocationHandler {
private OnClickListener listener;
public Handler(OnClickListener listener) {
this.listener = listener;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(System.currentTimeMillis());
// 这里才是真正的调用了onClick
Object object = method.invoke(listener, args);
System.out.println(System.currentTimeMillis());
return object;
}
}
我们建立测试类:
InvocationHandler handler = new Handler(new OnClickListenerImpl());
OnClickListener listener = (OnClickListener) Proxy.newProxyInstance(
OnClickListener.class.getClassLoader(),
new Class[]{OnClickListener.class},
handler
);
listener.onClick();
我们新建了一个handler对象,然后通过Proxy.newProxyInstance()方法返回对应的OnClickListener。但是由于返回的OnClickListener对象由Proxy代理产生的,所以在调用onClick()函数的时候,就会调用Handler的invoke方法。
从而实现在Java中动态的为代码块增加功能。
首先需要确定的是动态代理肯定是使用了Java的反射机制,很多人都说反射机制都会变慢。我们先做一个对比吧。
次数 100 1000 10000 100000 1000000
直接调用 0 0 2 3 7
反射调用 3 5 12 14 20
事件消耗都是毫秒级的消耗。也就是说,反射机制不会太影响性能,并且灵活度更高
上述使用了Proxy.newProxyInstance就返回了一个OnClickListener的对象了。我们还是需要好好学习原理的。在这之前,我们先看看Java的反射使用的。我们现在的调用方法都是通过直接调用的,有没有一直方式是通过方法反射回去的呢?答案肯定是有的。
Method method = OnClickListener.class.getMethod("onClick");
method.invoke(new OnClickListenerImpl());
上面只是简单的使用了Method.invoke()方法,第一个参数就是调用方,剩下的参数就是onClick这个函数需要的参数,由于上面没有参数的,所以就没有填写。具体更多的使用方式,可以再好好研究。比如说:通过Field反射更改对象属性的值,通过AnnotatedType获取对象实现的接口等等。。。顺便提一句,Java中所有的注解类型本质都是接口类型。
反射能做的事情太多太多了,真正的调用方不需要直接调用,而是通过方法反射调用,这就是很基本的动态代理的思想。而Proxy.newProxyInstance()的实现呢?
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
// 先判断Handler是否为null
Objects.requireNonNull(h);
// 将传入的接口clone一份
final Class<?>[] intfs = interfaces.clone();
// 获取安全管理器
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
// 这里判断是否有权限产生代理类
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
// 这里获取JDK生成的Proxy的Class对象,而生成的Proxy是实现了OnClickListener接口的。
Class<?> cl = getProxyClass0(loader, intfs);
try {
if (sm != null) {
// 判断权限
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
// 获取参数为InvocationHandler的Proxy的构造器
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
// 将方法都设置为public
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
// 返回的是OnClickListenerImpl对象,参数为Handler
return cons.newInstance(new Object[]{h});
} catch (IllegalAccessException|InstantiationException e) {
throw new InternalError(e.toString(), e);
} catch (InvocationTargetException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new InternalError(t.toString(), t);
}
} catch (NoSuchMethodException e) {
throw new InternalError(e.toString(), e);
}
}
但是有个很疑惑的问题,为什么在构造Proxy的时候需要传入一个Handler对象呢?我们并没有写那个构造器。唯一的解释就是——JDK在编译阶段为Proxy生成了一个带有Handler的构造器。我们怎样查看呢?
在main函数开始运行前,加上System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
就可以了,这里是我运行的结果。
生成了Proxy0.class。我们点开Proxy0.class文件。而我们很明显的就可以看到 Proxy0.class有一个带InvocationHandler参数的构造器。并且,我们可以看出Proxy0.class中所有的的实现,都是通过InvocationHandler.invoke()方法实现的,最终又会回到了我们自定义的Handler。
但是,如果我们没有使用Proxy.newProxyInstance的话,是不会生成Proxy.class的。
其实反射机制能完成很多很多很多事情:
1、结合类加载机制,实现热修复功能。我们可以将编译好的class文件,在代码运行时加载到jvm中,再运行新的代码。
2、完成很多很多框架。比如说Android中的xUtils通过注解的方式获取控件、Spring的面向切面等等。
3、调用编译时没有的代码,可以变成新加的功能。
4、。。。。。。