参考视频:【狂神说Java】SpringBoot最新教程IDEA版通俗易懂哔哩哔哩bilibili
一、实战项目:员工管理系统
1.1项目结构总览
这是一个典型的 SpringBoot + Thymeleaf 后端管理系统项目,采用 MVC 分层架构
1.后端 Java 代码(src/main/java/com.xing)
| 包名 | 作用 | 核心类 |
|---|---|---|
config |
复杂配置类(拦截器、国际化、MVC 配置) | LoginHandlerInterceptor(登录拦截) MyLocaleResolver(国际化解析) MyMvcConfig(MVC 扩展配置) |
controller |
控制层(处理请求、页面跳转) | EmployeeController(员工 CRUD)LoginController(登录 / 登出) |
dao |
数据访问层(模拟数据库操作) | EmployeeDao(员工增删改查)DepartmentDao(部门查询) |
pojo |
实体类(数据模型) | Employee(员工实体)Department(部门实体) |
service |
业务层(预留,当前未实现) | - |
BisheApplication |
项目启动类 | 主入口,启动 SpringBoot 容器 |
2.前端与资源文件(src/main/resources)
| 目录 | 作用 | 核心文件 |
|---|---|---|
i18n |
国际化资源包 | 登录页多语言配置(login 资源包) |
static |
静态资源(CSS/JS/ 图片) | css/(Bootstrap 样式)js/(jQuery、Bootstrap 等)img/(页面图片) |
templates |
Thymeleaf 模板页面 | commons/(公共页面) emp/(员工页面) error/(错误页) index.html(登录页) dashboard.html(登录后首页) |
application.properties |
简单配置文件 | 配置thymeleaf缓存、识别国际化文件等 |
1.2项目结构理解
MyMvcConfig 和 application.yaml 的区别
两者都是 SpringBoot 中用来做配置的,但定位不同。application.yaml 更偏向于简单、固定、无需编写逻辑的配置,比如端口号、数据库连接信息、日志级别、Thymeleaf 缓存开关这类内容,只需要通过键值对的方式书写即可,不需要写 Java 代码,适合项目中基础、标准化的设置。而MyMvcConfig 属于 Java 配置类,专门处理复杂、需要自定义逻辑的功能,比如登录拦截器、自定义国际化解析器、页面跳转规则、资源映射、跨域处理等,这些需求无法通过简单的配置项实现,必须通过代码编写逻辑,两者共同完成项目的整体配置工作。
LoginController 和 EmployeeController 为什么要分成两个
LoginController 专门处理和用户登录、登出、身份验证相关的所有操作。而 EmployeeController 专门负责员工模块的业务,也就是员工的增删改查。这其实是按照业务模块做的功能拆分,让每一个控制器只专注一件事,目的是让代码结构更清晰、更容易维护。
Controller 层和 Dao 层的分工
很多新手分不清这两层该写什么代码,其实它们的职责非常明确。Controller 层是项目的 "请求指挥中心",它的任务是接收前端发来的请求、获取前端传递的数据、调用对应的数据处理方法、将数据传递给页面、决定跳转的目标页面,它只负责流程调度,不直接操作数据。而 Dao 层是项目的 "数据操作中心",它只专注于数据的增删改查,不管前端是谁、不管页面长什么样、不管请求从哪里来,只负责把数据存进去、取出来、修改掉、删除掉。这种分层让代码结构更稳定、逻辑更清晰,也让 CRUD 操作可以被复用,而不是把所有代码都堆在一起。
1.3登录流程详解
1 、用户访问项目时,首先进入的是index.html登录页面,页面中的表单通过th:action="@{/user/login}"指定了数据提交的后端地址⬇️
<form class="form-signin" th:action="@{/user/login}">
</form>
输入框通过name="username"和name="password"标记用户填写的账号与密码⬇️
<input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
<input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required="">
2 、点击登录按钮后,name="username"和name="password"的输入内容会被发送到后端/user/login请求路径。这时候控制器会做两件事:一是验证账号密码是否正确,二是如果正确就把用户信息存入会话session,提供登录凭证 ,表示用户已登录,然后重定向到后台主页dashboard.html;如果账号密码错误,就重新跳回登录页,并提示错误信息。⬇️
@RequestMapping("/user/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
Model model, HttpSession session){
if(!StringUtils.isEmpty(username) && "123456".equals(password)) {
session.setAttribute("loginUser",username);
return "redirect:/main.html";
}else {
model.addAttribute("msg","用户名或者密码错误");
return "index";
}
}
3 、MyMvcConfig配置类写在config复杂配置文件,在项目启动时自动加载,重写的addInterceptors方法用于注册拦截器, /** 表示拦截所有请求,包括后台页面、员工管理、编辑、删除等所有路径,同时放行登录页、登录请求和静态资源。拦截器的核心逻辑为校验会话中是否存在凭证(只要有凭证,就全部放行)当前版本仅实现基础的登录拦截,不区分用户身份,若需实现管理员、普通用户等细粒度权限控制。这些不同控制它们的原理完全一样,都是依靠 session 里的不同标记,让拦截器做出不同判断⬇️
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**")
.excludePathPatterns("/index.html","/","/user/login","/css/**","/js/**","/img/**","/i18n/**");
}
1.3.1思考题
1.思考:用户是直接访问
localhost:8080/user/login来登录的吗?/user/login也是后端 Controller 路径?
错啦!大错特错
/user/login只是前端对接后端的代码,用户完全感知不到,用户只在登录页填写账号密码,点击登录后,表单会自动提取到/user/login路径,当 th:action="@{/user/login}"后端控制器写 @RequestMapping("/user/login")这两个路径一模一样,前端与后端才能对接上!
真正决定页面跳转到哪里,是后端控制器执行完成后,通过 return 转发或 redirect 重定向来控制的。
2.它怎么知道绑定 Controller 里哪个类中的方法?
SpringBoot 不需要知道类名!SpringBoot 是靠 "路径" 匹配,不是靠类名!
运行流程超级简单:
-
项目启动SpringBoot 自动扫描所有带
@Controller的类 -
把所有带
@RequestMapping("/xxx")的方法全部登记下来与对应路径统一注册成映射表 -
前端发来
/user/login,SpringBoot 去表里找这个路径,找到后,直接调用那个方法!
它不关心你是哪个类,只关心方法!
1.4重定向与转发详解
思考:为什么1.3的登陆成功要使用redirect 重定向而不是直接 return跳转呢?
先看return转发:转发是服务器自己偷偷换页面,浏览器根本不知道!你在浏览器里访问:/user/login
-
浏览器发送请求到
/user/login -
服务器内部找到主页页面并返回渲染好的页面给浏览器
-
浏览器从头到尾都以为自己还在 /user/login
-
因为跳转发生在服务器内部,浏览器没有发起新请求,所以地址栏不会变。
-
此时用户一旦刷新 →只会重新加载这个页面→浏览器以为你还停留在 "提交表单" 那个步骤→重复提交登录表单 → 重复执行登录逻辑
而重定向是告诉浏览器重新发起新请求:
-
地址栏会自动更新
-
刷新页面只会加载新页面,而新页面没有表单提交!自然不会重复提交登录表单
总结:重定向 = 换新请求 = 刷新不重复;转发 = 保留旧操作 = 刷新会重复
必须用重定向:增、删、改、登录、退出(会修改数据 / 状态,禁止重复执行)
推荐用转发:查询列表、页面展示、数据回显(只展示数据,无修改,刷新无影响)
简单来说,凡是会改变数据的操作,一律重定向;凡是只展示数据的操作,一律转发,这是企业项目中最标准、最稳定、最通用的写法
1.5config复杂配置类详解
项目中只有唯一的 SpringMVC 总配置类 ,通过在配置类中添加注解 @Configuration 标记、继承 WebMvcConfigurer 接口即可,项目启动时自动会被 Spring 加载生效。而我们通过重写接口中的方法,来修改 SpringBoot 的默认配置
当他重写方法后,具体的功能不需要写在配置类里,可以通过新建类去实现,去实现实现Spring 提供的标准接口,但是新建的其他类只是普通功能组件类哦~必须由总配置类手动注册才能生效。
以下是配置类比较常用的可以重写的方法
1.5.1拦截器
-
作用:添加请求拦截器,在请求进入 Controller 之前进行校验
-
拦截器必须排除哪些东西?
1)登录相关页面 & 接口
-
/→ 首页 -
/index.html→ 登录页 -
/user/login→ 登录提交接口(不放行进不去登录)
2)所有静态资源
3)国际化相关
/i18n/** -
-
编写功能类(普通 Java 类,实现 Spring 接口)
public class LoginHandlerInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //登陆成功后,用户会存在session中,有session说明已登录 Object loginUser = request.getSession().getAttribute("loginUser"); // 没有登录,拦截,返回首页 if (loginUser==null){ request.setAttribute("msg","没有权限,请先登录"); request.getRequestDispatcher("/index.html").forward(request,response); return false; }else { return true; } } }-
在总配置类中注册生效
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginHandlerInterceptor()) // 注册拦截器 .addPathPatterns("/**") // 拦截所有请求 .excludePathPatterns( // 放行的请求 "/index.html","/", "/user/login", "/css/**","/js/**","/img/**", "/i18n/**"); } -
1.5.2 视图控制器
作用:直接把 URL 映射到页面,不需要写 Controller
-
场景:首页、登录页、注册页等纯展示页面,简化代码
@Override public void addViewControllers(ViewControllerRegistry registry) { //配置根目录访问 registry.addViewController("/").setViewName("index"); registry.addViewController("/index.html").setViewName("index"); registry.addViewController("/main.html").setViewName("dashboard"); }
1.5.3国际化
SpringBoot 对国际化区域解析器 有一个死规定:
-
SpringBoot 自带了默认的国际化解析器 (
AcceptHeaderLocaleResolver) -
但是!如果容器中存在一个名字为
localeResolver的类型必须为LocaleResolver类型的 Bean,SpringBoot 会自动放弃默认的,直接用你自定义的!
实战:
1.基础配置
确保配置文件编码都为 UTF-8 
创建国际化配置文件:
-
login.properties默认 -
login_zh_CN.properties中文 -
login_en_US.properties英文
在 application.properties 绑定国际化文件路径:
在前端通过这个去显示信息
在通过以下代码返回参数决定controller层控制分支流向
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
2.对国际化功能类的实现
Ctrl+N找到默认解析器
实现了 LocaleResolver 接口!接口规定:必须重写所有方法!
public class MyLocaleResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
//获取请求中的语言参数
String language = request.getParameter("l");
Locale locale = Locale.getDefault(); //如果没有就使用默认的;
//如果请求的链接携带了国际化的参数
if (!StringUtils.isEmpty(language)){
//zh_CN
String[] split = language.split("_");
//国家,地区
locale = new Locale(split[0],split[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) {
//用不到
}
}
而MyLocaleResolver 只是一个普通功能类,通过在配置类注册@Bean把该类交给Spring管理
1.6公共页面复用+传参
把页面里重复的东西(导航栏、侧边栏、页脚)抽出来,让所有页面共用,不用每个页面都写一遍!
怎么用?(2 步搞定)
- 在公共页面
templates/commons/commons.html中定义
- 在别的页面引入(复用)
<!-- 引入:把上面的导航栏插进来 -->
<div th:replace="~{commons/commons::sidebar}"></div>
进阶:侧边栏点击高亮
直接复用公共片段,所有页面样式完全一样,无法实现点击侧边栏高亮
解决方案:公共片段接收参数 → 根据参数判断添加高亮样式 → 引入时公共页面时传参标记当前页
公告组件加入判断(首页传入 main 则高亮)
当我们再次复用公告组件时,要传参
//访问首页,传参 main
<div th:replace="~{commons/commons::sidebar(activeMenu='main')}"></div>
二、员工列表CRUD
2.1Thymeleaf 前端标签

1、文本展示类
-
th:text:展示普通文本内容,用户可见<span th:text="${user.name}"></span>
th:utext:展示富文本 / HTML 内容,不转义(适合带标签内容)
2、表单传值交互类
-
th:value:表单元素 value 赋值,传给后端程序<input th:value="${dept.id}" name="id"> -
th:name:绑定表单 name 属性,后端接收参数关键 -
th:action:指定表单提交地址<form th:action="@{/dept/update}" method="post">
3、链接跳转类
-
th:href:安全拼接跳转链接,支持路径表达式<a th:href="@{/dept/list}">部门列表</a>
4、条件 & 循环类
-
th:if:条件判断,满足才渲染元素<div th:if="${loginUser != null}">已登录</div> -
th:each:列表循环遍历(表格、下拉选项常用)<tr th:each="item : ${deptList}"> <td th:text="${item.name}"></td> </tr>
5、样式 & 属性动态修改
-
th:classappend:动态追加 class 样式(菜单高亮核心)<li th:classappend="${activeMenu=='main' ? 'active' : ''}">
6、公共页面片段复用(详见1.6)
-
th:fragment:定义公共页面片段 -
th:replace:引入替换公共片段(支持传参)<div th:replace="~{commons/commons::sidebar(activeMenu='main')}"></div>
7、内置对象取值
- 直接获取 session、request 等:
<span th:text="${session.loginUser.username}"></span>
2.2查找详解
前端请求→controller处理→数据传递与页面跳转→前端展示
用户在侧边栏点击「员工列表」,侧边栏代码通过herf跳转到对应方法(在这里跳转后端时会经过拦截器哦~)
<a th:class="${active=='emps'?'nav-link active':'nav-link'}" th:href="@{/emps}">员工管理</a>
在controller层中,Controller 调用业务层 Service方法(简易项目可直接调用 Dao 层),获取全部员工数据。将查询到的员工列表存入 Model返回员工列表页面路径
Collection<Employee> employees = employeeDao.getAll();
model.addAttribute("emps",employees);
return "emp/list";
页面获取 Model 中的员工数据,利用 th:each 循环遍历所有员工数据,搭配 th:text 逐个展示员工 ID、姓名、邮箱等字段,最终在浏览器渲染出完整员工列表页面。
<tr th:each="emp:${emps}">
<td>[[${emp.getId()}]]</td>
<td>[[${emp.getLastName()}]]</td>
<td>[[${emp.getEmail()}]]</td>
<td>[[${emp.getGender()==0?'女':'男'}]]</td>
<td>[[${emp.getDepartment().getDepartmentName}]]</td>
<td>[[${#dates.format(emp.getBirth(),'yyyy-MM-dd')}]]</td>
2.3增加详解
对于跳转会省略讲解(用户在员工列表页点击按钮,<a> 标签默认发送 GET 请求 ,后端匹配@GetMapping("/emp") 控制器方法)
这里详细解释新员工emplyee对象的创建过程与各个属性的保存
先看前端:
-
普通属性(自动封装)
-
(lastName、email):前端传字符串、字符串 、 0/1 → Spring 直接 set赋值
-
(gender):前端传递 0/1 数字→SpringMVC 自动匹配赋值
- (birth):前端传日期字符串 → Spring 自动转成 Date 后赋值
-
-
特殊属性(手动处理)
- id:save 方法里自增,不用前端传
- department:部门属性是关联对象,前端只传了id,对象不完整,必须手动补全
SpringMVC 数据绑定(核心专业机制)
进行对象创建时spring会先无参构造对象,再 setter 逐个赋值,未传输的属性则为null,这称为数据绑定
-
普通属性
- 当set时,SpringMVC 自动转换类型赋值哦~
-
特殊属性
-
有两个关键的方法
-
一个是点击提交时用到的controller层代码
@PostMapping("/emp") public String addEmp(Employee employee){ employeeDao.save(employee); //调用底层业务方法保存员工信息 return "redirect:/emps"; }-
一个是controller方法调用的Dao层方法,对于id使用自增赋值
-
而对于部门,前端只传入了id属性,通过获取残缺部门的 ID,查询数据库得到完整部门对象,再重新赋值给员工,保证关联数据完整。
-
最后将处理完成的员工对象存入集合,模拟数据库保存操作。
public void save(Employee employee){ //处理id if (employee.getId()==null){ employee.setId(initId++);} //处理部门 employee.setDepartment(departmentDao .getDepartmentById(employee.getDepartment().getId())); //保存对象 employees.put(employee.getId(),employee); } -
重定向回员工列表,重新查询并展示全部数据
2.3.1思考题
1、如果前端同时传 部门 id + 部门 name,Spring 能自动封装完整吗?
SpringMVC 是可以自动把两个属性都 set 进去,得到完整 Department 对象的!
但是!!!
-
SpringMVC 只把前端表单参数 → 映射 set 到实体属性里,不会自动帮你查数据库、关联查询!
-
前端页面是用户可篡改的,如果前端随便改「部门名称」提交,后端直接存,数据库数据就乱了!
-
所以关联部门这种业务对象,必须手动 Dao 查询补全,再 set 到员工里。
2、spring的自动赋值调用无参再set,这属于DI吗?
真正的 DI 依赖注入是 Spring 容器管理 Bean,给组件(Controller/Service/Dao)注入依赖:
这里属于前端表单自动封装 Employee,只是「临时接收参数的普通对象」,不是 Spring Bean,容器不管理它 ➜ 不是 DI
3、SpringMVC 无参构造+Setter 方法将前端请求参数自动注入到 Java 对象属性被称为数据绑定
4、spring的数据绑定过程是发生在什么时候呢?是在以下controller层完成的吗?
@PostMapping("/emp")
public String addEmp(Employee employee) { // <-- 这里只是【接收】!
}
错误!!!
在进入这个方法之前,SpringMVC 就已经做完了数据绑定,方法里的 Employee employee 只是一个 "接收已经创建好的对象" 的参数!真正的数据绑定在进入方法前就已经完成
2.4修改详解
在这个前端代码中,我们实现携带对应员工ID的唯一跳转
<a class="btn btn-sm btn-danger" th:href="@{/delemp/{id}(id=${emp.id})}">删除</a>
后端接收携带员工 ID 的请求,先根据 ID 查询当前要修改的完整员工数据,再查询所有部门数据;把员工原数据、全部部门数据一并传到前端 Model,跳转到修改表单页面。
//后端通过对应注解匹配该带ID的GET请求,接收前端传递的员工ID
@GetMapping("/emp/{id}")
//通过参数里的注解把路径占位符 `{id}` 里的值赋值给 Java 变量 `id`;
public String toUpdateEmp(@PathVariable("id")Integer id,Model model){
//查出原来的数据
Employee employee=employeeDao.getEmployeeById(id);
model.addAttribute("emp",employee);
//查出所有部门的信息
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("departments",departments);
return "emp/update";
}
修改核心关键:
-
id隐藏回显,提交表单时携带原有员工 ID ,后端
save靠「ID 不为空」判断是修改,不生成新 ID<input type="hidden" name="id" th:value="${emp.getId()}">
-
其他信息回显
-
姓名、邮箱
th:value="${emp.getLastName()}" -
性别(勾选)
th:checked="${emp.getGender()==1}" -
部门(下拉框)
<label>department</label> <select class="form-control" name="department.id"> <!-- 1. 遍历循环:把所有部门一个一个拿出来 --> <option th:each="dept:${departments}" <!-- 2. 页面显示:给用户看部门名字(人事部) --> th:text="${dept.getDepartmentName() }" <!-- 3. 提交数据:传给后端的是部门id(101) --> th:value="${dept.getId()}" <!-- 4. ✨核心回显:自动选中员工原来的部门! --> th:selected="${dept.getId() == emp.getDepartment().getId()}"> </option> </select>
-
修改页面把后端传来的原员工信息回填到表单,其实到这里和2.3的增加方法一样了
重定向回员工列表,重新查询并展示全部数据
2.5删除详解
跳转省略;
删除采用GET 请求方式,无需表单提交;(<a>默认Get请求)
<a class="btn btn-sm btn-danger" th:href="@{/delemp/{id}(id=${emp.id})}">删除</a>
后端调用 DAO 层删除逻辑,根据员工 id,在模拟数据库 Map 中remove移除对应 key 的员工数据,完成删除。
重定向回员工列表,重新查询并展示全部数据
2.6实现404
当用户访问项目中不存在的路径、无效接口或丢失的资源时,服务器会返回自定义 404 错误页面,替代系统默认的空白错误页
放在resources/templates/error/404.html ,SpringBoot 框架会对错误页面有自动化适配