springboot怎么替代jsp_SpringBoot 中使用 jsp 的坑

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 能和我心有灵犀

ab7653affab982b574eb7acc55df2e04.gif

结果我失败了...

简单推断一下: 肯定是 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 都包含哪些对象

ab7653affab982b574eb7acc55df2e04.gif

ab7653affab982b574eb7acc55df2e04.gif

可以看到 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();

}

}

}

});

}

}

ab7653affab982b574eb7acc55df2e04.gif

由于是在 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) 后路径变化导致的(我是查了好久才知道的 #_#)

ab7653affab982b574eb7acc55df2e04.gif

假设目标文件路径为: 项目根路径 / 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值