Redemption from Parallel World

Here comes the divine torture.


Why is it called redemption? Discuss a "death question" with you first. If your woman ticket or your wife asks you, "I fell into the water with your mother, who will you save first?"

Haha, yes, that's the old voice of China, this torture of your heart's century problem! Are you scared?
You can toss coins, you can find a fishing net to catch them all at once, but wait, do you really have so much time in this emergency?
At this time, you must most want to become superman, or to cultivate the secrecy of "separation" so that you don't have to do this difficult choice.
Parallel cosmology tells us that there are countless copies in the world, and you also have countless copies. As long as you borrow one from another world, your heart will be sacred redemption.

How do you do it?

Well, assuming that there is a parallel world, we need to make sure that this approach is feasible.

  • Parallel world is really me

    I represent not only the appearance of the name, but also the composition of my life is the complete me. What is the best result? Parallel world I am separated from me from this moment, is my real copy, through a version of copy!

  • Another one I can come to the world to help me save someone.

    This is the essence of our discussion of this problem. If we can't solve it, what about 100 parallel worlds? So he should be able to interfere in our world and act in it! But since it is a parallel world, it must be impossible to come over. What should we do? As we all know, as a god of high latitude, we can project into the world of low latitude and act by "projection". Maybe we can do that?

Organize the train of thought

Two of the same I saved two of the most important people in my life at the same time. Without trampling on the pit, I could save a few wives at the same time. It's perfect!

Saint (programmed ape) time

When we solve philosophical problems and sublimate our hearts, we return to the real self (reality) and use the remaining Saint model to think about the practical significance of this method.
In the process of being a "tall" engineer, we will encounter pits left in the code by our predecessors intentionally or unintentionally, such as

  • Some unreasonably designed singleton patterns allow us to have only one instance in a JVM
  • Existing some data, such as state, in a static field may result in running errors if modified
  • Or other eggs are painful without considering the design of later generations.

But when we need to create a new example of this kind of object to save the world, it seems that we can only feel the malice of the world when we are trampled by millions of grass-mud horses.
No, definitely not!
We are in the Saint mode!!
In the fucking society, we are just Diaoshi, but in the world of 0 and 1, we are gods!
God Almighty!

God loves the world, how can he let his lamb live in deep water?
Just like saving your mother, your wife and your heart, we can create a parallel world. Isn't it our instinct to create from nothingness?

Design

We have already discussed the solution to the century's difficult problems and given the design plan. At this time, we just need to transform this thinking into the expression of another world composed of 0 and 1, which seems to be OK?

We need to manipulate a bunch of "objects to be saved" through the Savior's object. Well, that's what the Savior should do.
But on the other side, there was a disaster, and there were a bunch of "objects to be saved" waiting for the savior to save.
The Savior said, "Bed in the trough, my TM is too weak. God didn't give me the superpower of being a part. I'm helpless, too."

Well, this is the time for heroes to shine on the stage.

Your parents don't give you divorce, we don't divorce, we open up a new world directly, pull one over, don't ask why, it's so capricious.

Well, it's like putting an elephant in a refrigerator in three steps.

1. Opening up a new world;
2. Reproduce the past of a savior;
3. Projecting the Savior;

There are steps. Analyse how to implement them.

  1. Opening up a New World

    We are pragmatic engineers and can't be pushed, so we shouldn't call for a new world. We should call for the creation of a relatively isolated environment. What is the requirement? This environment should be

    • The ability to work inside objects should be exactly the same as that outside objects.
    • The outside of the environment should not be able to perceive the inside.
    • The objects inside and outside the environment should be totally different.

    Let's name this environment Sandbox for the time being.
    For the reference of singleton design, singleton design is usually based on the existence of classes. In order to copy this object, we need to copy the whole Class.
    CL
    We know that Class in Java is loaded into memory by ClassLoader, and ClassLoader uses a parental delegation mechanism. A unique business object in ClassLoader does not exist for other ClassLoaders. Doesn't that satisfy the three points we mentioned above perfectly? Good, that's it!
    Scheme: ClassLoader is used as sandbox environment isolation

  2. Reproduce a Savior's Past

    We decided on the Class Loader scheme before, and the idea of copying the Class into the Class Loader is very simple.
    We know that when ClassLoader loads Class, it actually reads the. class file, and then defines a class through ClassLoader's defineClass. Well, we can also copy the definition of a class out of the sandbox. Two steps
    First read the. class content. Where is this document? When the jar package is loaded into memory by ClassLoader, the file data can be read through getResource. Perfect!
    Define classes in the sandbox. Simple, just a defineClass, finished.~
    Hey, don't worry. Watch out for class redefinitions. Remember to record which classes have been defined.
    LDY

  3. Projecting the Savior

    Yes, that's also a problem.
    As we have just said, the unique business objects of different ClassLoaders do not exist for other ClassLoaders! This leads to a problem, the outside can not use the object instance created inside!
    DGG
    For instance

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

    Why did it go wrong? Because the BizObject inside and outside the sandbox is different, the positive and negative particles will annihilate together...
    So we need projection.
    Well, it's not projection. We need an agent to cultivate a puppet outside the sandbox. Oh no, it's an agent. All the operations of the agent can be fed back to the sandbox for execution.

Well, so far, we've basically sorted out the problem, so the next step is...

God said, "Let there be light."

Through the above analysis and combing, we have basically determined the direction and logic, now, everything is ready, we can enter the new world without a magical east wind, then we start to code!

Wait a minute. Did we miss anything?
We need to design the code first!

Well, let's discuss this demand...
First of all, we assume that a magic "sandbox" has been set up, and the inside and outside of the sandbox are isolated, so the communication between the inside and outside can only be carried out through a very magical bridge, which is "agent";
When an external student needs to create an object but is subject to various constraints, he can create an identity of the object in the sandbox, and then operate through the agent of the identity to achieve the manipulation of the identity, so as to achieve the goal.
Well, there's only so much demand. Next, let's talk about design.
In the discussion above, we decided to use ClassLoader to isolate the inside and outside of the sandbox, but not directly expose the ClassLoader interface for external use?
ClassLoader can operate on the underlying classes. Although it has powerful functions, it has high complexity and is prone to problems if it is not careful. So we should encapsulate it, providing only the interfaces we expect users to use, and we think it should have these characteristics.

  • Single function
  • Don't expose anything unrelated to the sandbox
  • You can use it directly after you create an object

This is difficult for Class Loader, so we need to hide it, create a sandbox to provide services, and hide the Class Loader inside the sandbox, assuming it is called "Sandbox Class Loader".
So we have it.

  • caller
  • sandbox
  • SandboxClassLoader
  • External ClassLoader

Four objects.
Also, as mentioned above, our caller operates on objects in the sandbox through proxies. Do you remember why proxies are used? The essential reason for using proxy is that the classes inside and outside the sandbox belong to different Class Loaders, even though the classes with the same name are different!
Similarly, when we call through the proxy object, parameters are passed using objects outside the sandbox, and can not be directly used into the sandbox. Therefore, we also need to convert such objects.
Here we only consider the value object parameters. If you care about other object parameters, you need a similar proxy conversion. But if the value object, we just need to copy the value, without too complicated processing.
We illustrate this relationship with a picture.

The pictures are very intuitive, so I won't repeat the explanation anymore.
Well, the basic combing should be very clear. Only the blue "object in the sandbox" in the picture belongs to the work in the sandbox, created dynamically, and the rest are outside the sandbox.
The box draws the boundary of the sandbox component. The caller and the APPClassLoader belong to the existing instances without any concern. The inner part of the component belongs to the part that needs to be implemented.
List the key classes

As you can see, Sandbox's API has become very single and simple.
In order to simplify the design, it is stipulated here that the object to be created must have a parametric constructor. If the students need to construct the object through the parametric constructor, they can expand the implementation. Welcome to do this sandbox tool together.
Why are enumerated and non-enumerated objects separated here? Are any of you clear?
The concept of enumeration refers to something that can be enumerated in a limited way. In java, enumerated objects inherit from Enum and can not be constructed by new method, but can only be selected from the enumerated values.
Objects inherit from Objects, and we are all very familiar with them.

Genesis

Finally, the most important part of the code is...

Choose the key code, let's do it.

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());
    }

    /**
     * Sandbox Object Construction Method
     * @param redefinedPackages Bags to work in sandboxes
     *                          All the classes under this package are working in the sandbox
     */
    public Sandbox(String... redefinedPackages){
        this(Lists.newArrayList(redefinedPackages));
    }
    // ......
}

First of all, we will talk about the construction method.
Since it is a sandbox object, why should we design a constructive method?
In practice, we will consider cohesion between some classes. When one class runs in a sandbox, others suggest running in a sandbox. We have learned the principle of simplicity and know that a package is generally cohesive, so the design here is to specify some packing paths, and the sandbox will run in these packages. Object takes over.
What happens if we call sandboxes to construct classes that are not in these packages? So-called "Talk is cheap, show me the code"~~
Later, let's go on with the constructor. Haha ~~This problem is marked as Problem 1, which we will discuss later.
Sandbox ClassLoader appears here, passing getContextClassLoader() as a parameter. What is done here? Let's first look at the construction of Sandbox Class Loader

    /**
     * Sandbox Isolation Core
     *
     * Runtime isolation at the class level through ClassLoader
     *
     * This class essentially proxies the currentContextClassLoader object and adds the ability to handle some classes that need to run in the sandbox
     */
    class SandboxClassLoader extends ClassLoader{
        //ClassLoader in the current context for finding class instances and cloning them into sandboxes
        private final ClassLoader contextClassLoader;
        //Cache Class instances that have been created to avoid duplication of definitions
        private final Map<String, Class> cache = Maps.newHashMap();

        SandboxClassLoader(ClassLoader contextClassLoader) {
            this.contextClassLoader = contextClassLoader;
        }
        //......
    }

Sandbox ClassLoader is constructed by simply storing the incoming contextClassLoader as a temporary standby, so let's look at the getContextClassLoader method.

    /**
     * Class loader to get the current context
     *
     * Such loaders need to contain MQClient-related class definitions
     * PS: Individually defined as a method, it is concerned that the context class loader can be quickly replaced when it fails to meet the requirements.
     * @return Current class loader
     */
    private ClassLoader getContextClassLoader() {
        //In terms of class loader mechanism, the class loader in thread context is the most suitable one.
        return Thread.currentThread().getContextClassLoader();
    }

What a simple thing!
In fact, there are some design reasons: if we want to create an object, then the class definition of the object must be accessible in the current code.
Based on this consideration, we can ascertain that when users create objects in the sandbox using something like A = Sandbox. createObject (A. class), the context in which class A executes in this code must be accessible. At this time, we can get the corresponding. class resource file of class A through the ClassLoader of this context, and then. Redefining this class.
Continuing to look at the code, I reorganized the following code structure for ease of reading

public class Sandbox {
    private SandboxClassLoader classLoader;
    //......

    /**
     * Create a class instance with the specified name in the sandbox
     *
     * If the named class does not belong to the package specified by redefined Packages, the external class instance is returned directly.
     * @param clzName The class name of the instance to be created
     * @return Instance objects that specify class names
     */
    public <T extends Object> T createObject(String clzName) throws ClassNotFoundException, SandboxCannotCreateObjectException {
        Class clz = Class.forName(clzName);
        return (T) createObject(clz);
    }

    /**
     * Create an instance of the specified Class in the sandbox
     * @param clz Class OF THE EXAMPLE TO BE CREATED
     * @return Class instances that are functionally identical to clz and work in sandboxes
     */
    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();

            //If the class loader of the object is the same as the class loader of clz, it means that the object that does not need to work in the sandbox can be returned directly without proxy.
            if(objectInSandbox.getClass().getClassLoader() == clz.getClassLoader()){
                return (T) objectInSandbox;
            }

            /*
            Creating Producer Agents: Because objects inside and outside sandboxes are essentially different classes, bridging the capabilities of both needs to be done
                            The proxy mode is adopted here, by creating object instances outside the sandbox and forwarding all method calls to the sandbox for execution through the proxy.
                            In addition, since all instances inside and outside the sandbox belong to different classes, it is necessary to transform objects for parameters and return values, and clone objects inside and outside the sandbox equally.
             */

            //Creating subclass proxies of objects through cglib
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(clz);
            enhancer.setCallback((MethodInterceptor) (o, method, args, methodProxy) -> {
                Method targetMethod = clzInSandbox.getMethod(method.getName(), method.getParameterTypes());
                //Before calling, we need to clone the parameters and convert them into sandbox objects.
                Object[] targetArgs = args == null ? null : util.cloneTo(args, classLoader);
                Object result = targetMethod.invoke(objectInSandbox, targetArgs);
                //Call the subsequent cloning of the result and convert it to an object outside the sandbox
                return util.cloneTo(result, getContextClassLoader());
            });
            return (T) enhancer.create();
        }catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) {
            throw new SandboxCannotCreateObjectException("Cannot create objects in the sandbox", e);
        }
    }

    //......
}

The main way to create objects in Sandbox has emerged! To facilitate reading, I excluded irrelevant code, leaving only the createObject method.
T createObject(String clzName) method has no specific implementation. It only checks the parameter clzName and then transfers it to T createObject(Class clz), so this method is mainly analyzed.
In fact, the amount of code is not much (only 19 lines also include various curly braces), mainly comments, the context is as follows

  1. First, we get the parameter CLZ in the sandbox to define the clzInSandbox for the class, and create a concrete instance of the class objectInSandbox through the new Instance of clzInSandbox; therefore, we need the parameter constructor for clz.
  2. Determine whether clzInSandbox runs in the sandbox or not. If it does not run in the sandbox, it does not need to create an agent to directly return the object objectInSandbox.
    Why do you make this judgement, huh? Here you can answer the previous question 1 by the way, from the code.

    //If the class loader of the object is the same as the class loader of clz, it means that the object that does not need to work in the sandbox can be returned directly without proxy.
    if(objectInSandbox.getClass().getClassLoader() == &gt; clz.getClassLoader()){
        return (T) objectInSandbox;
    }

    We can see that when the created object in Sandbox is also running outside the Class Loader, it is not actually to create agents, because it is an object outside the sandbox, why create agents so much?
    But we call classLoader.loadClass(clz.getName()) to get the class definition in the sandbox. Why is it outside the sandbox? Does this contradict our design of Sandbox Class Loader?
    Okay, take a look at the corresponding code, show me the code

    class SandboxClassLoader extends ClassLoader{
        //ClassLoader in the current context for finding class instances and cloning them into sandboxes
        private final ClassLoader contextClassLoader;
        //......  
    
        /**
         * Override the method of reloading the parent class into memory
         * @param name Specify the class name
         * @return Class instance loaded into memory
         * @throws ClassNotFoundException
         */
        @Override
        public Class&lt;?&gt; loadClass(String name) throws ClassNotFoundException {
            return findClass(name);
        }
    
        /**
         * Redefining Class Reproduction Logic
         *
         * 1,For classes that need to run in the sandbox (declared in redefined Packages), run directly under this ClassLoader by copying the definition of the contextClassLoader class
         * 2,For classes that do not need to run in the sandbox, return the context class definition directly to reduce resource usage.
         * @param name Class name (full path)
         * @return Class Definition
         */
        @Override
        protected Class&lt;?&gt; findClass(String name) throws ClassNotFoundException {
            if(isRedefinedClass(name)) {
                return getSandboxClass(name);
            } else {
                return contextClassLoader.loadClass(name);
            }
        }
    
        //......
    }

    It can be seen that the actual implementation of logic code is findClass method, only a few sentences, translated as "we need to redefine the class from the sandbox, not directly from the outside", so there will be objects ClassLoader is external.
    So what are "classes that need to be redefined"?

    /**
     * Do you need classes running in sandboxes?
     * @param name Class name
     */
    boolean isRedefinedClass(String name) {
        //Check whether sandbox conventions require redefinition of packages
        for (String redefinedPackage : redefinedPackages) {
            if(name.startsWith(redefinedPackage)){
                return true;
            }
        }
        return false;
    }

    As long as the class under the package specified when the Sandbox class is constructed belongs to the class that needs to be redefined in the Sandbox Class Loader.

  3. Using cglib library to create proxy object of objectInSandbox, intercept all method execution of the proxy object, and transfer it to the actual object objectInSandbox for execution.
    The code for creating objects in cglib is not analyzed. The essence is to intercept methods by creating a subclass of a specified class.
    What should we care about is what the interceptor did?

    enhancer.setCallback((MethodInterceptor) (o, method, args, methodProxy) -&gt; {
                Method targetMethod = clzInSandbox.getMethod(method.getName(), method.getParameterTypes());
                //Before calling, we need to clone the parameters and convert them into sandbox objects.
                Object[] targetArgs = args == null ? null : util.cloneTo(args, classLoader);
                Object result = targetMethod.invoke(objectInSandbox, targetArgs);
                //Call the subsequent cloning of the result and convert it to an object outside the sandbox
                return util.cloneTo(result, getContextClassLoader());
            });

    We will get the method of the same name and the same parameter from the object in the sandbox, then convert the parameter into the sandbox, then execute the object method in the sandbox and get the result, and finally convert the result to the object outside the sandbox before returning.
    The logic is very clear, but how do objects inside and outside the sandbox be converted?
    The code here is a bit long and boring, so it can not be posted separately. Interested students can download it on github by themselves. The general logic is as follows.

    1. Determine whether the object needs to be converted into sandbox or not, then return the object if not needed, and turn 2 if necessary.
    2. Create object instances corresponding to inside / outside sandbox;
    3. Traverse each field of the object instance, perform step 1 on the field, and assign the duplicated value to the corresponding field in the new object.

    Well, that's it.
    As we mentioned earlier, we assume that all the reference objects are value objects, so the design here is relatively simple. If any students need to pass non-value objects, then they need to proxy the external objects.

  4. Return the proxy object;

Some students are concerned about how classes are copied from outside the sandbox to redefined in the sandbox, aren't they? This is the core part of Sandbox Class Loader, showing the code logic

class SandboxClassLoader extends ClassLoader {
    //......
    //Cache Class instances that have been created to avoid duplication of definitions
    private final Map<String, Class> cache = Maps.newHashMap();

    /**
        * Internal Method: Get Class instances that need to run in the sandbox
        * @param name Class name
        * @return Class instances in sandboxes
        * @throws ClassNotFoundException
        */
    private synchronized Class<?> getSandboxClass(String name) throws ClassNotFoundException {
        //1. Find out whether the class has been reproduced from the cache first, or return directly.
        if(cache.containsKey(name)){
            return cache.get(name);
        }
        //2. When the cache does not exist, copy a copy from the current ContextClassLoader to the current cache
        Class<?> clz = copyClass(name);
        cache.put(name, clz);
        return clz;
    }

    /**
        * Copy a class from the current ContextClassLoader to this ClassLoader
        *
        * This replication is defined by copy ing the bytecode to the current ClassLoader, so it is completely different from Classes outside sandbox and cannot be directly assigned to the outside.
        * @param name Class name to be copied
        * @return Class working in the current ClassLoader
        * @throws ClassNotFoundException
        */
    private synchronized Class<?> copyClass(String name) throws ClassNotFoundException {
        //Get the path of the. class file
        String path = name.replace('.', '/') + ".class";
        //Getting Resource Handles through Context Class Loader
        try (InputStream stream = contextClassLoader.getResourceAsStream(path)) {
            if(stream == null) throw new ClassNotFoundException(String.format("Class not found%s", name));

            //Read all byte contents
            byte[] content = readFromStream(stream);
            return defineClass(name, content, 0, content.length);
        } catch (IOException e) {
            throw new ClassNotFoundException("The specified class could not be found", e);
        }
    }

    //......
}

There are two main methods involved. The getSandbox Class method is mainly responsible for the cache level verification when acquiring objects. The purpose of caching is to accelerate the performance of acquiring class definitions, and to avoid errors caused by repeated execution of the same class definitions.
copyClass, as its name implies, is the process of copying class definitions from contextClassLoader. Class files that correspond to classes and defining class in Sandbox ClassLoader. Read the code for details.

We also have a getEnumValue method in Sandbox, which is similar in some ways and will not be repeated. Please download the code to read.

So far, we have completed the coding.
So far, we have completed the construction of a new world!
So far, we have finished all the work!!??
It's too early to be happy.

The Coming Redemption

Testing is the guarantee of code quality, design, operation, etc. In short, it is the guarantee.
So we also need to pass tests to verify our "world" and see if it matches our expectations.
This can only be done by using unit tests. Code

public class SandboxTest {

    @Test
    public void getEnumValue() throws SandboxCannotCreateObjectException {
        //Setting redefined packages
        Sandbox sandbox = new Sandbox("com.google.common.collect");

        //Obtaining objects in a sandbox, although of the same name and value, should be expected to vary because they belong to both inside and outside the sandbox.
        Enum type = sandbox.getEnumValue(com.google.common.collect.BoundType.CLOSED);
        assertNotEquals(type, com.google.common.collect.BoundType.CLOSED);

        //Getting non-defined in-package objects through sandboxes should be expected to be equal
        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 {
        //Setting redefined packages
        Sandbox sandbox = new Sandbox("com.google.common.eventbus");

        //Getting objects in the sandbox, the expected class definition should be different from that outside the sandbox
        com.google.common.eventbus.EventBus bus = sandbox.createObject(com.google.common.eventbus.EventBus.class);
        assertNotEquals(bus.getClass(), com.google.common.eventbus.EventBus.class);

        //Get by name, as above
        bus = sandbox.createObject("com.google.common.eventbus.EventBus");
        assertNotEquals(bus.getClass(), com.google.common.eventbus.EventBus.class);

        //Getting classes without redefinition through sandboxes should be expected to be equivalent to outside sandboxes
        List<String> list = sandbox.createObject(ArrayList.class);
        assertEquals(list.getClass(), ArrayList.class);
    }
}

Operation results

OK, the test passes~~~

World coordinates

Keywords: Programming Google Java github jvm

Added by Telemachus on Mon, 05 Aug 2019 07:19:48 +0300