聊到“用SpringBoot搭一个网页”,我发现很多新手会走进一个认知误区:把SpringBoot当成一个能“生成网页”的工具,装好依赖就开始幻想浏览器里能蹦出页面。实际上SpringBoot本身不生产任何HTML,它只是一个后端框架——但它确实是最快把“网页”跑起来的方式,尤其是做个简单的个人页面、内部工具、毕设后台或者给某个小团队做信息管理页,SpringBoot + Thymeleaf的组合几乎能在一杯咖啡的时间内完成。
这篇文章面向的就是:知道Java基本语法、可能还没接触过SpringBoot的人,或者是以前只做过静态HTML、想搞清楚后端数据怎么拼到网页里的人。我会按照我自己搭建项目时真实推进的顺序来讲,不跳过弯路,但会把弯路标出来。读完你能完成:创建项目、写一个带样式的页面、让页面显示后端的数据、提交一个表单并回显结果,最后打包成可运行的jar。
1. 先搞明白网页在SpringBoot项目里到底是谁在干活
1.1 一次请求的完整旅程
在动手写代码之前,有必要搞清楚一个最基本的问题:你在浏览器里输入localhost:8080之后,屏幕上渲染出来的HTML是哪来的?
服务器端渲染的过程大致是这样的:
- 浏览器向SpringBoot应用发了一个HTTP请求,比如
GET /hello。 - SpringBoot的DispatcherServlet根据URL匹配到对应的Controller方法。
- Controller方法处理完业务逻辑(可能是查了数据库、做了计算),返回一个逻辑视图名,比如
"hello"。 - SpringBoot找到模板引擎(比如Thymeleaf),定位到
resources/templates/hello.html这个模板文件。 - 模板引擎把Java对象的数据填进HTML模板的对应位置,生成一份完整的HTML。
- 这份HTML作为HTTP响应返回给浏览器。
关键点在于:模板引擎干的是“把Java变量的值填充进一个HTML字符串”的活儿。所以“简单网页”并不是什么高深的东西,本质上就是用Java产生数据,用一个模板把数据摆到该放的位置。
1.2 SpringBoot到底管不管“前端”
很多教程会混着讲,导致新手分不清边界。SpringBoot管的是端口监听、请求路由、业务数据组装、模板渲染这些事情。它不管CSS怎么写、JavaScript动画怎么做、页面长得好不好看。HTML模板文件里的<style>和<script>,还是老老实实的网页三件套写法。
明白了这个分工,你的学习路径就清晰了:
- 页面显示什么内容 → Thymeleaf模板里写标签
- 内容从哪里来 → Controller方法里造数据
- 页面长什么样 → CSS/Bootstrap负责
- 页面上的交互(弹窗、异步刷新) → JavaScript负责
SpringBoot把一个Java后端项目和网页串起来的核心,就是那一层Controller和一套模板引擎。搞懂了这中间的胶水,剩下的都是体力活。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与第一个SpringBoot网页项目
2.1 版本选择别纠结
很多人会在Spring Boot 2.x还是3.x之间耗掉大量时间。我给你一个非常务实的建议:如果是学习、做毕设、做内部系统,直接用Spring Boot 3.x最新稳定版即可。如果团队里已有的旧项目用了2.x,或者某些第三方依赖还没适配Jakarta包名,再退回去用2.7.x。
Spring Boot 3.x最大的变化是javax换成了jakarta,以及最低要求Java 17。如果你本机装的是Java 8,那就别硬上3.x了,老老实实用2.7.x,否则编译时会有一堆莫名其妙的报错。
我自己的推荐组合是:
| 场景 | 推荐配置 |
|---|---|
| 学习/新项目 | JDK 17 + Spring Boot 3.x |
| 公司老项目/JDK8环境 | JDK 8 + Spring Boot 2.7.x |
| 需要极速启动/Maven未配置镜像 | 任意版本 + 阿里云Maven镜像 |
注意:JDK安装完后,在命令行里执行
java -version确认一下版本。IDEA里也记得在File -> Project Structure -> Project SDK里选对JDK,否则代码能写但跑不起来,这是新手最常见的坑。
2.2 用IDEA创建项目,最快且无脑
IDEA的Spring Initializr引导功能是创建SpringBoot项目最省事的途径,没有之一。步骤如下:
- 打开IDEA,选择
New Project。 - 左侧选
Spring Boot,如果没有这个选项,就在生成器URL里填https://start.spring.io。 - 填写Group(一般是
com.example这种反写域名)、Artifact(项目名)。 - 项目类型选
Maven,语言选Java,Spring Boot版本选一个稳定的release版本。 - 在依赖选择页面,勾上
Spring Web和Thymeleaf。
这里我特别建议把Spring Boot DevTools也勾上,它会监控代码变化自动重启应用,开发时不用手动重启,能省掉大量无谓等待。注意它只是开发期工具,打包时不会被打进jar里。
点击Finish后,IDEA会自动联网拉取依赖。国内网络下第一次加载Maven依赖可能会比较慢,建议在~/.m2/settings.xml里配一个阿里云镜像:
xml复制<mirror>
<id>aliyunmaven</id>
<mirrorOf>central</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
配好镜像之后,新建项目的依赖下载基本是秒级完成。
2.3 骨架项目的结构怎么看
项目创建成功后,你会在目录里看到一个非常标准的骨架。不要被一堆文件吓住,真正需要关注的只有几个:
text复制src/main/java/com/example/demo/DemoApplication.java // 启动类
src/main/resources/application.properties // 配置文件
src/main/resources/templates/ // 放Thymeleaf模板,也就是网页
src/main/resources/static/ // 放CSS、JS、图片等静态资源
src/main/resources/application.yml // 也可以换成yml格式
DemoApplication里只有一个main方法,上面标着@SpringBootApplication注解,运行这个main方法就能启动整个项目。我见过有新手去删掉这个类里的注解,结果项目怎么都启动不了——这个注解包含组件扫描、自动配置,不要动它。
2.4 启动验证与浏览器访问
直接右键运行DemoApplication,等控制台出现Started DemoApplication in x.xxx seconds,就说明启动成功。默认端口是8080,浏览器访问http://localhost:8080。
如果你看到的是Whitelabel Error Page(白屏带错误信息),不要慌,这是好事,说明应用起来了,只是没有匹配到任何路径。接下来我们就让它有路径可走。
顺带说一个我经常遇到的坑:桌面应用比如微信、企业IM可能会占用8080端口。如果日志里出现Port 8080 was already in use,可以在配置文件里改端口:
properties复制server.port=8081
3. 不写一行Java代码,先让页面出来
3.1 SpringBoot的静态页面规则
很多人不知道,SpringBoot其实自带一个“静态网站”功能。只要把HTML文件放在src/main/resources/static/目录下,不需要Controller、不需要模板引擎,浏览器直接就能访问。
这个设计是为了方便你放前端资源和固定的页面。比如:
static/index.html→ 访问http://localhost:8080/(首页自动找index.html)static/about.html→ 访问http://localhost:8080/about.htmlstatic/css/style.css→ 访问http://localhost:8080/css/style.cssstatic/js/app.js→ 访问http://localhost:8080/js/app.js
你可以在static目录里放一个最简单的hello.html:
html复制<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>第一个页面</title>
</head>
<body>
<h1>这是一个静态页面</h1>
<p>没有任何Java代码参与,SpringBoot直接把这文件发给了浏览器。</p>
</body>
</html>
重启项目,访问http://localhost:8080/hello.html,如果能看到内容,说明你的项目静态资源路径配置是正常的。
3.2 静态页面和模板页面的区别
静态页面是“死”的,文件里写什么就显示什么。而放到templates/目录下的Thymeleaf模板是“活”的,它需要Controller触发才能渲染。如果你把HTML文件放到templates下,但没写Controller,直接访问http://localhost:8080/xxx.html,大概率会得到404或Whitelabel Error。
初学者最容易在这个地方迷路。记住两句口诀:
- 想让浏览器直接访问、固定不变的内容 → 放
static/ - 想让Java数据注入进去、按请求动态变化的内容 → 放
templates/并写Controller
我在最初学的时候,把页面都放进了static,结果想从后端传个用户名进去,折腾半天传不了,最后才反应过来放错了目录。这个目录选择的代价,值得你一开始就记牢。
3.3 静态资源访问不到时怎么排查
如果页面放到static目录还是访问不到,按这个顺序排查:
- 确认文件路径大小写:
Hello.html和hello.html在Linux部署时是不同的文件,URL严格区分大小写。 - 确认后缀:访问静态页面必须带后缀(
index.html除外),不像模板可以省略。 - 确认是否启动成功:看控制台有没有报错,端口是不是被改了。
- 确认目录位置:是
src/main/resources/static/,不是src/main/java下,也不是src/main/webapp。
另外,如果你改了静态文件但浏览器里看到的还是旧内容,大概率是浏览器缓存,按Ctrl+F5强制刷新即可。不过等到了Thymeleaf阶段,还有一个服务端缓存要处理,后面我会专门讲到。
4. 引入Thymeleaf,让Java数据进到网页里
4.1 Thymeleaf是什么,为什么不是JSP
在SpringBoot里渲染动态网页,有几种选择:JSP、Freemarker、Thymeleaf。JSP是Java老牌技术,但SpringBoot对JSP的支持比较麻烦——需要额外的依赖,默认不支持,打包成jar也有坑。Thymeleaf是SpringBoot官方推荐的首选模板引擎,语法更像HTML,浏览器直接打开模板文件时也能看到结构,不会像JSP那样显示出一堆标签。
Thymeleaf的核心思想是:在普通的HTML标签上,额外写一些th:开头的属性。渲染时这些属性会被解析,渲染完成后,生成一个干净的不带th:属性的HTML发送给浏览器。
举个例子:
html复制<p th:text="${message}">这里的内容会被替换</p>
当用户直接打开这个HTML文件时,看到的是“这里的内容会被替换”;但当SpringBoot渲染后,${message}的值会替代内部文字。这样设计有个好处:前端和后端可以并行开发,前端看到的是设计稿的默认文案,后端渲染后是实际数据。
4.2 编写第一个带动态数据的Controller
在项目里新建一个Controller类,包名建议是com.example.demo.controller。注意新建类要放在DemoApplication所在包的子包下面,否则组件扫描不到,Controller不生效。
java复制package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class PageController {
@GetMapping("/hello")
public String hello(Model model) {
model.addAttribute("message", "这是来自SpringBoot的问候");
return "hello";
}
}
关于return "hello"这个地方,新手一定要理解清楚:这里的hello不是URL,而是templates目录下模板文件的文件名(不需要写.html后缀)。SpringBoot会去templates/hello.html找模板。
4.3 让模板文件显示数据
在src/main/resources/templates/下新建hello.html:
html复制<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Thymeleaf页面</title>
</head>
<body>
<h1 th:text="${message}">默认标题</h1>
</body>
</html>
重启项目,访问http://localhost:8080/hello,你会看到<h1>标签的内容变成了“这是来自SpringBoot的问候”。如果你用@Controller而不是@RestController,这个返回过程就自动完成了。如果这里误用了@RestController,返回的就不是页面,而是一段纯文本,这是个高频踩坑点。
4.4 基础语法:变量、循环、条件判断
光能显示一个字符串还不够,实际页面肯定要处理列表、条件。Thymeleaf的三个最基础也最常用的语法:
- 变量输出:
th:text="${user.name}" - 循环:
th:each="item : ${list}" - 条件:
th:if="${condition}"
假设要显示一组用户,Controller里可以做这样的事:
java复制@GetMapping("/users")
public String users(Model model) {
List<User> userList = List.of(
new User("张三", "zhangsan@example.com"),
new User("李四", "lisi@example.com")
);
model.addAttribute("users", userList);
return "user-list";
}
模板里写:
html复制<table border="1">
<tr>
<th>姓名</th>
<th>邮箱</th>
</tr>
<tr th:each="user : ${users}">
<td th:text="${user.name}">张三</td>
<td th:text="${user.email}">zhangsan@example.com</td>
</tr>
</table>
渲染时,th:each会循环生成多行<tr>,每一行里再用th:text填入对应值。
条件判断也很直白:
html复制<p th:if="${user.name == '张三'}" th:text="'这是张三'"></p>
<p th:unless="${user.name == '张三'}" th:text="'这不是张三'"></p>
注意:在Thymeleaf里引用Java对象属性时,字符串比较尽量用
==来写,它在OGNL表达式里会被转换成equals处理。如果写${user.name.equals('张三')}也行,但没必要那么啰嗦。
5. 表单提交与数据回显:做出一个能交互的页面
5.1 一个带表单的完整小案例
网页的核心价值是交互。最典型的场景就是用户填一个表单,提交后后端接收数据、做处理、再显示结果。我们做一个“留言本”:用户在表单里填名字和内容,提交后数据被处理后重新渲染在页面上。
先定义一个简单的实体类,放在com.example.demo.entity包下:
java复制package com.example.demo.entity;
public class Message {
private String name;
private String content;
public Message() {
}
public Message(String name, String content) {
this.name = name;
this.content = content;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
实体类必须有getter/setter方法,这是Java Bean规范,SpringMVC在参数绑定时依赖它们。如果你用IDEA的快捷键Alt+Insert生成,就不用一个个手敲。
5.2 Controller处理GET和POST
Controller里同时提供两个方法:GET时显示表单页,POST时接收表单数据并回显。
java复制package com.example.demo.controller;
import com.example.demo.entity.Message;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class MessageController {
@GetMapping("/message")
public String showForm() {
return "message-form";
}
@PostMapping("/message")
public String submitMessage(Message message, Model model) {
// 模拟后端处理:把名字的首字母转大写
String processedName = message.getName().trim().substring(0, 1).toUpperCase()
+ message.getName().trim().substring(1);
message.setName(processedName);
model.addAttribute("message", message);
return "message-result";
}
}
这里有个SpringMVC的参数绑定机制值得说一下:submitMessage(Message message, Model model),SpringMVC会自动把HTTP请求里的参数(name=xxx&content=yyy)按属性名匹配到Message对象上,这要求表单字段的name属性必须和实体类的属性名一致。如果不一致,后端拿到的就是null,这是表单开发中最常见的毛病。
5.3 表单页面的写法
templates/message-form.html内容:
html复制<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>留言板</title>
</head>
<body>
<h1>写一条留言</h1>
<form th:action="@{/message}" method="post">
<p>
姓名:<input type="text" name="name">
</p>
<p>
留言内容:<input type="text" name="content">
</p>
<button type="submit">提交</button>
</form>
</body>
</html>
注意th:action="@{/message}",这里用了URL表达式@{}。它的作用是生成相对路径。如果页面部署时带了context-path(比如/demo),@{/message}会自动生成/demo/message,避免因为部署路径变化导致404。直接用action="/message"虽然也行,但应用部署到子路径下就会失效,这是一个部署后才会暴露出来的坑。
提交后进入message-result.html:
html复制<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>留言结果</title>
</head>
<body>
<h1>提交成功</h1>
<p>留言人:<span th:text="${message.name}">姓名</span></p>
<p>留言内容:<span th:text="${message.content}">内容</span></p>
<a th:href="@{/message}">再写一条</a>
</body>
</html>
完成之后,你就有了一条完整的“填表→提交→后端处理→回显”的链路。这种模式可以扩展成登录、注册、问卷、留言等各种业务。
5.4 跳转方式:转发与重定向
上面的代码里,POST完成后直接return "message-result",这是服务端转发(forward),浏览器上的URL还是/message(POST),刷新页面时浏览器会提示“重新提交表单”。
比较好的实践是PRG模式(Post-Redirect-Get):POST处理完后重定向到GET请求。具体做法是返回"redirect:/message/success",然后另一个GET方法负责渲染成功页。这样刷新时不会重复提交表单。
java复制@PostMapping("/message")
public String submitMessage(Message message) {
// 处理后跳转到成功页
return "redirect:/message/success?name=" + message.getName();
}
@GetMapping("/message/success")
public String success(@RequestParam String name, Model model) {
model.addAttribute("name", name);
return "message-result";
}
重定向的好处是URL干净,刷新安全,适合“提交完成”这种场景。转发的好处是能通过Model带数据,适合表单校验失败回显这种场景。两者各有分工,别混着用。
6. 给页面加点样式:不用写CSS也能好看一点
6.1 引入Bootstrap是最快方案
很多Java后端开发者的前端审美其实差不多,自己手写CSS纯属浪费时间。在页面里引入Bootstrap是性价比最高的方案,几分钟就能让页面从“原始HTML”变成“像回事的网页”。
选择本地引入还是CDN引入,我建议你这样判断:
- 如果应用会被内网部署,内网机器可能无法访问外网CDN,那就必须本地引入。
- 如果只是本机开发调试、面向公网访问,CDN引入最省事。
CDN引入只需要在模板的<head>里加两行:
html复制<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
本地引入则要把下载的bootstrap.min.css放到static/css/下,然后:
html复制<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
这一步是我自己最常踩坑的地方:如果CSS文件放到static/css下,但模板里写的是href="/css/bootstrap.min.css",开发环境能访问,部署到带context-path的环境就会404。在Thymeleaf模板里,引用任何静态资源都要用th:href或th:src加上@{}表达式,这是铁律。
6.2 给留言板套一个像样的框架
上面留言板的代码太简陋了,如果套上Bootstrap会体面很多。改造后的表单页body部分:
html复制<body class="bg-light">
<div class="container mt-5" style="max-width: 600px;">
<div class="card shadow-sm">
<div class="card-header">
<h4 class="mb-0">写一条留言</h4>
</div>
<div class="card-body">
<form th:action="@{/message}" method="post">
<div class="mb-3">
<label class="form-label">姓名</label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">留言内容</label>
<input type="text" name="content" class="form-control" required>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
</div>
</div>
</div>
</body>
不需要自己写复杂的CSS,页面立刻有了卡片、间距、圆角、按钮样式。Bootstrap的学习成本很低,记住几个class名就行:container负责居中,card负责卡片,btn负责按钮,form-control负责输入框。
6.3 CSS缓存与版本更新问题
前端资源和后端模板不同,CSS文件一旦被浏览器缓存,改了也看不到效果。开发阶段可以在源码不加版本号直接刷新,部署后想平滑更新,一个可行的做法是在引用静态资源时加版本号:
html复制<link th:href="@{/css/style.css?v=20240901}" rel="stylesheet">
?v=后面的值随便写,改了版本浏览器就会重新拉取。这是一种土办法,但很管用。对简单项目来说根本不需要引入webpack之类的构建工具。
7. 打包部署:把网页项目变成可运行的jar
7.1 打包前的配置检查
开发阶段完成后,要部署到服务器上,SpringBoot的默认打包方式是打成可执行jar。打包前确认几个地方:
pom.xml里<packaging>是jar。pom.xml里有spring-boot-maven-plugin,没有这个插件打出来的jar无法直接运行。- 确认
application.properties里没有写死本机相关的路径、IP等信息。
在IDEA右侧的Maven面板里,双击package,或者命令行在项目根目录执行:
bash复制mvn clean package -DskipTests
第一次打包会下载插件依赖,耐心等待。打包完成后,target/目录下会出现两个jar文件:一个带.original后缀的普通jar,一个是独立的可执行jar。运行的是不带.original的那个。
7.2 运行jar的常用命令与参数
在服务器上运行:
bash复制java -jar demo-0.0.1-SNAPSHOT.jar
默认端口是8080。如果需要临时改端口,不修改配置文件:
bash复制java -jar demo-0.0.1-SNAPSHOT.jar --server.port=9090
命令行参数会覆盖配置文件里的值,这是SpringBoot的配置优先级规则。这个技巧在同机跑多个SpringBoot应用时特别实用。
如果想让应用在后台运行,Linux下用:
bash复制nohup java -jar demo-0.0.1-SNAPSHOT.jar > app.log 2>&1 &
查看日志用tail -f app.log。
7.3 部署后页面404/静态资源丢失的排查
部署后访问http://服务器IP:8080/,如果出现404或样式丢失,按下面顺序排查,这几乎覆盖了90%的情况:
- 确认安全组/防火墙开放了端口:云服务器默认只开放22、80等少数端口,8080可能在防火墙外。登录运营商的控制台,在安全组里放行8080入方向。
- 确认进程是否存活:执行
ps -ef | grep java,或者curl http://localhost:8080/hello看有没有响应。 - 看控制台启动日志:有没有打印
Tomcat started on port。 - 确认有没有context-path:检查
server.servlet.context-path有没有设置,有的话访问路径要带上前缀。
我见过大量新手在本地跑得好好的,部署到服务器就白屏,最后查下来是防火墙问题——这不是代码问题,是环境问题。
8. 开发期提高效率的几个小设置
8.1 DevTools热重启
没配DevTools时,每次改Java代码都要手动重启,开发效率极低。加了DevTools依赖后,IDEA里改完代码,只要触发一次编译(Ctrl+F9),应用会自动重启。
DevTools的自动重启只是重启应用,不是像springloaded或jrebel那样热替换Java类。但实际体验已经不错了,因为启动速度本身就快。
如果在IDEA里改了模板文件不重启也能生效,需要额外配置模板缓存为false。在application.properties里加:
properties复制spring.thymeleaf.cache=false
spring.web.resources.cache-period=0
这段配置只在开发期有效。打成jar部署时建议把缓存改回true,或者不写这几行,因为生产环境模板热更新既没必要也有性能损耗。
8.2 模板页面修改后不生效
如果加了spring.thymeleaf.cache=false还是不生效,检查下面两个原因:
- 模板文件是不是放错了位置,放到了
static而不是templates。这个错误导致Thymeleaf根本找不到文件,报错信息是Error resolving template。 - 是不是IDEA没有自动编译
resources目录下的变更。执行一次Build -> Rebuild Project强制刷新资源。
一个非常隐蔽的坑是:模板文件名和Controller返回的逻辑视图名不一致,比如hello.html实际是全角字符,或者文件名带了空格,渲染时会报模板找不到。看控制台的错误信息里会给出实际查找的路径,对照排查即可。
8.3 乱码问题
页面中文乱码,绝大多数是编码不一致。三个位置必须统一为UTF-8:
- 模板文件本身的编码(IDEA右下角可以切换,统一用UTF-8)。
- HTTP响应编码(
spring.thymeleaf.encoding=UTF-8,默认就是,但显式写上更安心)。 - 数据库连接的编码(如果后面接了MySQL,在连接URL里加
characterEncoding=utf8)。
如果页面是浏览器编码识别错误,在模板的<head>里加上<meta charset="UTF-8">,这一个标签能解决大部分页面乱码。
9. 从“简单页面”到“完整项目”的进阶路径
9.1 什么时候该引入前后端分离
文章开头我提到SpringBoot + Thymeleaf这种服务器端渲染方式,对“简单项目”是最优解。但项目复杂到一定程度后,这种方式就开始吃力:前端页面交互越来越复杂、前端工程师和后端工程师并行开发、需要大量异步更新页面内容,这些时候前后端分离更合适——前端用Vue/React,后端只提供JSON接口,两者独立部署。
判断标准很简单:你的页面是否需要频繁无刷新更新局部内容、是否需要多个前端页面复用同一套后端接口。如果是,就得考虑分离。如果只是展示数据、提交表单、点击跳转,Thymeleaf完全扛得住,强行上前后端分离反而增加工程复杂度。不少毕设项目就是“伪前后端分离”,一个简单页面硬拆成Vue+SpringBoot,部署时还要处理跨域、Nginx,纯粹给自己添堵。
9.2 加数据库:从内存走向持久化
模板引擎的问题解决了,紧接着就是数据从哪来。本地写死的数据撑不起真实项目,需要接MySQL。Spring Boot里接入数据库的路径是这样的:
- 引入依赖:
spring-boot-starter-data-jpa或mybatis-spring-boot-starter。 - 配置数据源:在
application.properties里写URL、用户名、密码。 - 写实体映射和数据访问接口。
- 在Controller里调用数据访问层。
JPA适合简单场景,MyBatis适合SQL可控性要求高的场景。从“简单页面”跨向“完整项目”,关键是先把Controller改成从数据库取数据,而不是从List里硬造。
9.3 模板复用的思路
页面多了以后,每个页面都复制一份完整的HTML结构会非常难维护。Thymeleaf提供了模板布局功能:公共的导航栏、页脚抽到一个模板里,其他页面通过th:replace或th:insert引入。
html复制<header th:replace="~{layout/header :: header}"></header>
这样改动导航栏只需改一处。不过对于只有两三个页面的小项目,先不用上这个特性,等页面数量上来了再重构不迟。过早引入抽象反而浪费精力。
9.4 安全与拦截的起点
做个能提交表单的页面后,紧接着遇到的问题就是登录:某些页面要登录才能看。Spring Security的引入也会成为一个里程碑节点:
- 引入
spring-boot-starter-security。 - 配置用户信息与密码加密器。
- 定义哪些路径放行、哪些路径要认证。
- 处理登录成功/失败跳转。
注意,引入Spring Security后,默认情况下所有接口都会被拦截,页面会跳到登录页。如果应用没有任何登录按钮,会觉得自己写的东西突然被锁上了。这时要么配置放行,要么就顺手做一个登录功能——反正表单处理的套路你已经会了。
10. 我在反复操作中总结的几条经验
说几个我自己在实际操作中的体会,可能比前面的教程更值得记住。
第一,模板引擎的“模型”思维一定不要搞反。 我刚学的时候,总是想着“怎么在Java代码里拼HTML字符串”。比如写一段String html = "<p>" + name + "</p>"再返回给浏览器。这种方式虽然也能跑,但代码会随着页面复杂度膨胀到不可维护。Thymeleaf的思路是:页面框架是静态的,只有数据点是动态的。做页面时先在浏览器里把纯HTML做好看,再在需要变化的地方填th:属性。顺序应该是“先把页面写出来,再注入数据”,而不是反着来。
第二,每次新建项目,先把application.properties里几条基础配置存成模板。 我给自己存了一份包括端口、时区、编码、模板缓存的配置片段,创建新项目时直接粘上,省得反复踩乱码、时区的坑。
properties复制server.port=8080
spring.application.name=demo
spring.thymeleaf.cache=false
spring.thymeleaf.encoding=UTF-8
spring.jackson.time-zone=GMT+8
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
第三,模板文件里写URL时,能不带/开头就不带。 新手最容易把链接写死成/css/style.css,一旦部署到context-path目录下全部失效。统一用th:href="@{/css/style.css}",让框架帮你算前缀,就不会有这种问题。同理,Controller方法之间的跳转,用redirect:/xxx而不是直接在字符串里拼完整路径。
第四,学会看控制台启动日志。 很多人遇到报错就直接百度,其实SpringBoot的报错信息非常良心。启动失败时日志会告诉你:Port 8080 was already in use、Error resolving template、Failed to configure a DataSource。这些错误信息里的关键短语就是搜索引擎的关键词,比截图整个日志再提问强得多。
第五,不要被“前后端分离”绑架。 在社区里混久了,很容易产生一种错觉:不上Vue、不搞Nginx就不算正经Web项目。但对一个“简单项目”来说,Thymeleaf服务端渲染这套东西才是最快、最稳、最少代码的方案。我拿它做过给部门内部用的库存查询页、给客户做的单页申请表单、给毕设学生做的数据展示系统,都是两三个模板页面搞定。技术的选择服务于复杂度,别为了“主流”而自找麻烦。
第六,目录结构是可以随意调整的。 上面演示中,Controller放controller包,实体放entity包。这只是惯例,不是规范。三种经典分层(controller/service/mapper)可以等业务复杂后再引入。最开始甚至可以在启动类旁边放一个Controller类先跑通。我在小项目里从来不刻意拆分Service层——一个方法不调用其他类方法时,硬拆service反而增加文件跳转成本。等代码量大了再重构,Java世界里重构工具很成熟,前期别过度设计。
最后一个实用的小技巧:如果好奇某个页面模板有没有被Thymeleaf正确渲染,在浏览器里右键查看网页源代码。如果看到th:开头的东西还在页面上,说明模板没被处理——十有八九是访问的是static目录里的文件,而不是templates目录下的模板。如果看到干净的HTML,但数据没替换,那就要检查后端传的参数名和模板里${}中的名字是否一致。这一条排查思路,能解决你以后一大半的页面问题。
