Spring Boot 与数据访问全解析:MyBatis、Thymeleaf、拦截器实战
Spring Boot 与数据访问全解析:MyBatis、Thymeleaf、拦截器实战
结合我们之前学习的 Spring Boot 基础和配置知识,本文将聚焦 “数据访问 + 前端渲染 + 权限控制” 核心链路 —— 从 MyBatis 操作数据库,到 PageHelper 分页,再到 Thymeleaf 服务器端渲染,最后通过 WebMvcConfigurer 和拦截器实现静态资源管理与权限校验。同时补充 AJAX 异步通信知识点,详细对比 Thymeleaf 与传统 EL 表达式的差异,并完整实现 “权限菜单展示” 常见项目需求,帮我们形成从后端到前端的完整开发能力。
一、Spring Boot 整合 MyBatis:数据库访问核心
MyBatis 是 Java 生态主流的 ORM 框架,Spring Boot 通过mybatis-spring-boot-starter
简化整合,我们从 “项目创建→完整 CRUD” 逐步拆解,避免初学者常见的 “Mapper 注入失败”“配置错误” 等问题。
1.1 第一步:创建项目与依赖配置
1.1.1 用 IDEA 创建项目(可视化操作)
-
新建 Spring Boot 项目→选择 “Spring Initializr”,填写基础信息(Group:
com.lh
,Artifact:springboot03
,Java:17); -
选择核心依赖(必选 3 个):
-
MyBatis Framework:MyBatis 核心依赖,自动整合 Spring;
-
MySQL Driver:MySQL 数据库驱动(适配 MySQL 8.x);
-
JDBC API:提供 JDBC 核心支持(含数据源自动配置、JdbcTemplate 等),为数据访问提供底层基础;
-
Spring Web:提供 HTTP 接口,用于测试数据访问;
-
-
完成创建,IDEA 自动生成项目结构和
pom.xml
。
1.1.2 核心依赖解析(pom.xml)
<dependencies><!-- 1. Spring Web:支持HTTP接口,必选 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 2. MyBatis Starter:自动配置MyBatis,无需手动配SqlSessionFactory --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.5</version> <!-- 与Spring Boot 2.7.x兼容 --></dependency><!-- 3. 提供 JDBC 核心支持 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><!-- 4. MySQL驱动:runtime scope表示运行时依赖 --><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><!-- 5. 测试依赖:可选,用于单元测试 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>
-
依赖作用说明:
mybatis-spring-boot-starter已包含 MyBatis 核心、Spring 整合包,无需额外配置SqlSessionFactory;
mysql-connector-j是 MySQL 8.x 的驱动类名(旧版是mysql-connector-java)。
1.2 第二步:配置数据源(application.yml)
Spring Boot 通过spring.datasource
配置数据库连接,需注意 URL 参数(时区、编码)和驱动类名(MySQL 8.x 用com.mysql.cj.jdbc.Driver
):
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driver # MySQL 8.x驱动类名(必须带cj)username: root # 数据库用户名password: root # 数据库密码# URL参数说明:serverTimezone=Asia/Shanghai(时区,避免乱码)、useUnicode=true(支持中文)url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8
-
易错点:
若 URL 缺少serverTimezone,启动会报 “时区错误”;驱动类名用旧版com.mysql.jdbc.Driver会报 “弃用警告”。
1.3 第三步:开发核心业务代码(CRUD)
我们以 “用户表(user)” 为例,实现 “查询所有、新增、删除、修改” 操作,遵循 “实体类→Mapper→Service→Controller” 分层开发规范。
1.3.1 1. 实体类(User.java)
对应数据库user
表(字段:id、name、age、pwd),用 Lombok 简化 Getter/Setter(需引入 Lombok 依赖):
package com.lh.springboot03.entity;import lombok.Data; // Lombok注解:自动生成Getter/Setter/toStringimport lombok.NoArgsConstructor;import lombok.AllArgsConstructor;@Data@NoArgsConstructor // 无参构造@AllArgsConstructor // 全参构造public class User {private Integer id; // 主键private String name; // 用户名private Integer age; // 年龄private String pwd; // 密码(实际项目需加密)}
-
Lombok 依赖补充:在pom.xml中添加 Lombok 依赖,避免手动写 Getter/Setter:
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
1.3.2 2. Mapper 接口(UserMapperjava)
MyBatis 的 Mapper 接口用于定义数据库操作,通过注解写 SQL(简单 SQL 用注解,复杂 SQL 用 XML):
package com.lh.springboot03.mapper;import com.lh.springboot03.entity.User;import org.apache.ibatis.annotations.Delete;import org.apache.ibatis.annotations.Insert;import org.apache.ibatis.annotations.Select;import org.apache.ibatis.annotations.Update;import java.util.List;// 方式1:加@Mapper注解,Spring自动扫描(适用于Mapper少的场景)// @Mapperpublic interface UserMapper {// 1. 查询所有用户@Select("select id, name, age, pwd from user") // SQL字段需与实体类属性对应List<User> findAllUser();// 2. 新增用户(#{}对应实体类属性,自动防SQL注入)@Insert("insert into user(name, age, pwd) values(#{name}, #{age}, #{pwd})")int addUser(User user); // 返回值:影响行数// 3. 删除用户(#{id}对应方法参数)@Delete("delete from user where id = #{id}")int deleteUser(Integer id);// 4. 修改用户@Update("update user set name=#{name}, age=#{age}, pwd=#{pwd} where id=#{id}")int updateUser(User user);}
-
Mapper 扫描方式:
方式 1:在每个 Mapper 接口加@Mapper注解(繁琐);
方式 2(推荐):在主类加@MapperScan,批量扫描 Mapper 包:
package com.lh.springboot03;import org.mybatis.spring.annotation.MapperScan;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication// 扫描mapper包:指定Mapper接口所在路径@MapperScan("com.lh.springboot03.mapper")public class Springboot03Application {public static void main(String[] args) {SpringApplication.run(Springboot03Application.class, args);}}
1.3.3 3. Service 层(接口 + 实现类)
Service 层封装业务逻辑,调用 Mapper 接口:
// 1. Service接口(UserService.java)package com.lh.springboot03.service;import com.lh.springboot03.entity.User;import java.util.List;public interface UserService {List<User> findAllUser();int addUser(User user);int deleteUser(Integer id);int updateUser(User user);}
实现类:
// 2. Service实现类(UserServiceImpl.java)package com.lh.springboot03.service.impl;import com.lh.springboot03.entity.User;import com.lh.springboot03.mapper.UserMapper;import com.lh.springboot03.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.util.List;@Service // 标记为Service层Bean,交给Spring管理public class UserServiceImpl implements UserService {// 注入Mapper接口(Spring通过@MapperScan自动生成代理对象)@Autowiredprivate UserMapper userMapper;@Overridepublic List<User> findAllUser() {return userMapper.findAllUser(); // 调用Mapper方法}@Overridepublic int addUser(User user) {// 可添加业务逻辑(如密码加密:user.setPwd(BCrypt.hashpw(user.getPwd(), BCrypt.gensalt())))return userMapper.addUser(user);}@Overridepublic int deleteUser(Integer id) {return userMapper.deleteUser(id);}@Overridepublic int updateUser(User user) {return userMapper.updateUser(user);}}
1.3.4 4. Controller 层(UserController.java)
Controller 层提供 HTTP 接口,调用 Service 层,返回 JSON 响应(用@RestController
):
package com.lh.springboot03.controller;import com.lh.springboot03.entity.User;import com.lh.springboot03.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import java.util.List;// @RestController = @Controller + @ResponseBody(自动返回JSON,无需手动处理)@RestControllerpublic class UserController {@Autowiredprivate UserService userService;// 1. 查询所有用户:GET http://localhost:8080/user@RequestMapping("/user")public List<User> findAllUser() {return userService.findAllUser();}// 2. 新增用户:GET http://localhost:8080/adduser?name=李四&age=22&pwd=456@RequestMapping("/adduser")public int addUser(User user) {// 前端传参自动绑定到User对象(参数名与属性名一致)return userService.addUser(user);}// 3. 删除用户:GET http://localhost:8080/deleteUser?id=1@RequestMapping("/deleteUser")public int deleteUser(Integer id) {return userService.deleteUser(id);}// 4. 修改用户:GET http://localhost:8080/updateUser?id=2&name=李四2&age=23&pwd=789@RequestMapping("/updateUser")public int updateUser(User user) {return userService.updateUser(user);}}
1.3.5 测试:验证 CRUD 接口
-
启动项目,确保数据库
mybatis
已创建user
表(数据示例:id=1, name=zs, age=18, pwd=123); -
访问http://localhost:8080/user,返回用户列表 JSON:
[{"id":1,"name":"zs","age":18,"pwd":"123"},{"id":2,"name":"刘海","age":20,"pwd":"123"}]
-
其他接口测试:
-
新增:
http://localhost:8080/adduser?name=王五&age=25&pwd=123
,返回1
(成功); -
删除:
http://localhost:8080/deleteUser?id=3
,返回1
(成功)。
-
二、整合 PageHelper:实现分页查询
当用户表数据量大时,需要分页展示,PageHelper 是 MyBatis 的分页插件,能自动拦截 SQL 添加分页语句(limit
),无需手动写分页 SQL。
2.1 第一步:引入 PageHelper 依赖
在pom.xml
中添加 PageHelper Starter(自动整合 MyBatis):
<!-- PageHelper分页插件(与Spring Boot兼容) --><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>2.1.0</version></dependency>
2.2 第二步:修改 Controller 实现分页
在 UserController 中新增分页接口,核心是PageHelper.startPage(page, size)
开启分页:
import com.github.pagehelper.PageHelper;import com.github.pagehelper.PageInfo;@RestControllerpublic class UserController {// ... 其他方法省略// 分页查询:GET http://localhost:8080/userPage?page=1&size=2@RequestMapping("/userPage")public PageInfo<User> findAllUserPage(Integer page, Integer size) {// 1. 开启分页:page=当前页(从1开始),size=每页条数PageHelper.startPage(page, size);// 2. 查询数据(PageHelper自动拦截SQL,添加limit ? ?)List<User> userList = userService.findAllUser();// 3. 封装分页信息(总条数、总页数等)PageInfo<User> pageInfo = new PageInfo<>(userList);return pageInfo;}}
2.3 第三步:测试分页接口
访问http://localhost:8080/userPage?page=1&size=2
,返回分页结果 JSON,关键属性说明:
{"total": 4, // 总记录数"list": [ // 当前页数据(2条){"id":1,"name":"zs","age":18,"pwd":"123"},{"id":2,"name":"刘海","age":20,"pwd":"123"}],"pageNum": 1, // 当前页"pageSize": 2, // 每页条数"pages": 2, // 总页数"hasNextPage": true, // 是否有下一页"hasPreviousPage": false // 是否有上一页}
-
PageInfo 核心属性:
前端分页控件需要total(总记录数)、pageNum(当前页)、pages(总页数)、hasNextPage(是否下一页)等属性,无需手动计算。
三、Thymeleaf 深度解析:Spring Boot 推荐的模板引擎
传统 JSP 需要依赖 Tomcat 引擎,且不支持服务器端渲染优化,Thymeleaf 是 Spring Boot 官方推荐的模板引擎,支持纯 HTML 文件(无需 JSP),能直接在浏览器预览,同时提供丰富的服务器端渲染功能。
3.1 第一步:引入 Thymeleaf 依赖
在pom.xml
中添加 Thymeleaf Starter:
<!-- Thymeleaf模板引擎依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>
3.2 第二步:Thymeleaf 自动配置原理
Spring Boot 通过ThymeleafAutoConfiguration
自动配置,核心默认配置(无需手动写):
// ThymeleafProperties类中的默认配置public class ThymeleafProperties {public static final String DEFAULT_PREFIX = "classpath:/templates/"; // 页面存放路径public static final String DEFAULT_SUFFIX = ".html"; // 页面后缀private String prefix = DEFAULT_PREFIX;private String suffix = DEFAULT_SUFFIX;private boolean cache = false; // 开发环境关闭缓存(修改页面无需重启)}
-
页面存放路径:Thymeleaf 页面默认放在
src/main/resources/templates
目录下,访问时无需写路径和后缀(如userlist
对应templates/userlist.html
)。
3.3 第三步:Thymeleaf 核心语法(重点补充)
Thymeleaf 通过th:
前缀属性实现服务器端渲染,我们结合案例讲常用属性,对比传统 EL 表达式(JSP):
Thymeleaf 属性 | 作用 | 案例(Thymeleaf) | 传统 EL 表达式(JSP) |
---|---|---|---|
th:text | 设置元素文本内容(自动转义,防 XSS) | <td th:text="${user.name}"></td> | <td>${user.name}</td> (需 JSP 引擎) |
th:each | 循环遍历集合 | <tr th:each="user : ${userList}"> | <c:forEach items="${userList}" var="user"> (需 JSTL) |
th:href | 生成 URL(支持动态参数) | <a th:href="@{/userPage(page=1, size=2)}"> | <a href="/userPage?page=1&size=2"> (静态 URL) |
th:action | 表单提交地址 | <form th:action="@{/login}" method="post"> | <form action="/login" method="post"> |
th:if | 条件判断(true 时显示元素) | <div th:if="${user.age > 18}">成年</div> | <c:if test="${user.age > 18}">成年</c:if> |
th:object | 绑定对象(简化属性引用) | <div th:object="${user}"><span th:text="*{name}"></span></div> | 无直接对应,需${user.name} 多次写对象名 |
语法案例:用户列表页面(userlist.html)
<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><!-- 必须加xmlns:th命名空间,否则Thymeleaf属性不生效 --><head><meta charset="UTF-8"><title>用户列表</title><style>table {border-collapse: collapse;}td, th {border: 1px solid #000; padding: 8px;}</style></head><body><h1>用户列表(Thymeleaf渲染)</h1><table><tr><th>ID</th><th>用户名</th><th>年龄</th><th>密码</th></tr><!-- th:each循环:user为循环变量,${userList}为后端传的集合 --><tr th:each="user, stat : ${userList}"><!-- stat是循环状态对象,可获取index(索引)、count(计数) --><td th:text="${user.id}"></td><td th:text="${user.name}"></td><td th:text="${user.age}"></td><td th:text="${user.pwd}"></td><!-- th:href动态生成URL,传递id参数 --><td><a th:href="@{/updateUserPage(id=${user.id})}">修改</a><a th:href="@{/deleteUser(id=${user.id})}">删除</a></td></tr></table><!-- 分页控件 --><div style="margin-top: 10px;"><!-- 上一页:判断是否有上一页 --><a th:if="${pageInfo.hasPreviousPage}" th:href="@{/userPage(page=${pageInfo.pageNum-1}, size=${pageInfo.pageSize})}">上一页</a><!-- 当前页 --><span th:text="'当前页:' + ${pageInfo.pageNum}"></span><!-- 下一页:判断是否有下一页 --><a th:if="${pageInfo.hasNextPage}" th:href="@{/userPage(page=${pageInfo.pageNum+1}, size=${pageInfo.pageSize})}">下一页</a></div></body></html>
3.4 第四步:Controller 传递数据到 Thymeleaf
修改 Controller,用ModelAndView
或Model
传递数据到页面(服务器端渲染):
package com.lh.springboot03.controller;import com.github.pagehelper.PageHelper;import com.github.pagehelper.PageInfo;import com.lh.springboot03.entity.User;import com.lh.springboot03.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.servlet.ModelAndView;@Controller // 用@Controller(非@RestController),返回页面public class ThymeleafUserController {@Autowiredprivate UserService userService;// 分页查询用户,跳转到userlist.html:http://localhost:8080/findAllUserPage?page=1&size=2@RequestMapping("/findAllUserPage")public ModelAndView findAllUserPage(Integer page, Integer size) {// 1. 分页查询PageHelper.startPage(page, size);PageInfo<User> pageInfo = new PageInfo<>(userService.findAllUser());// 2. 创建ModelAndView,指定页面名称(对应templates/userlist.html)ModelAndView mv = new ModelAndView("userlist");// 3. 传递数据到页面:key为页面使用的变量名,value为数据mv.addObject("pageInfo", pageInfo);mv.addObject("userList", pageInfo.getList()); // 也可直接传pageInfo,页面用pageInfo.listreturn mv;}}
3.5 Thymeleaf vs 传统 EL 表达式(JSP):什么时候用哪个?
对比维度 | Thymeleaf | 传统 EL 表达式(JSP) |
---|---|---|
依赖环境 | 无依赖(纯 HTML),Spring Boot 自动支持 | 依赖 Tomcat JSP 引擎,需额外配置 |
页面预览 | 直接用浏览器打开 HTML,显示默认值 | 必须部署到服务器才能预览,否则 EL 表达式原样显示 |
安全性 | 自动转义 HTML,防 XSS 攻击 | 需手动处理转义(如c:out 标签) |
功能丰富度 | 支持循环、条件、片段引入、国际化 | 需依赖 JSTL 标签库(如c:forEach ) |
开发效率 | 语法简洁,IDEA 有自动提示 | 需记忆 JSTL 标签,语法繁琐 |
选择建议:
-
新项目(Spring Boot):优先用 Thymeleaf,无需 JSP 依赖,开发效率高;
-
旧项目(传统 SSM):若已用 JSP+EL,可继续使用,无需迁移;
-
纯静态页面:用 Thymeleaf 的
th:text="${xxx} ?: '默认值'"
,支持预览时显示默认值。
四、WebMvcConfigurer:配置静态资源与拦截器
WebMvcConfigurer 是 Spring MVC 的核心接口,用于定制 Spring MVC 行为(静态资源、拦截器、跨域等),无需 XML 配置,纯 Java 代码实现。
4.1 配置静态资源(addResourceHandlers)
Spring Boot 默认静态资源路径是classpath:/static/
(CSS、JS、图片),但有时需要配置自定义路径(如本地磁盘文件),通过addResourceHandlers
实现:
实战:配置静态资源(类路径 + 本地磁盘)
package com.lh.springboot03.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.file.Paths;@Configuration // 标记为配置类
public class WebMvcConfig implements WebMvcConfigurer {// 配置静态资源映射@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {// 1. 配置类路径静态资源:访问 /my-files/xxx → 对应 classpath:/files/xxxregistry.addResourceHandler("/my-files/**") // 前端访问URL模式(**匹配任意路径).addResourceLocations("classpath:/files/") // 后端资源路径(类路径下的files文件夹).setCachePeriod(3600); // 缓存时间(秒),开发环境可设0// 2. 配置本地磁盘静态资源:访问 /upload/xxx → 对应 D:/upload/xxxString localPath = "D:/upload/";// 本地路径需转成URI格式(处理路径分隔符)String resourceLocation = Paths.get(localPath).toUri().toString();registry.addResourceHandler("/upload/**").addResourceLocations(resourceLocation); // 本地磁盘路径,需加file:前缀(URI已包含)}
}
-
测试访问:
-
类路径资源:
src/main/resources/files/b.txt
→ 访问http://localhost:8080/my-files/b.txt
; -
本地磁盘资源:
D:/upload/a.txt
→ 访问http://localhost:8080/upload/a.txt
。
-
4.2 配置拦截器(addInterceptors)
拦截器用于 “预处理请求”(如登录验证、日志记录),Spring MVC 通过HandlerInterceptor
接口实现,需 3 步:创建拦截器→注册拦截器→测试。
4.2.1 1. 创建自定义拦截器(实现 HandlerInterceptor)
拦截器有 3 个核心方法,执行时机不同:
package com.lh.springboot03.interceptor;import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;// 1. 登录验证拦截器:未登录用户不能访问/auth/**路径
@Component // 交给Spring管理,后续注册时注入
public class LoginInterceptor implements HandlerInterceptor {/*** 1. preHandle:控制器方法执行前执行(核心)* @return true:继续执行控制器;false:中断请求(不执行控制器)*/@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 从Session获取登录用户(登录时需将用户存入Session)HttpSession session = request.getSession();Object loginUser = session.getAttribute("loginUser");if (loginUser == null) {// 未登录:跳转到登录页,中断请求response.sendRedirect("/login.html");return false;}// 已登录:继续执行控制器return true;}/*** 2. postHandle:控制器方法执行后,页面渲染前执行(少用)* 用途:修改ModelAndView中的数据*/// @Override// public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {// if (modelAndView != null) {// modelAndView.addObject("msg", "从拦截器添加的消息");// }// }/*** 3. afterCompletion:整个请求完成后执行(页面渲染后)* 用途:资源清理、日志记录*/// @Override// public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// System.out.println("请求完成,清理资源");// }
}// 2. 日志记录拦截器:记录所有请求的访问日志
@Component
public class LoggingInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 记录请求URL、用户、时间HttpSession session = request.getSession();String username = (String) session.getAttribute("loginUser");String url = request.getRequestURI();String time = new java.util.Date().toString();System.out.printf("用户:%s,访问URL:%s,时间:%s%n", username, url, time);return true; // 继续执行}
}
4.2.2 2. 注册拦截器(addInterceptors)
在 WebMvcConfig 中注册拦截器,指定拦截路径和排除路径:
package com.lh.springboot03.config;import com.lh.springboot03.interceptor.LoginInterceptor;
import com.lh.springboot03.interceptor.LoggingInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class WebMvcConfig implements WebMvcConfigurer {// 注入自定义拦截器@Autowiredprivate LoginInterceptor loginInterceptor;@Autowiredprivate LoggingInterceptor loggingInterceptor;// 注册拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 1. 注册登录拦截器:拦截/auth/**路径,排除登录页、静态资源registry.addInterceptor(loginInterceptor).addPathPatterns("/auth/**") // 拦截的路径(/**表示所有路径).excludePathPatterns( // 排除的路径(不拦截)"/login.html", // 登录页"/login", // 登录接口"/static/**", // 静态资源(CSS、JS)"/my-files/**", // 自定义静态资源"/error" // 错误页面);// 2. 注册日志拦截器:拦截所有路径registry.addInterceptor(loggingInterceptor).addPathPatterns("/**");}// 静态资源配置(之前的代码)...
}
4.2.3 3. 测试拦截器
-
创建登录页(
templates/login.html
):<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <body><form th:action="@{/login}" method="post">用户名:<input type="text" name="username"><br>密码:<input type="password" name="password"><br><input type="submit" value="登录"></form> </body> </html>
-
创建登录 Controller:
@Controller public class LoginController {// 跳转到登录页:http://localhost:8080/login.html@RequestMapping("/login.html")public String toLogin() {return "login"; // 对应templates/login.html}// 处理登录请求:POST /login@RequestMapping("/login")public String login(String username, String password, HttpServletRequest request) {// 模拟数据库校验(实际项目需查数据库)if ("admin".equals(username) && "123456".equals(password)) {// 登录成功:将用户存入Sessionrequest.getSession().setAttribute("loginUser", username);return "redirect:/auth/index"; // 跳转到需登录的路径}// 登录失败:返回登录页return "login";}// 需登录的路径:/auth/index@RequestMapping("/auth/index")public String index() {return "index"; // 对应templates/index.html} }
-
测试流程:
-
访问
http://localhost:8080/auth/index
(未登录)→ 被 LoginInterceptor 拦截,跳转到/login.html
; -
输入正确用户名密码(admin/123456)→ 登录成功,跳转到
/auth/index
; -
控制台输出日志(LoggingInterceptor):
用户:admin,访问URL:/auth/index,时间:...
。
-
4.3 拦截器 vs 过滤器(Filter):核心区别
对比维度 | 拦截器(Interceptor) | 过滤器(Filter) |
---|---|---|
技术依赖 | 依赖 Spring MVC 框架,仅 Spring Web 应用可用 | 依赖 Servlet 规范,所有 Web 应用可用(非 Spring 也能用) |
执行时机 | 控制器方法执行前后(Spring MVC 流程内) | 请求进入 Servlet 前、响应返回客户端前(Servlet 流程) |
拦截范围 | 仅拦截控制器请求(如/user ),不拦截静态资源 | 拦截所有请求(包括静态资源、Servlet) |
功能权限 | 可访问 Spring 容器中的 Bean(如 Service、Mapper) | 不可访问 Spring Bean,需手动获取容器 |
执行顺序 | 按注册顺序执行,支持 preHandle/postHandle/afterCompletion | 按 web.xml 或 @Order 排序,仅 doFilter 方法 |
选择建议:
-
业务逻辑拦截(登录验证、权限校验):用拦截器(可访问 Spring Bean);
-
通用请求处理(字符编码、压缩、跨域):用过滤器(Servlet 层面,范围更广)。
五、补充:AJAX 核心知识点(结合常见项目需求使用)
AJAX(Asynchronous JavaScript and XML)是 “异步 JavaScript 和 XML” 的缩写,核心作用是不刷新页面,异步与服务器通信,常用于动态加载数据(如作业中的菜单渲染)。现在主流用 JSON 传递数据(替代 XML),我们以 jQuery AJAX 为例讲解。
5.1 AJAX 核心作用与优势
-
核心作用:页面不刷新,局部更新数据(如点击 “加载更多” 不刷新页面,加载新数据);
-
优势:减少页面刷新,提升用户体验;减少数据传输量(仅传必要数据);
-
常见场景:动态菜单加载、表单异步提交、分页无刷新、搜索联想。
5.2 jQuery AJAX 基础语法
需先引入 jQuery(从 CDN 或本地静态资源),语法结构:
$.ajax({url: "/api/getMenuList", // 后端接口URLtype: "GET", // 请求方法(GET/POST)data: {roleId: 2}, // 请求参数(JSON格式)dataType: "json", // 预期后端返回数据类型(json/text/html)success: function(res) { // 请求成功回调(res为后端返回的JSON数据)console.log("成功:", res);// 处理数据(如渲染菜单)renderMenu(res.data);},error: function(xhr, status, error) { // 请求失败回调console.error("失败:", error);alert("加载菜单失败!");}
});// 简化写法(GET请求)
$.get("/api/getMenuList", {roleId: 2}, function(res) {renderMenu(res.data);
}, "json");// 简化写法(POST请求)
$.post("/api/getMenuList", {roleId: 2}, function(res) {renderMenu(res.data);
}, "json");
5.3 AJAX 跨域问题(补充)
若前端和后端不在同一域名(如前端localhost:8081
,后端localhost:8080
),会出现跨域错误,需在后端配置 CORS(跨域资源共享):
// 在WebMvcConfig中配置跨域
@Override
public void addCorsMappings(CorsRegistry registry) {registry.addMapping("/api/**") // 允许跨域的接口路径.allowedOrigins("http://localhost:8081") // 允许的前端域名.allowedMethods("GET", "POST") // 允许的请求方法.allowedHeaders("*") // 允许的请求头.allowCredentials(true); // 是否允许携带Cookie
}
六、项目需求实现:权限菜单展示功能
6.1 需求分析
基于用户 - 角色 - 菜单的权限模型,实现:
-
所有请求需拦截器验证登录;
-
登录时根据用户角色 ID,查询该角色拥有的菜单;
-
用 AJAX 异步加载菜单数据,渲染到页面。
6.2 表设计(4 张表)
表名 | 字段 | 说明 |
---|---|---|
user | id、username、password、roleId | 用户表,roleId 关联 role 表主键 |
role | id、rolename | 角色表(如 “管理员”“普通用户”) |
menu | id、menuName、menuUrl | 菜单表(如 “用户管理”→/auth/user ) |
r_m | role_id、menu_id | 角色 - 菜单中间表(多对多关系) |
测试数据:
-
role 表:id=2,rolename=“管理员”;
-
r_m 表:(2,1)、(2,3)、(2,6);
-
menu 表:id=1→菜单名 “用户管理”,menuUrl=“/auth/user”;id=3→“订单管理”,menuUrl=“/auth/order”;id=6→“菜单管理”,menuUrl=“/auth/menu”。
6.3 后端实现(分步骤)
6.3.1 1. 创建实体类(Menu.java、Role.java)
// Menu.java(菜单实体类)
package com.lh.springboot03.entity;import lombok.Data;@Data
public class Menu {private Integer id; // 菜单IDprivate String menuName; // 菜单名称private String menuUrl; // 菜单URL
}
// Role.java(角色实体类,可选,本作业用roleId查询)
package com.lh.springboot03.entity;import lombok.Data;@Data
public class Role {private Integer id; // 角色IDprivate String rolename; // 角色名称
}
6.3.2 2. 编写 Mapper 接口(MenuMapper.java)
通过角色 ID 查询菜单(联表查询 r_m 和 menu):
package com.lh.springboot03.mapper;import com.lh.springboot03.entity.Menu;
import org.apache.ibatis.annotations.Select;
import java.util.List;public interface MenuMapper {// 联表查询:通过roleId查菜单(r_m中间表关联menu表)@Select("SELECT m.id, m.menuName, m.menuUrl " +"FROM menu m " +"JOIN r_m rm ON m.id = rm.menu_id " +"WHERE rm.role_id = #{roleId}")List<Menu> findMenuByRoleId(Integer roleId);
}
6.3.3 3. Service 层(MenuService.java)
// 接口
public interface MenuService {List<Menu> findMenuByRoleId(Integer roleId);
}// 实现类
@Service
public class MenuServiceImpl implements MenuService {@Autowiredprivate MenuMapper menuMapper;@Overridepublic List<Menu> findMenuByRoleId(Integer roleId) {return menuMapper.findMenuByRoleId(roleId);}
}
6.3.4 4. Controller 层(MenuController.java)
提供 AJAX 接口,返回菜单列表:
@RestController
@RequestMapping("/api")
public class MenuController {@Autowiredprivate MenuService menuService;// AJAX接口:根据roleId查菜单,返回JSON@RequestMapping("/getMenuList")public Map<String, Object> getMenuList(Integer roleId) {Map<String, Object> result = new HashMap<>();try {List<Menu> menuList = menuService.findMenuByRoleId(roleId);result.put("code", 0); // 0=成功result.put("data", menuList);result.put("msg", "加载菜单成功");} catch (Exception e) {result.put("code", 500); // 500=失败result.put("msg", "加载菜单失败:" + e.getMessage());}return result;}
}
6.3.5 5. 完善拦截器(验证登录 + 获取角色 ID)
修改 LoginInterceptor,登录时获取用户的 roleId,存入 Session:
// 1. 修改User实体类,增加roleId字段
@Data
public class User {// ... 原有字段private Integer roleId; // 新增:关联角色表
}// 2. 修改LoginController,登录时查询用户的roleId,存入Session
@RequestMapping("/login")
public String login(String username, String password, HttpServletRequest request) {// 模拟数据库查询(实际项目需查user表)if ("admin".equals(username) && "123456".equals(password)) {// 模拟用户数据:id=1,username=admin,roleId=2(管理员角色)User user = new User();user.setId(1);user.setUsername(username);user.setRoleId(2);// 存入SessionHttpSession session = request.getSession();session.setAttribute("loginUser", user);return "redirect:/index.html"; // 跳转到首页}return "login";
}// 3. 修改LoginInterceptor,验证登录时获取roleId(可选,用于后续权限控制)
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {HttpSession session = request.getSession();User loginUser = (User) session.getAttribute("loginUser");if (loginUser == null) {response.sendRedirect("/login.html");return false;}// 可选:将roleId放入请求属性,供后续使用request.setAttribute("roleId", loginUser.getRoleId());return true;
}
6.4 前端实现(AJAX 渲染菜单)
6.4.1 1. 首页(index.html)
引入 jQuery,用 AJAX 加载菜单:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>首页</title><!-- 引入jQuery(CDN) --><script src="https://code.jquery.com/jquery-3.6.0.min.js"></script><style>.menu { list-style: none; padding: 0; }.menu li { margin: 5px 0; }.menu a { text-decoration: none; color: #333; }.menu a:hover { color: #1890ff; }</style>
</head>
<body><h1>首页(已登录)</h1><h3>菜单列表</h3><!-- 菜单容器 --><ul class="menu" id="menuContainer"></ul><script>// 页面加载完成后执行$(function() {// 1. 从Session获取roleId(需后端传递,这里简化用固定值2,实际项目需后端渲染到页面)var roleId = 2; // 实际项目:var roleId = [[${loginUser.roleId}]];(Thymeleaf渲染)// 2. AJAX请求菜单数据$.ajax({url: "/api/getMenuList",type: "GET",data: { roleId: roleId },dataType: "json",success: function(res) {if (res.code === 0) {// 3. 渲染菜单renderMenu(res.data);} else {alert(res.msg);}},error: function() {alert("加载菜单失败,请重试!");}});});// 3. 渲染菜单的函数function renderMenu(menuList) {var $menuContainer = $("#menuContainer");$menuContainer.empty(); // 清空容器// 循环遍历菜单列表$.each(menuList, function(index, menu) {// 创建菜单<li>和<a>标签var $li = $("<li></li>");var $a = $("<a></a>").attr("href", menu.menuUrl) // 设置菜单URL.text(menu.menuName); // 设置菜单名称$li.append($a);$menuContainer.append($li);});}</script>
</body>
</html>
6.4.2 2. 测试 AJAX 菜单加载
-
启动项目,登录(admin/123456)→ 跳转到 index.html;
-
页面加载时,AJAX 请求
/api/getMenuList?roleId=2
,后端返回菜单列表; -
前端渲染菜单,显示 “用户管理”“订单管理”“菜单管理”,点击菜单跳转到对应 URL。
七、总结
本文从 “数据访问→前端渲染→权限控制” 完整覆盖 Spring Boot 核心开发场景:
-
MyBatis 整合:掌握 CRUD 开发,理解 @MapperScan 的作用,避免 Mapper 注入失败;
-
PageHelper 分页:简化分页逻辑,理解 PageInfo 的核心属性;
-
Thymeleaf:掌握常用语法,对比 EL 表达式,知道什么时候用 Thymeleaf;
-
WebMvcConfigurer:配置静态资源和拦截器,理解拦截器与过滤器的区别;
-
AJAX:掌握异步通信语法,结合作业实现动态菜单渲染;
-
权限菜单作业:理解用户 - 角色 - 菜单的多对多关系,实现从后端查询到前端渲染的完整流程。
后续学习可扩展:菜单的父子级渲染(递归 AJAX)、权限的细粒度控制(按钮级权限)、用 Vue 替代 jQuery 实现前端渲染,逐步向前后端分离架构过渡。