Яма использования jsp в SpringBoot

Spring Boot Java задняя часть сервер Tomcat

Фоновое примечание:

Фоновый проект управления SpringBoot1.5+jsp+tomcat

Яма 1: зависимость пакета tomcat-embed-jasper

Процесс запроса jsp в SpringMVC:

  1. Контейнер сервлетов получает запрос и отправляет его DispatcherServlet SpringMVC.
  2. SpringMVC обрабатывается, возвращает имя представления jsp, а затем анализирует его с помощью InternalResourceViewResolver, чтобы получить InternalResourceView.
  3. InternalResourceView прыгает внутрь сервера через метод пересылки
  4. Контейнер сервлетов снова получает запрос.Поскольку URL-адрес в этом запросе имеет суффикс .jsp, он распространяется на JspServlet для обработки.
  5. JspServlet использует механизм jsp для анализа файла jsp при первом вызове, генерирует сервлет, регистрирует его и затем вызывает

Принцип синтаксического анализа представления SpringMVC см. это

Яма находится на шаге 4

Феномен:

Когда InternalResourceView пересылает запрос, он поступает в DispatcherServlet SpringMVC.

причина:

JspServlet не зарегистрирован в контейнере сервлетов, поэтому запрос отправляется DispatcherServlet для обработки.

原因是很简单,但是之前对Jsp处理流程不熟的我还是想了半天.甚至萌生手动解析jsp文件的想法#-_-

решение:

Добавьте зависимости следующего пакета

 <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?

После решения Pit 1 я подумал, что все в порядке, но обнаружил, что SpringBoot вообще не имеет каталога WEB-INF.

Куда мне поместить файлы Jsp? Могу ли я поместить их куда угодно?

С намерением попробовать, я создал 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 ко всем ресурсам, верно?

 @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目录下

Попробуйте снова получить доступ к следующему, вы действительно можете получить к нему доступ

в заключении:

Во встроенном коте нам нужно вручную прописать путь поиска ресурсов

Яма 3: запустите пакет jar и не можете получить доступ к jsp

На этот раз это немного странно: можно запускать напрямую с идеей, но это не работает после того, как она упакована в 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

Отладка отследила его и обнаружила, что полученный 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= /

Вы можете видеть, что URL-адрес, полученный в springboot-jar, очень особенный, а не стандартный URL-адрес.

считать:

  1. Почему URL-адрес SpringBoot-jar не является стандартным URL-адресом
  2. Как найти ресурсы (чтение ресурсов) через вариант URL

в заключении:

  1. Специальный формат пакета jar (jarInjar), упакованный jar SpringBoot не является стандартной структурой пакета jar (зависимая библиотека также вводится в виде jar)
  2. Variant Url, чтобы соответствовать собственному специальному формату упаковки для размещения ресурса (чтение ресурса), определяется набор вариантов Url
  3. Реализован URLStreamHandler путем реализации URLStreamHandler для удовлетворения метода получения ресурсов url.openConnection(). Он также наследует JarFile (Url находит различные реализации URLStreamHandler в соответствии с протоколом для поиска ресурсов)

Подробный анализ смотрите здесь

Давайте посмотрим на общие форматы упаковки Java-проектов, как правило, два

  1. jar, то зависимость не будет занесена в jar в виде jar-пакета, либо через ассоциацию -classpath в виде внешней зависимости, либо исходный код будет слит в jar
  2. War, по сути, это сжатый пакет, после распаковки это jar самого проекта и jar внешних зависимостей.

Вы можете видеть, что SpringBoot-jar немного похож на войну, а Tomcat поддерживает войну без распаковки, так что это должно бытьПоддержка метода чтения 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 действительно поддерживает чтение ресурсов в банках в банках

И сам 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, также имеет !/tail

Решения:

解析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->standard Url контейнер Tomcat можно разделить со стандартным URL-адресом.

Затем используйте ресурс jarInjar, поддерживаемый самим Tomcat, для чтения, вы можете получить ресурс

Что делать, если jsp помещен в зависимую банку

Точно так же, пока мы обрабатываем URL-адрес jarInjar, все будет в порядке.

@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 для поиска ресурсов в соответствии с вариантом Url, поэтому доступ к ресурсам возможен. В Tomcat вместо прямого получения ресурсов через Url.openConnect() он сам анализирует URL для получения ресурсов по пути, поэтому будут проблемы.