- Яма использования jsp в SpringBoot
Фоновое примечание:
Фоновый проект управления SpringBoot1.5+jsp+tomcat
Яма 1: зависимость пакета tomcat-embed-jasper
Процесс запроса jsp в SpringMVC:
- Контейнер сервлетов получает запрос и отправляет его DispatcherServlet SpringMVC.
- SpringMVC обрабатывается, возвращает имя представления jsp, а затем анализирует его с помощью InternalResourceViewResolver, чтобы получить InternalResourceView.
- InternalResourceView прыгает внутрь сервера через метод пересылки
- Контейнер сервлетов снова получает запрос.Поскольку URL-адрес в этом запросе имеет суффикс .jsp, он распространяется на JspServlet для обработки.
- 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)
- Во-первых, пакет tomcat-embed-jasper является независимым, и его нужно вводить отдельно.
- Встроенный 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 может быть со мной добрым сердцем.
В результате у меня не получилось...
Простой вывод: должно быть, 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 ко всем ресурсам, верно?
@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目录下
Попробуйте снова получить доступ к следующему, вы действительно можете получить к нему доступ
в заключении:
Во встроенном коте нам нужно вручную прописать путь поиска ресурсов
Яма 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) (я узнал об этом только после долгой проверки#_#)
假设目标文件路径为:项目根路径/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-адрес.
считать:
- Почему URL-адрес SpringBoot-jar не является стандартным URL-адресом
- Как найти ресурсы (чтение ресурсов) через вариант URL
в заключении:
- Специальный формат пакета jar (jarInjar), упакованный jar SpringBoot не является стандартной структурой пакета jar (зависимая библиотека также вводится в виде jar)
- Variant Url, чтобы соответствовать собственному специальному формату упаковки для размещения ресурса (чтение ресурса), определяется набор вариантов Url
- Реализован URLStreamHandler путем реализации URLStreamHandler для удовлетворения метода получения ресурсов url.openConnection(). Он также наследует JarFile (Url находит различные реализации URLStreamHandler в соответствии с протоколом для поиска ресурсов)
Подробный анализ смотрите здесь
Давайте посмотрим на общие форматы упаковки Java-проектов, как правило, два
- jar, то зависимость не будет занесена в jar в виде jar-пакета, либо через ассоциацию -classpath в виде внешней зависимости, либо исходный код будет слит в jar
- 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 для получения ресурсов по пути, поэтому будут проблемы.