SpringBoot 中使用 jsp 的坑
坑 1: tomcat-embed-jasper 包依赖
外置容器(Tomcat)
内嵌容器(Tomcat)
坑 2: Jsp 文件放哪?
坑点 3: 使用 jar 包方式运行 又访问不到 jsp
那如果 jsp 放在依赖的 jar 中怎么办
另外
背景说明:
SpringBoot1.5+jsp+tomcat 的管理后台项目
坑 1: tomcat-embed-jasper 包依赖
SpringMVC 中 jsp 请求流程:
servlet 容器收到请求, 分发到 SpringMVC 的 DispatcherServlet.
SpringMVC 经过处理, 返回 jsp 视图名称, 随后通过 InternalResourceViewResolver 解析得到 InternalResourceView
InternalResourceView 通过 forward 方式服务器内部跳转
servlet 容器再次收到请求, 由于本次请求中 url 中带有. jsp 后缀, 所以分发给 JspServlet 处理
JspServlet 在第一次被调用时使用 jsp 引擎解析 jsp 文件, 并生成 servlet, 并注册, 随后调用
SpringMVC 视图解析原理看这 https://blog.csdn.net/zmx729618/article/details/51554762
坑就坑在第 4 步中
现象:
当 InternalResourceView 进行 forward 之后, 请求又进入到了 SpringMVC 的 DispatcherServlet 中
原因:
JspServlet 没有被注册到 Servlet 容器中, 所以请求分发到 DispatcherServlet 来处理
原因是很简单, 但是之前对 Jsp 处理流程不熟的我还是想了半天. 甚至萌生手动解析 jsp 文件的想法 #-_-
解决方案:
添加下面这个包的依赖
org.apache.tomcat.embed
tomcat-embed-jasper
有人会奇怪之前使用 SpringMVC(非 SpringBoot)的时候不用管这些的啊?(我也是 *-*)
下面来细说
外置容器(Tomcat)
其实使用外置 Tomcat 的时候我们是不需要添加上面这个包的依赖的
因为这个包已经在 TOMCAT_HOME/lib 中引入, 同时 JspServet 也在 TOMCAT_HOME/Conf/web.xml(全局配置)被注册
jsp
org.apache.jasper.servlet.JspServlet
fork
false
xpoweredBy
false
3
jsp
*.jsp
*.jspx
所以当我们使用外置 Tomcat 的时候压根不用管这些.
然而到了内嵌 Tomcat 时就不太一样了
内嵌容器(Tomcat)
首先 tomcat-embed-jasper 包是独立出来的, 需要我们单独引入
内嵌 Tomcat 默认不注册 JspServet
这回都清楚了.
还有一点, 在 SpringBoot 中我们除了添加依赖也没注册 JspServlet 啊?
因为 SpringBoot 帮我们注册了//tomcat 启动准备
protectedvoidprepareContext(Hosthost,ServletContextInitializer[]initializers){
FiledocBase=getValidDocumentRoot();
docBase=(docBase!=null?docBase:createTempDir("tomcat-docbase"));
finalTomcatEmbeddedContextcontext=newTomcatEmbeddedContext();
...
// 是否 Classpath 中有 org.apache.jasper.servlet.JspServlet 这个类
// 有就注册
if(shouldRegisterJspServlet()){
addJspServlet(context);
addJasperInitializer(context);
context.addLifecycleListener(newStoreMergedWebXmlListener());
}
}
这里说一句, SpringBoot 真是好东西. 原先使用 Spring, 只会照着样子用. 现在可好, 用了 SpringBoot 逼着我去搞清楚这些原理, 要不然压根驾驭不了这货 #-_-
坑 2: Jsp 文件放哪?
当解决了坑 1 之后, 满心欢喜以为都 ok, 结果发现 SpringBoot 压根没 WEB-INF 目录
那我的 Jsp 文件放哪? 随便放可以吗?
抱着试一试的态度, 在 resources 下面建了个 WEB-INF, 希望 SpringBoot 能和我心有灵犀
结果我失败了...
简单推断一下: 肯定是 JspServlet 找不到我的 Jsp 的文件, 那么它是怎么寻找 Jsp 文件的呢?
打个断点跟踪一下#org.apache.jasper.servlet.JspServlet
// 被 JspServlet.service()调用
private void serviceJspFile(HttpServletRequest request,
HttpServletResponse response, String jspUri,booleanprecompile)
throwsServletException,IOException{
// 从缓存中取出 jsp->servlet 对象
JspServletWrapperwrapper=rctxt.getWrapper(jspUri);
if(wrapper==null){
synchronized(this){
// 双重校验
wrapper=rctxt.getWrapper(jspUri);
if(wrapper==null){
// 判断 jsp 文件是否存在
if(null==context.getResource(jspUri)){
handleMissingResource(request,response,jspUri);
return;
}
wrapper=newJspServletWrapper(config,options,jspUri,
rctxt);
rctxt.addWrapper(jspUri,wrapper);
}
}
}
try{
// 使用 Jsp 引擎解析得到的 Servlet
wrapper.service(request,response,precompile);
}catch(FileNotFoundExceptionfnfe){
handleMissingResource(request,response,jspUri);
}
}
一路跟着 context.getResource(jspUri)最终进到 StandardRoot#getResourceInternal 方法中#org.apache.catalina.webresources.StandardRoot
{// 构造代码块
allResources.add(preResources);
allResources.add(mainResources);
allResources.add(classResources);
allResources.add(jarResources);
allResources.add(postResources);
}
protectedfinalWebResourcegetResourceInternal(Stringpath,
booleanuseClassLoaderResources){
...
// 遍历
for(Listlist:allResources){
for(WebResourceSetwebResourceSet:list){
if(!useClassLoaderResources&&!webResourceSet.getClassLoaderOnly()||
useClassLoaderResources&&!webResourceSet.getStaticOnly()){
result=webResourceSet.getResource(path);
if(result.exists()){
returnresult;
}
...
}
}
}
...
}
我们调用一下看 allResources 都包含哪些对象
可以看到 allResource 中只有一个 DirResourceSet, 而且是一个临时目录(里面啥文件也没有)
理所当然 JspServlet 找不到我们的 jsp 文件
基于这个想法, 我们只要手动添加一个 ResourceSet 到 allResources, 是不是就可以了@Bean
publicCustomTomcatEmbeddedServletContainerFactorycustomTomcatEmbeddedServletContainerFactory(){
returnnewCustomTomcatEmbeddedServletContainerFactory();
}
publicstaticclassCustomTomcatEmbeddedServletContainerFactoryextendsTomcatEmbeddedServletContainerFactory{
// 在 prepareContext 中被调用
@Override
protectedvoidpostProcessContext(Contextcontext){
super.postProcessContext(context);
// 添加监听器
context.addLifecycleListener(newLifecycleListener(){
@Override
publicvoidlifecycleEvent(LifecycleEventevent){
if(event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)){
try{
//!!! 资源所在 url
URL url=ResourceUtils.getURL(ResourceUtils.CLASSPATH_URL_PREFIX);
//!!! 资源搜索路径
Stringpath="/";
// 手动创建一个 ResourceSet
context.getResources().createWebResourceSet(
WebResourceRoot.ResourceSetType.RESOURCE_JAR,"/",url,path);
}catch(Exceptione){
e.printStackTrace();
}
}
}
});
}
}
由于是在 Idea 中直接运行, 所以 base 是在 target/classes 目录下
再尝试访问以下, 果真可以访问到了
结论:
内嵌 tomcat 中, 需要我们手动注册资源搜索路径
坑点 3: 使用 jar 包方式运行 又访问不到 jsp
这回有点奇怪了, 使用 idea 直接运行都没问题 , 可是打成 jar 包后运行却又不行了
查看了一下日志, 发现报错了Causedby:org.apache.catalina.LifecycleException:Failedto initialize component[org.apache.catalina.webresources.JarWarResourceSet@59119757]
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:112)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:140)
at org.apache.catalina.webresources.JarWarResourceSet.(JarWarResourceSet.java:76)
...12more
Causedby:java.lang.NullPointerException:entry
at java.util.zip.ZipFile.getInputStream(ZipFile.java:346)
at java.util.jar.JarFile.getInputStream(JarFile.java:447)
at org.apache.catalina.webresources.JarWarResourceSet.initInternal(JarWarResourceSet.java:173)
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:107)
...14more
debug 跟踪了一下 发现取到的 url 是
jar:file:/Users/mic/IdeaProjects/mobileHall/mobileHall-start/target/mobileHall-start-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/
看着很奇怪 不太像正常的 Url 按正常的 Url 表示 应该是这样的
file:/Users/mic/IdeaProjects/mobileHall/mobileHall-start/target/mobileHall-start-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes
推测是 springboot 打包 (简称 springboot-jar) 后路径变化导致的(我是查了好久才知道的 #_#)
假设目标文件路径为: 项目根路径 / resource/a.jsp
1.idea 中(以 classpath 关联)url=file:/Users/mic/IdeaProjects/mobileHall/mobileHall-start/target/classes/(资源所在Url)
path=/(资源搜索路径)
2. 普通 jarurl=jar:file:/Users/mic/IdeaProjects/mobileHall/mobileHall-start/target/mobileHall-start-0.0.1-SNAPSHOT.jar
path=/BOOT-INF/classes
3.springboot-jar
url=jar:file:/Users/mic/IdeaProjects/mobileHall/mobileHall-start/target/mobileHall-start-0.0.1-SNAPSHOT.jar!/BOOT-INF/classes!/
path=/
可以看到 springboot-jar 中获取的 Url 很特殊, 不是一个标准 Url
思考:
SpringBoot-jar 的 url 为何不是个标准 Url
如何通过变种 Url 来进行资源定位(资源读取)
结论:
特殊 jar 包格式(jarInjar),SpringBoot 打包 jar 不是标准 jar 包结构(把依赖 lib 也以 jar 的形式打进去了)
变种 Url, 为了满足自身特殊的打包格式进行资源定位(资源读取), 定义了一套变种 Url
URLStreamHandler 实现, 通过实现 URLStreamHandler 来满足 url.openConnection()获取资源方法. 同时还继承了 JarFile (Url 根据 Protocol 找到不同 URLStreamHandler 实现来进行资源定位)
详细分析请看这 https://blog.csdn.net/hengyunabc/article/details/50120001
再来看 java 项目常见的打包格式一般就为两种
jar, 依赖不会以 jar 包方式打入 jar, 要么以外部依赖的方式通过 - classpath 关联, 要么将源码合并打入 jar 中
war, 其实就是个压缩包, 解压后就是一个项目自身的 jar 和外部依赖的 jar
可以看到 SpringBoot-jar 和 war 有点像. 而 Tomcat 支持 war 不解压运行, 那么想必应该支持 jarInjar 的读取方式
再回到 Tomcat 的资源搜索来
Tomcat 支持一下两种方式添加资源搜索路径#org.apache.catalina.WebResourceRoot
// 方法 1. 拆分 Url 为 base,archivePath 调用方法 2
voidcreateWebResourceSet(ResourceSetTypetype,StringwebAppMount,URL url,
StringinternalPath);
// 方法 2
/**
* 添加一个 ResourceSet(资源集合)到 Tomcat 的资源搜索路径中
* @param type 资源类型(jar,file 等)
* @param webAppMount 挂载点
* @param base 资源路径
* @param archivePath jar 中 jar 相对路径
* @param internalPath jar 中 jar 中 resource 的相对路径
*/
voidcreateWebResourceSet(ResourceSetTypetype,StringwebAppMount,Stringbase,StringarchivePath,StringinternalPath);
#org.apache.catalina.webresources.StandardRoot
// 方法 1 具体实现
@Override
publicvoidcreateWebResourceSet(ResourceSetTypetype,StringwebAppMount,
URL url,StringinternalPath){
// 解析 Url 拆分为 base,archivePath
BaseLocationbaseLocation=newBaseLocation(url);
createWebResourceSet(type,webAppMount,baseLocation.getBasePath(),
baseLocation.getArchivePath(),internalPath);
}
Tomcat 果然支持 jar 中 jar 内资源的读取
并且 Tomcat 本身提供了方法 1, 可以通过传入 Url 来进行拆分
问题:
那么为何变种 Url 直接传入却不行呢
来看 Tomcat 的拆分过程#org.apache.catalina.webresources.StandardRoot.BaseLocation
// 假设标准 url= jar:file:/a.jar!/lib/b.jar
// 拆分得到 base= /a.jar archivePath= /lib/b.jar
// 而此时变种 url= jar:file:/a.jar!/lib/b.jar!/
// 拆分得到 base= /a.jar archivePath= /lib/b.jar!/
BaseLocation(URL url){
Filef=null;
if("jar".equals(url.getProtocol())||"war".equals(url.getProtocol())){
StringjarUrl=url.toString();
intendOfFileUrl=-1;
if("jar".equals(url.getProtocol())){
endOfFileUrl=jarUrl.indexOf("!/");
}else{
endOfFileUrl=jarUrl.indexOf(UriUtil.getWarSeparator());
}
StringfileUrl=jarUrl.substring(4,endOfFileUrl);
try{
f=newFile(newURL(fileUrl).toURI());
}catch(MalformedURLException|URISyntaxExceptione){
thrownewIllegalArgumentException(e);
}
intstartOfArchivePath=endOfFileUrl+2;
if(jarUrl.length()>startOfArchivePath){
archivePath=jarUrl.substring(startOfArchivePath);
}else{
archivePath=null;
}
}
...
basePath=f.getAbsolutePath();
}
问题很明显了 就是变种 Url 中拆分出的 archivePath 还带了!/ 尾巴
解决思路:
解析 SpringBoot 的变种 Url, 去掉 archivePath 中的尾巴
注意: SpringBoot 的变种 Url 中 Boot-INF/classes 也被当做一个 jar, 但在标准 Url 中只是个目录而已, 所以要特殊处理@Override
publicvoidlifecycleEvent(LifecycleEventevent){
if(event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)){
try{
//jar:file:/a.jar!/BOOT-INF/classes!/
URL url=ResourceUtils.getURL(ResourceUtils.CLASSPATH_URL_PREFIX);
Stringpath="/";
BaseLocationbaseLocation=newBaseLocation(url);
if(baseLocation.getArchivePath()!=null){// 当有 archivePath 时肯定是 jar 包运行
//url= jar:file:/a.jar
// 此时 Tomcat 再拆分出 base = /a.jar archivePath= /
url=newURL(url.getPath().replace("!/"+baseLocation.getArchivePath(),""));
//path=/BOOT-INF/classes
path="/"+baseLocation.getArchivePath().replace("!/","");
}
context.getResources().createWebResourceSet(
WebResourceRoot.ResourceSetType.RESOURCE_JAR,"/",url,path);
}catch(Exceptione){
e.printStackTrace();
}
}
}
通过处理变种 Url->标准 Url,, 使得 Tomcat 容器能以标准 Url 进行拆分
再利用 Tomcat 本身支持的 jarInjar 资源读取, 就能获取到资源了
那如果 jsp 放在依赖的 jar 中怎么办
同样的只要我们 jarInjar 的 Url 进行处理就好了@Bean
publicCustomTomcatEmbeddedServletContainerFactorycustomTomcatEmbeddedServletContainerFactory(){
returnnewCustomTomcatEmbeddedServletContainerFactory();
}
publicstaticclassCustomTomcatEmbeddedServletContainerFactoryextendsTomcatEmbeddedServletContainerFactory{
@Override
protectedvoidpostProcessContext(Contextcontext){
super.postProcessContext(context);
context.addLifecycleListener(newLifecycleListener(){
privatebooleanisResourcesJar(JarFilejar)throwsIOException{
try{
returnjar.getName().endsWith(".jar")
&&(jar.getJarEntry("WEB-INF")!=null);
}finally{
jar.close();
}
}
@Override
publicvoidlifecycleEvent(LifecycleEventevent){
if(event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)){
try{
ClassLoaderclassLoader=getClass().getClassLoader();
ListstaticResourceUrls=newArrayList();
if(classLoaderinstanceofURLClassLoader){
// 遍历 Classpath 中装载的所有资源 url
for(URL url:((URLClassLoader)classLoader).getURLs()){
URLConnectionconnection=url.openConnection();
// 如果是 jar 包资源且 jar 包中含有 WEB-INF 目录 则添加到集合中
if(connectioninstanceofJarURLConnection){
if(isResourcesJar(((JarURLConnection)connection).getJarFile())){
staticResourceUrls.add(url);
}
}
}
}
// 遍历集合 添加到容器的资源搜索路径中
for(URL url:staticResourceUrls){
Stringfile=url.getFile();
if(file.endsWith(".jar")||file.endsWith(".jar!/")){
Stringjar=url.toString();
if(!jar.startsWith("jar:")){
jar="jar:"+jar+"!/";
}
// 如果是 jarinjar 去掉!/ 尾巴
if((jar+"1").split("!/").length==3){//jarInjar
jar=jar.substring(0,jar.length()-2);
}
URL newUrl=newURL(jar);
Stringpath="/";
context.getResources().createWebResourceSet(
WebResourceRoot.ResourceSetType.RESOURCE_JAR,"/",newUrl,path);
}
...
}
}catch(Exceptione){
e.printStackTrace();
}
}
}
});
}
}
参考 org.springframework.boot.context.embedded.tomcat.TomcatResources.Tomcat8Resources#addResourceSet
另外
其实 SpringBoot 已经帮我们处理 lib 中资源的读取了(主要是用于 webjar)#org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory#prepareContext
protectedvoidprepareContext(Hosthost,ServletContextInitializer[]initializers){
...
context.addLifecycleListener(newLifecycleListener(){
@Override
publicvoidlifecycleEvent(LifecycleEventevent){
// 添加 lib 中(不包括项目自身)META/resource 目录到资源搜索路径中
if(event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)){
TomcatResources.get(context)
.addResourceJars(getUrlsOfJarsWithMetaInfResources());
}
}
});
...
}
来源: https://juejin.im/post/5ad21eb5f265da23945feb62