
最近不止一个朋友问我说自己在网上找了不少 Spring Boot 的教程但跟着敲了半天要么版本对不上要么页面跑不出来最后连一个最简单的网页都没看到。他们问我的问题多半是这个能不能用最短的时间搭一个能访问的 Spring Boot 网页项目把整个链路跑通再说这篇就按我实际动手的习惯来讲。目标很简单从零开始用 Spring Boot 做一个能在浏览器里打开的网页中间把常见坑都踩给你看最后再说怎么把它打包部署出去。无论你是刚学 Java Web 的学生还是想快速做个内部小工具的开发这条链路都适用。1. 项目到底要解决什么问题一个网页应用的完整链路1.1 先想清楚这四层东西是什么很多人搭 Spring Boot 网页项目失败不是代码写错而是脑子里对“网页应用”这件事的层次是模糊的。我把这个链路拆成四层第一层是 HTTP 入口。浏览器输入 localhost:8080请求怎么进来、谁来接收这一层由 Spring MVC 负责。Spring Boot 启动后内置了一个 Tomcat端口默认 8080所有请求先到这里。第二层是业务处理。Controller 接收请求之后调用业务方法、查数据、组织返回内容。这一层是代码逻辑的主体。第三层是视图渲染。服务端把数据填充到 HTML 模板里生成完整页面返回浏览器。Spring Boot 推荐用 Thymeleaf不用学一套新语言它本质就是 HTML 加几个特殊属性。第四层是数据访问。页面要展示动态数据必然要连数据库。Spring Data JPA 能让你少写大量 SQL几乎只要写接口。把这四层串起来理解再去看代码就不会觉得 Spring Boot 是一坨“自动魔法”。它的自动配置帮我们省去了以前 SSM 时代繁琐的 XML 配置但底层逻辑没有变。1.2 为什么用 Thymeleaf 而不是一上来就 Vue 前后端分离说到网页很多人第一反应是 Vue、React。但“快速搭一个简单网页”我的建议是先用 Thymeleaf。原因很实在前后端分离意味着你要维护两个工程、处理跨域、处理接口联调对一个目标是“尽快跑通”的项目来说成本太高了。Thymeleaf 是服务端模板引擎页面和接口在一个工程里启动就是一个完整应用。你访问一个 URL服务端把数据塞进 HTML返回的就是能看到的页面。整个过程只是多了几个 th: 开头的属性HTML 本身还是那套 HTML。如果以后项目规模变大再拆成 Vue 前端 Spring Boot 接口时机也完全来得及。Spring Boot 提供 RestController 返回 JSON天然支持前后端分离。一个项目从小到大的演变路径应该从一个完整可用的单体网页开始而不是一开始就拆成微服务。2. 从零创建工程五分钟跑通 Hello World2.1 用 Spring Initializr 快速生成骨架现在创建 Spring Boot 工程不用自己慢慢搭了。打开 start.spring.io左边选构建工具 Maven、语言 Java、Spring Boot 版本新项目我一般选 3.2 稳定版但要看本机 JDK 版本是不是 17如果环境还是 JDK8选 2.7.18 更稳依赖勾上 Spring Web、Thymeleaf、Spring Data JPA、H2 Database。点 Generate 下载压缩包解压后用 IDEA 打开等 Maven 拉完依赖。生成的工程目录是固定的这套结构就是 Spring Boot 项目的标准骨架src/main/java/com/example/demo ├── DemoApplication.java └── controller/ 自己新建 src/main/resources ├── static/ CSS、JS、图片等静态资源 ├── templates/ Thymeleaf 模板页面 └── application.properties src/test/java这里要理解两个资源路径的区别templates 是服务端模板引擎找页面文件的地方Controller 返回一个视图名Thymeleaf 会去 templates 里找同名 HTML 渲染static 里的文件则是原样返回给浏览器不经过模板引擎。很多人后面遇到页面 404都是因为把模板放错了目录。2.2 手动写一个最小工程也不是不行如果你公司的网络访问 start.spring.io 不方便或者你更想理解每个文件是干什么的手动创建也不复杂。核心就一个 pom.xml 和启动类。先看 pom 的关键部分parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.0/version relativePath/ /parent再添加依赖spring-boot-starter-webWeb 应用核心内置 Tomcat 和 Spring MVC、spring-boot-starter-thymeleaf模板引擎、spring-boot-starter-data-jpa数据库访问、h2内存数据库开发期用、lombok省去 Getter/Setter 模板代码。启动类只需要一个注解和一个 main 方法这也是 Spring Boot“约定优于配置”思想的直观体现SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }运行 main 方法控制台出现 “Tomcat started on port(s): 8080”项目就算是起来了。到这一步还没有页面但整条 HTTP 链路已经通了后面是在这个骨架上填肉。2.3 Maven 依赖下载慢的老问题国内拉 Maven 依赖慢是很多新人碰到就想放弃的第一个坎。解决办法是把中央仓库换成镜像仓库在 Maven 的 settings.xml 里配置mirror idaliyun/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror这是一步纯粹为了加速下载公共依赖包的常规开发配置。配置完在 IDEA 里刷新 Maven依赖会明显快起来。如果某个依赖还是拉不下来去本地仓库目录默认在用户目录下的 .m2/repository找到对应的 .lastUpdated 文件这是之前下载失败留下的坏缓存把对应目录删掉重新刷新即可。3. 真正写一个能访问的网页3.1 用 Controller 渲染模板而不是 RestController先建一个 controller 包新建 PageController 类。这里我特别想强调注解的选择必须用 Controller不是 RestController。这两个注解的区别是新手最容易踩的坑。Controller 配合模板引擎方法返回的是视图名称“index”对应 templates/index.htmlRestController 是 Controller 加 ResponseBody 的组合方法返回什么就原样写回浏览器返回“index”浏览器看到的是一行纯文本“index”不是页面。网页应用要用 Controller接口服务才用 RestController。写一个最简单的首页Controller public class PageController { GetMapping(/) public String index() { return index; } }然后在 templates 下新建 index.html写一句“HelloSpring Boot 网页”。重启工程打开 http://localhost:8080页面就出来了。从零到你第一次在浏览器里看到自己的页面链路其实就这三个环节请求进来、Controller 接收、返回模板。3.2 让页面动态起来向模板传数据如果页面永远是写死的用模板引擎就没有意义了直接放静态页面还更快。模板的威力在于能填充动态数据。改造 Controller往 Model 里塞数据GetMapping(/) public String index(Model model) { model.addAttribute(siteName, 我的工具站); model.addAttribute(welcome, 欢迎来到 Spring Boot 演示页面); return index; }模板里用 th:text 取出来!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title首页/title /head body h1 th:text${siteName}默认标题/h1 p th:text${welcome}默认欢迎语/p /body /html刷新页面能看到中文说明数据传递链路已经通了。注意 HTML 里写了默认文字是有作用的万一模板渲染失败页面至少显示默认值不会白屏排查问题也更直观。很多人一上来就用空标签结果数据没传成功时根本看不出来。3.3 搞清楚静态资源CSS 和 JS 放哪里页面光秃秃不好看得引入 CSS 和 JS。文件放 src/main/resources/static 目录下比如 static/css/style.css。页面里引入时推荐用 Thymeleaf 语法link relstylesheet th:href{/css/style.css}{} 会自动给资源地址加上应用的上下文路径将来部署在服务器子路径下也不会出错。这是个很多上过当的人才会注意的细节如果你直接写 href/css/style.css本地开发是没问题的但项目部署到带子路径的服务器上时静态资源全会 404。JS 文件同理放 static/js 下用 th:src{/js/app.js} 引入。另一个细节是 Spring Boot 对静态文件目录有默认搜索顺序META-INF/resources、resources、static、public。我习惯统一放 static不要东一个目录西一个目录。3.4 页面之间怎么跳转多页面应用要有导航。Spring MVC 里写第二个页面对应一个路由方法GetMapping(/about) public String about() { return about; }页面间跳转就用链接a th:href{/about}关于本站/a这里要记得 Controller 里必须有对应的 GetMapping 映射否则用户点过去就是白标错误页。建议自己维护一份简单的路由清单把 URL 路径、Controller 方法、模板文件名一一对应页面少的时候看不出价值页面多到十个以上时这份清单能救你。4. 常见问题排查版本、404、端口、乱码一次说清4.1 Spring Boot 版本太高导致的启动失败这几年大家从 Spring Boot 2.x 迁到 3.x踩坑的核心差异就两个JDK 版本和包名。Spring Boot 3 要求 JDK 17 以上如果你本机是 JDK8启动 3.x 工程会直接报错。这时候要么升级 JDK要么把 Spring Boot 版本降到 2.7.x。2.7 是维护版本对 JDK8 的朋友非常友好。另一个问题是包名。Spring Boot 3 里原来的 javax.servlet 变成了 jakarta.servlet。如果你用的工程是 Spring Boot 3代码还按网上的 2.x 教程写 javax.*编译就会失败。这个问题在 IDEA 里往往还能看出点线索但最稳妥的办法是创建工程前先确认 JDK 版本再决定用哪个 Spring Boot 大版本不要照搬网上教程的版本。4.2 页面出现 Whitelabel Error Page 怎么定位白标错误页是 Spring Boot 新手最常见的拦路虎。看到这个页面第一反应不要慌而是去看控制台日志真正的异常堆栈在那里。大概率是下面几种情况Controller 返回的模板名和 templates 下的文件名对不上大小写也算。页面放错了目录把模板放进了 static。路径映射不对访问的 URL 没有对应的 GetMapping。我的排查顺序是看 URL 对不对再看控制台有没有 ERROR再检查 Controller 返回值最后确认模板在 templates 目录。模板放错目录是我见过最多的低级错误因为看起来两个目录都能放 HTML实际上一个是给后端模板引擎处理的一个是给前端原样返回的完全是两种处理方式。4.3 端口被占用别慌换一个就行启动时报 8080 端口被占用很多人会到处找进程。其实最简单的办法是换端口在 application.properties 里改一行server.port8081改完重启立刻就能用。如果你非要揪出是谁占了端口Windows 下用 netstat -ano 查 PID再到任务管理器里定位进程macOS 或 Linux 下用 lsof -i:8080。注意别乱杀系统进程确认是自己之前没关掉的 Java 进程再结束。还有一种情况是 IDE 里同时启动了多个实例。IDEA 右上角把上一个进程停掉再启动新的就不会有端口冲突了。4.4 中文乱码先统一编码再动手Spring Boot 默认 UTF-8正常不会乱码。如果你发现乱码先看是不是文件编码被工具改成 GBK 了。IDEA 右下角会显示当前文件编码把它统一成 UTF-8 再启动。如果项目连了 MySQL乱码可能出在 JDBC 连接串上。在连接参数后面加useUnicodetruecharacterEncodingutf8中文乱码问题最忌乱试一会儿改页面编码一会儿改配置很容易越调越乱。先确定是文件层、响应层还是数据库层的问题再对症下药。4.5 JPA 连接数据库的初始化问题如果引入了 JPA 依赖但没有配数据源Spring Boot 启动时会因为找不到合适的数据库驱动而报错。开发阶段先用内置的 H2 内存库最省事配置一行搞定spring.datasource.urljdbc:h2:mem:testdb spring.h2.console.enabledtrue spring.jpa.hibernate.ddl-autoupdatespring.jpa.hibernate.ddl-autoupdate 意思是根据实体类自动创建或更新表结构非常适合开发期。上线之前记得改成 validate 或 none避免应用启动时不小心改动生产环境的表。H2 还自带一个网页控制台启动后访问 /h2-console 就能可视化看表数据调试数据访问逻辑很方便。5. 升级到带数据的网页展示和录入5.1 实体类数据库表在 Java 里的映射网页要有价值不能只读写死的文字。我用一个最经典的场景来说明文章列表。先建实体类Entity public class Article { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String title; private String content; // 有 Lombok 的话加 Data没有就写 Getter/Setter }Entity 标注这是一个 JPA 实体Id 标记主键GeneratedValue 表示主键自增。用生活化的话说实体类就是数据库表在 Java 世界里的一张“行数据模板”一个字段对应一个列。配置了 ddl-autoupdate 后JPA 会自动根据这个类建表省去手写建表 SQL。5.2 Repository 接口最省事的数据库访问层Spring Data JPA 最大的优势是只需要写接口不用写实现类public interface ArticleRepository extends JpaRepositoryArticle, Long { ListArticle findByTitleContaining(String keyword); }JpaRepository 内置了一堆增删改查方法。findByTitleContaining 这种按照方法名自动生成查询的写法看起来很魔法但它好处很大如果实体里没有对应字段启动时就会直接报错问题提前暴露比等运行时 SQL 报错容易排查。你可以很快做一个搜索功能Controller 接收关键词调用这个接口把结果塞进 Model页面重新渲染列表。5.3 Controller 把数据交给页面Controller public class ArticleController { private final ArticleRepository articleRepository; public ArticleController(ArticleRepository articleRepository) { this.articleRepository articleRepository; } GetMapping(/articles) public String list(Model model) { model.addAttribute(articles, articleRepository.findAll()); return articles; } }关于依赖注入我推荐构造器注入而不是 Autowired 字段注入。字段注入写起来少几行但构造器注入的依赖关系一目了然测试时写起来也更顺。Spring 官方也推荐构造器注入这不是什么偏门技巧。页面里循环渲染数据ul li th:eacharticle : ${articles} th:text${article.title}/li /ul到这里一个最典型的 Spring Boot 网页数据链路就完整了页面 → Controller → Repository → 数据库 → 页面展示。很多课程设计、毕业设计的前半部分其实就是这个结构。5.4 表单提交让网页能写数据库只有展示没有录入系统不完整。加一个表单页面把用户输入提交到数据库。Controller 里加一个接收 POST 的方法PostMapping(/articles) public String create(ModelAttribute Article article) { articleRepository.save(article); return redirect:/articles; }ModelAttribute 会自动把请求参数绑定到 Article 实体的字段上表单里 input 的 name 和实体字段名对上就行。保存成功后用 redirect 重定向到列表页而不是直接返回模板。这个细节非常重要如果直接返回页面模板浏览器地址栏还是 POST 路径用户按 F5 刷新就会把同一条数据重复插入。重定向虽然多了一次请求但避免了重复提交问题。再加校验的话在实体字段上放 NotBlankController 参数加 Valid模板里用 th:errors 展示错误提示。这部分对表单项目几乎必用建议按这个路线走一遍。6. 打包部署让别人也能访问你的网页6.1 打成可执行 JAR开发时用 IDEA 启动但要交付给别人用需要打成一个可执行 jar。先确认 pom 里有 spring-boot-maven-plugin然后执行mvn clean package -DskipTests打包完成后target 目录下会生成一个 demo-0.0.1-SNAPSHOT.jar几十兆大小内部带了 Tomcat。在服务器上运行java -jar demo-0.0.1-SNAPSHOT.jar启动成功后访问 http://服务器IP:8080局域网里的其他人就能用了。如果希望进程在后台持续运行用 nohupnohup java -jar demo-0.0.1-SNAPSHOT.jar app.log 21 日志重定向到 app.log以后排查问题就看这个文件。我见过太多人上线时不看日志出了问题抓瞎这个习惯越早养成越好。6.2 用 Docker 做环境一致性部署如果服务器上装了 Docker用容器部署能省不少事。写一个最简单的 DockerfileFROM eclipse-temurin:17-jre COPY target/demo-0.0.1-SNAPSHOT.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app.jar]构建并运行docker build -t demo-app . docker run -d -p 8080:8080 --name demo-app demo-appDocker 的价值在于环境一致。你本地是这套环境容器里也是这套环境不会出现“在我电脑上明明能跑”的情况。基础镜像的 JDK 版本要和自己项目匹配Spring Boot 3 用 17Spring Boot 2.7 用 8选错会在启动时报版本错误。6.3 从简单网页走向完整项目的几个方向一个包含页面、数据、表单、部署的 Spring Boot 网页项目到这里就完整了。想继续升我建议按下面几个方向挑一个做定时任务启动类加 EnableScheduling方法加 Scheduled(cron 0 0 9 * * ?)就能实现每天固定时间执行任务。文件上传用 MultipartFile 接收文件保存到本地或对象存储。日志增强引入 Slf4j在请求入口记录参数和耗时排查问题时特别有帮助。登录与会话用户登录写 Session模板里根据登录状态展示不同内容。如果以后有多个 Spring Boot 项目要共享登录状态可以引入 Spring Session Redis 做统一会话这是单体项目走向多模块时要考虑的事。我给朋友做的小工具升级路径基本都是先跑通页面再加数据访问再套登录权限最后部署。每一步都能独立交付风险很低。6.4 什么时候该转向 Vue 前后端分离最后把前后端分离这件事说透。如果页面交互变得复杂比如大量局部刷新、组件复用、要和前端工程师协作这时候再拆不迟。Spring Boot 侧只需要提供一个 REST APIRestController RequestMapping(/api/articles) public class ArticleApiController { GetMapping public ListArticle list() { return articleRepository.findAll(); } }前端用 Vue 的 axios 请求这个接口拿到 JSON 渲染页面。前后端分离不是比模板引擎更高端而是更适合多人协作和复杂交互。简单项目继续用 Thymeleaf反而更高效。我见过不少基于 Vue3 Spring Boot 的毕业设计类项目比如全流程进度管理系统本质也是这个思路Spring Boot 管数据接口Vue 管页面交互。真正的关键点在于把接口返回格式统一比如约定好 code、message、data 的结构前后端对接就会顺畅很多。7. 一个让开发效率明显提升的小习惯说一个我一直保持的习惯。每次新建 Spring Boot 网页项目我会给自己定一份“自检清单”按顺序过一遍启动后控制台出现 “Tomcat started”说明服务起来了。打开首页浏览器开发者工具 Network 面板里看 CSS、JS 是否返回 200。用一个接口做测试看返回的是 JSON 还是 HTML确认用的是 RestController 还是 Controller。往数据库写一条测试数据刷新页面看展示是否正常。故意访问一个不存在的路径看看容错是否到位。这个清单看起来简单但它能让“项目能不能跑”这件事从玄学变成科学。网页应用常见的坑比如静态资源 404、端口占用、模板路径错误基本都能在前三步暴露出来。坚持一段时间你再去看别人贴出来的报错会突然发现大部分问题一眼就能定位。Spring Boot 的强大之处本来就是把开发者从繁琐配置里解放出来让人更专注于业务逻辑。但新手往往会在环境、版本、依赖上花掉大半时间。我的建议始终是先把我上面这条链路完整走一遍让一个带数据的页面跑起来再慢慢加功能。你在浏览器地址栏敲下回车、看到自己的页面稳稳出现的那一刻这个项目最核心的部分就已经完成了。后面所有东西都是在这个地基上添砖加瓦。