获取类路径
获取类路径
已经完成了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)
?
接下来将使用组合模式来设计和实现类路径。
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接口中有个两方法。
**readClass()方法:**负责寻找和加载class 文件。 参数是class文件的相对路径,路径之间用斜线
/
分隔,文件名有.class
后缀。比如要读取java.lang.Object
类,传 入的参数应该是java/lang/Object.class
。返回值是读取到的字节数据、最终定位到class文件的Entry,以及错误信息。**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创建ZipEntry
。Walk()
函数的第二个参数 也是一个函数。
在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文件!
