应用在JDBC的Class.forName和ClassLoader.loadClass区别

本文解释了在JDBC中为何要使用Class.forName而非ClassLoader.loadClass来加载数据库驱动。详细剖析了两者的工作原理及其区别,强调了静态代码块在驱动注册中的作用。

目录

一  结论

二  为什么在JDBC中要用Class.forName,不能用ClassLoader.loadClass

三  Class.forName和ClassLoader.loadClass原理剖析


一  结论

Class.forName和ClassLoader.loadClass相同点

  1. 两者都实现了根据类的完全限定名将类加载到JVM中
  2. 两者都返回一个Class对象
  3. @exception ClassNotFoundException if the class cannot be located

Class.forName和ClassLoader.loadClass不同点

  1. 前者加载类的同时对类进行了初始化操作。(默认true)
  2. 后者只是将类加载到了JVM,并没有初始化

二  为什么在JDBC中要用Class.forName,不能用ClassLoader.loadClass

2.1  使用示例

// JDBC连接数据库
// 1. 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 获取连接
Connection conn = DriverManager.getConnection(url);

2.2  注册驱动原理

       注册驱动是什么意思?是指在用JDBC连接数据库时,JDBC规范要求将Driver注册到它的DriverManager,因为SQL数据源有多种。看下

com.mysql.cj.jdbc.Driver类,该类在依赖包 mysql-connector-java-xxx.jar包。

       可以看到Driver类里是通过在static静态代码块完成注册Driver实例到DriverManager的。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // Register ourselves with the DriverManager
    //
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    ...
}

2.3  结论

       因为ClassLoader.loadClass加载类到JVM没有对类初始化,所以不会执行Driver类里的static代码块,所以无法完成驱动注册;

       Class.forName()加载类时默认完成类初始化,所以执行static代码块从而完成注册驱动。

       所以,这就是为什么在JDBC中要用Class.forName,不能用ClassLoader.loadClass的原因。

三  Class.forName和ClassLoader.loadClass原理剖析

3.1  Class.forName()原理

       Class.forName()在Class类里有2个重载方法。

       我们在JDBC里使用的Class.forName("com.mysql.cj.jdbc.Driver")是直接使用的第一个方法,第二个参数是boolean initialize,默认是直接传的true。

       @param initialize if {@code true} the class will be initialized ,即确定该class是否需要初始化。

       两个重载方法底层都是调的forName0().

@CallerSensitive
public static Class<?> forName(String className)
           throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

@CallerSensitive
public static Class<?> forName(String name, boolean initialize,
                               ClassLoader loader)
        throws ClassNotFoundException
        /.../
        return forName0(name, initialize, loader, caller);
}


/** Called after security check for system loader access checks have been made. */
private static native Class<?> forName0(String name, boolean initialize,
                                        ClassLoader loader,
                                        Class<?> caller)
    throws ClassNotFoundException;

3.2  ClassLoader.loadClass原理

       ClassLoader.loadClass在ClassLoader类也有2个重载方法。

       ClassLoader的loadClass方法有个boolean resolve参数

       @param resolve * If <tt>true</tt> then resolve the class ,即是否解析该类。

       我们知道类加载过程包括了加载、验证、准备、解析、初始化五个阶段,所以这里显然没有到初始化阶段。

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 首先,检查类是否已经被加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

Java中,`Class.forName()``ClassLoader.loadClass()`都是用于动态加载类的机制,但它们在加载行为、初始化触发异常处理方面存在关键差异。以下是两者的详细对比分析: ### 核心区别对比 | 特性 | `Class.forName(String className)` | `ClassLoader.loadClass(String name)` | | :--- | :--- | :--- | | **来源** | `java.lang.Class`类的静态方法。 | `java.lang.ClassLoader`类的实例方法。 | | **主要功能** | 加载并**初始化**类。 | 加载类,但**默认不进行初始化**。 | | **初始化行为** | 默认会执行类的静态初始化块(`static {}`)静态变量赋值。 | 默认不会触发初始化。需要显式调用`Class.forName(name, false, loader)`或后续使用`Class.newInstance()`等才会初始化。 | | **异常处理** | 抛出`ClassNotFoundException`。 | 抛出`ClassNotFoundException`。 | | **典型应用场景** | JDBC驱动注册(如`Class.forName("com.mysql.cj.jdbc.Driver")`),需要立即初始化并执行静态注册代码。 | 框架、容器(如Tomcat、OSGi)实现类隔离、热部署,需要精细控制类加载生命周期。 | | **资源消耗** | 较高,因为立即执行初始化可能涉及大量静态资源加载。 | 较低,延迟初始化节省资源,适合大量类按需加载。 | ### 技术细节与工作原理 #### 1. `Class.forName()`:主动初始化加载 该方法调用最终会委托给调用者的`ClassLoader`,并设置`initialize`参数为`true`。 ```java // Class.forName 典型实现路径(简化) public static Class<?> forName(String className) throws ClassNotFoundException { return forName(className, true, ClassLoader.getCallerClassLoader()); } ``` **关键过程**: * **加载**:通过类加载器查找并定义类。 * **链接**:验证字节码、准备内存(分配静态变量默认值)。 * **初始化**:执行`<clinit>`方法(即静态块静态变量显式赋值)。 **示例:JDBC驱动注册** ```java Class.forName("com.mysql.cj.jdbc.Driver"); // 驱动类静态块会向DriverManager注册自身 // static { // try { // DriverManager.registerDriver(new Driver()); // } catch (SQLException var1) { // throw new RuntimeException("Can't register driver!"); // } // } ``` #### 2. `ClassLoader.loadClass()`:被动延迟加载 这是类加载器的核心方法,采用**双亲委派模型**,默认`resolve`参数为`false`,即不进行链接阶段的解析初始化。 ```java // ClassLoader.loadClass 默认实现(简化) protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1. 检查是否已加载 Class<?> c = findLoadedClass(name); if (c == null) { try { // 2. 优先委托父加载器 if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 父加载器无法完成时,由自身查找 } if (c == null) { // 3. 自身查找类定义 c = findClass(name); } } // 4. 如需解析(链接+初始化),则执行 if (resolve) { resolveClass(c); } return c; } } ``` **关键行为**: * **双亲委派**:优先让父加载器尝试加载,避免核心类被篡改。 * **延迟初始化**:除非显式调用`resolveClass()`或后续触发(如`newInstance()`),否则不执行静态初始化。 **示例:框架中的按需加载** ```java ClassLoader customLoader = ...; Class<?> targetClass = customLoader.loadClass("com.example.Plugin"); // 此时类未初始化,静态块未执行 // 当首次创建实例或访问静态字段时,才会触发初始化 Object instance = targetClass.newInstance(); ``` ### 应用场景与选择策略 | 场景 | 推荐方法 | 理由 | | :--- | :--- | :--- | | **数据库驱动注册** | `Class.forName()` | 必须立即初始化以执行静态注册代码。 | | **插件化架构** | `ClassLoader.loadClass()` | 避免插件静态代码过早执行,隔离类空间。 | | **反射创建实例(无需立即初始化)** | `ClassLoader.loadClass()` + `Class.newInstance()` | 节省资源,实例化时自然触发初始化。 | | **热部署/类重载** | 自定义`ClassLoader.loadClass()` | 可打破双亲委派,实现同一类的多版本隔离。 | ### 性能与安全考量 1. **性能**: * `Class.forName()`的立即初始化可能导致启动时间延长,尤其当类依赖复杂静态资源时。 * `ClassLoader.loadClass()`的延迟加载更利于内存管理启动速度,但可能将初始化延迟到运行时,引起瞬时性能波动。 2. **安全与隔离**: * 自定义`ClassLoader`配合`loadClass()`可实现**类隔离**,防止类冲突。例如,Web容器为每个Web应用分配独立的`ClassLoader`,确保应用间类不干扰。 * `Class.forName()`默认使用当前线程的上下文类加载器,在复杂模块化环境中可能意外加载到错误版本的类。 ### 代码示例:结合使用 ```java // 需要加载但不立即初始化时,可结合两者 ClassLoader cl = Thread.currentThread().getContextClassLoader(); Class<?> clazz = Class.forName("com.example.MyClass", false, cl); // 此时类已加载但未初始化 // ... 后续在需要时手动触发初始化 Field staticField = clazz.getDeclaredField("STATIC_FIELD"); // 访问静态字段将触发初始化 Object value = staticField.get(null); ``` **结论**:选择取决于是否需要立即执行类的静态初始化代码。`Class.forName()`是一种“急切实例化”模式,适合驱动注册等需立即生效的场景;而`ClassLoader.loadClass()`是一种“懒加载”模式,为框架容器提供了更精细的类生命周期控制能力,是现代模块化系统的基础。在实际开发中,理解其差异有助于避免类初始化导致的意外行为或性能问题。
出现 `java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver` 错误的原因是:Java 程序在运行时找不到 MySQL 的 JDBC 驱动类 `com.mysql.cj.jdbc.Driver`。这个类是 MySQL 提供的用于连接数据库的 JDBC 驱动程序的一部分。 ### 原因分析: 1. **缺少 MySQL JDBC 驱动包(mysql-connector-java)**:项目中没有引入 MySQL 的 JDBC 驱动 JAR 文件。 2. **JAR 包未添加到类路径(classpath)中**:即使你下载了驱动包,如果没有将其添加到项目的类路径中,Java 也无法加载该类。 3. **驱动类名错误(适用于旧版本 MySQL)**:在 MySQL 8.x 中使用的是 `com.mysql.cj.jdbc.Driver`,而在旧版本(如 MySQL 5.x)中使用的是 `com.mysql.jdbc.Driver`。如果使用了错误的类名也会导致该异常。 --- ### 解决方法: #### ✅ 方法一:添加 MySQL JDBC 驱动到项目中 1. **下载 MySQL JDBC 驱动** - 官网地址:https://dev.mysql.com/downloads/connector/j/ - Maven 依赖(推荐): ```xml <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> ``` 2. **如果使用 IDE(如 IntelliJ IDEA / Eclipse)** - 如果你不是使用 Maven 或 Gradle,请手动下载 `mysql-connector-java-8.x.x.jar`,并将其添加到项目的 `lib` 目录,并通过项目设置将其加入 `classpath`。 #### ✅ 方法二:确保使用正确的驱动类名 MySQL 8.x 推荐使用: ```java Class.forName("com.mysql.cj.jdbc.Driver"); ``` 如果你使用的是 MySQL 5.x,请使用: ```java Class.forName("com.mysql.jdbc.Driver"); ``` --- ### 示例代码(正确加载 MySQL JDBC 驱动): ```java import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class JdbcDao { public Connection getConnection() { Connection conn = null; try { // 加载驱动类 Class.forName("com.mysql.cj.jdbc.Driver"); // 连接数据库 String url = "jdbc:mysql://localhost:3306/your_database?useSSL=false&serverTimezone=UTC"; String user = "root"; String password = "123456"; conn = DriverManager.getConnection(url, user, password); } catch (ClassNotFoundException | SQLException e) { e.printStackTrace(); } return conn; } public static void main(String[] args) { JdbcDao dao = new JdbcDao(); if (dao.getConnection() != null) { System.out.println("数据库连接成功!"); } else { System.out.println("数据库连接失败!"); } } } ``` --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值