来自平行世界的救赎
神级的拷问来了
为什么要说是救赎呢?先跟各位讨论个“死亡问题”,如果你的女票或者你老婆问你,“我跟你妈落水了,你先救谁?”
哈哈,没错,就是这个来之中国的古老声音,这个拷问你内心的世纪难题!怕了没?
可以抛硬币,也可以找个渔网一次性捞起来,可是等等,在这紧急关头你真的有这么多时间?
此时的你肯定最想变成超人,或者修得绝世秘法“分身术”,这样就不用做这道艰难的选择题了。
平行宇宙论告诉我们,这世界有无数个copy,你也有无数个copy,只要找另外一个世界借一个你过来,你的内心就能得到神圣的救赎了。
让客户满意是我们工作的目标,不断超越客户的期望值来自于我们对这个行业的热爱。我们立志把好的技术通过有效、简单的方式提供给客户,将通过不懈努力成为客户在信息化领域值得信任、有价值的长期合作伙伴,公司提供的服务项目有:空间域名、网络空间、营销软件、网站建设、渭滨网站维护、网站推广。
怎么做
好了,假设真的由一个平行世界,为了保证这个方法落地的可行性,我们还需要保证
- 平行世界的真的是我
我代表着不仅仅是名字外貌,还有我的人生种种组成才是完整的我,最佳结果是什么?平行世界的我就是从这一刻跟我分离出来的,是我的真正copy,通一个版本的copy!
- 另一个我能到这个世界来帮我救一个人
这是我们讨论这个问题的本质,解决不了的话,再来100个平行世界的我又能怎么样?所以他要能干涉到我们这个世界,能在这个世界行动!可是既然是平行世界,那么肯定是无法过来的啊,怎么办呢?大家都知道,作为高纬度的神可以投影到低纬度的世界中,通过“投影”来行动,或许我们可以这么干?
整理下思路
两个同样的我同时救了两个生命中最重要的人,不踩坑不扎心,再来几个老婆都救得了,简直完美!
圣者(程序猿)时间
解决哲学问题而内心得到升华的我们,此时回归真我(现实),利用仅存的圣者模式思考这个方法的现实意义。
“高大上”的工程师职业过程中,我们会遇上前人有意或者无意在代码中留下的坑,譬如
- 某些设计得很不合理的单例模式,这让我们在一个JVM中只有一个实例存在
- 将某些数据(如状态)存在静态字段中,如果修改可能导致运行出错
- 或者其它蛋疼不考虑后来人的设计
但我们需要为这类对象创建全新一个实例去拯救世界时,除了内心被千万草泥马践踏而过之外,似乎只能感受到这世界满满的恶意了。
不,肯定不是!
我们可是在圣者模式!!
在操蛋的现实社会中我们只是屌丝,但在0和1的世界里,我们可是神!
无所不能的神!
神爱世人,怎么会让自己的羔羊生活在水深火热中呢?
就像拯救你妈你老婆和你内心那样,我们可以创造出一个平行世界出来啊,从虚无中造物不就是我们的本能么?
设计
前面我们已经讨论过世纪难题的解决方案,也给出了设计图,此时的我们只要把这个思维转换为由0和1组成的另一个世界的方式表达,似乎就可以了?
我们要通过救世主对象去操作一堆“待拯救的对象”,嗯,这就是救世主应该做的。
但是,另外一边出现灾难了,又有一堆“待拯救的对象”排排坐,等着救世主来拯救。
救世主说,卧槽,我TM分身乏术啊,上帝没给我分身这个超能力,我也很无助啊。
好了,这个时候就是英雄闪亮登场的机会啦。
你爹妈不给你分身术,咱不分身啦,咱直接开一个新的世界,拉一个过来呗,别问为啥,就是这么任性。
嗯,具体操作就像如何把大象放进冰箱一样分3步
1、新开辟一个世界;
2、复制一个救世主过去;
3、把救世主投影过来;
步骤有啦,分析下怎么执行。
- 新开辟一个世界
我们是务实的工程师,不能吹逼,所以不应该叫新开辟世界,应该叫做制作一个相对比较隔离的环境出来,要求呢?这个环境应该
- 工作在里面的对象跟外面的能力应该是完全一样的
- 环境外面应该是无法感知里面的情况的
- 环境内外的对象应该是完全不同的
我们暂且为这个环境命名为“沙箱”(Sandbox)吧。
以单例设计为参考,单例设计一般是寄托于类(Class)存在的,为了复制这个对象,我们需要做的是将整个Class复制一份。
我们知道Java中的Class是由ClassLoader装载进内存的,而ClassLoader采用的是双亲委派机制,一个ClassLoader内独有的业务对象对其它ClassLoader是不存在的,这不就完美满足我们上面说的三个点吗?Good,就它了!
方案:采用ClassLoader作为沙箱环境隔离 复制一个救世主过去
前面我们确定了ClassLoader方案后思路自然豁然开朗,现在考虑将Class复制进沙箱(ClassLoader)内就非常简单啦!
我们知道,ClassLoader装载Class时候其实是读取.class文件,再通过ClassLoader的defineClass来实际定义一个类的,嗯,那我们将沙箱外的类定义复制过来也可以这样,两步
首先读取.class内容。这个文件在哪里呢?当jar包被ClassLoader装入内存后,通过getResource就可以将文件数据读取到啦,完美!
在沙箱内定义类。简单,就一个defineClass,打完收工~
嘿,别急,小心类重新定义哦,记得记录下定义过哪些类。- 把救世主投影过来
对,这也是个问题。
刚刚我们有说过,不同ClassLoader的独有业务对象对其它ClassLoader而言是不存在的!这就引发出问题了,外面无法使用里面创造出来的对象实例!
举个例子BizObject biz = new BizObject(); //OK BizObject biz2 = Sandbox.createObject(BizObject.class); //出错
为什么出错呢?因为沙箱内外的BizObject是不一样的啊,正反粒子在一起会湮灭的。。。
所以我们需要投影。
好吧,不是投影,我们需要有一个代理,在沙箱外培养一个傀儡,哦不是,是代理,对这个代理的所有操作都能反馈到沙箱内去执行。
嗯,到这里为止,我们基本将问题梳理一遍了,那么下一步。。。。。。
神说,要有光
通过上面分析和梳理,我们基本已经确定了方向和逻辑,现在呢,万事俱备,只缺一道神奇的东风我们就可以进入全新世界里了,那我们开始撸代码!
等等这位同学,我们是不是漏了什么?
撸代码前我们先要进行设计啊!
好吧,我们讨论下本次需求。。。
首先,我们假定了已经设定了一个神奇的“沙箱”,沙箱内外隔离,所以内外的通信只能通过一座也是非常神奇的桥梁来进行,这就是“代理”;
当外部的某位同学需要创建一个对象但又受到各种限制的时候,他可以在沙箱内创建一个此对象的分身,然后通过分身的代理进行操作就可以实现对分身的操纵,从而达成目的。
嗯,需求只有这么多,接下来我们谈谈设计。
上面讨论中我们决定了使用ClassLoader对沙箱内外进行隔离,可是不是直接暴露ClassLoader接口给外部使用呢?
ClassLoader能对底层类进行操作,虽然功能强大,但操作复杂度高,一不留神容易出现问题,所以我们应该对它进行封装,仅提供我们期望用户去使用的接口,而且我们认为它应该具备这些特点
- 功能单一
- 与沙箱不相干的都不要暴露
- 创建对象后直接可以使用
这对ClassLoader来说有些强人所难,所以我们需要把它隐藏起来,创造一个沙箱对外提供服务,而将ClassLoader隐藏在沙箱内部,假定它叫“SandboxClassLoader”。
这样我们就有了
- 调用者
- 沙箱
- SandboxClassLoader
- 外部ClassLoader
四个对象了。
还有一点,上面说过我们的调用者通过代理对沙箱内对象进行操作,还记得为什么要使用代理吗?使用代理的本质原因是沙箱内外的类分属不同ClassLoader,即使同名类也是不同的!
同样道理,当我们通过代理对象进行调用时,参数传递使用的是沙箱外的对象,进入沙箱内也是不能直接使用的,因此,我们同样需要对这类对象进行转换。
此处我们仅考虑值对象参数,各位同学如果关心其它对象传参的话,需要进行类似的代理转换,但值对象的话,我们只要进行值复制就行了,无需太过复杂处理
我们通过一幅图来说明下这个关系
图片很直观,就不再重复解说啦
嗯,基本梳理应该已经非常清晰了,图中只有蓝色的“沙箱内某对象”属于工作在沙箱内,动态创建出来的,其它都是在沙箱外;
而方框画出了沙箱组件边界,调用者和APPClassLoader都属于已存在的实例无需关心,组件内部就属于需要实现的部分了。
列一下关键几个类
可以看出,Sandbox的API已经变得非常单一和简单了。
为了简化设计,这里规定了待创建的对象必须有无参构造函数,如果同学有需要通过有参构造函数构造对象的话,可以进行扩展实现,欢迎一起做好这个沙箱工具
为什么这里要分开枚举和非枚举对象呢?有同学清楚吗?
枚举的概念是指能有限列举出来的东西,在java中,枚举对象继承自Enum,不能通过new方法进行构造,只能从枚举的值中选取
而对象继承自Object,大家都非常的熟悉
创世纪
终于进入最重要的撸代码环节了。。。
挑重点的代码出来,咱撸一撸
public class Sandbox {
private SandboxClassLoader classLoader;
private SandboxUtil util = new SandboxUtil();
private List redefinedPackages;
public Sandbox(List packages){
redefinedPackages = packages;
classLoader = new SandboxClassLoader(getContextClassLoader());
}
/**
* 沙箱对象构造方法
* @param redefinedPackages 需工作在沙箱内的包
* 此包下面所有类都在工作在沙箱内
*/
public Sandbox(String... redefinedPackages){
this(Lists.newArrayList(redefinedPackages));
}
// ......
}
先说说构造方法。
既然是沙箱对象,为什么要设计有参构造方法呢?
实际使用中,我们会考虑某些类之间内聚,当一个类放在沙箱内运行时,其它也建议放在沙箱内跑,而我们学过“单一性原则”,知道一个包内一般都是比较内聚的,所以这里设计就是指定某些package路径,沙箱将会对这些包内对象进行接管。
对于不在这些包内的类,如果我们调用了沙箱来构造会怎么样呢?所谓“Talk is cheap, show me the code”~~
请稍后,我们继续构造函数,哈哈~~这个问题我们标记为问题1稍后讨论
这里出现了SandboxClassLoader,使用了getContextClassLoader()
作为参数传递,此处做了什么呢?我们先看看SandboxClassLoader的构造方法
/**
* 沙箱隔离核心
*
* 通过ClassLoader将进行类级别的运行时隔离
*
* 此类本质上是代理了currentContextClassLoader对象,并增加了对部分需要在沙箱内运行的类处理能力
*/
class SandboxClassLoader extends ClassLoader{
//当前上下文的ClassLoader,用于寻找类实例并克隆进沙箱
private final ClassLoader contextClassLoader;
//缓存已经创建过的Class实例,避免重复定义
private final Map cache = Maps.newHashMap();
SandboxClassLoader(ClassLoader contextClassLoader) {
this.contextClassLoader = contextClassLoader;
}
//......
}
SandboxClassLoader的构造方法仅仅是将传入的contextClassLoader
进行暂存备用,那么我们还是看看getContextClassLoader
方法
/**
* 获取当前上下文的类装载器
*
* 此类装载器需包含MQClient相关类定义
* PS:单独定义为一个方法,是担心当这个上下文类装载器满足不了要求时可以快速更换
* @return 当前类装载器
*/
private ClassLoader getContextClassLoader() {
//从类装载器机制而言,线程上下文的类转载器是最符合要求的
return Thread.currentThread().getContextClassLoader();
}
好简单!!
其实这里是有一些设计依据的:我们要去创建一个对象,那么这个对象的类定义必然在当前代码可访问的。
基于这个考虑,我们可以确定,当用户使用类似A a = Sandbox.createObject(A.class)
进行创建沙箱内对象时,A类在这段代码执行的上下文必然可以访问,此时我们可以通过此上下文的ClassLoader去获取到这个A类对应的.class资源文件,然后重定义该类了。
继续看看相关代码,为了阅读方便,我重新组织了下代码结构
public class Sandbox {
private SandboxClassLoader classLoader;
//......
/**
* 在沙箱内创建指定名称的类实例
*
* 如该名称类不属于redefinedPackages所指定的包内,则直接返回外部类实例
* @param clzName 待创建实例的类名称
* @return 指定类名称的实例对象
*/
public T createObject(String clzName) throws ClassNotFoundException, SandboxCannotCreateObjectException {
Class clz = Class.forName(clzName);
return (T) createObject(clz);
}
/**
* 在沙箱内创建指定Class的实例
* @param clz 待创建实例的Class
* @return 跟clz功能相同并工作在沙箱内的类实例
*/
public synchronized T createObject(Class clz) throws SandboxCannotCreateObjectException {
try {
final Class> clzInSandbox = classLoader.loadClass(clz.getName());
final Object objectInSandbox = clzInSandbox.newInstance();
//如果对象的类装载器和clz的类装载器一致,说明不是需要工作在沙箱内的对象,直接返回即可,无需代理
if(objectInSandbox.getClass().getClassLoader() == clz.getClassLoader()){
return (T) objectInSandbox;
}
/*
创建生产者的代理:由于沙箱内外的对象本质上属于不同的类,因此需要将两者能力桥接起来
这里采用了代理模式,通过创建沙箱外的对象实例,并将其所有方法调用通过代理转发到沙箱内执行
另外,由于沙箱内外的所有实例都属于不同的类,因此,对于参数和返回值还需要进行对象转换,将沙箱内外的对象进行对等克隆
*/
//通过cglib创建对象的子类代理
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(clz);
enhancer.setCallback((MethodInterceptor) (o, method, args, methodProxy) -> {
Method targetMethod = clzInSandbox.getMethod(method.getName(), method.getParameterTypes());
//调用前需对参数进行克隆,转换为沙箱内对象
Object[] targetArgs = args == null ? null : util.cloneTo(args, classLoader);
Object result = targetMethod.invoke(objectInSandbox, targetArgs);
//调用后续对结果进行克隆,转换为沙箱外对象
return util.cloneTo(result, getContextClassLoader());
});
return (T) enhancer.create();
}catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) {
throw new SandboxCannotCreateObjectException("无法在沙箱内创建对象", e);
}
}
//......
}
Sandbox中创建对象的主要方法出现了!为了方便阅读,我将无关代码剔除,仅保留createObject
方法。T createObject(String clzName)
方法无具体实现,仅进行参数clzName
的校验,然后就转给T createObject(Class clz)
,因此主要分析这个方法。
其实代码量不多(仅19行还包括各种花括号),主要都是注释,脉络如下
- 先获取参数
clz
在沙箱内的对于类定义clzInSandbox
,并通过clzInSandbox
的newInstance
创建该类的一个具体实例objectInSandbox
;因此这里要求clz
有无参构造函数 判断
clzInSandbox
是否运行在沙箱内,如果不是运行在沙箱内的话,无需创建代理直接将对象objectInSandbox
返回;
为什么要做这个判断嗯?这里可以顺带解答前面的问题1了,从代码//如果对象的类装载器和clz的类装载器一致,说明不是需要工作在沙箱内的对象,直接返回即可,无需代理 if(objectInSandbox.getClass().getClassLoader() == > clz.getClassLoader()){ return (T) objectInSandbox; }
我们可以看出来,当创建出来的
objectInSandbox
也是运行在外部的ClassLoader时,其实是不去创建代理的,因为它就是一个沙箱外的对象,又何必去创建代理这么多此一举呢?
可我们明明调用的是classLoader.loadClass(clz.getName())
去取得沙箱内的类定义,为什么得到的却是沙箱外的呢?这跟我们对SandboxClassLoader这个类的设计是否矛盾了呢?
好,去看看对应的代码,show me the codeclass SandboxClassLoader extends ClassLoader{ //当前上下文的ClassLoader,用于寻找类实例并克隆进沙箱 private final ClassLoader contextClassLoader; //...... /** * 覆盖父类的转载类进内存的方法 * @param name 指定类名称 * @return 已转载进内存的Class实例 * @throws ClassNotFoundException */ @Override public Class<?> loadClass(String name) throws ClassNotFoundException { return findClass(name); } /** * 重定义类转载逻辑 * * 1、对于需要运行在沙箱内的类(redefinedPackages中声明),通过复制contextClassLoader类定义的方式,直接运行在此ClassLoader下 * 2、对于不需要运行在沙箱内的类,直接返回上下文类定义,以减少资源占用 * @param name 类名称(全路径) * @return 类定义 */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { if(isRedefinedClass(name)) { return getSandboxClass(name); } else { return contextClassLoader.loadClass(name); } } //...... }
看得出实际实现逻辑的代码是
findClass
方法,仅几句而已,翻译过来就是“需要重定义的类我们从沙箱内取得,不需要的直接从外部取”,所以会有对象的ClassLoader是外部的。
那什么是“需要重定义的类”呢?/** * 是否需要运行在沙箱内的类 * @param name 类名称 */ boolean isRedefinedClass(String name) { //校验是否沙箱约定的需要重定义的包 for (String redefinedPackage : redefinedPackages) { if(name.startsWith(redefinedPackage)){ return true; } } return false; }
只要是Sandbox类构造时指定的包下面的类,统统都属于需要重新在SandboxClassLoader中重定义的。
利用cglib库创建
objectInSandbox
的代理对象,拦截该代理对象的所有方法执行,全部转去实际的对象objectInSandbox
中执行;
cglib创建对象的代码不分析了,本质就是通过创建一个指定类的子类对方法进行拦截的过程;
我们关心的应该是拦截器干了什么?enhancer.setCallback((MethodInterceptor) (o, method, args, methodProxy) -> { Method targetMethod = clzInSandbox.getMethod(method.getName(), method.getParameterTypes()); //调用前需对参数进行克隆,转换为沙箱内对象 Object[] targetArgs = args == null ? null : util.cloneTo(args, classLoader); Object result = targetMethod.invoke(objectInSandbox, targetArgs); //调用后续对结果进行克隆,转换为沙箱外对象 return util.cloneTo(result, getContextClassLoader()); });
我们会从沙箱内的对象中取得同名同参的方法,然后将参数进行转换到沙箱内,再执行沙箱内对象方法并得到结果,最后还要将结果进行转换到沙箱外对象才返回;
逻辑非常清晰,但沙箱内外对象如何转换呢?
这里代码有些长且无聊就不单独贴出来了,有兴趣的同学可以上github上自行下载,大体逻辑如下- 判断对象是否需要转换成沙箱内/外,不需要则返回此对象,需要就转2;
- 创建沙箱内/外对应的对象实例;
- 遍历该对象实例的每一个字段,对该字段执行步骤1,并将复制后的值赋值给新对象中对应字段;
嗯,就是这样。
前面我们有提到,我们假定传参对象都是值对象,所以这里的设计相对简单,如有哪位同学需要传非值对象,那么就需要对外部对象做代理- 将代理对象返回;
有些同学关心类如何从沙箱外复制到沙箱内重定义的是吧?这是SandboxClassLoader的核心部分,展示下代码逻辑
class SandboxClassLoader extends ClassLoader {
//......
//缓存已经创建过的Class实例,避免重复定义
private final Map cache = Maps.newHashMap();
/**
* 内部方法:获取需要在沙箱内运行的Class实例
* @param name 类名称
* @return 沙箱内的类实例
* @throws ClassNotFoundException
*/
private synchronized Class> getSandboxClass(String name) throws ClassNotFoundException {
//1、先从缓存中查找是否已经转载过该类,有则直接返回
if(cache.containsKey(name)){
return cache.get(name);
}
//2、缓存不存在该类时,从currentContextClassLoader中复制一份到当前缓存中
Class> clz = copyClass(name);
cache.put(name, clz);
return clz;
}
/**
* 从currentContextClassLoader中复制一份类到本ClassLoader中
*
* 此复制是将字节码copy到当前ClassLoader进行定义,因此与sandbox外部的Class已经完全不同实例,不能给外部直接赋值
* @param name 待复制的类名称
* @return 工作在当前ClassLoader中的Class
* @throws ClassNotFoundException
*/
private synchronized Class> copyClass(String name) throws ClassNotFoundException {
//取得.class文件所在路径
String path = name.replace('.', '/') + ".class";
//通过上下文类装载器获取资源句柄
try (InputStream stream = contextClassLoader.getResourceAsStream(path)) {
if(stream == null) throw new ClassNotFoundException(String.format("找不到类%s", name));
//读取所有字节内容
byte[] content = readFromStream(stream);
return defineClass(name, content, 0, content.length);
} catch (IOException e) {
throw new ClassNotFoundException("找不到指定的类", e);
}
}
//......
}
涉及到的方法主要有两个,getSandboxClass
方法主要负责获取对象时进行缓存层面的校验,缓存的目的一个是加速获取类定义的性能,一个是避免同一个类定义重复多次执行导致出错。copyClass
顾名思义就是复制类定义,是从contextClassLoader
中将类对应的.class文件进行复制,并在SandboxClassLoader中defineClass的过程,具体请阅读代码。
Sandbox中我们还有一个getEnumValue
方法,过程有些类似就不重复介绍,请下载代码阅读。
至此,我们完成了代码的编写了。
至此,我们完成了新世界的构建了!
至此,我们完成了所有工作了!!??
高兴得太早了。。。
到来的救赎
测试是代码质量的保障,是设计的保障,是运行的保障,是......的保障,总之,就是保障。
所以,我们还要通过测试,为我们的“世界”进行验证,看看它是否跟我们预期一致。
这只需要使用单元测试就可以做到了。代码
public class SandboxTest {
@Test
public void getEnumValue() throws SandboxCannotCreateObjectException {
//设定重定义的包
Sandbox sandbox = new Sandbox("com.google.common.collect");
//获取沙箱内对象,虽然是同名同值,但由于分属沙箱内外,因此预期应该不等
Enum type = sandbox.getEnumValue(com.google.common.collect.BoundType.CLOSED);
assertNotEquals(type, com.google.common.collect.BoundType.CLOSED);
//通过沙箱获取非设定需要重定义的包内对象,预期应该是相等
Enum property = sandbox.getEnumValue(com.google.common.base.StandardSystemProperty.JAVA_CLASS_PATH);
assertEquals(property, com.google.common.base.StandardSystemProperty.JAVA_CLASS_PATH);
}
@Test
public void createObject() throws SandboxCannotCreateObjectException, ClassNotFoundException {
//设定重定义的包
Sandbox sandbox = new Sandbox("com.google.common.eventbus");
//获取沙箱内对象,预期中类定义应该与沙箱外的类定义不等
com.google.common.eventbus.EventBus bus = sandbox.createObject(com.google.common.eventbus.EventBus.class);
assertNotEquals(bus.getClass(), com.google.common.eventbus.EventBus.class);
//通过名称获取,如上
bus = sandbox.createObject("com.google.common.eventbus.EventBus");
assertNotEquals(bus.getClass(), com.google.common.eventbus.EventBus.class);
//通过沙箱获取无需重定义的类,预期应该跟沙箱外相等
List list = sandbox.createObject(ArrayList.class);
assertEquals(list.getClass(), ArrayList.class);
}
}
运行结果
OK,测试通过~~~
世界的坐标
- -> github
- -> 码云gitee
落地案例:如何在同一个Java进程中连接多个RocketMQ服务器
文章标题:来自平行世界的救赎
文章链接:http://pwwzsj.com/article/poscie.html