1. 先想清楚:你要的"网页"到底是哪种
很多刚接触Java后端的朋友都会问同一个问题:怎么快速搭建一个SpringBoot项目网页?我刚开始学的时候也绕了不少弯路,要么卡在环境上,要么项目能启动但访问不到页面,网上教程又各说各话。这篇文章就是把我这些年反复用到的那套流程整理出来——从创建项目、写Controller、配置模板引擎、到打包部署,所有关键操作一次讲透,按着做基本20分钟内能跑出一个干净的Web页面。
先别急着写代码。动手之前花两分钟想清楚一件事:你说的"网页"是哪种网页?这个选择直接决定你后面用模板引擎还是前后端分离方案,也决定整个项目的复杂程度。很多人一上来就搭Vue、配代理、搞跨域,结果花了一晚上还是白屏,本质上是把简单问题复杂化了。
1.1 模板渲染型还是前后端分离型
网页项目有两条主流路线,一条是服务端模板渲染,一条是前后端分离。我每次带新手都建议先明确自己属于哪种场景,否则后面每一步都会纠结。
| 对比项 | 模板渲染(Thymeleaf等) | 前后端分离(Vue/React + REST API) |
|---|---|---|
| 项目形态 | 一个SpringBoot工程搞定 | 前端工程 + 后端工程,两个项目 |
| 页面数据来源 | 后端Model直接传数据 | 前端Ajax/Fetch调用接口取数 |
| 开发环境 | 不需要Node.js | 需要Node.js和前端构建工具 |
| 部署方式 | 打包一个jar就完事 | 前端构建后部署到Nginx或挂到后端静态目录 |
| 适合场景 | 简单页面、后台管理原型、毕设演示 | 交互复杂的中大型系统、多端复用接口 |
就拿"快速搭建简单SpringBoot项目网页"这个需求来说,我的建议非常明确:选Thymeleaf模板引擎。原因很简单,它的依赖少、学习成本低、调试直观,页面和后端代码在一个进程里,改完页面不用管跨域和代理问题,部署也是一个jar包走天下。
这不是说Vue不好。你后面如果要做复杂的单页应用、要跟前端团队配合,再拆SpringBoot + Vue也不迟。但第一步的目标是"跑通一个干净网页",那就别给自己加戏。
1.2 SpringBoot为什么能让你在20分钟内跑起来
说到SpringBoot,很多人背了概念却想不明白它到底简化了什么。一句话解释:它把"搭建一个能跑起来的Web服务"这件事所需要的手工配置全部自动化了。
在Spring之前,你要是用SpringMVC搭一个Web项目,得自己配web.xml、配DispatcherServlet、配视图解析器、配Tomcat,再找一个外部Tomcat部署上去。SpringBoot把这些步骤全部干掉,你只需要引入spring-boot-starter-web,它自动带上嵌入式的Tomcat和SpringMVC相关依赖,你启动一个main方法,服务就起来了。
这背后就是常被面试官问的"SpringBoot自动装配原理"。@SpringBootApplication其实由三个注解组成:@SpringBootConfiguration(表明这是一个配置类)、@EnableAutoConfiguration(开启自动装配)、@ComponentScan(扫描当前包及其子包下的Bean)。自动装配的核心逻辑是,SpringBoot在启动时会去读取META-INF/spring/...AutoConfiguration.imports文件里列出的所有自动配置类,然后根据类上的条件注解(比如@ConditionalOnClass、@ConditionalOnProperty)判断本次启动是否该生效。简单类比就是:你开了一家店,SpringBoot会自动帮你把水电、燃气、货架全部接好,至于你最终卖什么,才需要你自己操心。
所以搭建一个"简单SpringBoot项目网页"这个目标,技术含量真不在配置上,而在你能否理解约定优于配置这套规则,以及知道每个核心注解到底帮你做了什么。
1.3 最小化项目的骨架
一个最简单的页面项目,源码结构其实就这几个目录和文件:
demo-web/ ├── src/main/java/com/example/demo/ │ ├── DemoApplication.java // 启动类,带 @SpringBootApplication │ └── controller/ │ └── IndexController.java // 页面控制器 ├── src/main/resources/ │ ├── templates/ // Thymeleaf模板文件放这里 │ │ └── index.html │ ├── static/ // CSS、JS、图片放这里 │ │ └── css/ │ │ └── index.css │ └── application.yml // 配置文件 ├── pom.xml // (或用 build.gradle) └── target/ // 打包输出目录这个结构里,templates和static是SpringBoot的"约定优于配置"的经典体现——模板文件放templates,静态资源放static,你不需要在配置里声明它们的路径,框架会按默认规则去加载。我第一次用SpringBoot的时候不理解为什么页面跑不起来,后来发现就是我把index.html放错了目录,放到static下面直接被当静态文件返回了,模板语法一个字都没解析。
记住这句话:Controller返回的是视图名,视图名在templates下找同名模板;static下的文件是直接按路径访问的静态资源。这两个目录职责不同,别混着放。
2. 创建项目:三步走,别做多余动作
2.1 用Spring Initializr创建项目
创建SpringBoot项目最省事的方式是Spring Initializr,IDEA里内置了这个功能,也可以直接打开浏览器访问start.spring.io生成后导入。
我用IDEA的步骤是这样的:
- 打开IDEA,选择
New Project,左侧选Spring Initializr(高版本IDEA里叫Spring Boot)。 - 填写
Group(一般用com.example这种域名倒写格式)和Artifact(这就是项目名,比如demo-web)。 - 选择Java版本,这个必须和你本机JDK版本匹配,后面会专门讲版本坑。
- 在依赖列表里勾上
Spring Web、Thymeleaf、Spring Boot DevTools。 - 点击创建,等Maven或Gradle把依赖下载完。
如果用的是网页版Initializr,下载下来的zip包解压之后,用IDEA的Open选择目录,IDEA会识别出它是一个Maven/Gradle工程并自动导入。
创建的时候有一个细节容易被忽略:Package name默认会跟着Group和Artifact生成,比如com.example.demo。启动类DemoApplication.java会被放在这个包下。后面写Controller的时候,一定要把类放在这个包或者它的子包下,否则启动类扫描不到,接口全部404。这个坑我听人踩过无数次。
2.2 版本怎么选
版本选择是我最想说的话题。我知道很多人喜欢一上来就用最新版,但这在SpringBoot世界里特别容易翻车。热词里有"springboot版本太高",说的就是这件事。
我的建议是:如果你用JDK 8,直接选2.7.18。这是SpringBoot 2.x分支的最后一个正式版本,维护期结束得晚,兼容性经过大量项目验证,绝大多数教程、毕设、公司老项目用的都是这条线。你用2.7.18,网上搜问题基本都能搜到答案。
如果你本机是JDK 17或更高,可以选SpringBoot 3.x系列。但要注意,SpringBoot 3.0开始有两个重大变化:
- 基础要求JDK 17及以上;
- 原来的Java EE规范包名从
javax.*换成了jakarta.*。
这意味着你如果拿着2.x时代的代码去跑3.x,所有import javax.servlet之类的代码都要改成jakarta.servlet。我现在偶尔还会在老项目和新项目之间切换,两套包名混久了真的容易搞混。所以创建项目时,先确认JDK版本再选SpringBoot版本,这个顺序不能乱。
用Maven的话,pom.xml里的版本控制一般通过parent完成:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent>只要定义了parent版本,下面依赖里可以不写<version>,由SpringBoot统一管理。这个机制防止了很多依赖版本冲突的问题。
2.3 Maven和Gradle的差别
现在主流的项目构建工具两个:Maven和Gradle。IDEA新建项目默认支持两种,热词里有个"2020年的springboot项目早期gradle构建的项目配置文件",说明不少朋友接手过用Gradle构建的老项目。
如果你是自己新建项目,用Maven就够了,pom.xml的依赖声明长这样:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> </dependencies>如果是维护老项目,build.gradle可能是这样的:
plugins { id 'java' id 'org.springframework.boot' version '2.7.18' id 'io.spring.dependency-management' version '1.0.15.RELEASE' } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' }Gradle老配置文件里常见的坑是插件版本、仓库地址和Java版本不匹配。接手这类项目时,先看gradle/wrapper/gradle-wrapper.properties里的版本,再看build.gradle里的插件版本,最后看JDK版本。这个顺序能解决90%的导入报错。我个人的建议是:自己新建项目一律Maven,老项目保持原有构建方式,别因为"Gradle更酷"就顺手迁移,迁移不等于学习,改配置的时间够你写三个页面了。
3. 让首页跑起来:Controller + 模板引擎
项目创建好之后,最激动人心的一步就是让它返回一个网页。前面说过,页面方案选了Thymeleaf,所以核心工作就是写一个Controller,再写一个HTML模板。
3.1 第一个Controller:返回页面
在controller包下新建一个IndexController.java:
package com.example.demo.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class IndexController { @GetMapping("/") public String index() { return "index"; } }这段代码里最关键的是@Controller和返回字符串"index"。@Controller告诉Spring这个类专门处理Web请求;方法返回"index"不是返回文本内容,而是返回视图名。SpringBoot会拿着这个视图名去src/main/resources/templates/目录下找index.html,找到后渲染成页面返回给浏览器。
这里必须区分清楚@Controller和@RestController,这是新手最容易混的:
@Controller:返回视图名,配合模板引擎渲染页面。@RestController:等价于@Controller加@ResponseBody,返回的字符串或对象直接作为响应体,通常是JSON接口。
如果你现在写的是@RestController,return "index"浏览器只会看到一行字index,而不会渲染出页面。我当年就因为这个困惑了一下午,以为Thymeleaf没生效,其实选错了注解。
还有一个小知识点:如果你在@Controller方法里返回"redirect:/index",Spring会先发一个302响应让浏览器重新请求/index地址,用于登录跳转或表单提交后的跳转。如果只是单纯展示页面,直接返回视图名就行,不要动不动就用redirect。
3.2 Thymeleaf页面模板怎么写
在templates目录下新建index.html:
<!DOCTYPE html> <html lang="zh" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>我的第一个SpringBoot页面</title> <link rel="stylesheet" th:href="@{/css/index.css}"> </head> <body> <h1 th:text="${message}">默认内容</h1> <ul> <li th:each="item : ${list}" th:text="${item}"></li> </ul> </body> </html>模板语法不用全学,先把两个最常用的掌握就行了:
th:text:把后端传过来的数据渲染成HTML文本。${message}表示取Model中的message属性。th:each:循环遍历,item是集合中的元素,${list}是后端传过来的集合,语法很像Java的for-each,上手几乎没有难度。th:href:用来生成带上下文路径的链接。为什么要用它而不是直接写/css/index.css?因为你如果配了server.servlet.context-path,直接写死路径就会404,而th:href="@{/css/index.css}"会自动拼上上下文前缀。
<html>标签上的xmlns:th声明,主要是给IDE做提示用的,运行时没有它也能正常工作。我建议保留,因为IDEA有模板语法的高亮和提示,排查${}拼写错误会方便很多。
3.3 往页面传数据:几种写法对比
页面要展示的内容通常不是写死的,而是由Controller传入。最常见的写法是往方法里加一个Model参数:
@GetMapping("/hello") public String hello( @RequestParam(name = "name", defaultValue = "朋友") String name, Model model) { model.addAttribute("message", "你好," + name); model.addAttribute("list", Arrays.asList("SpringBoot", "Thymeleaf", "小项目", "部署")); return "index"; }这里两个点要理解:
第一,@RequestParam用来接收URL上的参数。比如访问/hello?name=张三,name就是张三;如果不带参数,就用defaultValue里的朋友作为兜底。这样即使参数缺失页面也不会崩。
第二,model.addAttribute("message", ...)把数据塞进Model,它在Controller和模板之间充当了一个临时搬运工。Thymeleaf里用${message}就能取到。整个过程可以类比成:Controller把材料放进一个筐子,Thymeleaf从筐子里拿出材料拼装成页面。
实际项目中,你还可以把查询到的用户列表、商品列表这类对象塞进去。Thymeleaf的th:each对这种场景几乎是标配,这也是为什么毕设里"SpringBoot + Thymeleaf"组合那么多见——列表页面的实现成本极低。
4. 配置细节决定"好用"程度
项目跑通之后,接着就是要让它用得顺手。这里说的"好用",第一指开发时改代码不用反复重启,第二指多环境切换时不用改代码,第三指启动信息清晰可读,这些全靠几个配置文件的小技巧。
4.1 application.yml核心配置项
SpringBoot默认的配置文件是src/main/resources/application.yml。一个页面项目最常用的配置如下:
server: port: 8080 servlet: context-path: /demo spring: application: name: demo-web thymeleaf: cache: false logging: level: org.springframework.web: info逐条说:
server.port:服务端口。默认是8080,如果跟你本机其他服务冲突,直接改这里。server.servlet.context-path:上下文路径。配置成/demo之后,访问路径就变成http://localhost:8080/demo/。如果你的页面接口将来要被网关或Nginx转发,这个配置很关键,因为所有路由都会多一层前缀。spring.thymeleaf.cache: false:关闭模板缓存。开发时改HTML立刻生效,不用重启项目。生产环境记得打开,或者用profiles分别配置。spring.application.name:服务名。单机页面项目它影响不大,但将来接Spring Cloud或者用日志系统时,有这个配置能少踩很多坑。
还有一个热词是"springboot yml随机端口",这个在本地联调多实例时很实用:
server: port: ${random.int[1024,65535]}每次启动都会随机分配一个1024到65535之间的端口,适合临时开多个实例验证某些行为。不过正常项目里我不建议把随机端口写进主配置,不然你永远不知道自己服务开在哪个端口上。
4.2 Banner生成与启动信息
热词里有"springboot banner生成器",这个东西确实有意思。SpringBoot启动时打印的ASCII字符图案就是Banner,网上有不少在线生成器可以把自己想写的文字转成字符画。你只需要把生成的文本保存成src/main/resources/banner.txt,下次启动时就会显示你自定义的图案。
我自己的习惯是给不同环境配不同的Banner,本地启动显示"LOCAL"、测试环境显示"TEST"、生产环境显示"PROD"。这样服务器上日志一滚屏,扫一眼就知道当前跑的是哪个环境,省得反复确认配置文件。如果嫌Banner碍眼,也可以直接关掉:
spring: main: banner-mode: "off"配置项的off要加引号,这是YAML解析的细节,我在这里栽过一次,YAML把裸的off可能会当成布尔值处理。
4.3 热更新配置:开发体验的关键
"SpringBoot Thymeleaf热更新"这个场景,其实就是我前面说的两个配置的组合:
spring.thymeleaf.cache: false——让模板内容不缓存。- 引入
spring-boot-devtools——让代码、配置文件、静态资源的改动触发自动重启。
spring-boot-devtools的机制不是热替换,而是"自动重启"。它内部维护两个类加载器,一个用于加载你写的业务代码,一个用于加载第三方依赖。当你改了代码,它只重启"业务类加载器",速度比手动重启快不少。实测下来,小项目的重启基本在一两秒内。
用IDEA的话,还需要在设置里开启Build project automatically,并且在Advanced Settings里勾选允许运行时编译。改了Java代码之后按Ctrl+F9重新编译,SpringBoot会感知到变化并自动重启;改了HTML和CSS之后,配合cache: false通常连重启都不用,刷新浏览器就能看到新页面。
有一点必须提醒:spring-boot-devtools在打包成可执行jar时默认会被排除掉,所以你不需要担心它污染生产环境。但是生产环境上的thymeleaf.cache一定要是true,否则性能会明显下降。
5. 从开发到上线:打包、部署与进阶
本地跑通只是第一步,一个完整的小项目最终要能部署到服务器上让别人访问。这里讲讲最常见的打包、Docker部署,以及后续接数据库、Redis、Vue时要注意的方向。
5.1 打包成jar并启动
在项目根目录执行:
mvn clean package -DskipTests如果你用的是Gradle老项目,对应的是:
./gradlew clean build -x test没有安装Maven但有mvnw文件的话,用./mvnw clean package -DskipTests,效果一样。
打包完成后,target目录下会生成一个demo-web-0.0.1-SNAPSHOT.jar。这个jar是SpringBoot的可执行fat jar,里面内嵌了Tomcat,不需要额外安装外部服务器。启动命令:
java -jar target/demo-web-0.0.1-SNAPSHOT.jar也可以临时指定端口和激活环境:
java -jar target/demo-web-0.0.1-SNAPSHOT.jar --server.port=9090 --spring.profiles.active=prod这两个参数会覆盖配置文件里的值,适合部署到不同环境时快速调整,不用为每个环境单独打一次包。
热词里有"怎么将springboot jar反编译成项目",我多说一句:fat jar里只有编译后的class文件和静态资源,注释、目录层级、元数据信息都会丢失,反编译回来的代码可读性极差,基本没办法继续开发。我见过有人因为源码丢失而去反编译jar,最后折腾两天还是重写了一个。我的建议是,项目一创建就初始化Git仓库,每个阶段提交一次,这比任何反编译工具都靠谱。
5.2 Docker部署示例
Docker部署SpringBoot页面项目的门槛非常低。在项目根目录放一个Dockerfile:
FROM openjdk:8-jre-alpine COPY target/demo-web-0.0.1-SNAPSHOT.jar /app/app.jar WORKDIR /app EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]如果用的是SpringBoot 3.x,基础镜像改成openjdk:17-jre-alpine或eclipse-temurin:17-jre。构建和运行:
docker build -t demo-web . docker run -d -p 8080:8080 --name demo-web demo-web-d表示后台运行,-p 8080:8080把宿主机端口映射到容器端口。启动后想看日志用:
docker logs -f demo-web页面项目部署到这一步已经绰绰有余了,不需要再上K8s或者微服务架构。Docker的好处是环境一致性,本地跑通的样子跟服务器上跑的样子一致,很多"在我电脑上明明可以"的问题直接消失。
5.3 如果后面要接数据库、Redis、Vue
页面项目早晚要接数据。最常见的组合是SpringBoot + MyBatis。需要注意,MyBatis官方starter的版本要跟SpringBoot大版本匹配:
- SpringBoot 2.x 用
mybatis-spring-boot-starter 2.3.x - SpringBoot 3.x 用
mybatis-spring-boot-starter 3.x
很多人直接复制网上的依赖,结果SpringBoot 3项目里引入了一个2.x的starter,启动直接报错。
Redis的接入相对简单,引入spring-boot-starter-data-redis,配一下连接信息,用StringRedisTemplate就能操作常见的缓存场景。如果你的项目要对外提供接口给前端,那就走SpringBoot + Vue前后端分离路线。后端全部写@RestController返回JSON,用springdoc-openapi生成接口文档。不过这里有个热词相关问题:如果不需要接口文档,怎么关闭springdoc?
springdoc: api-docs: enabled: false swagger-ui: enabled: false这两行关掉之后,/swagger-ui.html和/v3/api-docs就不会暴露了。小型内部系统我一般直接关掉,少一个暴露面,也少一点无用流量。
至于热词里的MinIO、EMQX、ActiveMQ、HanLP这些,都是SpringBoot生态里的进阶集成方向,各有适用场景,但不属于"快速搭建简单网页"的必选项。我的经验是先把基础的Web功能做扎实,需要哪个技术再单独加哪个,避免项目一开始就背上十几个依赖。
6. 踩坑实录与常见问题速查
最后这部分是我最想分享的实操内容。框架语法看几遍都能记住,但运行时的坑往往毫无征兆,我把最常遇到的几类整理成了速查表,再逐个展开说说。
6.1 问题速查表
| 现象 | 排查思路 | 常见解决方案 |
|---|---|---|
| 页面显示Whitelabel Error Page | Controller没有被扫描到,或映射路径不对 | 确认Controller在启动类的子包下;确认@Controller注解;确认路径与方法上的@GetMapping匹配 |
| 端口被占用,启动失败 | 8080被其他程序占用 | 改server.port;或者用命令查进程并清理(见下) |
| 页面500 | 模板语法错误或数据缺失 | 看启动日志最顶部的异常堆栈;检查${变量名}和后端Model里的属性名是否一致 |
| 静态资源404 | 资源没放在static目录,或路径写错 | 确认目录是resources/static;页面里用th:href="@{/css/xxx.css}"生成路径 |
| 改了HTML不生效 | 模板缓存没有关闭 | 设置spring.thymeleaf.cache: false,配合DevTools热更新 |
| 页面中文乱码 | 文件编码或响应编码不一致 | 所有源文件用UTF-8;确认HTML里<meta charset="UTF-8">,必要时配置server.servlet.encoding |
6.2 端口被占用和页面404的排查顺序
端口被占用这件事,我在本地调试时碰到得最多。Java进程没杀干净,或者别的服务占了端口,启动日志会明确报Port 8080 was already in use。
Windows下的排查命令:
netstat -ano | findstr :8080 taskkill /PID 进程号 /FLinux和macOS下:
lsof -i:8080 kill -9 进程号与其说这是技术问题,不如说是按顺序排查的习惯问题。先确认端口,再确认进程,最后确认是不是自己上一次启动的实例没关干净。
页面404的排查顺序我跟新手强调过很多次:
- 先看启动日志有没有报错,有没有成功注册了
/映射; - 确认访问路径带没带
context-path前缀; - 确认Controller类是不是在启动类同一个包或子包下;
- 确认是
@Controller而不是@RestController。
按这个顺序走,99%的404都能在五分钟内定位。不要一上来就怀疑SpringBoot配置有问题,先怀疑自己的目录和注解。
6.3 版本相关坑:SpringBoot版本太高怎么办
热词里有"springboot版本太高"和"springboot 2.7.18",这说明版本兼容问题确实是高频痛点。做个简单的对照:
| 维度 | SpringBoot 2.7.x | SpringBoot 3.x |
|---|---|---|
| 最低JDK | JDK 8 | JDK 17 |
| 标准包名 | javax.* | jakarta.* |
| MyBatis starter | 2.3.x | 3.0.x |
| 嵌入式Tomcat | Tomcat 9 | Tomcat 10 |
| Thymeleaf版本 | 3.0.x | 3.1.x |
如果你接了老项目,发现代码里全是javax.servlet而项目依赖却用了SpringBoot 3,最常见的处理方式是退版本或改包名,二选一。退版本通常更快,因为改包名还牵连到其他第三方库的兼容判断。
排查依赖冲突时,Maven项目用这条命令看依赖树:
mvn dependency:tree -Dverbose版本问题我吃过的亏是:为了某个新特性升了SpringBoot大版本,结果Redis、MyBatis、文件上传这些周边全部跟着升级,联调两天才稳定。所以我的原则是,没有强需求就不升大版本,升之前先查兼容矩阵。
6.4 我个人在这些小项目上的习惯
写了这么多,最后说几个我自己一直保持的习惯,算不上什么高深技巧,但对"快速搭建简单SpringBoot项目网页"这种小项目很有用。
第一,我会把多环境配置拆开。application.yml只放公共配置,再建application-dev.yml和application-prod.yml分别放开发和生产配置。启动时用--spring.profiles.active=dev指定。页面项目再简单,只要涉及部署,这套拆分迟早用得上。
第二,每次改配置前先想"这个值会不会因环境变化"。端口、数据库地址、Redis地址都是典型的环境相关配置,绝不能写死在代码里。写死配置的项目,换一台机器就变成灾难现场。
第三,模板引擎只是工具,页面快速跑通后,重点应该放在数据的来源和接口的稳定性上。我见过太多项目页面漂亮得一塌糊涂,后端就一个Controller返回了一堆测试数据。等你接上数据库和真实业务逻辑,那才是项目真正的开始。
回到开头那个问题:快速搭建一个SpringBoot项目网页,真的不难。难的是理解每一步背后的规则,并在项目变大之前把基础结构打稳。希望这篇教程能帮你绕过我当年踩过的那些坑,让你的第一个SpringBoot页面稳稳地跑起来。