如果小伙伴们在java项目开发过程中遇到引用路径、文件IO等感到头疼,看完本文想必多少会豁然开朗;话不多说直接进入正题。
项目开发过程中,使用路径的使用如果是“绝对路径”,那么项目移植性会很差——今儿我把项目跑在linux,明儿我在window,后天我又把服务器换了个文件夹,一来二去可能自己都改懵掉了。
如果代码中使用相对路径,只要项目目录结构不变,随便把项目丢到哪里跑都不会影响功能。那么我们在使用相对路径的时候需要注意什么问题呢?
搞清楚相对路径,只要搞清楚相对于哪个路径,那么问题都将迎刃而解。
下面几个有关路径的地方可能会让我们头晕:
JAVASE
File file = new File("test.txt");
InputStream is = new FileInputStream("/test.txt");
Thread.currentThread().getContextClassLoader().getResource("");
Thread.currentThread().getContextClassLoader().getResource("/");
Test.class.getResource("");
Test.class.getResource("/");
Test.class.getClassLoader().getResource(""));
Test.class.getClassLoader().getResource("/");
System.getProperty("user.dir");
JAVAEE
request.getContextPath();
<form action="/login">
<img src="/123.jpg">
<a href="123.jpg">
下面解析JAVASE各个路径的原理
File类构造方法参数以"/"开头
则file路径相对于 操作系统根目录 windows下 C:\
File类构造方法参数不以“/”开头
则file路径相对于System.getProperty("user.dir")
InputStream 同File
System.getProperty("user.dir")
启动JVM时所在的目录也就是执行java命令所在路径。Eclipse、IDEA、命令行,启动有所区别,这也就解释了为什么在开发环境中没有出错的路径在生产环境或者部署应用的时候却报错。
Thread.currentThread().getContextClassLoader().getResource("");
Thread.currentThread().getContextClassLoader().getResource("/");
Test.class.getClassLoader().getResource(""));
Test.class.getClassLoader().getResource("/");
ClassLoader.getResource以“/”开头,表示相对于根类加载器加载范围,因为这个类加载器是C++实现的,所以加载范围为null
ClassLoader.getResource不以“/”开头表示相对于当前classpath的根路径
Test.class.getResource("");
Test.class.getResource("/");
Class.getResource以“/”开头表示相对于classpath的根路径
Class.getResource不以“/”开头表示相对于class文件所在路径
关于Thread.currentThread().getContextClassLoader()和Test.class.getClassLoader() 以及 Test.class.getClassLoader().getResource("") 和 Test.class.getResource("")
这里引申出类加载器的相关问题
首先讨论线程上下文加载器与类class的加载器的问题:
java类加载器根据双亲委派机制加载class文件,下面这张图是加载器的层次结构,有关双亲委派机制的详情网上有许多的介绍。
那么Thread得到的contextClassLoader和Class得到的ClassLoader有什么区别呢。
当一个类引用了另一个类,但引用的类却无法使用当前类的加载器去加载时,可以使用contextClassLoader来加载引用的类。
比如jdk中的jdbc API 和具体数据库厂商的实现类SPI的类加载问题。在jdbc API的类是由BootStrap加载的,那么如果在jdbc API需要用到spi的实现类时,根据默认规则2,则实现类也会由BootStrap加载,但是spi实现类却没法由BootStrap加载,只能由Ext或者App加载,如何解决这个问题?于是ContextClassLoader就起作用了。
当DriverManager需要加载SPI中的实现类是,可以获取ContextClassLoader(默认是App),然后用此classLoader来加载spi中的类。很简单的过程。当然不使用ContextClassLoader,自己找个地方把classLoader保存起来,在其他地方能得到此classLoader就可以。Tomcat就是这么做的。比如StandardContext是由Common加载的,而StandardContext要用到项目下的类时怎么办,显然不能用Common来加载,而只能用WebAppClassLoader来加载,怎么办?当然可以采用ContextClassLoader的方式来解决,但是tomcat不是这样解决的,而是为每个App创建一个Loader,里面保存着此WebApp的ClassLoader。需要加载WebApp下的类时,就取出ClassLoader来使用,原理和ContextClassLoader是一样的。至于tomcat为什么要这么做呢?因为tomcat中有关类加载的问题,是由一个main线程来处理的,而并没有为每个WebApp单独创建一个线程,故没办法用ContextClassLoader的方式来解决,而是用自己的属性来解决。
如果感兴趣可以阅读《深入理解JVM虚拟机》从而更深的理解类加载过程
我们再来讨论一下ClassLoader().getResource("") 和 Class.getResource("")的区别:
分析一下源码:
Test.class.getResource("");
//以下为class.getResource()源码
public java.net.URL getResource(String name) {
name = resolveName(name);
ClassLoader cl = getClassLoader0();// 获取加载该Class的ClassLoader,sun.misc.Launcher$AppClassLoader@18b4aac2
if (cl==null) { //如果加载该Class的ClassLoader为null,则表示这是一个系统class
// A system class.
return ClassLoader.getSystemResource(name); //如果是系统class
}
return cl.getResource(name);//调用ClassLoader的getResource方法
}
resolveName 方法
private String resolveName(String name) {
if (name == null) {
return name;
}
if (!name.startsWith("/")) { //对于不以/开头的文件,
Class<?> c = this; //获取当前加载类的完整的类路径,我这里是com.zsk.java.TestClassLoader
while (c.isArray()) {
c = c.getComponentType();
}
String baseName = c.getName();
int index = baseName.lastIndexOf('.');//找到文件的包名称
if (index != -1) {
name = baseName.substring(0, index).replace('.', '/')
+"/"+name;//将包名称中的.替换为/ 并在最后加上/ 文件名
}
} else {
name = name.substring(1); //对于/开头的文件名,会只保留文件名称部分。
}
return name;
}
下面是ClassLoader的getResource方法
public URL getResource(String name) {
URL url;
if (parent != null) {//这里的parent为sun.misc.Launcher$ExtClassLoader@7d4793a8
url = parent.getResource(name);//这里是一个递归调用,再次进入之后parent为null
} else {
url = getBootstrapResource(name);//到达系统启动类加载器
}
if (url == null) {//系统启动类加载器没有加载到,递归回退到第一次调用然后是扩展类加载器
url = findResource(name);
}
return url;//最后如果都没有加载到,双亲委派加载失败,则加载应用本身自己的加载器。
}
可以发现,其实,Class.getResource最终调用的是ClassLoader 类的getResource方法。只不过Class.getResource先调用方法resolveName方法将路径处理成为ClassLoader.getResource方法合适的输入参数,再去调用ClassLoader 类的getResource。所以Class.getResource("/")实质与ClassLoader.getResource("")是一样的。
讲到现在JAVASE的路径其实已经差不多都可以理解了。当我们使用的时候例如new File();构造参数可以传入以classpath为根目录的绝对路径;比如 new File( Test.class.getResource("/").getPath()+"123.txt"));这样保证文件是与项目是关联的而不是与操作系统相关联的。
现在我们来说javaWeb相关路径问题
前台路径:参照路径是web服务器的根路径即http://localhost:8080/将前台路径转化为绝对路径,是由浏览器自动完成的。
例如<img src="/123.jpg"> 表示localhost:8080/123.jpg
<from action="/abc"> 表示localhost:8080/abc
后台路径:后台路径的参考路径是web应用的根路径,如http://localhost:8080/webAppName/(项目工程名字)。将后台路径转化为绝对路径的工作由服务器完成。
例如springmvc viewResolver 的前缀 /WEB-INF/JSP/表示webapp/webAppName/WEB-INF/JSP/下的资源
这里要说明一下转发与重定向的问题
redirect:/abc 重定向后 浏览器访问 localhost:8080/abc
redirect:abc 重定向后 浏览器访问url同级的abc url路径
forward:/abc 转发后 服务器转发至 localhost:8080/webAppName/abc
forward:abc 转发后 服务器转发至url同级的abc url路径
总结:页面中的url可以理解为客户端的请求行为,以“/”开头, 路径相对于服务器地址: 协议+IP+端口
不以“/”开头,路径相对于当前浏览器url。所以JSP页面可以将路径前加上basePath,来保证每次页面的请求的资源都是相对于当前项目的根路径。
<% String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
服务端扫描文件时 以”/“开头,路径相对于tomcat中webapp内项目根目录
服务端发送请求时 以“/”开头,路径相对于项目请求根路径
到这里已经总结的差不多了。由于笔者水平有限,更何况java博大精深,难免有些错误,如有问题可以在评论区提出来,我会好好研究以下~嘻嘻。 如果您觉得整理的不错,请支持一下我这位初出茅庐的javaer,点个赞就可以喽。