类路径

6/11/2024 JVM

# 类路径

早期我们项目中需要连接数据库时会编写如下代码:

public class DbTest {
    public static void main(String[] args) {
        try {
            Class.forName("com.mysql.jdbc.Driver");
            connection = DriverManager.getConnection(url, username, password);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10

之前我一直有个疑惑,为啥需要Class.forName("com.mysql.jdbc.Driver");,我直接 new 一个不就行了吗, 教材中会直接告诉你不行,原因是为了解耦;最近,我在重温 maven 打包作用域的时,了解到 java包含三种类路径: 编译时类路径、测试时类路径、运行时类路径。

  • 编译时类路径:项目编译时需要引入的路径
  • 测试时类路径:
  • 运行时类路径: 项目运行需要引入的包路径

看似解释了,其实啥也没解释,为了方便理解,下面,我将用代码的方式简单解释一下:

项目中有个如下类:

package com.king.im.client;

public class IMClient {
    public static void main(String[] args)  {
        System.out.println("hello, world!");
    }
}
1
2
3
4
5
6
7

通过如下命令进行编译:

# src\main\java\com\king\im\client\IMClient.java 需要编译的java源文件路径位置
# 编译后类路径生成位置
javac src\main\java\com\king\im\client\IMClient.java -d dist
1
2
3

原文件编译为类文件路径结构.png

从图中我们可以确定由javac编译后产生的类文件路径为源文件的包路径, 因此 dist 目录即为我们源代码的类路径

现在,我们可以通过命令行运行该类文件, 首先需要确保进入类路径中,否则执行会报错

java com.king.im.client.IMClient
1

现在我们进行一些改造,定义一个服务类接口和对应的服务类实现,然后编写如下代码

public interface DemoService {

    void demo();
}
1
2
3
4
public class DemoServiceImpl implements DemoService {

    @Override
    public void demo() {
        System.out.println("this is a demo class impl");
    }
}
1
2
3
4
5
6
7
package com.king.im.client;

public class IMClient {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        Class<?> clz = Class.forName("com.king.im.client.DemoServiceImpl");
        DemoService demoService = (DemoService) clz.newInstance();
        demoService.demo();
        System.out.println("hello, world!");
    }
}
1
2
3
4
5
6
7
8
9
10

进行编译后,会发现报错了。报错内容如下:

PS F:\Project\java\own\mini-project\mini-im-client> javac src\main\java\com\king\im\client\IMClient.java -d dist                                                                                        
src\main\java\com\king\im\client\IMClient.java:6: 错误: 找不到符号
        DemoService demoService = (DemoService) clz.newInstance();
        ^
  符号:   类 DemoService
  位置: 类 IMClient
src\main\java\com\king\im\client\IMClient.java:6: 错误: 找不到符号
        DemoService demoService = (DemoService) clz.newInstance();
                                   ^
  符号:   类 DemoService
  位置: 类 IMClient
2 个错误

1
2
3
4
5
6
7
8
9
10
11
12
13

找不到类错误,看下目录结构:

原文件关联编译报错.png

发现是在同一个包下面的,找不到类,由此可以断点我们源代码没有包含在类路径中,我们在编译时,需要添加编译时类路径的编译项目:

# -cp 指定额外类路径,默认包含了我们在安装java环境时已有的类路径:JAVA_HOME的配置
javac -cp "F:\Project\java\mini-im-client\src\main\java;" src\main\java\com\king\im\client\IMClient.java -d dist
1
2

编译通过!现在让我们立即运行该类文件,发现依然报错,内容如下:

PS F:\Project\java\own\mini-project\mini-im-client\dist> java com.king.im.client.IMClient
Exception in thread "main" java.lang.ClassNotFoundException: com.king.im.client.DemoServiceImpl
        at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:338)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:264)
        at com.king.im.client.IMClient.main(IMClient.java:5)
1
2
3
4
5
6
7
8
9

找不到实现类,从编译后类路径中发现,实现类确实没有打包进来,并且我们代码中也并没有显示的引入该类进来,因此对应的实现类不会打包进我们项目的类路径中。

如果我们原代码中通过 new 方式创建实现类,实现类最终是会包含到源代码类路径的打包结果中。

DemoService demoService = new DemoServiceImpl();

流程走到此处,不知道大家是否注意到,之前教科书上解释说的 “解耦” 是否可以理解了,我们编写驱动接口只是面向接口编程,不注重具体的实现,各个厂商负责 具体实现细节,当我们需要使用某个厂商的实现时,只需要导入厂商提供实现代码,而不需要修改接口的驱动代码,这就是解耦的真正含义。

现在让我们自己当一回“软件厂商”,来实现这么一个服务

我们将 DemoServiceImpl 进行单独的编译,并打包到另外一个类路径中

javac -cp "F:\Project\java\mini-im-client\src\main\java;" src\main\java\com\king\im\client\DemoServiceImpl.java -d dist1
1

然后我们运行时,指定我们的实现类类路径:

java -cp "F:\Project\java\mini-im-client\dist1;" com.king.im.client.IMClient
1

运行成功!现在我们在来回顾一下加载流程:我们在代码中有使用到DemoService,但并没有引入他的实现,而是程序运行时,我们通过添加 加载的运行时类路径 的参数,通过反射方式拿到实现类的实例,然后进行具体操作。

javac 进行编译后的类路径中会包含非当前模块的其他类,如果不想在当前模块中包含其他的类,推荐使用包管理工具进行编译打包,比如常用的maven。