背景说明:java
SpringBoot1.5+jsp+tomcat的管理后台项目web
SpringMVC中jsp请求流程:spring
SpringMVC视图解析原理看这apache
坑就坑在第4步中缓存
现象:tomcat
当InternalResourceView进行forward以后,请求又进入到了SpringMVC的DispatcherServlet中springboot
缘由:bash
JspServlet没有被注册到Servlet容器中,因此请求分发到DispatcherServlet来处理服务器
缘由是很简单,可是以前对Jsp处理流程不熟的我仍是想了半天.甚至萌生手动解析jsp文件的想法#-_-
app
解决方案:
添加下面这个包的依赖
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
复制代码
有人会奇怪以前使用SpringMVC(非SpringBoot)的时候不用管这些的啊?(我也是*-*)
下面来细说
其实使用外置Tomcat的时候咱们是不须要添加上面这个包的依赖的
由于这个包已经在TOMCAT_HOME/lib中引入,同时JspServet也在TOMCAT_HOME/Conf/web.xml(全局配置)被注册
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
<init-param>
<param-name>fork</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>xpoweredBy</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
复制代码
因此当咱们使用外置Tomcat的时候压根不用管这些.
然而到了内嵌Tomcat时就不太同样了
这回都清楚了.
还有一点,在SpringBoot中咱们除了添加依赖也没注册JspServlet啊?
由于SpringBoot帮咱们注册了
//tomcat启动准备
protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
File docBase = getValidDocumentRoot();
docBase = (docBase != null ? docBase : createTempDir("tomcat-docbase"));
final TomcatEmbeddedContext context = new TomcatEmbeddedContext();
...
//是否Classpath中有org.apache.jasper.servlet.JspServlet这个类
//有就注册
if (shouldRegisterJspServlet()) {
addJspServlet(context);
addJasperInitializer(context);
context.addLifecycleListener(new StoreMergedWebXmlListener());
}
}
复制代码
这里说一句,SpringBoot真是好东西.原先使用Spring,只会照着样子用.如今可好,用了SpringBoot逼着我去搞清楚这些原理,要否则压根驾驭不了这货#-_-
当解决了坑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,
boolean precompile)
throws ServletException, IOException {
//从缓存中取出jsp->servlet对象
JspServletWrapper wrapper = 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 = new JspServletWrapper(config, options, jspUri,
rctxt);
rctxt.addWrapper(jspUri,wrapper);
}
}
}
try {
//使用Jsp引擎解析获得的Servlet
wrapper.service(request, response, precompile);
} catch (FileNotFoundException fnfe) {
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);
}
protected final WebResource getResourceInternal(String path,
boolean useClassLoaderResources) {
...
//遍历
for (List<WebResourceSet> list : allResources) {
for (WebResourceSet webResourceSet : list) {
if (!useClassLoaderResources && !webResourceSet.getClassLoaderOnly() ||
useClassLoaderResources && !webResourceSet.getStaticOnly()) {
result = webResourceSet.getResource(path);
if (result.exists()) {
return result;
}
...
}
}
}
...
}
复制代码
咱们调用一下看allResources都包含哪些对象
能够看到allResource中只有一个DirResourceSet,并且是一个临时目录(里面啥文件也没有)
理所固然JspServlet找不到咱们的jsp文件
基于这个想法,咱们只要手动添加一个ResourceSet到allResources,是否是就能够了
@Bean
public CustomTomcatEmbeddedServletContainerFactory customTomcatEmbeddedServletContainerFactory() {
return new CustomTomcatEmbeddedServletContainerFactory();
}
public static class CustomTomcatEmbeddedServletContainerFactory extends TomcatEmbeddedServletContainerFactory {
//在prepareContext中被调用
@Override
protected void postProcessContext(Context context) {
super.postProcessContext(context);
//添加监听器
context.addLifecycleListener(new LifecycleListener() {
@Override
public void lifecycleEvent(LifecycleEvent event) {
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
try {
//!!!资源所在url
URL url = ResourceUtils.getURL(ResourceUtils.CLASSPATH_URL_PREFIX);
//!!!资源搜索路径
String path = "/";
//手动建立一个ResourceSet
context.getResources().createWebResourceSet(
WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/", url, path);
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
}
复制代码
因为是在Idea中直接运行,因此base是在target/classes目录下
再尝试访问如下,果然能够访问到了
结论:
内嵌tomcat中,须要咱们手动注册资源搜索路径
这回有点奇怪了,使用idea直接运行都没问题 ,但是打成jar包后运行却又不行了
查看了一下日志,发现报错了
Caused by: org.apache.catalina.LifecycleException: Failed to 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.<init>(JarWarResourceSet.java:76)
... 12 more
Caused by: 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)
... 14 more
复制代码
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.普通jar
url= 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
思考:
结论:
再来看java项目常见的打包格式通常就为两种
能够看到SpringBoot-jar和war有点像.而Tomcat支持war不解压运行,那么想必应该支持jarInjar的读取方式
再回到Tomcat的资源搜索来
Tomcat支持一下两种方式添加资源搜索路径
#org.apache.catalina.WebResourceRoot
//方法1.拆分Url为base,archivePath 调用方法2
void createWebResourceSet(ResourceSetType type, String webAppMount, URL url,
String internalPath);
//方法2
/**
* 添加一个ResourceSet(资源集合)到Tomcat的资源搜索路径中
* @param type 资源类型(jar,file等)
* @param webAppMount 挂载点
* @param base 资源路径
* @param archivePath jar中jar相对路径
* @param internalPath jar中jar中resource的相对路径
*/
void createWebResourceSet(ResourceSetType type, String webAppMount, String base, String archivePath, String internalPath);
#org.apache.catalina.webresources.StandardRoot
//方法1具体实现
@Override
public void createWebResourceSet(ResourceSetType type, String webAppMount,
URL url, String internalPath) {
//解析Url拆分为base,archivePath
BaseLocation baseLocation = new BaseLocation(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) {
File f = null;
if ("jar".equals(url.getProtocol()) || "war".equals(url.getProtocol())) {
String jarUrl = url.toString();
int endOfFileUrl = -1;
if ("jar".equals(url.getProtocol())) {
endOfFileUrl = jarUrl.indexOf("!/");
} else {
endOfFileUrl = jarUrl.indexOf(UriUtil.getWarSeparator());
}
String fileUrl = jarUrl.substring(4, endOfFileUrl);
try {
f = new File(new URL(fileUrl).toURI());
} catch (MalformedURLException | URISyntaxException e) {
throw new IllegalArgumentException(e);
}
int startOfArchivePath = 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
public void lifecycleEvent(LifecycleEvent event) {
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
try {
//jar:file:/a.jar!/BOOT-INF/classes!/
URL url = ResourceUtils.getURL(ResourceUtils.CLASSPATH_URL_PREFIX);
String path = "/";
BaseLocation baseLocation = new BaseLocation(url);
if (baseLocation.getArchivePath() != null) {//当有archivePath时确定是jar包运行
//url= jar:file:/a.jar
//此时Tomcat再拆分出base = /a.jar archivePath= /
url = new URL(url.getPath().replace("!/" + baseLocation.getArchivePath(), ""));
//path=/BOOT-INF/classes
path = "/" + baseLocation.getArchivePath().replace("!/", "");
}
context.getResources().createWebResourceSet(
WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/", url, path);
} catch (Exception e) {
e.printStackTrace();
}
}
}
复制代码
经过处理变种Url->标准Url,,使得Tomcat容器能以标准Url进行拆分
再利用Tomcat自己支持的jarInjar资源读取,就能获取到资源了
一样的只要咱们jarInjar的Url进行处理就行了
@Bean
public CustomTomcatEmbeddedServletContainerFactory customTomcatEmbeddedServletContainerFactory() {
return new CustomTomcatEmbeddedServletContainerFactory();
}
public static class CustomTomcatEmbeddedServletContainerFactory extends TomcatEmbeddedServletContainerFactory {
@Override
protected void postProcessContext(Context context) {
super.postProcessContext(context);
context.addLifecycleListener(new LifecycleListener() {
private boolean isResourcesJar(JarFile jar) throws IOException {
try {
return jar.getName().endsWith(".jar")
&& (jar.getJarEntry("WEB-INF") != null);
} finally {
jar.close();
}
}
@Override
public void lifecycleEvent(LifecycleEvent event) {
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
try {
ClassLoader classLoader = getClass().getClassLoader();
List<URL> staticResourceUrls = new ArrayList<URL>();
if (classLoader instanceof URLClassLoader) {
//遍历Classpath中装载的全部资源url
for (URL url : ((URLClassLoader) classLoader).getURLs()) {
URLConnection connection = url.openConnection();
//若是是jar包资源且jar包中含有WEB-INF目录 则添加到集合中
if (connection instanceof JarURLConnection) {
if (isResourcesJar(((JarURLConnection) connection).getJarFile())) {
staticResourceUrls.add(url);
}
}
}
}
//遍历集合 添加到容器的资源搜索路径中
for (URL url : staticResourceUrls) {
String file = url.getFile();
if (file.endsWith(".jar") || file.endsWith(".jar!/")) {
String jar = 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 = new URL(jar);
String path = "/";
context.getResources().createWebResourceSet(
WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/", newUrl, path);
}
...
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
});
}
}
复制代码
参考org.springframework.boot.context.embedded.tomcat.TomcatResources.Tomcat8Resources#addResourceSet
其实SpringBoot已经帮咱们处理lib中资源的读取了(主要是用于webjar)
#org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory#prepareContext
protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
...
context.addLifecycleListener(new LifecycleListener() {
@Override
public void lifecycleEvent(LifecycleEvent event) {
//添加lib中(不包括项目自身)META/resource目录到资源搜索路径中
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
TomcatResources.get(context)
.addResourceJars(getUrlsOfJarsWithMetaInfResources());
}
}
});
...
}
复制代码
若是SpringBoot也是利用Tomcat资源访问(DefaultServlet),那么确定也会出现变种Url的问题. 在SpringMVC中有大体有两种方式进行静态资源访问: 1. 使用DefaultServlet进行资源访问 2. 使用ResourceHttpRequestHandler. 而在SpringBoot中默认是使用ResourceHttpRequestHandler进行静态资源访问
#org.springframework.http.converter.ResourceHttpMessageConverter
protected void writeContent(Resource resource, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
try {
//写入http输出流
InputStream in = resource.getInputStream();
try {
StreamUtils.copy(in, outputMessage.getBody());
}
catch (NullPointerException ex) {
// ignore, see SPR-13620
}
...
}
#org.springframework.core.io.ClassPathResource
@Override
public InputStream getInputStream() throws IOException {
InputStream is;
if (this.clazz != null) {
// 利用ClassLoader获取资源
is = this.clazz.getResourceAsStream(this.path);
}
else if (this.classLoader != null) {
is = this.classLoader.getResourceAsStream(this.path);
}
else {
is = ClassLoader.getSystemResourceAsStream(this.path);
}
if (is == null) {
throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
}
return is;
}
复制代码
能够看到ResourceHttpRequestHandler最后是利用ClassLoader获取资源,最后经过Url.openConnect()获取资源,而SpringBoot-jar中注册了handler来根据变种Url进行资源定位.因此能够成功访问到资源. 而Tomcat中不是经过Url.openConnect()直接获取资源,而是本身解析Url在根据路径获取资源,因此会出现问题