SpringBoot中使用jsp的坑

本文深入探讨SpringBoot项目中整合JSP所遇到的问题及解决方案,包括tomcat-embed-jasper依赖、JSP文件放置位置、特殊jar包格式下资源访问等问题,并提供详细的实践案例。

背景说明:

SpringBoot1.5+jsp+tomcat的管理后台项目

坑1: tomcat-embed-jasper包依赖

SpringMVC中jsp请求流程:

  1. servlet容器收到请求,分发到SpringMVC的DispatcherServlet.
  2. SpringMVC经过处理,返回jsp视图名称,随后通过InternalResourceViewResolver解析得到InternalResourceView
  3. InternalResourceView通过forward方式服务器内部跳转
  4. servlet容器再次收到请求,由于本次请求中url中带有.jsp后缀,所以分发给JspServlet处理
  5. JspServlet在第一次被调用时使用jsp引擎解析jsp文件,并生成servlet,并注册,随后调用

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的时候我们是不需要添加上面这个包的依赖的

因为这个包已经在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能和我心有灵犀

结果我失败了...

简单推断一下: 肯定是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中,需要我们手动注册资源搜索路径

坑点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)后路径变化导致的(我是查了好久才知道的#_#)

假设目标文件路径为:项目根路径/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在根据路径获取资源,所以会出现问题

<think>嗯,用户想学习如何使用Spring Boot搭建JSP项目。根据我之前整理的资料,首先需要确认Spring Boot对JSP的支持情况。记得Spring Boot官方其实不推荐用JSP了,但有时候项目可能需要,所以还是得知道怎么配置。 首先,用户需要创建一个Maven项目,因为依赖管理更方便。然后,添加必要的依赖,比如spring-boot-starter-web和tomcat-embed-jasper。可能还要注意JSP的scope是provided,避免打包问题。接下来,配置application.properties,设置视图前缀和后缀,指向src/main/webapp目录,这样Spring MVC才能找到JSP文件。 然后,创建Controller,用@Controller注解,处理请求返回视图名称。用户可能会遇到JSP无法访问的问题,这时候要检查目录结构是否正确,webapp是否在main下,还有IDEA中是否把webapp标记为Web资源目录。另外,打包的时候需要确保JSP文件被打包进去,可能需要配置spring-boot-maven-plugin,并指定打包方式为war,方便部署到外部Tomcat。 用户可能遇到的问题包括启动类的位置,如果放在默认包下会导致组件扫描失败,所以应该放在根包下。还有依赖冲突,比如引入其他模块导致JSP无法渲染,需要排除冲突的依赖。另外,JSP在嵌入式容器中的支持有限,可能需要调整Tomcat版本或者改用War包部署。 需要提醒用户虽然可以这么做,但现代项目更推荐使用Thymeleaf或FreeMarker,JSP在Spring Boot中可能会有兼容性问题。如果必须使用,要严格按照步骤配置,并注意可能遇到的,比如目录结构、依赖版本和打包设置。</think>### 使用Spring Boot创建JSP项目的详细教程 #### 一、环境准备与项目创建 1. **创建Maven项目** 在IntelliJ IDEA中选择`New Project > Spring Initializr`,设置`Packaging`为**War**格式(JSP部署需要),添加`Spring Web`依赖。 2. **添加JSP相关依赖** 在`pom.xml`中添加以下依赖: ```xml <!-- JSP支持 --> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <scope>provided</scope> </dependency> <!-- JSTL标签库 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> </dependency> ``` #### 二、目录结构与配置 1. **创建webapp目录** 在`src/main`下新建`webapp/WEB-INF/views`目录,用于存放JSP文件。需在IDEA中右键标记为`Web Resource Directory`[^2]。 2. **配置视图解析器** 在`application.properties`中添加: ```properties spring.mvc.view.prefix=/WEB-INF/views/ spring.mvc.view.suffix=.jsp ``` #### 三、编写Controller与JSP页面 1. **创建Controller类** ```java @Controller public class DemoController { @GetMapping("/hello") public String hello(Model model) { model.addAttribute("message", "Hello Spring Boot + JSP!"); return "demo"; //对应WEB-INF/views/demo.jsp } } ``` 2. **编写JSP文件** 在`webapp/WEB-INF/views`下创建`demo.jsp`: ```jsp <%@ page contentType="text/html;charset=UTF-8" %> <html> <body> <h1>${message}</h1> </body> </html> ``` #### 四、解决常见问题 1. **访问404错误** - 确认`webapp`目录位于`src/main`下 - 检查是否添加`@SpringBootApplication`注解的主类 - 验证是否配置了`spring.mvc.view`前缀后缀 2. **打包部署问题** 修改`pom.xml`的打包方式: ```xml <packaging>war</packaging> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> ``` #### 五、扩展建议 - 对于新项目推荐使用Thymeleaf模板引擎(Spring Boot官方推荐) - JSP适用于需要直接使用Java代码的遗留系统改造 - 使用`<c:forEach>`等JSTL标签替代Scriptlet代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值