本篇文章主要介绍一下动态代理在 Java 和 Android 中的应用。了解动态代理之前,你需要知道什么是代理以及什么是静态代理。由于篇幅所限,本文就不介绍这些了。你可以点击这里查看或者知乎上的讨论,你也可以自行 Google 一下。另外文中的两个实例 Demo 你可以在这里取到。
动态代理是 Java 一大特性。它的显著优势就是无侵入式的扩展代码。通俗来讲就是可以用来做方法的增强,让你可以在不修改源码的情况下,增强一些方法或者功能,在方法执行前后做任何你想做的事情。具体应用的话,比如可以添加调用日志,做事务控制等。
目前动态代理主要分为 Java 自己提供的动态代理和 CGLIB 类似框架。Java 自带的动态代理是需要接口来辅助的。CGLIB 这种则是直接修改字节码。
我们通过一个实例来看看 JDK 中时如何来使用动态代理的,我们要实现的效果是不改变原有代码结构的情况下,在调用一个方法前面插入另外的方法,达到在调用原方法前后打印日志信息的目的。
由于JDK 动态代理只能为接口创建代理,故提供一个接口以及其实现。Dog 接口定义如下:
interface Dog{
void info();
void run();
}
Dog 的具体实现如下:
class GunDog implements Dog{
@Override
public void info() {
System.out.println("我是一只狗");
}
@Override
public void run() {
System.out.println("我迅速奔跑");
}
}
接着提供一个 DogUtil,里面包含着两个通用方法,也就是我们希望在执行 GunDog 里面的两个方法的前后能够执行下面的两个通用方法:
class DogUtil{
// 模拟方法一
public void method1(){
System.out.println("==模拟第一个通用方法==");
}
// 模拟方法二
public void method2(){
System.out.println("==模拟第二个通用方法==");
}
}
动态代理的核心环节就是要实现 InvocaitonHandler 接口。每个动态代理类都必须要实现 InvocationHandler 接口,并且每个代理类的实例都关联到了一个 InvocationHandler 接口实现类的实例对象,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由 InvocationHandler 这个接口的 invoke() 方法来进行调用,具体的实现如下:
// 实现调度处理器接口
class MyInvocationHandler implements InvocationHandler{
// 需要被代理的那个对象
private Object target;
public void setTarget(Object target){
this.target = target;
}
// 当我们去调用被代理对象的方法时,invoke 方法将会取而代之。
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
DogUtil du = new DogUtil();
// 调用 GunDog 中的 info() 或者 run() 方法前
du.method1();
// 用反射去调用 Dog class 中的方法
Object result = method.invoke(target,args);// 注释一
// 调用 GunDog 中的 info() 或者 run() 方法后
du.method2();
return result;
}
}
对于上述代码,我们最直接的疑惑就是 method.invoke(target,args) 方法中的参数是什么意思?在哪里被调用的?先不要着急,我们先把过程走一遍,然后再仔细分析。
提供一个工厂方法,为指定的 target 生成代理对象:
class MyProxyFactory{
public static Object getProxy(Object target) throws Exception{
// 实例化 MyInvocationHandler 对象
MyInvocationHandler handler = new MyInvocationHandler();
handler.setTarget(target);
// 调用 proxy 的 newProxyInstance() 方法传入三个参数,创建代理类实例
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),handler);
}
}
上面的 newProxyInstance() 中的三个参数还是很重要的,我们稍微解释一下三个参数的含义:
参数一:类加载器对象即用哪个类加载器来加载这个代理类到 JVM 的方法区
参数二:接口表明该代理类需要实现的接口
参数三:是调用处理器类实例即 InvocationHandler 的实现的实例对象
这个函数是 JDK 为了程序员方便创建代理对象而封装的一个函数,因此你调用newProxyInstance()
时直接创建了代理对象。而对于类加载机制,推荐你看一下热修复入门:Android 中的 ClassLoader这篇博文。
最后写一个测试用例:
// 实例化一个被代理对象
Dog target = new GunDog();// 注意二
// 用被代理对象去产生一个代理类对象 dog
Dog dog = (Dog) MyProxyFactory.getProxy(target);
// 代理对象调用被代理对象的方法啦
dog.info();
dog.run();
结果如下:
==模拟第一个通用方法==
我是一只狗
==模拟第二个通用方法==
==模拟第一个通用方法==
我迅速奔跑
==模拟第二个通用方法==
如上我们用代理对象 dog 去调用了本不属于它的 info() 和 run() 方法。实际上,当调用代理对象的每个方法时,都会被替换执行 InvocationHandler 对象的 invoke() 方法。
当然啦,此时我们不得不进去 invoke() 方法中看看啦,也就是 2.3 小节中的 invoke(target,args) 方法。等等,method.invoke() 中的 method 是啥啊?其实这个就是我们想要 调用代理对象中的方法。那 invoke(target,args) 中的两个参数是什么啊?哈哈,往上看一眼呗,target 原来就是被代理对象啊,那 args 呢?这不容易知道,但是笔者查阅资料,发现该参数会传入被代理对象的方法中,由于本例子不需要这些参数,所以不用多做考虑啦。然而我们进去并没有发现什么可用的信息:
@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
那我们得调转枪头,看看另一个切入点 Proxy.newProxyInstance() 啦。贴出部分源码如下:
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
final Class>[] intfs = interfaces.clone();
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
// 调用 getProxyClass0() 方法
Class> cl = getProxyClass0(loader, intfs);
final Constructor> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
// 返回代理类的实例
return cons.newInstance(new Object[]{h});
}
上述代码中,除了最后返回的代理类实例,我们为一个的突破口就是 getProxyClass0() 方法啦,跟进去看看呗:
private static Class> getProxyClass0(ClassLoader loader,
Class>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
// proxyClassCache 来做一个中间的缓存
return proxyClassCache.get(loader, interfaces);
}
我们可以跟进 proxyClassCache 对象所对应的类中去看看,但是由于篇幅原因(有兴趣可以跟进这个类中看看),就大概说说吧。顾名思义其作用是一个缓存。将所有生成的代理类都存入一个 Map 中,由生成的接口与其对应。当已经拥有此代理类时则直接取出,无此代理类时则动态生成。此外还做了一些多线程的设计。由上继续跟,发现一个生成代理类的工厂类,不用说,里面肯定有能够生成代理类相关的内容:
private static final class ProxyClassFactory
implements BiFunction<ClassLoader, Class>[], Class>>
{
// prefix for all proxy class names
private static final String proxyClassNamePrefix = "$Proxy";
// next number to use for generation of unique proxy class names
private static final AtomicLong nextUniqueNumber = new AtomicLong();
@Override
public Class> apply(ClassLoader loader, Class>[] interfaces) {
// ...
// 注释一
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
// 注释二
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
}
}
}
我们理解一下注释一处的代码,这个地方提前说一下 generateProxyClass() 用来生成代理类的二进制字节码数据。下面我们会去验证的。不过你可以从 byte[] proxyClassFile 的定义猜到一点。注释二处再调用 defineClass0() 方法加载类。其内部实现是native层的,作用应该就类似于 ClassLoader.loadClass。
接着跟下去,其实也没啥必要了,不过还是要看一下,为了验证之前的猜想:
public static byte[] generateProxyClass(final String var0, Class>[] var1, int var2) {
ProxyGenerator var3 = new ProxyGenerator(var0, var1, var2);
final byte[] var4 = var3.generateClassFile();
if(saveGeneratedFiles) {
AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
try {
int var1 = var0.lastIndexOf(46);
Path var2;
if(var1 > 0) {
Path var3 = Paths.get(var0.substring(0, var1).replace('.', File.separatorChar), new String[0]);
Files.createDirectories(var3, new FileAttribute[0]);
var2 = var3.resolve(var0.substring(var1 + 1, var0.length()) + ".class");
} else {
var2 = Paths.get(var0 + ".class", new String[0]);
}
// 将数据写进文件
Files.write(var2, var4, new OpenOption[0]);
return null;
} catch (IOException var4x) {
throw new InternalError("I/O exception saving generated file: " + var4x);
}
}
});
}
return var4;
}
好了,以上就差不多了。其实也没必要挖这么深,对于这些我们只需要明白,最后会生成代理类的二进制字节码文件,并且会在运行时动态的加载该字节码文件。我们借助于反编译工具 dump 一下生成的 .class 文件,其核心代码如下:
public final class $Proxy1 extends Proxy implements Subject{
private InvocationHandler h;
private $Proxy1(){}
public $Proxy1(InvocationHandler h){
this.h = h;
}
public int request(int i){
// 创建 method 对象
Method method = Subject.class.getMethod("request", new Class[]{int.class});
// 调用了 invoke() 方法
return (Integer)h.invoke(this, method, new Object[]{new Integer(i)});
}
}
看到上面,我们大家想必都已经明白了整个流程。在我们的入口处即 newProxyInstance() 动态生成的代理类对象 $Proxy1 会去回调 MyInvocationHandler 的 invoke() 方法的。
上述的简单例子,帮助我们初步理解了动态代理到底是怎么一个回事,以及动态代理的基本用法。接下来我们看看动态代理在 Android 中的一个典型应用。
大名鼎鼎的 retrofit2.0 相信大家都已经用过了,里面的原理大家也应该略知一二。现在我们就在本文第一部分的的基础上看看 Retrofit2.0 中怎么用到动态代理以及使用动态代理带来的好处。下面用 retrofit2.0 来作为通信支持,来访问一下 GitHub 获得用户基本信息。
首先来根据 Retrofit2.0 的用法定义一个 GitHubApi 接口:
public interface GitHubApi {
@GET("users/{user}")
Call<Repo> listInfos(@Path("user") String user);
}
实体类 Repo 的代码如下:
public class Repo{
private String blog;
// 省略 get and set
}
MainActivity 中的主要方法如下实现:
btnSearch.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(API_URL)
.addConverterFactory(GsonConverterFactory.create())
.build();
// 注释一
GitHubApi github = retrofit.create(GitHubApi.class);
// 注释二
Call<Repo> call = github.listInfos("wuchangfeng");
call.enqueue(new Callback<Repo>() {
@Override
public void onResponse(Call<Repo> call, Response<Repo> response) {
// 打印出查询信息
Log.d(TAG,response.body().getBlog());
}
@Override
public void onFailure(Call<Repo> call, Throwable t) {
Log.d(TAG,t.getMessage());
}
});
}
});
OK. 如上代码,就能获取 GitHub 账号个人的信息了。接下来在如上注释一处,我们是否应该有点疑问?我们传入进去一个 GithubApi 接口的 .class 对象进去,结果又给我们返回了一个 GithubApi 接口的对象?那不是多此一举吗?为了一探究竟。我们进入 create() 方法中看看:
public <T> T create(final Class<T> service) {
//...
// 注释三 返回代理类的实例
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();
// 注释四
@Override public Object invoke(Object proxy, Method method, Object... args)
throws Throwable {
// 不用关心
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
// 兼容 java8
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
// 注释五
ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
// 注释六
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}
咦,代码总体结构上我们是不是似曾相识,特别从注释三开始。没错,是动态代理。从这里我们就大概知道了,retrofit2.0 内部也应用了动态代理技术。从而我们大致上也应该知道注释一处返回的 github 对象,已经是一个 GitHupApi 接口的代理对象啦。
还记得我们在注释二处的 github.listInfos(“allen”) 方法嘛,虽然我们用代理对象去调用 listinfos() 方法,但是基于前面的分析,实际上是去调用代理对象的 invoke() 方法啦。想必代理对象的 invoke() 方法内必定影藏了很多黑科技。
我们接着看看 invoke() 中的三个参数,有了一部分中的分析,我们很容易知道:
proxy 即 github 对象
args 参数,实际上需要传入的参数,在本例中就是 “allen”
method 即在接口 GitHubApi 中函数定义,明显我们可以获取到 @GET 注解以及参数 ,再次贴出其定义如下
@GET("users/{user}")
Call<Repo> listInfos(@Path("user") String user);
总体上而言,我们知道了当我们去调用 listInfos() 时,会去调用代理对象的 invoke() 方法,其实我们可以从注释五追踪下去,但是没啥必要了(文章侧重点在动态代理的应用)。而至此我们需要明白的就是为什么使用动态代理对象?再看注释五处,得到的 serviceMethod 和 okhttpcall 这意味 retrofit 内部已经将 GitHubApi 内部的方法转换成代理对象的方法并且 @GET 注解也已经转换成 HTTP 请求了。接下来都是 okHttp 的事情了…
而到这里我们也明白了动态代理的作用啦:基于其本身的特性,是在运行时动态生成代理类,灵活性可见一斑。另外 Retrofit2.0 中基于接口生成动态代理类的,并且接口的规范上设置了注解。HTTP 的请求就这样被简化了。另一个方面记得我们开始定义的那个 GitHubAPi 的接口吗?想想,按照正常的 Java 编程逻辑,我们是不是应该去实现接口,并且实现其里面的逻辑方法?如果没有注解的使用和动态代理,我们将不可避免的去写许多类似于 XXXImpl 的实现类。
因为文章是分析动态代理在 Retrofit2.0 中的作用,所以就不继续深挖下去了。
文章主要讲述动态代理在 Java 和 Android 的一些应用。其实对于动态代理,我们可以从两个方面来分析和学习:动态和代理。动态生成代码相对于静态编译期所带来的好处是非常明显的。而对于代理,我们可以从设计模式中的代理模式略窥一二。
在一部分中我们是想在调方法前后加上日志打印的功能。当然,这种功能太小了,这里也只是举个例子。一般的逻辑思维,是在调用方法前后,去硬编码调用其他的方法,而由此也会让程序出现耦合…而如上所示的动态代理能够解决完美的避免耦合发生的问题。
而在二部分中我们稍微探究了一下动态代理在 Retrofit2.0 中的应用。Retrofit2.0 用注解来描述一个 HTTP 请求,将其抽象成一个接口,接着动态的将这个接口的注解翻译成一个 HTTP 请求,并且最后再执行这个HTTP请求。
最后,本来想分析分析 Android 中 Hook 技术的。也成功的根据作者的文章实现了 Demo。后来改造了一下,一直没有成功,发邮件问了原作者,也没得到回应。同时也按照自己的思路写了一半关于 Hook 的文章,但是后来发现很难离开原作者的思路。大概是本人没有理解以及原作者写的太好了的原因吧。如果你有兴趣了解 Hook 技术,推荐你看Android插件化原理解析——Hook机制之动态代理。
简单来说:
静态代理:持有原有对象引用,在调用方法的时候添加操作
动态代理:InvocationHandler 重写其invoke方法,参数1 为被代理类,2为 被代理类方法,3为被代理方法参数。Proxy.newProxyInstance(被代理类ClassLoader,被代理类接口,invocationHandler实例)。基于接口才能做动态代理。用到反射。会在运行期间动态生成代理类的二进制字节码并进行替换。
HOOK:静动都能实现,拿到原有被代理类,并进行替换。
转自:http://allenwu.itscoder.com/use-of-proxy
参考:http://www.cnblogs.com/huhx/p/dynamicTheoryAdvance.html#dynamic_proxy_theory