发生场景&问题描述:
最近使用SpringBoot开发一个项目,项目的前端部分采用的不是前后端分离的模式,而是打成一个jar包,最后通过 java -jar 的形式运行的。 前端的项目,在idea开发的时候,启动起来访问页面切正常,但是当我打成jar包后,通过 java -jar形式启动的时候,访问页面发现好多资源404,F12跟踪发现404的问题,是因为资源文件大小写造成的。 比如:index.html 页面引用的css文件,写的是APP.css ,但是我们的css文件名称是app.css。疑惑: 我们开发的时候,通过idea启动服务,不论你是大写还是小写,在浏览器访问都不会有404的问题,都可以访问到。但是一旦通过 java -jar启动就会出现这个问题。
原因分析:
发生这个问题后,虽然很好解决,大家遵守规范,大小写注意就可以了。但是让我产生很大兴趣,想研究一下到底是什么原因导致了 idea启动没问题,打成jar包启动就有问题了呢?
这两中启动方式,有什么不同,才导致了这个问题??
下面我就按照我的思路一步一步去探索:
1、Java -jar 与IDEA启动SpringBoot的不同
java命令启动
这个不用多说,最简单的一个命令,java -jar xxx.jar 直接启动
idea启动
这个只需要看一下控制台就行了,就能看出idea是通过什么命令启动的SpringBoot
/Library/Java/JavaVirtualMachines/jdk1.8.0_45.jdk/Contents/Home/bin/java
-XX:TieredStopAtLevel=1
-noverify
-Dspring.output.ansi.enabled=always
-Dcom.sun.management.jmxremote
-Dspring.jmx.enabled=true
-Dspring.liveBeansView.mbeanDomain
-Dspring.application.admin.enabled=true
-Dfile.encoding=UTF-8
-classpath ...(这里是依赖的jar包省略)
com.test.T
有什么不同?
一种是直接指定jar文件启动运行,idea是直接指定了class文件运行。
java -jar运行,需要打的 jar 包的 META-INF/MANIFEST.MF 文件里指定的 Main-Class
接下来,解压我们运行的jar包看一下,这个Main-Class
找到META-INF/MANIFEST.MF打开
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: testjar
Implementation-Version: 1.0-SNAPSHOT
Start-Class: com.test.T
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.2.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher
可以看到,我们运行的其实是org.springframework.boot.loader.JarLauncher类的main函数。
这里引出了一个问题,我们在开发SpringBoot项目的时候,为能打出可以直接 java -jar 运行的jar包通常都会在pom文件中添加一个springboot提供的一个maven插件spring-boot-maven-plugin
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.test.T</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
这个插件的作用会将依赖的jar包全部打包进去,并且将程序的运行入口修改为一个org.springframework.boot.loader.JarLauncher类的main函数。
2、总结一下
到现在为止,好像这两种启动方式的差别就是:一个直接运行的我们自己程序的main函数,一个是运行的SpringBoot打包后的一个JarLauncher类的man函数。
那我们记下来就去分析一个JarLauncher这个函数是干嘛的?
3、继续分析 JarLauncher
大家可以自行去深入研究一下,SpringBoot的JarLauncher类。
这里总的来说,一下是JarLauncher类的大概流程:
1、注册一个自定义URL的jar协议
2、创建指定的类加载器LaunchedURLClassLoader
3、获取Start-Class属性对应的类
4、利用反射调用Start-Class,执行main方法
4、看一下代码
有一个判断方法isExploded(),这个是判断当前是否是展开状态?
如果不是展开状态,也就是jar的状态,会执行JarFile.registerUrlProtocolHandler();
会注册一个自定义URL的jar协议。
如果是jar被解压成文件夹,就不会注册。
这个是什么意思呢?
意思就是:如果你执行 java -jar xxx.jar ,isExploded()就是 false。
但是如果,你把jar包解压开,然后通过 java org.springframework.boot.loader.JarLauncher 命令来启动的话,isExploded()就是 true。
通过这里我们可以看出来,直接执行java -jar 执行jar 和执行java xxx 一个class文件还是有区别的。
那么注册的这个UrlProtocol是做什么的?
5、JarFile.registerUrlProtocolHandler();
SpringBoot定义的JarFile,处理“jar:”这样的协议,处理jar in jar 以及加载其他资源。也就是处理
jar包里面还有jar包的情况。因为,springboot打成的jar里面包括程序所依赖的所有jar,那么这个jar运行的时候,就需要把里面包括的所有三方jar包都加载进来。默认java.util.jar.JarFile是不会处理,jar里面的jar文件的,所以springboot就在java的基础上进行了扩展,使得可以支持jar in jar。
6、ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
JarLauncher做的另外一件事情,就是创建了一个classLoader,把所需要的class加载进来。这里跟进去看代码,就是重写定了查找class资源的方式。
7、最后就是通过反射调用了我们写的main方法
这里反射调用没什么好说的,代码比较简单
8、总结一下,思考问题可能在哪里
同上面几个大步骤的分析,发现一个问题,直接通过 java -jar 的方式启动jar包和 通过java mainClass类的方式启动,还是有一些区别的。
而idea就是通过 java mainClass方式启动的。
那问题是不是出在这里?
我们试一下就知道了,我把之前 java -jar 启动的jar包解压,然后进入解压目录,执行
java org.springframework.boot.loader.JarLauncher
服务还是能够正常启动。
这时候我们去访问一下服务,看看里面的静态文件是不是对大小写敏感。
通过验证,发现这时候,和idea里启动的效果是一样的,不论是大写还是小写,都可以访问到不会出现404的情况。
9、缩小了问题范围,继续下一步
同上面验证的验证结果,看出来,貌似是与是否打成jar包启动的形式有关系。
那么我就思考一下,jar启动和mainclass启动有什么不同吗?
jar启动,所有class和静态资源都在jar包里,是一个压缩文件。
以mainClass文件启动,class文件和静态资源都在系统文件夹里。
这里也就是 JarLauncher里面通过isExploded()进行判断的。
10、jar包中的资源和系统中的资源
两个方式其实就是对读取文件的路径的不同,idea启动或者将jar解压后,执行 java mainClass方式的启动,这个时候访问的静态资源文件路径是一个操作系统的路径,windows和mac 系统里的文件名字是不区分大小写的,所有访问的时候都可以取得的到,不会404。
但是,一旦你把静态资源打成jar包后,再访问,那就是一个jar压缩包文件中的资源,jar压缩包里面的资源文件,读取的时候,是对文件名大小写敏感的,所以如果大小写不一致的情况下,会出现文件找到不到,从而springboot会抛出404 的错误。
11、 还有一种方式验证
如果上面的分析还不够的话,我们还可以通过一种方式验证。
那就是你的SpringBoot配置一个指向操作系统路径,存放静态文件。
这样的话,springBoot的静态资源访问路径包括两个:一个是classpath:/static 这个是jar里面的文件
一个是file:/xxxxx 这个就是操作系统的一个路径
通过这样的配置后,你再通过 java -jar 的方式启动,然后分别测试 static下面的文件(也就是在jar中的),访问的时候,大小写是敏感的。而你访问,配置的操作系统的路径下面的文件是大小写不敏感的(我说的是window下和mac默认情况下,Linux不清楚,没测试)
12、 最后做个总结
所以说,打成jar的时候,如果你的静态资源都打包在jar包里,那么就需要注意大小写的问题,如果你的静态资源是在jar外部,那大小写是否敏感就的看你运行的操作系统了。