SpringBoot 实战篇

参考视频:【狂神说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项目结构理解

MyMvcConfigapplication.yaml 的区别

两者都是 SpringBoot 中用来做配置的,但定位不同。application.yaml 更偏向于简单、固定、无需编写逻辑的配置,比如端口号、数据库连接信息、日志级别、Thymeleaf 缓存开关这类内容,只需要通过键值对的方式书写即可,不需要写 Java 代码,适合项目中基础、标准化的设置。而MyMvcConfig 属于 Java 配置类,专门处理复杂、需要自定义逻辑的功能,比如登录拦截器、自定义国际化解析器、页面跳转规则、资源映射、跨域处理等,这些需求无法通过简单的配置项实现,必须通过代码编写逻辑,两者共同完成项目的整体配置工作。

LoginControllerEmployeeController 为什么要分成两个

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";
     }
 }

3MyMvcConfig配置类写在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 是靠 "路径" 匹配,不是靠类名!

运行流程超级简单:

  1. 项目启动SpringBoot 自动扫描所有带 @Controller 的类

  2. 把所有带 @RequestMapping("/xxx") 的方法全部登记下来与对应路径统一注册成映射表

  3. 前端发来 /user/login,SpringBoot 去表里找这个路径,找到后,直接调用那个方法!

它不关心你是哪个类,只关心方法!

1.4重定向与转发详解

思考:为什么1.3的登陆成功要使用redirect 重定向而不是直接 return跳转呢?

先看return转发:转发是服务器自己偷偷换页面,浏览器根本不知道!你在浏览器里访问:/user/login

  1. 浏览器发送请求到 /user/login

  2. 服务器内部找到主页页面并返回渲染好的页面给浏览器

  3. 浏览器从头到尾都以为自己还在 /user/login

  4. 因为跳转发生在服务器内部,浏览器没有发起新请求,所以地址栏不会变。

  5. 此时用户一旦刷新 →只会重新加载这个页面→浏览器以为你还停留在 "提交表单" 那个步骤→重复提交登录表单 → 重复执行登录逻辑

而重定向是告诉浏览器重新发起新请求

  • 地址栏会自动更新

  • 刷新页面只会加载新页面,而新页面没有表单提交!自然不会重复提交登录表单

总结:重定向 = 换新请求 = 刷新不重复;转发 = 保留旧操作 = 刷新会重复

必须用重定向:增、删、改、登录、退出(会修改数据 / 状态,禁止重复执行)

推荐用转发:查询列表、页面展示、数据回显(只展示数据,无修改,刷新无影响)

简单来说,凡是会改变数据的操作,一律重定向;凡是只展示数据的操作,一律转发,这是企业项目中最标准、最稳定、最通用的写法

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 对国际化区域解析器 有一个死规定

  1. SpringBoot 自带了默认的国际化解析器AcceptHeaderLocaleResolver

  2. 但是!如果容器中存在一个名字为 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 步搞定)

  1. 在公共页面templates/commons/commons.html中定义
  1. 在别的页面引入(复用)
复制代码
 <!-- 引入:把上面的导航栏插进来 -->
 <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 框架会对错误页面有自动化适配

相关推荐
lclcooky2 小时前
Spring 核心技术解析【纯干货版】- XII:Spring 数据访问模块 Spring-R2dbc 模块精讲
java·后端·spring
神奇小汤圆2 小时前
Java 集合容器 - 高级篇
后端
李白的粉2 小时前
基于springboot的相亲网站
java·spring boot·毕业设计·课程设计·相亲网站
毕设源码-邱学长2 小时前
【开题答辩全过程】以 基于 java web 的篮球赛事管理系统的设计与实现为例,包含答辩的问题和答案
java·开发语言
aygh2 小时前
Java八股文复习指南
java·面试·八股文·后端开发
小则又沐风a2 小时前
类和对象(C++)---上
java·c++·算法
季明洵2 小时前
动态规划及背包问题
java·数据结构·算法·动态规划·背包问题
祭曦念2 小时前
学Rust3次都放弃?这篇文章帮你避开90%的新手劝退
后端
侠客行03173 小时前
Tomcat 从陌生到熟悉
java·tomcat·源码阅读