来自平行世界的救赎


为什么要说是救赎呢?先跟各位讨论个“死亡问题”,如果你的女票或者你老婆问你,“我跟你妈落水了,你先救谁?”

哈哈,没错,就是这个来之中国的古老声音,这个拷问你内心的世纪难题!怕了没?
可以抛硬币,也可以找个渔网一次性捞起来,可是等等,在这紧急关头你真的有这么多时间?
此时的你肯定最想变成超人,或者修得绝世秘法“分身术”,这样就不用做这道艰难的选择题了。
平行宇宙论告诉我们,这世界有无数个copy,你也有无数个copy,只要找另外一个世界借一个你过来,你的内心就能得到神圣的救赎了。
好了,假设真的由一个平行世界,为了保证这个方法落地的可行性,我们还需要保证 我代表着不仅仅是名字外貌,还有我的人生种种组成才是完整的我,最佳结果是什么?平行世界的我就是从这一刻跟我分离出来的,是我的真正copy,通一个版本的copy! 这是我们讨论这个问题的本质,解决不了的话,再来100个平行世界的我又能怎么样?所以他要能干涉到我们这个世界,能在这个世界行动!可是既然是平行世界,那么肯定是无法过来的啊,怎么办呢?大家都知道,作为高纬度的神可以投影到低纬度的世界中,通过“投影”来行动,或许我们可以这么干? 整理下思路
两个同样的我同时救了两个生命中最重要的人,不踩坑不扎心,再来几个老婆都救得了,简直完美!解决哲学问题而内心得到升华的我们,此时回归真我(现实),利用仅存的圣者模式思考这个方法的现实意义。
“高大上”的工程师职业过程中,我们会遇上前人有意或者无意在代码中留下的坑,譬如但我们需要为这类对象创建全新一个实例去拯救世界时,除了内心被千万草泥马践踏而过之外,似乎只能感受到这世界满满的恶意了。
不,肯定不是!开发云主机域名
我们可是在圣者模式!!
在操蛋的现实社会中我们只是屌丝,但在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是不一样的啊,正反粒子在一起会湮灭的。。。
所以我们需要投影。
好吧,不是投影,我们需要有一个代理,在沙箱外培养一个傀儡,哦不是,是代理,对这个代理的所有操作都能反馈到沙箱内去执行。
嗯,到这里为止,我们基本将问题梳理一遍了,那么下一步。。。。。。
通过上面分析和梳理,我们基本已经确定了方向和逻辑,现在呢,万事俱备,只缺一道神奇的东风我们就可以进入全新世界里了,那我们开始撸代码!

等等这位同学,我们是不是漏了什么?
撸代码前我们先要进行设计啊!

好吧,我们讨论下本次需求。。。
首先,我们假定了已经设定了一个神奇的“沙箱”,沙箱内外隔离,所以内外的通信只能通过一座也是非常神奇的桥梁来进行,这就是“代理”;
当外部的某位同学需要创建一个对象但又受到各种限制的时候,他可以在沙箱内创建一个此对象的分身,然后通过分身的代理进行操作就可以实现对分身的操纵,从而达成目的。
嗯,需求只有这么多,接下来我们谈谈设计。
上面讨论中我们决定了使用ClassLoader对沙箱内外进行隔离,可是不是直接暴露ClassLoader接口给外部使用呢?
ClassLoader能对底层类进行操作,虽然功能强大,但操作复杂度高,一不留神容易出现问题,所以我们应该对它进行封装,仅提供我们期望用户去使用的接口,而且我们认为它应该具备这些特点 这对ClassLoader来说有些强人所难,所以我们需要把它隐藏起来,创造一个沙箱对外提供服务,而将ClassLoader隐藏在沙箱内部,假定它叫“SandboxClassLoader”。
这样我们就有了四个对象了。
还有一点,上面说过我们的调用者通过代理对沙箱内对象进行操作,还记得为什么要使用代理吗?使用代理的本质原因是沙箱内外的类分属不同ClassLoader,即使同名类也是不同的
同样道理,当我们通过代理对象进行调用时,参数传递使用的是沙箱外的对象,进入沙箱内也是不能直接使用的,因此,我们同样需要对这类对象进行转换。
此处我们仅考虑值对象参数,各位同学如果关心其它对象传参的话,需要进行类似的代理转换,但值对象的话,我们只要进行值复制就行了,无需太过复杂处理
我们通过一幅图来说明下这个关系

图片很直观,就不再重复解说啦
嗯,基本梳理应该已经非常清晰了,图中只有蓝色的“沙箱内某对象”属于工作在沙箱内,动态创建出来的,其它都是在沙箱外;
而方框画出了沙箱组件边界,调用者和APPClassLoader都属于已存在的实例无需关心,组件内部就属于需要实现的部分了。
列一下关键几个类

可以看出,Sandbox的API已经变得非常单一和简单了。
为了简化设计,这里规定了待创建的对象必须有无参构造函数,如果同学有需要通过有参构造函数构造对象的话,可以进行扩展实现,欢迎一起做好这个沙箱工具
为什么这里要分开枚举和非枚举对象呢?有同学清楚吗?
枚举的概念是指能有限列举出来的东西,在java中,枚举对象继承自Enum,不能通过new方法进行构造,只能从枚举的值中选取
而对象继承自Object,大家都非常的熟悉终于进入最重要的撸代码环节了。。。

挑重点的代码出来,咱撸一撸 先说说构造方法
既然是沙箱对象,为什么要设计有参构造方法呢?
实际使用中,我们会考虑某些类之间内聚,当一个类放在沙箱内运行时,其它也建议放在沙箱内跑,而我们学过“单一性原则”,知道一个包内一般都是比较内聚的,所以这里设计就是指定某些package路径,沙箱将会对这些包内对象进行接管。
对于不在这些包内的类,如果我们调用了沙箱来构造会怎么样呢?所谓“Talk is cheap, show me the code”~~
请稍后,我们继续构造函数,哈哈~~这个问题我们标记为问题1稍后讨论
这里出现了SandboxClassLoader,使用了getContextClassLoader()作为参数传递,此处做了什么呢?我们先看看SandboxClassLoader的构造方法SandboxClassLoader的构造方法仅仅是将传入的contextClassLoader进行暂存备用,那么我们还是看看getContextClassLoader方法好简单!!
其实这里是有一些设计依据的:我们要去创建一个对象,那么这个对象的类定义必然在当前代码可访问的。
基于这个考虑,我们可以确定,当用户使用类似A a = Sandbox.createObject(A.class)进行创建沙箱内对象时,A类在这段代码执行的上下文必然可以访问,此时我们可以通过此上下文的ClassLoader去获取到这个A类对应的.class资源文件,然后重定义该类了。
继续看看相关代码,为了阅读方便,我重新组织了下代码结构Sandbox中创建对象的主要方法出现了!为了方便阅读,我将无关代码剔除,仅保留createObject方法。
T createObject(String clzName)方法无具体实现,仅进行参数clzName的校验,然后就转给T createObject(Class clz),因此主要分析这个方法。
其实代码量不多(仅19行还包括各种花括号),主要都是注释,脉络如下判断clzInSandbox是否运行在沙箱内,如果不是运行在沙箱内的话,无需创建代理直接将对象objectInSandbox返回;
为什么要做这个判断嗯?这里可以顺带解答前面的问题1了,从代码我们可以看出来,当创建出来的objectInSandbox也是运行在外部的ClassLoader时,其实是不去创建代理的,因为它就是一个沙箱外的对象,又何必去创建代理这么多此一举呢?
可我们明明调用的是classLoader.loadClass(clz.getName())去取得沙箱内的类定义,为什么得到的却是沙箱外的呢?这跟我们对SandboxClassLoader这个类的设计是否矛盾了呢?
好,去看看对应的代码,show me the code 看得出实际实现逻辑的代码是findClass方法,仅几句而已,翻译过来就是“需要重定义的类我们从沙箱内取得,不需要的直接从外部取”,所以会有对象的ClassLoader是外部的。
那什么是“需要重定义的类”呢? 只要是Sandbox类构造时指定的包下面的类,统统都属于需要重新在SandboxClassLoader中重定义的。 利用cglib库创建objectInSandbox的代理对象,拦截该代理对象的所有方法执行,全部转去实际的对象objectInSandbox中执行;
cglib创建对象的代码不分析了,本质就是通过创建一个指定类的子类对方法进行拦截的过程;
我们关心的应该是拦截器干了什么? 我们会从沙箱内的对象中取得同名同参的方法,然后将参数进行转换到沙箱内,再执行沙箱内对象方法并得到结果,最后还要将结果进行转换到沙箱外对象才返回;
逻辑非常清晰,但沙箱内外对象如何转换呢?
这里代码有些长且无聊就不单独贴出来了,有兴趣的同学可以上github上自行下载,大体逻辑如下 嗯,就是这样。
前面我们有提到,我们假定传参对象都是值对象,所以这里的设计相对简单,如有哪位同学需要传非值对象,那么就需要对外部对象做代理 有些同学关心类如何从沙箱外复制到沙箱内重定义的是吧?这是SandboxClassLoader的核心部分,展示下代码逻辑 涉及到的方法主要有两个,getSandboxClass方法主要负责获取对象时进行缓存层面的校验,缓存的目的一个是加速获取类定义的性能,一个是避免同一个类定义重复多次执行导致出错。
copyClass顾名思义就是复制类定义,是从contextClassLoader中将类对应的.class文件进行复制,并在SandboxClassLoader中defineClass的过程,具体请阅读代码。Sandbox中我们还有一个getEnumValue方法,过程有些类似就不重复介绍,请下载代码阅读。 至此,我们完成了代码的编写了。
至此,我们完成了新世界的构建了!
至此,我们完成了所有工作了!!??
高兴得太早了。。。
测试是代码质量的保障,是设计的保障,是运行的保障,是……的保障,总之,就是保障。
所以,我们还要通过测试,为我们的“世界”进行验证,看看它是否跟我们预期一致。
这只需要使用单元测试就可以做到了。代码 运行结果

OK,测试通过~~~
落地案例:如何在同一个Java进程中连接多个RocketMQ服务器

相关推荐: 进销存系统流程

翻出三年前学习时 自己接开发云主机域名手第一个项目前总结的进销存流程,感觉还是很有价值的。故分享出来。相关推荐: 查看端口是否被占用查看本机端口是否被占用:netstat -ano | findstr “端口号” 找到该端口,看一下最后一列,是进程号pid然后…

免责声明:本站发布的图片视频文字,以转载和分享为主,文章观点不代表本站立场,本站不承担相关法律责任;如果涉及侵权请联系邮箱:360163164@qq.com举报,并提供相关证据,经查实将立刻删除涉嫌侵权内容。

(0)
打赏 微信扫一扫 微信扫一扫
上一篇 05/05 16:23
下一篇 05/05 16:24