SpringMVC中jsp请求流程:
SpringMVC视图解析原理看这
坑就坑在第4步中
当InternalResourceView进行forward之后,请求又进入到了SpringMVC的DispatcherServlet中
JspServlet没有被注册到Servlet容器中,所以请求分发到DispatcherServlet来处理
原因是很简单,但是之前对Jsp处理流程不熟的我还是想了半天.甚至萌生手动解析jsp文件的想法#-_-
添加下面这个包的依赖
<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 && ResourceSet.getClassLoaderOnly() || useClassLoaderResources && ResourceSet.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()); } } }); ... }