SpringBoot中使用jsp的坑

背景說明:java

SpringBoot1.5+jsp+tomcat的管理後臺項目web

坑1: tomcat-embed-jasper包依賴

SpringMVC中jsp請求流程:spring

  1. servlet容器收到請求,分發到SpringMVC的DispatcherServlet.
  2. SpringMVC通過處理,返回jsp視圖名稱,隨後經過InternalResourceViewResolver解析獲得InternalResourceView
  3. InternalResourceView經過forward方式服務器內部跳轉
  4. servlet容器再次收到請求,因爲本次請求中url中帶有.jsp後綴,因此分發給JspServlet處理
  5. JspServlet在第一次被調用時使用jsp引擎解析jsp文件,並生成servlet,並註冊,隨後調用

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的時候咱們是不須要添加上面這個包的依賴的

由於這個包已經在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時就不太同樣了

內嵌容器(Tomcat)

  1. 首先tomcat-embed-jasper包是獨立出來的,須要咱們單獨引入
  2. 內嵌Tomcat默認不註冊JspServet

這回都清楚了.

還有一點,在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逼着我去搞清楚這些原理,要否則壓根駕馭不了這貨#-_-

坑2: Jsp文件放哪?

當解決了坑1以後,滿心歡喜覺得都ok,結果發現SpringBoot壓根沒WEB-INF目錄

那個人Jsp文件放哪?隨便放能夠嗎?

抱着試一試的態度,在resources下面建了個WEB-INF,但願SpringBoot能和我心有靈犀

jsp路徑

結果我失敗了...

簡單推斷一下: 確定是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都包含哪些對象

resource

dir

能夠看到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();
					}
				}
			}
		});
	}
}
複製代碼

手動添加ResourceSet

因爲是在Idea中直接運行,因此base是在target/classes目錄下

再嘗試訪問如下,果然能夠訪問到了

結論:

內嵌tomcat中,須要咱們手動註冊資源搜索路徑

坑點3:使用jar包方式運行 又訪問不到jsp

這回有點奇怪了,使用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)後路徑變化致使的(我是查了很久才知道的#_#)

springboot打包目錄

假設目標文件路徑爲:項目根路徑/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

思考:

  1. SpringBoot-jar的url爲什麼不是個標準Url
  2. 如何經過變種Url來進行資源定位(資源讀取)

結論:

  1. 特殊jar包格式(jarInjar),SpringBoot打包jar不是標準jar包結構(把依賴lib也以jar的形式打進去了)
  2. 變種Url,爲了知足自身特殊的打包格式進行資源定位(資源讀取),定義了一套變種Url
  3. URLStreamHandler實現,經過實現URLStreamHandler來知足url.openConnection()獲取資源方法.同時還繼承了JarFile (Url根據Protocol找到不一樣URLStreamHandler實現來進行資源定位)

詳細分析請看這

再來看java項目常見的打包格式通常就爲兩種

  1. jar,依賴不會以jar包方式打入jar,要麼之外部依賴的方式經過-classpath關聯,要麼將源碼合併打入jar中
  2. war,其實就是個壓縮包,解壓後就是一個項目自身的jar和外部依賴的jar

能夠看到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資源讀取,就能獲取到資源了

那若是jsp放在依賴的jar中怎麼辦

一樣的只要咱們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在根據路徑獲取資源,因此會出現問題

相關文章
相關標籤/搜索