前言
今天天气很好,小编的心情也很好,于是兴致勃勃的开始了今天的学习之旅。
代码编写的很顺利,但是在 Run Bootstrap
的时候,小编的心情似乎就不是那么好了,就感觉脑瓜上有一片乌云时不时的还下点雨。
没错就是今天的主题 SpringBoot 视图模板
关于这个关键词在度娘上肯定是不少的,我刚开始也是度娘上寻找答案的,无奈没有一丢丢收获,他们的文章或者说他们那些笔者可能是一个师傅教出来的,内容大体都如出一辙。
于似乎小编心中有了一些怒火,如果都这样借鉴别人的文章去发博客,那让小白该怎么去学,遇到问题了该怎么解决。如今互联网人才空缺的原因之一就是这么一群互相抄袭博客的人才,不明白其中的原理就断章取义大肆传播。
不说那么多了,下面把舞台交给小编,看我操作。
SpringBoot Thymeleaf
介绍
Thymeleaf is a modern server-side Java template engine that emphasizes natural HTML templates that can be previewed in a browser by double-clicking, which is very helpful for independent work on UI templates (for example, by a designer) without the need for a running server. If you want to replace JSPs, Thymeleaf offers one of the most extensive sets of features to make such a transition easier. Thymeleaf is actively developed and maintained. For a more complete introduction, see the Thymeleaf project home page.
The Thymeleaf integration with Spring MVC is managed by the Thymeleaf project. The configuration involves a few bean declarations, such as ServletContextTemplateResolver, SpringTemplateEngine, and ThymeleafViewResolver. See Thymeleaf+Spring for more details.
- 这是
Spring
官网对 Thymeleaf 的描述 点我跳转 - 霹雳扒拉的讲了一堆,核心是
Thymeleaf to is Java server template engine
(散装英语有点差劲,意思就是 Thymeleaf 是一个Java 服务端模板引擎) - SpringBoot 将Thymeleaf 作为首选视图模板引擎肯定是出于性能和使用考虑的,所以Thymeleaf 的利大于弊,大家放心使用就好了。
使用
- 这里的使用我是建立在
SpringBoot
环境的项目上的 - 首先在
pom
文件中添加Thymeleaf
的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- 编写
Thymeleaf HTML
模板 (这里是演示嘛,所以怎么简单怎么来哈)
<!-- classpath:/templates/thymeleaf/index.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="th">
<body>
<p th:text="${message}">!!!</p>
</body>
</html>
- 编写 Thymeleaf 测试类
public class ThymeleafTemplateEngineBootstrap {
public static void main(String[] args) throws IOException {
// Spring Thymeleaf 模板引擎
ITemplateEngine templateEngine = new SpringTemplateEngine();
// 渲染上下文
Context context = new Context();
context.setVariable("message", "Hello World");
// classpath:/templates/thymeleaf/index.html
ResourceLoader resourceLoader = new DefaultResourceLoader();
Resource resource = resourceLoader.getResource("classpath:/templates/thymeleaf/index.html");
File file = resource.getFile();
FileInputStream inputStream = new FileInputStream(file);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
IOUtils.copy(inputStream, outputStream);
// 模板内容 采用外部配置方式 减少硬编码
String content = outputStream.toString("UTF-8");
// String content = "<p th:text=\"${message}\">!!!</p>";
// 渲染
String result = templateEngine.process(content, context);
// 渲染结果
System.out.println(result);
}
}
------------------------ output console ------------------------
<!-- classpath:/templates/thymeleaf/index.html -->
<!DOCTYPE html>
<html lang="th">
<body>
<p>Hello World</p>
</body>
</html>
- Thymeleaf 在Spring Boot 中的使用也非常简单,编写
application.properties
配置文件。(注意:访问不到视图模板的情况下,如果控制层和模板没有问题的话,那么问题肯定是在这里,spring.thymeleaf.prefix/suffix
这两个属性切记一定要配置正确,否则度娘上的文章会让你彻底失去继续学下去的信心)
# Thymeleaf 模板所在的文件夹
spring.thymeleaf.prefix = /templates/thymeleaf/
# Thymeleaf 模板的后缀
spring.thymeleaf.suffix = .html
# Thymeleaf 是否缓存
spring.thymeleaf.cache = false
# Thymeleaf 文件编码
spring.thymeleaf.encoding = UTF-8
- 编写控制层
@Controller
public class CombatController {
@RequestMapping("/")
public String index() {
return "index";
}
@ModelAttribute("message")
public String message() {
return "Hello World";
}
}
- 编写引导类
@SpringBootApplication
public class CombatApplication {
public static void main(String[] args) {
SpringApplication.run(CombatApplication.class, args);
}
}
- 启动项目 测试访问 http://localhost:8080/
SpringBoot Jsp
介绍
- 相信各位对
JSP
并不陌生,从JavaWeb
的那只三脚猫就开始用这玩意了,这玩意确实难用,还特别容易报错。 - 但这毕竟是
JavaWeb
的基石,同时也是这篇文章的重点,下面开始!!!
使用
- 相信大家在度娘上搜索
SpringBoot 整合 JSP
的文章,大致都如下- 创建
webapp
文件夹 - 创建
WEB-INF
文件夹 - 创建
jsp
文件夹并在文件夹下创建jsp
文件 - 在
pom
文件中添加build
让src/main/webapp
编译到META-INF/resources
文件夹下 - 在
application.properties
文件中添加配置
- 创建
- 然后
application.proeprties
就是这个样子
# Jsp 模板前缀
spring.mvc.view.prefix = /WEB-INF/jsp/
# Jsp 模板后缀
spring.mvc.view.suffix = .jsp
- 然后编译后的文件夹是这个样子
- 对于将
JavaWeb
升级到SpringBoot
项目的这类人来说上面的一切都很正常,很正常非常正常,WEB-INF 对吧 没毛病。 - 对比一下
Thymeleaf
的配置
# Jsp 模板前缀
spring.mvc.view.prefix = /WEB-INF/jsp/
# Jsp 模板后缀
spring.mvc.view.suffix = .jsp
# Thymeleaf 模板所在的文件夹
spring.thymeleaf.prefix = /templates/thymeleaf/
# Thymeleaf 模板的后缀
spring.thymeleaf.suffix = .html
# Thymeleaf 是否缓存
spring.thymeleaf.cache = false
# Thymeleaf 文件编码
spring.thymeleaf.encoding = UTF-8
- 这样真的好吗,对于
SpringBoot
采用JSP
首当模板引擎来说,这样好吗,这样不好。我得创建个webapp
用来单独存储jsp
同时这也极大的增加了小白看见404
的极大几率。 - 对于从
JavaWeb
升级到SpringBoot
的项目来说,这样其实没关系,但是对于一些早期的开发人员。比如说我们架构群里的吉尔哥
,那从小到大都是用JSP
的,那突然有一天项目经理讲话吉尔
你用SpringBoot
做JSP
吧,别用SSM
那老架构了扛不住高并发,咱们公司也得跟着互联网的脚步走。对于吉尔哥
这样的开发人员来说是非常容易404
的。
找茬
- 呸呸呸,这个
找茬
标题起的不对,应该叫定制
。 - 上面我们说到的
吉尔哥
对于这样的开发人员来说,JSP
的配置无疑难度是非常大的,这完全超出了自身技术能力,CPU
有可能还得烧坏了。 - 言回正传,对于
JSP
的配置着实让人有点烧脑,如果不按照网上那一群人的方式来的话,那resources
就得有/META-INF/resources/WEB-INF/jsp
这个文件夹,而application.properties
还是这样,这完全是卖狗肉挂羊头、驴头不对马嘴。
# Jsp 模板前缀
spring.mvc.view.prefix = /WEB-INF/jsp/
# Jsp 模板后缀
spring.mvc.view.suffix = .jsp
- 于是准备将
resources
下调整为/templates/jsp/
这个文件夹,而application.properties
配置调整为下面这样。
# Jsp 模板前缀
spring.mvc.view.prefix = /templates/jsp/
# Jsp 模板后缀
spring.mvc.view.suffix = .jsp
- 说干就干,开始分析源码
- 从
/WEB-INF/jsp/xx.jsp
到/META-INF/resources/WEB-INF/jsp/xx.jsp
这个过程是怎么完成的?- 通过源码分析断点调试,在
org.apache.catalina.webresources.StandardRoot#getResourceInternal
方法中找到了猫腻 - 这个方法是为了找到
JSP
资源文件,是在JSP -> Servlet
之前。 - 在
allResources
变量中存在两个地址 一个是Tomcat
的临时地址 (System.getProperty("java.io.tmpdir")\...
),一个是SpringBoot
内部配置 (...\target\classes\META-INF\resources
) - 这也不难看出,
SpringBoot
还是非常遵守Servlet
规范的,铁打的JSP
就是必须在META-INF/resources
下。
- 通过源码分析断点调试,在
protected final WebResource getResourceInternal(String path,
boolean useClassLoaderResources) {
WebResource result = null;
WebResource virtual = null;
WebResource mainEmpty = null;
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;
}
if (virtual == null) {
if (result.isVirtual()) {
virtual = result;
} else if (main.equals(webResourceSet)) {
mainEmpty = result;
}
}
}
}
}
// Use the first virtual result if no real result was found
if (virtual != null) {
return virtual;
}
// Default is empty resource in main resources
return mainEmpty;
}
- SpringBoot 是怎么配置 Tomcat 内部资源路径的?
- 在
org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.StaticResourceConfigurer
监听器中配置内部资源地址 - 首先
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh
方法中会调用创建WebServer
的方法 - 进入到
org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory#getWebServer
方法中后会添加下面的一个监听器,这个监听器是在 Tomcat 启动的时候会被触发,里面有事件判断。 - 接收到
Lifecycle.CONFIGURE_START_EVENT
事件后,这个监听器会在Tomcat context
中加入一个静态资源地址。
- 在
private final class StaticResourceConfigurer implements LifecycleListener {
private final Context context;
private StaticResourceConfigurer(Context context) {
this.context = context;
}
@Override
public void lifecycleEvent(LifecycleEvent event) {
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
addResourceJars(getUrlsOfJarsWithMetaInfResources());
}
}
private void addResourceJars(List<URL> resourceJarUrls) {
for (URL url : resourceJarUrls) {
String path = url.getPath();
if (path.endsWith(".jar") || path.endsWith(".jar!/")) {
String jar = url.toString();
if (!jar.startsWith("jar:")) {
// A jar file in the file system. Convert to Jar URL.
jar = "jar:" + jar + "!/";
}
addResourceSet(jar);
}
else {
addResourceSet(url.toString());
}
}
}
private void addResourceSet(String resource) {
try {
if (isInsideNestedJar(resource)) {
// It's a nested jar but we now don't want the suffix because Tomcat
// is going to try and locate it as a root URL (not the resource
// inside it)
resource = resource.substring(0, resource.length() - 2);
}
URL url = new URL(resource);
String path = "/META-INF/resources";
this.context.getResources().createWebResourceSet(ResourceSetType.RESOURCE_JAR, "/", url, path);
}
catch (Exception ex) {
// Ignore (probably not a directory)
}
}
private boolean isInsideNestedJar(String dir) {
return dir.indexOf("!/") < dir.lastIndexOf("!/");
}
}
- 如何像
SpringBoot
一样配置Tomcat.context
资源地址?- 首先继承
org.springframework.boot.web.server.WebServerFactoryCustomizer
并在泛型类型中添加 需要Customizer
的具体WebServerFactory
实现类。(这里针对的是Tomcat
所以是TomcatServletWebServerFactory
) - 此配置类让
SpringBoot
加载到后,会让org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor
这个BeanPostProcessor
进行处理。 - 其中就会调用
org.springframework.boot.web.server.WebServerFactoryCustomizer#customize
方法,这个处理是在初始化Tomcat
之前的,所以不用担心刚才的Listener
不会被加载到
- 首先继承
public class TomcatConfiguration implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addContextCustomizers((context -> {
context.addLifecycleListener(new LifecycleListener() {
@Override
public void lifecycleEvent(LifecycleEvent event) {
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
try {
// 这里并没有像 SpringBoot 一样对运行环境进行区分,因为 jar 和 !jar 的地址是不一样的
// 如果将此代码应用在生产环境这里还需要添加逻辑
URL url = new URL(this.getClass().getResource("/").toString());
String path = "/";
context.getResources().createWebResourceSet(WebResourceRoot.ResourceSetType.RESOURCE_JAR, "/", url, path);
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
});
}));
}
}
- 至此刚才的 applicatio.properties 配置文件就生效了
# Jsp 模板前缀
spring.mvc.view.prefix = /templates/jsp/
# Jsp 模板后缀
spring.mvc.view.suffix = .jsp
- 启动项目 测试访问 http://localhost:8080/
留言
对于问题的解决方案还是是挺多的,送大家一句话 别向这个世界认输 因为你还有个牛逼的梦想
。