From the parallel world of salvation

God came level of torture

From the parallel world of salvation
Why say it is salvation? You should talk to discuss a "dead issue", if your female ticket or your wife asks you, "I fell into the water with your mother, you first save Who?"
From the parallel world of salvation
Haha, yes, is this to the ancient Chinese sounds, this torture your inner century-old problem! Afraid yet?
You can flip a coin, you can find net one-time finish it, however, and so, at this critical moment that you really have so much time?
At this time, you most certainly want to become Superman, or repair was peerless Arcane "two places at once," so do not do this road a tough choice.
Parallel universe theory tells us that this world there are numerous copy, you also have numerous copy, just need to find another world lend a you over, your heart can get divine redeemed.
TT


How to do it

Well, assuming that really made a parallel world, in order to ensure the feasibility of this approach landing, we also need to ensure

  • Parallel world really me

    I represent not only the name of appearance, there is what my life is full of all sorts make up my best result? Parallel world I was separated from this moment with me, my true copy, pass a version of the copy!

  • Another I can come into this world to help me save a person

    This is the essence of our discussion of the problem, not solve it, but also how come I 100 parallel world? So he has to be able to interfere in our world, we can act in this world! But since it is a parallel world, it is certainly not over, ah, how to do it? As we all know, as God's high latitudes can be projected to the world at lower latitudes by "projection" to action, perhaps we can do it?

Check your ideas
COMPANY

Two identical I also saved two of life's most important people, do not step on pit does not cut to the heart, come several wives all they save, simply perfect!


Holy (program ape) time

Solving philosophical problems and heart sublimated us, this time returning to the real (reality), think practical significance of this method using the remaining saints mode.
Professional process engineer "the tall", we will encounter predecessors intentionally or unintentionally left in the pit code, for example,

  • Some designed to be unreasonable Singleton pattern , which gives us only one instance of a presence in the JVM
  • 将某些数据(如状态)存在静态字段中,如果修改可能导致运行出错
  • 或者其它蛋疼不考虑后来人的设计

但我们需要为这类对象创建全新一个实例去拯救世界时,除了内心被千万草泥马践踏而过之外,似乎只能感受到这世界满满的恶意了。
不,肯定不是!
我们可是在圣者模式!!
在操蛋的现实社会中我们只是屌丝,但在0和1的世界里,我们可是神!
无所不能的神!
S
神爱世人,怎么会让自己的羔羊生活在水深火热中呢?
就像拯救你妈你老婆和你内心那样,我们可以创造出一个平行世界出来啊,从虚无中造物不就是我们的本能么?


设计

前面我们已经讨论过世纪难题的解决方案,也给出了设计图,此时的我们只要把这个思维转换为由0和1组成的另一个世界的方式表达,似乎就可以了?
OTY
我们要通过救世主对象去操作一堆“待拯救的对象”,嗯,这就是救世主应该做的。
但是,另外一边出现灾难了,又有一堆“待拯救的对象”排排坐,等着救世主来拯救。
救世主说,卧槽,我TM分身乏术啊,上帝没给我分身这个超能力,我也很无助啊。
GG
好了,这个时候就是英雄闪亮登场的机会啦。
DC
你爹妈不给你分身术,咱不分身啦,咱直接开一个新的世界,拉一个过来呗,别问为啥,就是这么任性。
YQRX
嗯,具体操作就像如何把大象放进冰箱一样分3步

1、新开辟一个世界;
2、复制一个救世主过去;
3、把救世主投影过来;

步骤有啦,分析下怎么执行。

  1. 新开辟一个世界

    我们是务实的工程师,不能吹逼,所以不应该叫新开辟世界,应该叫做制作一个相对比较隔离的环境出来,要求呢?这个环境应该

    • 工作在里面的对象跟外面的能力应该是完全一样的
    • 环境外面应该是无法感知里面的情况的
    • 环境内外的对象应该是完全不同的

    我们暂且为这个环境命名为“沙箱”(Sandbox)吧。
    以单例设计为参考,单例设计一般是寄托于类(Class)存在的,为了复制这个对象,我们需要做的是将整个Class复制一份。
    CL
    我们知道Java中的Class是由ClassLoader装载进内存的,而ClassLoader采用的是双亲委派机制,一个ClassLoader内独有的业务对象对其它ClassLoader是不存在的,这不就完美满足我们上面说的三个点吗?Good,就它了!
    方案:采用ClassLoader作为沙箱环境隔离

  2. 复制一个救世主过去

    前面我们确定了ClassLoader方案后思路自然豁然开朗,现在考虑将Class复制进沙箱(ClassLoader)内就非常简单啦!
    我们知道,ClassLoader装载Class时候其实是读取.class文件,再通过ClassLoader的defineClass来实际定义一个类的,嗯,那我们将沙箱外的类定义复制过来也可以这样,两步
    首先读取.class内容。这个文件在哪里呢?当jar包被ClassLoader装入内存后,通过getResource就可以将文件数据读取到啦,完美!
    在沙箱内定义类。简单,就一个defineClass,打完收工~
    嘿,别急,小心类重新定义哦,记得记录下定义过哪些类。
    LDY

  3. 把救世主投影过来

    对,这也是个问题。
    刚刚我们有说过,不同ClassLoader的独有业务对象对其它ClassLoader而言是不存在的!这就引发出问题了,外面无法使用里面创造出来的对象实例!
    DGG
    举个例子

    BizObject biz = new BizObject(); //OK
    BizObject biz2 = Sandbox.createObject(BizObject.class); //出错

    为什么出错呢?因为沙箱内外的BizObject是不一样的啊,正反粒子在一起会湮灭的。。。
    所以我们需要投影。
    好吧,不是投影,我们需要有一个代理,在沙箱外培养一个傀儡,哦不是,是代理,对这个代理的所有操作都能反馈到沙箱内去执行。
    DL

嗯,到这里为止,我们基本将问题梳理一遍了,那么下一步。。。。。。
KND


神说,要有光

通过上面分析和梳理,我们基本已经确定了方向和逻辑,现在呢,万事俱备,只缺一道神奇的东风我们就可以进入全新世界里了,那我们开始撸代码!
DF
等等这位同学,我们是不是漏了什么?
撸代码前我们先要进行设计啊!
JX
好吧,我们讨论下本次需求。。。
首先,我们假定了已经设定了一个神奇的“沙箱”,沙箱内外隔离,所以内外的通信只能通过一座也是非常神奇的桥梁来进行,这就是“代理”;
当外部的某位同学需要创建一个对象但又受到各种限制的时候,他可以在沙箱内创建一个此对象的分身,然后通过分身的代理进行操作就可以实现对分身的操纵,从而达成目的。
嗯,需求只有这么多,接下来我们谈谈设计。
上面讨论中我们决定了使用ClassLoader对沙箱内外进行隔离,可是不是直接暴露ClassLoader接口给外部使用呢?
ClassLoader能对底层类进行操作,虽然功能强大,但操作复杂度高,一不留神容易出现问题,所以我们应该对它进行封装,仅提供我们期望用户去使用的接口,而且我们认为它应该具备这些特点

  • 功能单一
  • 与沙箱不相干的都不要暴露
  • 创建对象后直接可以使用

这对ClassLoader来说有些强人所难,所以我们需要把它隐藏起来,创造一个沙箱对外提供服务,而将ClassLoader隐藏在沙箱内部,假定它叫“SandboxClassLoader”。
这样我们就有了

  • 调用者
  • 沙箱
  • SandboxClassLoader
  • 外部ClassLoader

四个对象了。
还有一点,上面说过我们的调用者通过代理对沙箱内对象进行操作,还记得为什么要使用代理吗?使用代理的本质原因是沙箱内外的类分属不同ClassLoader,即使同名类也是不同的
同样道理,当我们通过代理对象进行调用时,参数传递使用的是沙箱外的对象,进入沙箱内也是不能直接使用的,因此,我们同样需要对这类对象进行转换。
此处我们仅考虑值对象参数,各位同学如果关心其它对象传参的话,需要进行类似的代理转换,但值对象的话,我们只要进行值复制就行了,无需太过复杂处理
我们通过一幅图来说明下这个关系
TXBZ
图片很直观,就不再重复解说啦
嗯,基本梳理应该已经非常清晰了,图中只有蓝色的“沙箱内某对象”属于工作在沙箱内,动态创建出来的,其它都是在沙箱外;
而方框画出了沙箱组件边界,调用者和APPClassLoader都属于已存在的实例无需关心,组件内部就属于需要实现的部分了。
列一下关键几个类
L
可以看出,Sandbox的API已经变得非常单一和简单了。
为了简化设计,这里规定了待创建的对象必须有无参构造函数,如果同学有需要通过有参构造函数构造对象的话,可以进行扩展实现,欢迎一起做好这个沙箱工具
为什么这里要分开枚举和非枚举对象呢?有同学清楚吗?
枚举的概念是指能有限列举出来的东西,在java中,枚举对象继承自Enum,不能通过new方法进行构造,只能从枚举的值中选取
而对象继承自Object,大家都非常的熟悉

创世纪

终于进入最重要的撸代码环节了。。。
WLK
挑重点的代码出来,咱撸一撸

public class Sandbox {
    private SandboxClassLoader classLoader;
    private SandboxUtil util = new SandboxUtil();
    private List<String> redefinedPackages;

    public Sandbox(List<String> 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<String, Class> 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 extends Object> 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 extends Object> T createObject(Class<T> 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行还包括各种花括号),主要都是注释,脉络如下

  1. 先获取参数clz在沙箱内的对于类定义clzInSandbox,并通过clzInSandboxnewInstance创建该类的一个具体实例objectInSandbox因此这里要求clz有无参构造函数
  2. 判断clzInSandbox是否运行在沙箱内,如果不是运行在沙箱内的话,无需创建代理直接将对象objectInSandbox返回;
    为什么要做这个判断嗯?这里可以顺带解答前面的问题1了,从代码

    //如果对象的类装载器和clz的类装载器一致,说明不是需要工作在沙箱内的对象,直接返回即可,无需代理
    if(objectInSandbox.getClass().getClassLoader() == &gt; clz.getClassLoader()){
        return (T) objectInSandbox;
    }

    我们可以看出来,当创建出来的objectInSandbox也是运行在外部的ClassLoader时,其实是不去创建代理的,因为它就是一个沙箱外的对象,又何必去创建代理这么多此一举呢?
    可我们明明调用的是classLoader.loadClass(clz.getName())去取得沙箱内的类定义,为什么得到的却是沙箱外的呢?这跟我们对SandboxClassLoader这个类的设计是否矛盾了呢?
    好,去看看对应的代码,show me the code

    class SandboxClassLoader extends ClassLoader{
        //当前上下文的ClassLoader,用于寻找类实例并克隆进沙箱
        private final ClassLoader contextClassLoader;
        //......  
    
        /**
         * 覆盖父类的转载类进内存的方法
         * @param name 指定类名称
         * @return 已转载进内存的Class实例
         * @throws ClassNotFoundException
         */
        @Override
        public Class&lt;?&gt; loadClass(String name) throws ClassNotFoundException {
            return findClass(name);
        }
    
        /**
         * 重定义类转载逻辑
         *
         * 1、对于需要运行在沙箱内的类(redefinedPackages中声明),通过复制contextClassLoader类定义的方式,直接运行在此ClassLoader下
         * 2、对于不需要运行在沙箱内的类,直接返回上下文类定义,以减少资源占用
         * @param name 类名称(全路径)
         * @return 类定义
         */
        @Override
        protected Class&lt;?&gt; 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中重定义的。

  3. 利用cglib库创建objectInSandbox的代理对象,拦截该代理对象的所有方法执行,全部转去实际的对象objectInSandbox中执行;
    cglib创建对象的代码不分析了,本质就是通过创建一个指定类的子类对方法进行拦截的过程;
    我们关心的应该是拦截器干了什么?

    enhancer.setCallback((MethodInterceptor) (o, method, args, methodProxy) -&gt; {
                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上自行下载,大体逻辑如下

    1. 判断对象是否需要转换成沙箱内/外,不需要则返回此对象,需要就转2;
    2. 创建沙箱内/外对应的对象实例;
    3. 遍历该对象实例的每一个字段,对该字段执行步骤1,并将复制后的值赋值给新对象中对应字段;

    嗯,就是这样。
    前面我们有提到,我们假定传参对象都是值对象,所以这里的设计相对简单,如有哪位同学需要传非值对象,那么就需要对外部对象做代理

  4. 将代理对象返回;

有些同学关心类如何从沙箱外复制到沙箱内重定义的是吧?这是SandboxClassLoader的核心部分,展示下代码逻辑

class SandboxClassLoader extends ClassLoader {
    //......
    //缓存已经创建过的Class实例,避免重复定义
    private final Map<String, Class> 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);
        }
    }

    //......
}

The method involves mainly two getSandboxClassmethods are mainly responsible for getting the cache level calibration target, the purpose of a cache is to accelerate the acquisition performance class definition, a class definition is to avoid repeating the same mistakes repeatedly cause.
copyClassClass definition is to copy the name suggests, from the contextClassLoadercopy in the .class file corresponding to a class, and the process SandboxClassLoader defineClass, the specific read code.

Sandbox and we have a getEnumValuemethod, the process is somewhat similar will not repeat, please download the code read.

So far, we have completed the preparation of the code.
So far, we have completed the construction of the New World it!
So far, we have completed all the work! ! ? ?
I get too excited. . .
XS


The arrival of the Redeemer

Testing is to protect the quality of the code is to protect the design is to protect the run, is ...... protection, in short, is to protect.
So, we have, for us to verify the "world" to pass the test to see if it is consistent with our expectations.
This only requires the use of unit testing can be done. Code

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<String> list = sandbox.createObject(ArrayList.class);
        assertEquals(list.getClass(), ArrayList.class);
    }
}

Operating results
JG
OK, the test passes ~~~
JG


The coordinates of the world

Guess you like

Origin blog.51cto.com/14478649/2426549