杰瑞科技汇

Java动态加载class如何实现?

类加载机制

在讲动态加载之前,必须先理解 Java 的类加载机制,Java 类的加载不是在编译时完成的,而是在运行时由 类加载器 负责的。

  1. 加载:查找并加载类的二进制数据(.class 文件)。
  2. 链接
    • 验证:确保 .class 文件的正确性。
    • 准备:为类的静态变量分配内存,并初始化默认值。
    • 解析:将常量池中的符号引用替换为直接引用。
  3. 初始化:执行类的静态初始化块和静态变量的赋值。

Java 默认有三种类加载器,它们形成了双亲委派模型

  • Bootstrap ClassLoader (启动类加载器):加载 Java 核心库 (JAVA_HOME/jre/lib/rt.jar),它是 C++ 实现的,不是 ClassLoader 的子类。
  • Extension ClassLoader (扩展类加载器):加载 Java 扩展库 (JAVA_HOME/jre/lib/ext/)。
  • Application ClassLoader (应用程序类加载器):加载应用程序的类路径 (classpath) 下的类,我们通常使用的 Class.forName()ClassLoader.getSystemClassLoader() 默认都是它。

双亲委派模型:当一个类加载器收到加载请求时,它首先会把这个请求委派给父类加载器,直到顶层的启动类加载器,如果父类加载器无法完成加载(在其搜索范围内没找到),子加载器才会尝试自己去加载。

这个模型的好处是安全性,可以防止核心 API 被随意替换。


动态加载 Class 的主要方式

动态加载的核心是获取一个 ClassLoader 实例,然后调用其 loadClass() 方法,这与 Class.forName() 有本质区别,我们后面会讲。

使用 ClassLoader.loadClass() (最核心的方式)

这是最直接、最灵活的动态加载方式,它允许你指定使用哪个类加载器来加载类,并且不会自动初始化该类(除非设置了 resolve 参数)。

代码示例:

假设我们有一个简单的类 MyService,它位于 com.example 包下,并且编译后的 .class 文件在 D:/temp/classes 目录中。

MyService.java

package com.example;
public class MyService {
    static {
        System.out.println("MyService 静态代码块被初始化了!");
    }
    public void sayHello() {
        System.out.println("Hello from MyService!");
    }
}

DynamicLoaderDemo.java

import java.io.File;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
public class DynamicLoaderDemo {
    public static void main(String[] args) throws Exception {
        // 1. 定义存放 .class 文件的目录
        String classPath = "D:/temp/classes";
        File file = new File(classPath);
        // 2. 创建一个 URLClassLoader,它可以从指定的文件系统路径加载类
        URL url = file.toURI().toURL();
        URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
        // 3. 动态加载类 (这里使用 loadClass)
        //    第一个参数是类的全限定名
        //    第二个参数 `false` 表示不解析(即不链接),通常我们只需要加载,后续通过反射使用时会自动链接和初始化
        Class<?> clazz = classLoader.loadClass("com.example.MyService");
        System.out.println("类加载器: " + clazz.getClassLoader());
        System.out.println("类是否被初始化: " + (clazz.getDeclaredField("initialized") != null)); // 这行只是示例,实际判断初始化更复杂
        // 4. 使用反射创建实例并调用方法
        Object instance = clazz.getDeclaredConstructor().newInstance();
        Method method = clazz.getMethod("sayHello");
        method.invoke(instance);
        // 关闭类加载器(可选,但推荐)
        classLoader.close();
    }
}

如何运行:

  1. 编译 MyService.javajavac -d D:/temp/classes MyService.java
  2. 运行 DynamicLoaderDemo.javajava DynamicLoaderDemo

输出:

类加载器: sun.net.www.protocol.file.FileURLClassLoader@xxxxx
Hello from MyService!

注意:你会发现 MyService 的静态代码块没有被执行,这是因为 loadClass() 默认不会初始化类,当你第一次调用 clazz.newInstance() (在 Java 9+ 中是 getDeclaredConstructor().newInstance()) 或者访问类的静态变量/方法时,JVM 才会触发类的初始化。


使用 Class.forName() (经典方式)

Class.forName() 是另一种常用的方式,但它与 loadClass() 有显著区别。

Class.forName(String className)

  • 默认使用当前类的类加载器来加载类。
  • 加载完成后,会立即初始化该类(执行静态代码块)。

代码示例:

public class ForNameDemo {
    public static void main(String[] args) throws Exception {
        // 默认使用调用 ForNameDemo 这个类的类加载器(通常是AppClassLoader)
        // 并且会立即初始化 MyService
        Class<?> clazz = Class.forName("com.example.MyService");
        System.out.println("类加载器: " + clazz.getClassLoader());
        // 因为上面已经初始化了,所以这里直接创建实例即可
        Object instance = clazz.getDeclaredConstructor().newInstance();
        instance.getClass().getMethod("sayHello").invoke(instance);
    }
}

输出:

MyService 静态代码块被初始化了!
类加载器: sun.misc.Launcher$AppClassLoader@xxxxx
Hello from MyService!

可以看到,静态代码块输出了,说明类被初始化了。

Class.forName(String className, boolean initialize, ClassLoader loader): 这个重载版本结合了两者的优点,非常强大。

  • className: 类的全限定名。
  • initialize: 是否初始化该类,设为 false 时,行为与 loadClass() 类似。
  • `loader 指定使用哪个类加载器**来加载类。

代码示例:

public class ForNameWithLoaderDemo {
    public static void main(String[] args) throws Exception {
        String classPath = "D:/temp/classes";
        File file = new File(classPath);
        URL url = file.toURI().toURL();
        URLClassLoader customClassLoader = new URLClassLoader(new URL[]{url});
        // 使用自定义类加载器,并且不进行初始化
        Class<?> clazz = Class.forName("com.example.MyService", false, customClassLoader);
        System.out.println("类加载器: " + clazz.getClassLoader());
        System.out.println("类是否被初始化: " + (clazz.getDeclaredField("initialized") != null)); // 同样,这只是示意
        // 当我们创建实例时,才会触发初始化
        Object instance = clazz.getDeclaredConstructor().newInstance();
        instance.getClass().getMethod("sayHello").invoke(instance);
        customClassLoader.close();
    }
}

这个版本提供了最大的灵活性,既可以指定类加载器,又可以控制是否初始化。


使用 ServiceLoader (Java SPI 机制)

这是 Java 标准库提供的一种服务发现机制,常用于驱动加载(如 JDBC),它背后也使用了自定义的类加载器。

场景:你定义一个服务接口,第三方可以提供该接口的实现,你的程序可以在运行时动态发现并加载这些实现,而无需在编译时依赖它们。

代码示例:

  1. 定义服务接口 src/main/java/com/example/MyServiceInterface.java

    package com.example;
    public interface MyServiceInterface {
        void execute();
    }
  2. 提供服务实现 假设我们有两个不同的实现,放在不同的 jar 包或目录中。 impl1/src/main/java/com/example/impl/MyServiceImpl1.java

    package com.example.impl;
    import com.example.MyServiceInterface;
    public class MyServiceImpl1 implements MyServiceInterface {
        @Override
        public void execute() {
            System.out.println("Executing MyServiceImpl1");
        }
    }

    impl2/src/main/java/com/example/impl/MyServiceImpl2.java

    package com.example.impl;
    import com.example.MyServiceInterface;
    public class MyServiceImpl2 implements MyServiceInterface {
        @Override
        public void execute() {
            System.out.println("Executing MyServiceImpl2");
        }
    }
  3. 创建服务配置文件 在每个实现的 META-INF/services/ 目录下创建一个文件,文件名是服务接口的全限定名。 impl1/META-INF/services/com.example.MyServiceInterface 文件内容: com.example.impl.MyServiceImpl1

    impl2/META-INF/services/com.example.MyServiceInterface 文件内容: com.example.impl.MyServiceImpl2

  4. 使用 ServiceLoader 加载 src/main/java/com/example/ServiceLoaderDemo.java

    import com.example.MyServiceInterface;
    import java.util.ServiceLoader;
    public class ServiceLoaderDemo {
        public static void main(String[] args) {
            // ServiceLoader 会使用上下文类加载器来查找配置文件并加载实现类
            ServiceLoader<MyServiceInterface> loader = ServiceLoader.load(MyServiceInterface.class);
            System.out.println("Found service implementations:");
            for (MyServiceInterface service : loader) {
                service.execute();
            }
        }
    }

    如何运行:你需要将 impl1.jarimpl2.jar 添加到 classpath 中运行。ServiceLoader 会自动扫描所有 jar 包中的 META-INF/services 目录,找到所有匹配的实现。


特性 ClassLoader.loadClass() Class.forName()
初始化 默认不初始化,延迟到第一次使用时。 默认会立即初始化
类加载器 可以明确指定使用哪个 ClassLoader 默认使用调用该方法的类的类加载器
灵活性 非常高,是底层动态加载的基础。 相对较低,但 forName(name, init, loader) 重载版本也很灵活。
主要用途 框架、插件系统、热部署,需要精确控制加载过程和类加载器隔离的场景。 JDBC 加载驱动、ORM 框架、通用反射工具,通常希望类加载后立即可用。

应用场景

  1. 插件化架构:应用程序可以动态加载位于特定目录(如 plugins/)的 jar 包,实现功能的扩展和更新,而无需重新主程序。
  2. 热部署:在开发或运行时,替换掉旧的 .class 文件,然后通过新的类加载器加载新类,实现不重启应用更新代码。
  3. 框架集成
    • Spring:大量使用 Class.forName() 加载配置、注解等。
    • MyBatis/JDBC:使用 Class.forName() 加载数据库驱动类。
    • 各种 SPI:使用 ServiceLoader 加载日志实现、数据库方言等。
  4. 测试工具:如 JUnit、Mockito,需要动态加载用户编写的测试类。

注意事项

  • 类加载器隔离:不同类加载器加载的类,即使全限定名相同,在 JVM 中也被视为不同的类,它们不能直接相互转换,也无法访问对方的私有成员,这是实现插件隔离的关键。
  • 内存泄漏:类加载器及其加载的所有类都会占用内存,在热部署或插件卸载时,如果不正确地关闭和释放类加载器,可能会导致内存泄漏。URLClassLoader 实现了 AutoCloseable,建议使用 try-with-resources
  • 安全性:动态加载代码可能带来安全风险,确保加载的来源是可信的。
  • Java 模块系统 (JPMS): 在 Java 9+ 的模块化项目中,类加载机制变得更加复杂。URLClassLoader 的使用受到限制,推荐使用 ModuleLayerServiceLoader 来实现更安全的动态加载。
分享:
扫描分享到社交APP
上一篇
下一篇