YSMull
<-- home

Java 中如何做类隔离

准备被依赖模块

模块A 1.0

package com.youdata;

public class A {

    static {
        System.out.println("A 1.0 loaded");
    }

    public static void printVersion() {
        System.out.println("I am A 1.0");
    }

    public static void printDepVersion() {
        B.printVersion();
    }
}
<dependencies>
  <dependency>
    <groupId>com.youdata</groupId>
    <artifactId>B</artifactId>
    <version>1.0</version>
  </dependency>
</dependencies>

模块A 2.0

package com.youdata;

public class A {

    static {
        System.out.println("A 2.0 loaded");
    }

    public static void printVersion() {
        System.out.println("I am A 2.0");
    }

    // 新版通过反射调用
    public static void printDepVersion() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Method printVersionNew = B.class.getMethod("printVersionNew");
        printVersionNew.invoke(null);
    }
}
<dependencies>
  <dependency>
    <groupId>com.youdata</groupId>
    <artifactId>B</artifactId>
    <version>1.0</version>
  </dependency>
</dependencies>

模块B 1.0

package com.youdata;

public class B {

    static {
        System.out.println("B 1.0 loaded");
    }

    public static void printVersion() {
        System.out.println("I am B 1.0");
    }
}

模块B 2.0

package com.youdata;

public class B {

    static {
        System.out.println("B 2.0 loaded");
    }

    public static void printVersionNew() { // 方法名改了
        System.out.println("I am B 2.0");
    }
}

使用 Maven 加载依赖

package com.youdata;

public class Main {
    public static void main(String[] args) {
        A.printVersion();
        A.printDepVersion();
    }
}
<dependencies>
  <dependency>
    <groupId>com.youdata</groupId>
    <artifactId>A</artifactId>
    <version>1.0</version>
  </dependency>
</dependencies>

我们运行一下:

A 1.0 loaded
I am A 1.0
Exception in thread "main" java.lang.NoClassDefFoundError: com/youdata/B
	at com.youdata.A.printDepVersion(A.java:14)
	at com.youdata.Main.main(Main.java:7)
Caused by: java.lang.ClassNotFoundException: com.youdata.B
	at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
	... 2 more

报错原因是因为 A 在执行 B 的静态方法时,找不到类 B,从而无法加载类 B,我们在 pom.xml 中引入 B 这个依赖,并重新运行:

<dependencies>
  <dependency>
    <groupId>com.youdata</groupId>
    <artifactId>A</artifactId>
    <version>1.0</version>
  </dependency>
  <dependency>
    <groupId>com.youdata</groupId>
    <artifactId>B</artifactId>
    <version>1.0</version>
  </dependency>
</dependencies>
A 1.0 loaded
I am A 1.0
B 1.0 loaded
I am B 1.0

如果我们把 B 的依赖版本升级到 2.0,重新运行时,A 会调用 B 2.0 版本的静态方法:

<dependencies>
  <dependency>
    <groupId>com.youdata</groupId>
    <artifactId>A</artifactId>
    <version>1.0</version>
  </dependency>
  <dependency>
    <groupId>com.youdata</groupId>
    <artifactId>B</artifactId>
    <version>2.0</version>
  </dependency>
</dependencies>
A 1.0 loaded
I am A 1.0
B 2.0 loaded
I am B 2.0

可以看到,使用 Maven 的方式,调用方声明自己需要一个 A 模块,如果不提供 B 模块的任何一个版本,A 模块是无法正常工作的。

当我们提供 1.0 版本的 B 模块,A 就是用 1.0 的 B 模块工作,当我们提供 2.0 版本的 B 模块,A 就是用 2.0 的 B 模块工作。

使用类加载器加载多版本

我们改造调用方的项目结构如下,使用 URLClassLoader 同时加载指定路径的所有 jar 包。

.
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── youdata
        │           └── Main.java
        └── resources
            └── lib
                ├── 1.0
                │   ├── A-1.0.jar
                │   └── B-1.0.jar
                └── 2.0
                    ├── A-2.0.jar
                    └── B-2.0.jar

package com.youdata;

import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Objects;
import java.util.stream.Stream;

public class Main {

    public static Class<?> loadClassA(String jarPath, ClassLoader loader) throws Exception {
        String lib = Main.class.getClassLoader().getResource(jarPath).getPath();
        Stream<Path> walk = Files.walk(Paths.get(lib));
        URL[] jars = walk.filter(f -> f.getFileName().toString().endsWith(".jar"))
            .map(p -> {
                try {
                    return p.toUri().toURL();
                } catch (MalformedURLException e) {
                    return null;
                }
            }).filter(Objects::nonNull).toArray(URL[]::new);
        URLClassLoader classLoader = new URLClassLoader(jars, loader);
        return classLoader.loadClass("com.youdata.A");
    }

    public static void invokeAsMethod(Class<?> aClass) throws Exception {
        Method printVersion = aClass.getMethod("printVersion");
        printVersion.invoke(null);
        Method printDepVersion = aClass.getMethod("printDepVersion");
        printDepVersion.invoke(null);
        System.out.println();
    }

    public static void loadClassInNewThread(String jarPath) {
        new Thread(() -> {
            try {
                Class<?> aClass = loadClassA(jarPath, Thread.currentThread().getContextClassLoader());
                while (true) {
                    invokeAsMethod(aClass);
                    Thread.sleep(2000);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();
    }

    public static void main(String[] args) throws InterruptedException {
        loadClassInNewThread("lib/1.0");
        Thread.sleep(1000); // 时间上错位打印
        loadClassInNewThread("lib/2.0");
    }
}
A 1.0 loaded
I am A 1.0
B 1.0 loaded
I am B 1.0

A 2.0 loaded
I am A 2.0
B 2.0 loaded
I am B 2.0

I am A 1.0
I am B 1.0

I am A 2.0
I am B 2.0

I am A 1.0
I am B 1.0

I am A 2.0
I am B 2.0

I am A 1.0
I am B 1.0

...

我们可以把 jar 包修改一下,让 2.0 目录下的 A-2.0.jar 使用 B-1.0.jar:

.
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── youdata
        │           └── Main.java
        └── resources
            └── lib
                ├── 1.0
                │   ├── A-1.0.jar
                │   └── B-1.0.jar
                └── 2.0
                    ├── A-2.0.jar
                    └── B-1.0.jar     <====== 修改为 B-1.0.jar

重新执行代码报了 NoSuchMethodException 错误:

java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.youdata.Main.invokeAsMethod(Main.java:34)
    at com.youdata.Main.lambda$loadClassInNewThread$3(Main.java:43)
    at java.lang.Thread.run(Thread.java:748)
    Caused by: java.lang.NoSuchMethodException: com.youdata.B.printVersionNew()
    at java.lang.Class.getMethod(Class.java:1786)
    at com.youdata.A.printDepVersion(A.java:17)
    ... 7 more

并且实验还发现:

  1. Thread.currentThread().getContextClassLoader() 换成 Main.class.getClassLoader() 甚至换成 null 也是 work 的。
  2. 去掉 sleep,每一次循环都重新加载类并且,两个线程并发运行,也不会发生调用错误,这说明被加载的类没有相互覆盖。
public static void loadClassInNewThread(String jarPath) {
    new Thread(() -> {
        try {
            while (true) {
                Class<?> aClass = loadClassA(jarPath, null);
                invokeAsMethod(aClass);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }).start();
}