跳至主要內容

获取类路径

Mr.Hope...大约 10 分钟

获取类路径

已经完成了JAVA应用程序如何启动:命令行启动,并获取到了启动时需要的选项和参数。

但是,如果要启动一个最简单的“Hello World”程序(如下),也需要加载很多所需的类进入JVM

public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, world!");
  }
}

加载HelloWorld类之前,需要加载该类的父类(超类),也就是java.lang.Object,main函数的参数为String[]类型,因此也需要将java.lang.String类和java.lang.String[]加载,输出字符串又需要加载java.lang.System类,等等。接下来就来解决如何获取这些类的路径。

类路径介绍

Java虚拟机规范并没有规定虚拟机应该从哪里寻找类,因此不同的虚拟机实现可以采用不同的方法。

Oracle的Java虚拟机实现根据类路径(class path)来搜索类。

按照搜索的先后顺序,类路径可以 分为以下3个部分:

  • 启动类路径(bootstrap classpath)
  • 扩展类路径(extension classpath)
  • 用户类路径(user classpath)

启动类路径默认对应jre\lib目录,Java标准库(大部分在rt.jar里) 位于该路径。

扩展类路径默认对应jre\lib\ext目录,使用Java扩展机制的类位于这个路径。

用户类路径为自己实现的类,以及第三方类库的路径。可以通过-Xbootclasspath选项修改启动类路径,不过一般不需要这样做。 用户类路径的默认值是当前目录,也就是. 。可以设置 CLASSPATH环境变量来修改用户类路径,但是这样做不够灵活,所以不推荐使用。 更好的办法是给java命令传递-classpath(或简写为-cp)选项。-classpath/-cp选项的优先级更高,可以覆盖CLASSPATH环境变量设置。如下:

java -cp path\to\classes ...
java -cp path\to\lib1.jar ...
java -cp path\to\lib2.zip ...

解析用户类路径

该功能建立在命令行工具上,因此复制上次的代码,并创建classpath子目录。

Java虚拟机将使用JDK的启动类路径来寻找和加载Java 标准库中的类,因此需要某种方式指定jre目录的位置。

命令行选项可以获取,所以增加一个非标准选项-Xjre。

修改Cmd结构体,添加XjreOption字段;parseCmd()函数也要相应修改:

type Cmd struct {
	// 标注是否为 --help
	helpFlag bool
	//标注是否为 --version
	versionFlag bool
	//选项
	cpOption string
	//主类名,或者是jar文件
	class string
	//参数
	args []string
	// jre路径
	XjreOption string
}

func parseCmd() *Cmd {
	cmd := &Cmd{}

	flag.Usage = printUsage
	flag.BoolVar(&cmd.helpFlag, "help", false, "print help message")
	flag.BoolVar(&cmd.helpFlag, "?", false, "print help message")
	flag.BoolVar(&cmd.versionFlag, "version", false, "print version and exit")
	flag.StringVar(&cmd.cpOption, "classpath", "", "classpath")
	flag.StringVar(&cmd.cpOption, "cp", "", "classpath")
	flag.StringVar(&cmd.XjreOption, "Xjre", "", "path to jre")
	flag.Parse()

	args := flag.Args()
	if len(args) > 0 {
		//第一个参数是主类名
		cmd.class = args[0]
		cmd.args = args[1:]
	}

	return cmd
}

获取用户类路径

可以把类路径想象成一个大的整体,它由启动类路径、扩展类路径和用户类路径三个小路径构成。

三个小路径又分别由更小的路径构成。是不是很像组合模式(composite pattern)

接下来将使用组合模式open in new window来设计和实现类路径。

1.Entry接口

定义一个Entry接口,作为所有类的基准。

package classpath

import "os"

// :(linux/unix) or ;(windows)
const pathListSeparator = string(os.PathListSeparator)

type Entry interface {
    // className: fully/qualified/ClassName.class
    readClass(classpath string) ([]byte, Entry, error)
    String() string
}

常量pathListSeparator是string类型,存放路径分隔符,后面会用到。

Entry接口中有个两方法。

  1. **readClass()方法:**负责寻找和加载class 文件。 参数是class文件的相对路径,路径之间用斜线/分隔,文件名有.class后缀。比如要读取java.lang.Object类,传 入的参数应该是java/lang/Object.class。返回值是读取到的字节数据、最终定位到class文件的Entry,以及错误信息。

  2. **String()方法:**作用相当于Java中的toString(),用于返回变量 的字符串表示。

Go的函数或方法允许返回多个值,按照惯例,可以使用最后一个返回值作为错误信息。

还需要一个类似于JAVA构造函数的函数,但在Go语言中没有构造函数的概念,对象的创建通常交由一个全局的创建函数来完成,以NewXXX来命令,表示"构造函数"

newEntry()函数根据参数创建不同类型的Entry实例,代码如下:

func newEntry(path string) Entry {
	////如果路径包含分隔符 表示有多个文件 
	if strings.Contains(path, pathListSeparator) {
		return newCompositeEntry(path)
	}
	//包含*,则说明要将相应目录下的所有class文件加载
	if strings.HasSuffix(path, "*") {
		return newWildcardEntry(path)
	}
	//包含.jar,则说明是jar文件,通过zip方式加载
	if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") ||
		strings.HasSuffix(path, ".zip") || strings.HasSuffix(path, ".ZIP") {

		return newZipEntry(path)
	}

	return newDirEntry(path)
}

2.实现类

存在四种类路径指定方式:

  • 普通路径形式:gyb/gyb
  • jar/zip形式:/gyb/gyb.jar
  • 通配符形式:gyb/*
  • 多个路径形式:gyb/1:/gyb/2
DirEntry(普通形式)

创建entry_dir.go,定义DirEntry结构体:

package classpath

import "io/ioutil"
import "path/filepath"

type DirEntry struct {
	absDir string
}

func newDirEntry(path string) *DirEntry {
	//转化为绝对路径
	absDir, err := filepath.Abs(path)
	if err != nil {
		panic(err)
	}
	return &DirEntry{absDir}
}

func (self *DirEntry) readClass(className string) ([]byte, Entry, error) {
	//拼接类文件目录 和 类文件名  
	// '/gyb/xxx/' + 'helloworld.class' = '/gyb/xxx/helloworld.class'
	fileName := filepath.Join(self.absDir, className)
	data, err := ioutil.ReadFile(fileName)
	return data, self, err
}

func (self *DirEntry) String() string {
	return self.absDir
}

DirEntry只有一个字段,用于存放目录的绝对路径。

和Java语言不同,Go结构体不需要显示实现接口,只要方法匹配即可。

ZipEntry(jar/zip形式)
package classpath

import "archive/zip"
import "errors"
import "io/ioutil"
import "path/filepath"

type ZipEntry struct {
	absPath string
}

func newZipEntry(path string) *ZipEntry {
	absPath, err := filepath.Abs(path)
	if err != nil {
		panic(err)
	}
	return &ZipEntry{absPath}
}

func (self *ZipEntry) readClass(className string) ([]byte, Entry, error) {
	r, err := zip.OpenReader(self.absPath)
	if err != nil {
		return nil, nil, err
	}

	defer r.Close()
	for _, f := range r.File {
		if f.Name == className {
			rc, err := f.Open()
			if err != nil {
				return nil, nil, err
			}

			defer rc.Close()
			data, err := ioutil.ReadAll(rc)
			if err != nil {
				return nil, nil, err
			}

			return data, self, nil
		}
	}

	return nil, nil, errors.New("class not found: " + className)
}

func (self *ZipEntry) String() string {
	return self.absPath
}

首先打开ZIP文件,如果这一步出错的话,直接返回。然后遍历 ZIP压缩包里的文件,看能否找到class文件。如果能找到,则打开 class文件,把内容读取出来,并返回。如果找不到,或者出现其他错 误,则返回错误信息。有两处使用了defer语句来确保打开的文件得 以关闭。

CompositeEntry(多路径形式)

CompositeEntry由更小的Entry组成,正好可以表示成[]Entry。

在Go语言中,数组属于比较低层的数据结构,很少直接使用。大部分情况下,使用更便利的slice类型。

构造函数把参数(路径列表)按分隔符分成小路径,然后把每个小路径都转换成具体的 Entry实例。

package classpath

import "errors"
import "strings"

type CompositeEntry []Entry

func newCompositeEntry(pathList string) CompositeEntry {
	compositeEntry := []Entry{}

	for _, path := range strings.Split(pathList, pathListSeparator) {
		//去判断 path 属于哪其他三种哪一种情况 生成对应的 ClassDirEntry类目录对象
		entry := newEntry(path)
		compositeEntry = append(compositeEntry, entry)
	}

	return compositeEntry
}

func (self CompositeEntry) readClass(className string) ([]byte, Entry, error) {
	//遍历切片 中的 类目录对象
	for _, entry := range self {
		//如果找到了 对应的 类 直接返回
		data, from, err := entry.readClass(className)
		if err == nil {
			return data, from, nil
		}

	}
	//没找到 返回错误
	return nil, nil, errors.New("class not found: " + className)
}

func (self CompositeEntry) String() string {
	strs := make([]string, len(self))

	for i, entry := range self {
		strs[i] = entry.String()
	}

	return strings.Join(strs, pathListSeparator)
}
WildcardEntry(通配符形式)

WildcardEntry实际上也是CompositeEntry,所以就不再定义新的类型了。

首先把路径末尾的星号去掉,得到baseDir,然后调用filepath包的Walk()函数遍历baseDir创建ZipEntryWalk()函数的第二个参数 也是一个函数。

walkFn中,根据后缀名选出JAR文件,并且返回SkipDir跳过子目录(通配符类路径不能递归匹配子目录下的JAR文件)。

package classpath

import "os"
import "path/filepath"
import "strings"

func newWildcardEntry(path string) CompositeEntry {
	//截取通用匹配符 /gyb/* 截取掉 *
	baseDir := path[:len(path)-1] // remove *
	//多个 类目录对象
	compositeEntry := []Entry{}

	walkFn := func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		//如果为空
		if info.IsDir() && path != baseDir {
			return filepath.SkipDir
		}
		//如果是 .jar  或者 .JAR 结尾的文件
		if strings.HasSuffix(path, ".jar") || strings.HasSuffix(path, ".JAR") {
			jarEntry := newZipEntry(path)
			compositeEntry = append(compositeEntry, jarEntry)
		}
		return nil
	}
	//遍历 目录下所有 .jar .JAR 文件 生成ZipEntry目录对象 放在切片中返回
	//walFn为函数
	filepath.Walk(baseDir, walkFn)

	return compositeEntry
}

实现类目录

前面提到了 java 虚拟机默认 会先从启动路径--->扩展类路径 --->用户类路径 按顺序依次去寻找,加载类。 那么就会有3个类目录对象,所以就要定义一个结构体去存放它。

type Classpath struct {
	BootClasspath Entry
	ExtClasspath  Entry
	UserClasspath Entry
}

启动类路径

启动路径,其实对应Jre目录下``lib` 也就是运行java 程序必须可少的基本运行库。

通过 -Xjre 指定 如果不指定 会在当前路径下寻找jre 如果找不到 就会从我们在装java是配置的JAVA_HOME环境变量 中去寻找。

所以获取验证环境变量的方法如下:

func getJreDir(jreOption string) string {
	//如果 从cmd  -Xjre 获取到目录 并且存在
	if jreOption != "" && exists(jreOption) {
	//返回目录
		return jreOption 
	}
	//如果 当前路径下 有 jre 返回目录
	if exists("./jre") { 
		return "./jre"
	}
	//如果 上面都找不到 到系统环境 变量中寻找 
	if jh := os.Getenv("JAVA_HOME"); jh != "" {
		//存在 就返回
		return filepath.Join(jh, "jre") 
	}
	//都找不到 就报错
	panic("Can not find jre folder!") 
}
//判断 目录是否存在
func exists(path string) bool {
	if _, err := os.Stat(path); err != nil {
	if os.IsNotExist(err) { return false
} }
return true }

扩展类路径

扩展类 路径一般 在启动路径 的子目录下 jre/lib/ext

func (self *Classpath) parseBootAndExtClasspath(jreOption string) {
	jreDir := getJreDir(jreOption)

	// 拼接成jre 的路径
	// jre/lib/*
	jreLibPath := filepath.Join(jreDir, "lib", "*")
	//加载 所有底下的 jar包
	self.BootClasspath = newWildcardEntry(jreLibPath)

	// 拼接 扩展类 的路径
	// jre/lib/ext/*
	jreExtPath := filepath.Join(jreDir, "lib", "ext", "*")
	//加载 所有底下的jar包
	self.ExtClasspath = newWildcardEntry(jreExtPath)
}

用户类路径

用户类路径通过前面提到的 -classpath 来指定 ,如果没有指定 就默认为当前路径就好

func (self *Classpath) parseUserClasspath(cpOption string) {
	//如果没有指定
	if cpOption == "" {
	// . 作为当前路径
	cpOption = "." 
	}
	//创建 类目录对象
	self.UserClasspath = newEntry(cpOption)
}

实现类的加载

对于指定文件类名取查找 我们是按前面提到的(启动路径--->扩展类路径 --->用户类路径 按顺序依次去寻找,加载类),没找到就挨个查找下去。

如果用户没有提供-classpath/-cp选项,则使用当前目录作为用 户类路径。ReadClass()方法依次从启动类路径、扩展类路径和用户 类路径中搜索class文件,

//根据类名 分别从 bootClasspath,extClasspath,userClasspath 依次加载类目录
func (self *Classpath) ReadClass(className string) ([]byte, ClassDirEntry, error) {
	className = className + ".class"
	if data, entry, err := self.BootClasspath.readClass(className); err == nil{ return data, entry, err }
	if data, entry, err := self.ExtClasspath.readClass(className); err == nil { return data, entry, err }
	return self.UserClasspath.readClass(className)
}

初始化类加载目录

定义一个初始化函数,来作为初始函数,执行后生成一个 Classpath对象。

//jreOption 为启动类目录  cpOption 为 用户指定类目录 从cmd 命令行 中解析获取
func InitClassPath(jreOption, cpOption string) *Classpath {
	cp := &Classpath{}
	//初始化 启动类目录
	cp.parseBootAndExtClasspath(jreOption)
	//初始化 用户类目录
	cp.parseUserClasspath(cpOption)
	return cp
}

注意,传递给ReadClass()方法的类名不包含“.class”后缀。

总结

测试

成功获取到class文件!