有点深度的聊聊JDK动态代理
在接触SpringAOP的时候,大家一定会被这神奇的功能所折服,想知道其中的奥秘,底层到底是如何实现的。于是,大家会通过搜索引擎,知道了一个陌生的名词:动态代理,慢慢的又知道了动态代理有多种实现方式,比如 JDK动态代理,Cglib 等等。今天我就来简单说说JDK动态代理。
JDK动态代理的简单应用
我们还是从一个最简单的例子着手:
首先我们需要定义一个接口:
public interface UserService {
void query();
}
然后实现这个接口:
public class UserServiceImpl implements UserService {
public void query() {
System.out.println("查询用户信息");
}
}
定义一个类,需要实现InvocationHandler:
public class MyInvocationHandler implements InvocationHandler {
Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("进入了invoke");
method.invoke(target);
System.out.println("执行了invoke");
return null;
}
}
然后就是Main方法了:
public class Main {
public static void main(String[] args) {
MyInvocationHandler myInvocationHandler = new MyInvocationHandler(new UserServiceImpl());
Object o = Proxy.newProxyInstance(Main.class.getClassLoader(),
new Class[]{UserService.class}
, myInvocationHandler);
((UserService)o).query();
}
}
运行:
可以看到,一切正常,成功的执行了增强的逻辑,也执行了目标方法。
三个疑惑
虽然说这是最简单的一个例子了,但是在初学的时候,大家肯定和我一样,有不少疑惑:一是不知道为什么需要传入接口,二是不知道为什么JDK动态代理只能代理接口,三是不知道类加载器的作用。还有,就是代码比较复杂。
这三个疑惑困扰我很久,直到我跟着博客,自己手撸一个阉割版的JDK动态代理,并且简单的看了下JDK最终生成的代码以及源码才明白。
写一个阉割版的JDK动态代理
我们先来分析下MyInvocationHandler类中的invoke方法,方法有三个参数,第一个参数是代理类,第二个参数是方法,第三个参数是 执行方法需要用到的参数。方法内部实现了两个逻辑,一个是增强逻辑 ,一个是执行目标方法。我们不禁的想,如果我们可以自动生成一个类,去调用MyInvocationHandler中的invoke方法是不是就可以实现动态代理了。
人有多大胆,地有多大产,这的确是一个大胆疯狂的想法,但是这确实可以办到,主要有如下几个步骤:
- 拼接代理类的代码
- 输出.java文件
- 编译.java文件成.class文件
- 装载.class文件
- 创建并返回代理类对象
为了方便,就不考虑返回值和带参的情况了,我仿照现有的MyInvocationHandler 写了一个阉割版的MockInvocationHandler类:
public class MockInvocationHandler {
private Object targetObject;
public MockInvocationHandler(Object targetObject) {
this.targetObject = targetObject;
}
public void invoke(Method targetMethod) {
try {
System.out.println("进入了invoke");
targetMethod.invoke(targetObject, null);
System.out.println("结束了invoke");
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
要调用到MockInvocationHandler 中的invoke方法,生成的代理类大概可能也许长这个样子:
public class $Proxy implements 需要代理的接口{
MockInvocationHandler h;
public $Proxy (MockInvocationHandler h ) {this.h = h; }
public void query(){
try{
//method=需要的执行方法
this.h.invoke(method);
}catch(Exception ex){}
}
}
好了,接下来就是体力活了,直接贴上代码:
public class MockProxy {
final static String ENTER = "\n";
final static String TAB = "\t";
public static Object newProxyInstance(Class interfaceClass,MockInvocationHandler h) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("package com.codebear;");
stringBuilder.append(ENTER);
stringBuilder.append("import java.lang.reflect.*;");
stringBuilder.append(ENTER);
stringBuilder.append("public class $Proxy implements " + interfaceClass.getName() + "{");
stringBuilder.append(ENTER);
stringBuilder.append(TAB);
stringBuilder.append(" MockInvocationHandler h;");
stringBuilder.append(ENTER);
stringBuilder.append(TAB);
stringBuilder.append(" public $Proxy (MockInvocationHandler h ) {this.h = h; }");
stringBuilder.append(ENTER);
stringBuilder.append(TAB);
for (Method method : interfaceClass.getMethods()) {
stringBuilder.append(" public void " + method.getName() + "(){");
stringBuilder.append(ENTER);
stringBuilder.append(TAB);
stringBuilder.append(" try{ ");
stringBuilder.append(ENTER);
stringBuilder.append(TAB);
stringBuilder.append(TAB);
stringBuilder.append(" Method method = " + interfaceClass.getName() + ".class.getMethod(\"" + method.getName() + "\");");
stringBuilder.append(ENTER);
stringBuilder.append(TAB);
stringBuilder.append(TAB);
stringBuilder.append(" this.h.invoke(method);");
stringBuilder.append(ENTER);
stringBuilder.append(TAB);
stringBuilder.append(TAB);
stringBuilder.append("}catch(Exception ex){}");
stringBuilder.append(ENTER);
stringBuilder.append(TAB);
stringBuilder.append("}");
stringBuilder.append(ENTER);
stringBuilder.append("}");
}
String content = stringBuilder.toString();
try {
String filePath = "D:\\com\\codebear\\$Proxy.java";
File file = new File(filePath);
File fileParent = file.getParentFile();
if (!fileParent.exists()) {
fileParent.mkdirs();
}
FileWriter fileWriter = new FileWriter(file);
fileWriter.write(content);
fileWriter.flush();
fileWriter.close();
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = compiler.getStandardFileManager
(null, null, null);
Iterable iterable = fileManager.getJavaFileObjects(filePath);
JavaCompiler.CompilationTask task = compiler.getTask
(null, fileManager, null, null, null, iterable);
task.call();
fileManager.close();
URLClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:D:\\\\")});
Class<?> clazz = classLoader.loadClass("com.codebear.$Proxy");
Constructor<?> constructor = clazz.getConstructor(MockInvocationHandler.class);
return constructor.newInstance(h);
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
}
然后测试一下:
public class Main {
public static void main(String[] args) {
MockInvocationHandler mockInvocationHandler=new MockInvocationHandler(new UserServiceImpl());
UserService userService = (UserService)MockProxy.
newProxyInstance(UserService.class, mockInvocationHandler);
userService.query();
}
}
运行结果:
好了,在不考虑性能,可维护性,安全性的情况下,我们阉割版的动态代理就完成了。代码难度不是很大,就是比较考验反射和耐心。
简单分析下JDK源码
源码基于JDK1.8
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);
}
/*
* 得到代理类
*/
Class<?> cl = getProxyClass0(loader, intfs);
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}
final Constructor<?> cons = cl.getConstructor(constructorParams);//获得构造方法
final InvocationHandler ih = h;
//如果构造器器不是公共的,需要修改访问权限,使其可以访问
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});//通过构造方法,创建对象,传入InvocationHandler 对象
} 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);
}
}
简单的看下源码,我们一下子就能把目光移动到getProxyClass0方法了,这才是我们需要关心的,我们点进去:
private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
//当接口大于65535报错
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
return proxyClassCache.get(loader, interfaces);
}
这方法可以说什么事情也没干,但是通过最后的proxyClassCache.get可以很容易的知道JDK的动态代理是用了缓存的,我们需要关注的方法在get里面,继续点进去:
public V get(K key, P parameter) {
Objects.requireNonNull(parameter);
expungeStaleEntries();
//通过上游方法,可以知道key是类加载器,这里是通过类加载器可以获得第一层key
Object cacheKey = CacheKey.valueOf(key, refQueue);
//我们查看map的定义,可以看到map变量是一个两层的ConcurrentMap
ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);//通过第一层key尝试获取数据
//如果valuesMap 为空,就新建一个ConcurrentHashMap,
//key就是生成出来的cacheKey,并把这个新建的ConcurrentHashMap推到map
if (valuesMap == null) {
ConcurrentMap<Object, Supplier<V>> oldValuesMap
= map.putIfAbsent(cacheKey,
valuesMap = new ConcurrentHashMap<>());
if (oldValuesMap != null) {
valuesMap = oldValuesMap;
}
}
//通过上游方法可以知道key是类加载器,parameter是类本身,这里是通过类加载器和类本身获得第二层key
Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
Supplier<V> supplier = valuesMap.get(subKey);
Factory factory = null;
while (true) {
if (supplier != null) {
//如果有缓存,直接调用get方法后返回,当没有缓存,会继续执行后面的代码,
//由于while (true),会第二次跑到这里,再get返回出去,
//其中get方法调用的是WeakCahce中的静态内部类Factory的get方法
V value = supplier.get();
if (value != null) {
return value;
}
}
//当factory为空,会创建Factory对象
if (factory == null) {
factory = new Factory(key, parameter, subKey, valuesMap);
}
if (supplier == null) {
supplier = valuesMap.putIfAbsent(subKey, factory);
if (supplier == null) {
//当没有代理类缓存的时候,会运行到这里,把Factory的对象赋值给supplier ,
//进行下一次循环,supplier就不为空了,可以调用get方法返回出去了,
//这个Factory位于WeakCahce类中,是一个静态内部类
supplier = factory;
}
} else {
if (valuesMap.replace(subKey, supplier, factory)) {
supplier = factory;
} else {
supplier = valuesMap.get(subKey);
}
}
}
}
这里面的代码比较复杂,简单的来说:
- JDK动态代理是用了两层的map去缓存,第一个层是类加载器,第二层是 类加载器+本身
- 当有缓存,直接调用get并且返回,反之继续执行下面的代码,为supplier进行赋值,由于while (true),会第二次跑到这里,再调用get()返回出去。核心在于supplier.get(),它调用的是WeakCahce中的静态内部类Factory的get(),里面就是 获取代理类的方法了。
让我们看下supplier.get()方法:
value = Objects.requireNonNull(valueFactory.apply(key, parameter));
核心在于这一句话,但是valueFactory是什么?我们可以查看它的定义:
private final BiFunction<K, P, V> valueFactory;
我们再看下它的WeakCahce构造方法:
public WeakCache(BiFunction<K, P, ?> subKeyFactory,
BiFunction<K, P, V> valueFactory) {
this.subKeyFactory = Objects.requireNonNull(subKeyFactory);
this.valueFactory = Objects.requireNonNull(valueFactory);
}
我们肯定在哪边调用过这个构造方法了,在Proxy类中有这样的定义:
private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
这个proxyClassCache有没有很熟悉, 是的,它就在getProxyClass0方法中用到了,这里创建了WeakCache对象,并且调用了带两个参数的构造方法,第二个参数是ProxyClassFactory对象,也就对应了WeakCache中第二个参数BiFunction<K, P, V> valueFactory,然后把值赋值给了final valueFactory,valueFactory.apply所以最终会调用ProxyClassFactory中的apply方法。关键在于:
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);//生成代理类的二进制数组
try {
//内部是native标记的方法,是用C或者C++实现的,这里不深究
//方法内部就是通过类加载器和上面生成的代理类的二进制数组等数据,经过处理,成为Class
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
throw new IllegalArgumentException(e.toString());
}
generateProxyClass方法内部生成了代理类的二进制数组,具体是怎么生成的,大家可以点进去自己看看,这里就不再继续往下了,因为我们的目标就是找到generateProxyClass方法,然后自己写一个方法,去执行generateProxyClass,把返回的byte[]输出到.class文件,利用idea的反编译功能,看看最终生成出来的代理类是什么样子的:
byte[] $proxies = ProxyGenerator.generateProxyClass("$Proxy", new Class[]{UserService.class});
File file=new File("D:\\$Proxy.class");
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(file);
try {
outputStream.write($proxies);
} catch (IOException e) {
e.printStackTrace();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
运行,发现D盘出现了$Proxy.class文件,我们把它拖到idea里面,看看它的真面目,因为生成的代码还是比较长的,我这里只把核心代码贴出来:
//继承了Proxy类
public final class $Proxy extends Proxy implements UserService {
public $Proxy(InvocationHandler var1) throws {
super(var1);
}
public final void query() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
}
这代码有没有很熟悉,很接近我们自己手写动态代理生成的代理类。
解开疑惑
好了,先是自己手写了一个阉割版的动态代理,然后简单的看了下JDK动态代理源码,也看了下JDK动态代理生成的代理类。这样,就可以解开上面的三个疑惑了:
- 类加载器是干嘛的:其一:JDK内部需要通过类加载作为缓存的key 其二:需要类加载器生成class
- 为什么需要接口:因为生成的代理类需要实现这个接口
- 为什么JDK动态代理只能代理接口:因为生成的代理类已经继承了Proxy类,Java是单继承的,所以没法再继承另外一个类了。
有一些博客上可能会说cglib和JDK动态代理的区别,cglib是通过操作字节码去完成代理的,其实JDK动态代理也操作了字节码。
经过这么一分析,相信大家对JDK动态代理有了一个新的认识。
有点深度的聊聊JDK动态代理的更多相关文章
- 深度剖析java中JDK动态代理机制
https://www.jb51.net/article/110342.htm 本篇文章主要介绍了深度剖析java中JDK动态代理机制 ,动态代理避免了开发人员编写各个繁锁的静态代理类,只需简单地指定 ...
- 深度剖析JDK动态代理机制
摘要 相比于静态代理,动态代理避免了开发人员编写各个繁锁的静态代理类,只需简单地指定一组接口及目标类对象就能动态的获得代理对象. 代理模式 使用代理模式必须要让代理类和目标类实现相同的接口,客户端通过 ...
- 静态代理和jdk动态代理
要说动态代理,必须先聊聊静态代理. 静态代理 假设现在项目经理有一个需求:在项目现有所有类的方法前后打印日志. 你如何在不修改已有代码的前提下,完成这个需求? 我首先想到的是静态代理.具体做法是: 1 ...
- 面试造火箭系列,栽在了cglib和jdk动态代理
"喂,你好,我是XX巴巴公司的技术面试官,请问你是张小帅吗".声音是从电话那头传来的 "是的,你好".小帅暗喜,大厂终于找上我了. "下面我们来进行一 ...
- JDK动态代理为什么必须要基于接口?
原创:微信公众号 码农参上,欢迎分享,转载请保留出处. 前几天的时候,交流群里的小伙伴抛出了一个问题,为什么JDK的动态代理一定要基于接口实现呢? 好的安排,其实要想弄懂这个问题还是需要一些关于代理和 ...
- JDK动态代理
一.基本概念 1.什么是代理? 在阐述JDK动态代理之前,我们很有必要先来弄明白代理的概念.代理这个词本身并不是计算机专用术语,它是生活中一个常用的概念.这里引用维基百科上的一句话对代理进行定义: A ...
- 静态代理和利用反射形成的动态代理(JDK动态代理)
代理模式 代理模式的定义:为其他对象提供一种代理以控制对这个对象的访问.在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用. 静态代理 1.新建 ...
- Spring中的JDK动态代理
Spring中的JDK动态代理 在JDK1.3以后提供了动态代理的技术,允许开发者在运行期创建接口的代理实例.在Sun刚推出动态代理时,还很难想象它有多大的实际用途,现在动态代理是实现AOP的绝好底层 ...
- AOP学习心得&jdk动态代理与cglib比较
什么是AOP AOP(Aspect-OrientedProgramming,面向方面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善.OOP引入 ...
随机推荐
- CF1093
题解: D: 比较显然这个图得是二分图才行 然后每个二分图上的方案是$(2^a+2^b) (a,b是两种颜色的个数)$ E: 我tm就不该先写bitset的 正解和bitset都很好想 因为是个排列, ...
- IE外挂
//引用 Windows/system32/Shell32.dll //引用COM组件 shdocvw.dll (Microsoft Internet Controls) //引用COM组件 msht ...
- 在Eclipse中使用git把项目导入到git中--转载
[转载出处注明:http://www.zhangxiaofu.cn/java/commonTools/2015/0607/764.html] 一.原有项目: 项目名为TestGit 二.在osc@g ...
- Shell编程-控制结构 | 基础篇
if-then-else分支结构 if-then-else是一种基于条件测试结果的流程控制结构.如果测试结果为真,则执行控制结构中相应的命令列表:否则将进行另外一个条件测试或者退出该控制结构. if- ...
- Alpha冲刺(2/10)——2019.4.24
作业描述 课程 软件工程1916|W(福州大学) 团队名称 修!咻咻! 作业要求 项目Alpha冲刺(团队) 团队目标 切实可行的计算机协会维修预约平台 开发工具 Eclipse 团队信息 队员学号 ...
- 【Vue-Cli3.0】【1】创建一个Vue-Cli3.0的项目
最近在做爬虫,然后要爬好多数据,代码写完了,就让它在爬了.不想闲着就复习一下Vue吧! 开始开始! ***正式讲解之前 先下载一个node.js吧! 一.首先检查一下 版本 PS D:\徐孟林\D D ...
- Navicat Premium 12.1.11.0安装与激活
本文介绍Navicat Premium 12.1.11.0的安装.激活与基本使用. 博主所提供的激活文件理论支持Navicat Premium 12.0.x系列和Navicat Premium 12. ...
- supervisor 配置程序挂起自启动
使用 supervisor 服务,将程序监控起来,如果程序挂掉了,可以实现自启动 编写 c++ 程序 test.c #include <stdio.h> #include <stri ...
- centos6.5使用LVM
1.添加硬盘 centos6以前的版本用kudzu来不重启识别新硬件. [root@xen01 ~]# /etc/init.d/kudzu start -bash: /etc/init.d/kudzu ...
- Run Keyword And Ignore Error,Run Keyword And Return Status,Run Keyword And Continue On Failure,Run Keyword And Expect Error,Wait Until Keyword Succeeds用法
*** Test Cases ***case1 #即使错误也继续执行,也不记录失败,且可以返回执行状态和错误信息 ${Run Keyword And Ignore Error status} ${st ...