后端 JavaWeb 回顾javaweb全流程下 Silence 2026-03-02 2026-03-02 SpringMVC RequestMapping 1. 概述 在Spring MVC中,@RequestMapping注解用于将HTTP请求映射到控制器类或控制器方法。它是Spring MVC的核心注解之一,用于构建RESTful API。
2. 映射方式 2.1 类级别映射 在控制器类上使用@RequestMapping注解,可以为该控制器的所有方法指定一个公共的URL前缀。
1 2 3 4 5 @RequestMapping("/api/users") @RestController public class UserController { }
2.2 方法级别映射 在控制器方法上使用@RequestMapping注解,指定具体的URL路径和HTTP方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RequestMapping("/api/users") @RestController public class UserController { @RequestMapping(value = "/{id}", method = RequestMethod.GET) public User findById (@PathVariable Long id) { } @RequestMapping(method = RequestMethod.POST) public User create (@RequestBody User user) { } }
3. 注解属性 3.1 value/path 指定请求的URL路径,可以是单个字符串或字符串数组。
1 2 3 4 5 @RequestMapping("/users") @RequestMapping({"/users", "/api/v1/users"})
3.2 method 指定请求的HTTP方法,可以是单个RequestMethod枚举值或数组。
1 2 3 4 5 @RequestMapping(method = RequestMethod.GET) @RequestMapping(method = {RequestMethod.GET, RequestMethod.POST})
4. 快捷注解 为了简化开发,Spring MVC提供了一系列快捷注解,它们是@RequestMapping的特殊形式:
@GetMapping :处理GET请求
@PostMapping :处理POST请求
@PutMapping :处理PUT请求
@DeleteMapping :处理DELETE请求
@PatchMapping :处理PATCH请求
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @RestController @RequestMapping("/api/users") public class UserController { @GetMapping("/{id}") public User findById (@PathVariable Long id) { } @PostMapping public User create (@RequestBody User user) { } @PutMapping("/{id}") public User update (@PathVariable Long id, @RequestBody User user) { } @DeleteMapping("/{id}")
5. 控制器注解 5.1 @Controller与@RestController 在Spring MVC中,有两种主要的控制器注解:
5.1.1 @Controller @Controller注解用于标记一个类作为Spring MVC的控制器。默认情况下,它的方法返回值会被解析为视图名称(如JSP页面)。
返回JSON数据 :需要配合@ResponseBody注解使用
1 2 3 4 5 6 7 8 9 10 11 @Controller @RequestMapping("/api/users") public class UserController { @GetMapping("/{id}") @ResponseBody public User findById (@PathVariable Long id) { return userService.findById(id); } }
5.1.2 @RestController @RestController是Spring 4.0引入的组合注解,它相当于@Controller + @ResponseBody。
特点 :
所有方法默认返回JSON格式数据
无需在每个方法上单独添加@ResponseBody注解
使用示例 :
1 2 3 4 5 6 7 8 9 10 @RestController @RequestMapping("/api/users") public class UserController { @GetMapping("/{id}") public User findById (@PathVariable Long id) { return userService.findById(id); } }
5.1.3 对比总结
注解
说明
返回值处理
JSON支持
@Controller
普通控制器注解
默认解析为视图名称
需要配合@ResponseBody
@RestController
组合注解(@Controller + @ResponseBody)
直接写入HTTP响应体
默认支持
5.2 最佳实践
RESTful API开发 :优先使用@RestController
传统Web应用 :使用@Controller返回视图
混合场景 :根据实际需求选择合适的注解
5. 参数绑定 5.1 @RequestParam 将请求参数绑定到方法参数上。
1 2 3 4 @GetMapping("/search") public List<User> search (@RequestParam String keyword, @RequestParam(required = false, defaultValue = "1") Integer page) { }
5.2 @PathVariable 将URL路径中的变量绑定到方法参数上。
1 2 3 4 @GetMapping("/{id}") public User findById (@PathVariable Long id) { }
5.3 @RequestBody 将请求体中的JSON或XML数据绑定到方法参数上(通常用于POST/PUT请求)。
1 2 3 4 @PostMapping public User create (@RequestBody User user) { }
6. 完整示例 UserController.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @GetMapping public List<User> findAll () { return userService.findAll(); } @GetMapping("/{id}") public User findById (@PathVariable Long id) { return userService.findById(id); } @PostMapping public User create (@RequestBody User user) { return userService.save(user); } @PutMapping("/{id}") public User update (@PathVariable Long id, @RequestBody User user) { user.setId(id); return userService.update(user); } @DeleteMapping("/{id}") public void delete (@PathVariable Long id) { userService.deleteById(id); } @GetMapping("/page") public Page<User> findPage (@RequestParam Integer pageNum, @RequestParam Integer pageSize) { return userService.findPage(pageNum, pageSize); } }
7. JSON处理 7.1 概述 在Spring MVC中,处理JSON数据是构建RESTful API的核心需求之一。Spring MVC提供了便捷的方式将Java对象转换为JSON格式,并将JSON数据包含在HTTP响应体中。
7.2 @ResponseBody注解 @ResponseBody注解用于将控制器方法的返回值转换为JSON格式,并将其包含在HTTP响应体中。
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Controller @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @RequestMapping(value = "/{id}", method = RequestMethod.GET) @ResponseBody public User findById (@PathVariable Long id) { return userService.findById(id); } }
7.3 @RestController注解 @RestController是一个组合注解,它相当于@Controller和@ResponseBody的结合。使用@RestController注解的控制器类,其所有方法都会默认返回JSON格式的数据。
使用示例:
1 2 3 4 5 6 7 8 9 10 11 12 @RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @GetMapping("/{id}") public User findById (@PathVariable Long id) { return userService.findById(id); } }
7.4 JSON转换器 Spring MVC默认使用Jackson库作为JSON转换器,它会自动将Java对象转换为JSON格式。
Maven依赖(Spring Boot项目中通常已包含):
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency >
7.5 复杂对象JSON转换 Spring MVC可以处理复杂对象的JSON转换,包括嵌套对象、集合、数组等。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class User { private Long id; private String username; private String name; private Integer age; private List<Order> orders; } public class Order { private Long id; private String orderNo; private BigDecimal amount; } @RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @GetMapping("/{id}/orders") public User findUserWithOrders (@PathVariable Long id) { return userService.findUserWithOrders(id); } }
7.6 注意事项
确保实体类有正确的getter和setter方法,Jackson通过这些方法访问属性
可以使用Jackson的注解(如@JsonProperty、@JsonFormat等)自定义JSON输出格式
默认情况下,null值会被包含在JSON输出中,可以通过配置排除null值
排除null值的配置(application.properties):
1 2 spring.jackson.default-property-inclusion =non_null
7.7 统一响应结果封装 7.7.1 问题背景 在开发RESTful API时,如果没有统一的响应格式,会导致:
响应格式不一致(有时返回字符串,有时返回对象,有时返回数组)
难以管理和维护
前端处理复杂
不一致的响应示例:
成功时返回:OK
失败时返回:教研部
有时返回对象:{"id": 1, "name": "教研部", "updateTime": "2024-10-20 00:00:00"}
有时返回数组:[{"id": 1, "name": "教研部", "updateTime": "2024-10-20 00:00:00"}]
7.7.2 解决方案 创建统一的响应结果封装类Result,用于标准化API响应格式。
Result.java:
1 2 3 4 5 6 7 public class Result { private Integer code; private String msg; private Object data; }
7.7.3 统一响应格式 成功响应:
1 2 3 4 5 { "code" : 1 , "msg" : "操作成功" , "data" : ... }
失败响应:
1 2 3 4 5 { "code" : 0 , "msg" : "密码错误" , "data" : ... }
7.7.4 完善Result类 添加静态方法,方便快速创建响应对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class Result { private Integer code; private String msg; private Object data; public static Result success (Object data) { Result result = new Result (); result.setCode(1 ); result.setMsg("操作成功" ); result.setData(data); return result; } public static Result success () { return success(null ); } public static Result error (String msg) { Result result = new Result (); result.setCode(0 ); result.setMsg(msg); result.setData(null ); return result; } public static Result error (Integer code, String msg) { Result result = new Result (); result.setCode(code); result.setMsg(msg); result.setData(null ); return result; } }
7.7.5 使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @GetMapping public Result findAll () { List<User> users = userService.findAll(); return Result.success(users); } @GetMapping("/{id}") public Result findById (@PathVariable Long id) { User user = userService.findById(id); if (user == null ) { return Result.error("用户不存在" ); } return Result.success(user); } @PostMapping public Result create (@RequestBody User user) { userService.save(user); return Result.success("用户创建成功" ); } }
7.7.6 优势
统一格式 :所有API响应都遵循相同的格式
易于维护 :集中管理响应格式,便于后续修改
前端友好 :前端可以统一处理响应,降低复杂度
错误处理 :清晰区分成功和失败响应,包含错误信息
扩展性强 :可以根据需要添加更多字段(如时间戳、请求ID等)
开发相关问题 1. 统计人数与数据封装 1.1 场景说明 在开发中,经常需要对数据进行统计分析,例如统计不同职位的员工人数。为了保证灵活性,通常使用List<Map<String, Object>>来封装统计结果。
1.2 SQL实现 1.2.1 SQL流程控制函数 1.2.1.1 CASE函数 CASE函数是SQL中的流程控制函数,有两种语法形式:
语法一 :适用于复杂条件判断
1 case when cond1 then res1 [when cond2 then res2] else res end ;
语法二 (适用于等值匹配):
1 case expr when val1 then res1 [when val2 then res2] else res end ;
1.2.1.2 IF函数 IF函数也是SQL中常用的流程控制函数,主要有两种形式:
IF函数 :如果表达式成立,取第一个值,否则取第二个值
IFNULL函数 :如果表达式不为NULL,取其自身值,否则取指定值
1.2.2 职位统计实现 使用SQL的CASE函数可以将数字类型的职位代码转换为具体的职位名称,同时进行分组统计:
1.2.2.1 基础版本 1 2 3 4 5 6 7 8 9 select (case job when 1 then '班主任' when 2 then '讲师' when 3 then '学工主管' when 4 then '教研主管' when 5 then '咨询师' else '其他' end ) pos, count (* ) num from emp group by job order by num;
1.2.2.2 优化版本(推荐) 1 2 3 4 5 6 7 8 9 10 11 select count (* ) as num, case job when 1 then '班主任' when 2 then '讲师' when 3 then '学工主管' when 4 then '教研主管' when 5 then '咨询师' else '其他' end as pos from emp group by job order by num desc ;
优化说明:
使用as关键字明确指定列别名,提高可读性
将count(*)放在前面,符合SQL编写习惯
使用order by num desc按人数降序排序,更直观地展示数据分布
语法结构更加清晰,便于维护和扩展
1.3 Mapper接口定义 使用List<Map<String, Object>>来封装统计结果,确保灵活性:
1 List<Map<String, Object>> countEmpJobData () ;
1.4 数据结构 统计结果通常包含两个部分:
职位列表 :[“教研主管”, “学工主管”, “其他”, “班主任”, “咨询师”, “讲师”]
数据列表 :[1, 1, 2, 6, 8, 13]
1.5 优势
灵活性高 :Map<String, Object>可以存储任意键值对,适应不同的统计需求
扩展性强 :可以根据需要添加更多的统计维度和指标
前端友好 :返回的数据结构清晰,便于前端处理和展示
SQL功能强大 :利用SQL的CASE函数和分组统计,减少后端处理逻辑
1.6 Service层实现模板 1.6.1 数据传输对象(DTO) 首先定义一个用于封装统计结果的DTO类:
1 2 3 4 5 6 7 @Data @NoArgsConstructor @AllArgsConstructor public class JobOption { private List<Object> jobList; private List<Object> dataList; }
1.6.2 Service层实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override public JobOption getEmpJobData () { List<Map<String, Object>> list = empMapper.countEmpJobData(); List<Object> jobList = list.stream().map(dataMap -> dataMap.get("pos" )).toList(); List<Object> dataList = list.stream().map(dataMap -> dataMap.get("num" )).toList(); return new JobOption (jobList, dataList); }
1.7 使用模板的步骤
定义DTO类 :根据统计需求定义对应的DTO类,例如JobOption
编写SQL :使用CASE函数和分组统计编写SQL语句
定义Mapper接口 :使用List<Map<String, Object>>接收统计结果
实现Service层 :参考上述模板,通过Stream流处理数据并封装到DTO中
调用Service方法 :在Controller中调用Service方法获取统计结果
1.8 适用场景 此模板适用于以下场景:
统计不同类别的数据数量(如职位、性别、部门等)
需要将统计结果转换为前端图表所需的数据结构
任意需要分组统计并返回结构化数据的场景
1.9 模板扩展 如需扩展到其他统计场景,只需修改以下部分:
SQL语句 :调整CASE函数的条件和分组字段
Mapper方法名 :根据统计维度命名(如countEmpGenderData)
DTO类 :根据返回数据结构调整字段
Service层 :修改Stream流处理的键名(如从"pos"改为"gender")
2. Controller接收参数的常见问题 在实际开发中,Controller接收参数是开发RESTful API时的核心操作之一。以下是一些常见的参数接收场景和解决方案:
2.1 DELETE请求处理查询参数 当使用DELETE请求删除资源时,有时需要通过查询参数传递ID或其他标识符。
请求示例:
处理方式:
1 2 3 4 5 @DeleteMapping("/depts") public Result delete (@RequestParam("id") Integer deptId) { System.out.println("根据ID删除部门:" + deptId); return Result.success(); }
注意事项:
@RequestParam注解的required属性默认值为true,表示该参数必须传递。如果不传递该参数,Spring MVC会抛出异常。
如果参数是可选的,可以将required属性设置为false:
1 2 3 4 5 @DeleteMapping("/depts") public Result delete (@RequestParam(value = "id", required = false) Integer deptId) { return Result.success(); }
1.2 参数名不一致的处理 当URL参数名与方法参数名不一致时,可以使用@RequestParam注解的value属性进行映射:
1 2 3 4 5 6 7 8 @GetMapping("/search") public Result search ( @RequestParam("kw") String keyword, @RequestParam("page_no") Integer pageNo, @RequestParam("page_size", defaultValue = "10") Integer pageSize) { return Result.success(); }
2.3 批量操作参数处理 对于批量删除等操作,可以使用数组或集合接收多个参数:
请求示例:
1 DELETE /users?ids=1,2,3,4,5
处理方式:
1 2 3 4 5 @DeleteMapping("/users") public Result batchDelete (@RequestParam List<Long> ids) { userService.batchDelete(ids); return Result.success("批量删除成功" ); }
3. 统一响应格式的最佳实践 使用统一的响应结果封装类Result可以大大提高API的可维护性和前端友好性:
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Result { private Integer code; private String msg; private Object data; public static Result success (Object data) { } public static Result success () { } public static Result error (String msg) { } public static Result error (Integer code, String msg) { } }
3. 开发建议
参数验证 :使用@Valid或@Validated注解进行参数验证,确保数据有效性
异常处理 :统一处理控制器层异常,避免直接暴露错误信息
日志记录 :在关键操作处添加日志,便于问题排查
接口文档 :使用Swagger等工具自动生成API文档
版本管理 :在URL中添加版本号(如/api/v1/users),便于API升级和兼容
通过以上实践,可以构建出更加健壮、可维护的JavaWeb应用,提高开发效率和用户体验。
4. MyBatis批量操作最佳实践 4.1 批量删除的正确实现 4.1.1 错误写法及问题分析 以下是一段有问题的MyBatis批量删除SQL语句:
1 2 3 4 5 6 7 8 <delete id ="deleteByIds" > <foreach collection ="ids" item ="id" separator =";" > delete from student where id = #{id} </foreach > </delete >
核心问题 :
语法错误 :使用;分隔多个DELETE语句,需要数据库开启允许执行多条语句的配置(如MySQL的allowMultiQueries=true),否则会直接报错
性能问题 :循环生成多条DELETE语句,多次操作数据库,批量删除效率极低
风险问题 :开启多语句执行可能引入SQL注入风险,不符合开发最佳实践
4.1.2 正确写法(推荐) 利用IN关键字实现单条SQL批量删除,性能高且无需特殊配置:
1 2 3 4 5 6 7 8 <delete id ="deleteByIds" > DELETE FROM student WHERE id IN <foreach collection ="ids" item ="id" open ="(" separator ="," close =")" > #{id} </foreach > </delete >
4.1.3 代码关键说明 foreach标签参数解释 :
collection="ids":对应传入的集合参数名(如List<Integer> ids)
item="id":遍历集合时的单个元素别名
open="(":遍历开始时拼接左括号
separator=",":元素之间用逗号分隔
close=")":遍历结束时拼接右括号
最终生成的SQL示例 :
1 DELETE FROM student WHERE id IN (1 ,2 ,3 )
4.1.4 特殊场景补充(若id数量极多) 如果需要删除的id数量超过千级(如1000+),IN关键字可能导致SQL执行效率下降,可分批次删除:
1 2 3 4 5 6 7 8 9 10 public void batchDeleteByIds (List<Integer> ids) { int batchSize = 1000 ; for (int i = 0 ; i < ids.size(); i += batchSize) { int end = Math.min(i + batchSize, ids.size()); List<Integer> batchIds = ids.subList(i, end); studentMapper.deleteByIds(batchIds); } }
4.1.5 总结
优先使用 :IN + foreach的方式实现批量删除,避免多条DELETE语句拼接
禁用 :使用;分隔多语句的写法,无需开启数据库多语句执行配置,更安全通用
特殊情况 :若id数量极大,可在业务层分批次删除,平衡性能和SQL长度限制
日志技术-Logback Logback概述 Logback是基于Log4j升级而来的日志框架,提供了更多的功能和配置选项,性能优于Log4j。它与SLF4J(Simple Logging Facade for Java)无缝集成,是Java项目中常用的日志解决方案。
**Slf4j(Simple Logging Facade for Java)**:简单日志门面,提供了一套日志操作的标准接口及抽象类,允许应用程序使用不同的底层日志框架。
占位符替换 :Logback可以接收参数并在日志中进行占位符替换,例如:logger.info("用户{}登录成功", username)。
Lombok集成 :使用@Slf4j注解即可,无需手动创建日志对象。
Logback快速入门 1. 准备工作 1.1 引入依赖 在Maven项目中,需要引入Logback的依赖:
1 2 3 4 5 <dependency > <groupId > ch.qos.logback</groupId > <artifactId > logback-classic</artifactId > <version > 1.4.11</version > </dependency >
注意:在Spring Boot项目中,该依赖会通过spring-boot-starter-logging自动传递,无需手动引入。
1.2 配置文件 Logback默认会在类路径下查找配置文件:logback.xml或logback-test.xml。
2. 基本使用示例 Java代码示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class LogTest { private static final Logger log = LoggerFactory.getLogger(LogTest.class); public void testLog () { log.debug("开始计算..." ); int sum = 0 ; int [] nums = {1 , 5 , 3 , 2 , 1 , 4 , 5 , 4 , 6 , 7 , 4 , 34 , 2 , 23 }; for (int i = 0 ; i < nums.length; i++) { sum += nums[i]; } log.info("计算结果为: " + sum); log.debug("结束计算..." ); } }
Logback日志级别 日志级别指的是日志信息的类型,日志都会分级别,不同级别的日志用于记录不同重要程度的信息。常见的日志级别如下(级别由低到高 ):
日志级别
说明
记录方式
trace
追踪,记录程序运行轨迹【使用很少】
log.trace("...")
debug
调试,记录程序调试过程中的信息,实际应用中一般将其视为最低级别【使用较多】
log.debug("...")
info
记录一般信息,描述程序运行的关键事件,如:网络连接、IO操作【使用较多】
log.info("...")
warn
警告信息,记录潜在有害的情况【使用较多】
log.warn("...")
error
错误信息【使用较多】
log.error("...")
日志级别使用原则
trace :仅用于详细的程序流程追踪,一般不用于生产环境
debug :用于开发和测试环境的调试信息,生产环境通常关闭
info :记录程序的正常运行状态,如系统启动、关键业务流程完成等
warn :记录可能的问题或异常,但不会影响程序的正常运行
error :记录严重的错误信息,通常表示程序发生了异常或错误
日志级别配置 在Logback配置文件中,可以通过<root>标签设置全局日志级别:
1 2 3 4 <root level ="info" > <appender-ref ref ="STDOUT" /> <appender-ref ref ="FILE" /> </root >
说明:当设置了某个日志级别后,只有该级别及以上级别的日志才会被记录。例如,设置为info级别时,info、warn和error级别的日志会被记录,而trace和debug级别的日志会被忽略。
优化案例-日志记录 使用@Slf4j注解优化日志记录 在实际开发中,我们可以使用Lombok的@Slf4j注解来简化日志对象的创建。以下是一个优化前后的对比示例:
优化前(使用System.out.println) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @RestController public class DeptController { @Autowired private DeptService deptService; @DeleteMapping("/depts") public Result delete (Integer id) { System.out.println("根据ID删除部门:" + id); deptService.delete(id); return Result.success(); } }
优化后(使用@Slf4j注解) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Slf4j @RestController public class DeptController { @Autowired private DeptService deptService; @DeleteMapping("/depts") public Result delete (Integer id) { log.info("根据ID删除部门:{}" , id); deptService.delete(id); return Result.success(); } }
日志配置示例 以下是一个简单的Logback配置文件示例(logback.xml):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <?xml version="1.0" encoding="UTF-8" ?> <configuration > <appender name ="STDOUT" class ="ch.qos.logback.core.ConsoleAppender" > <encoder class ="ch.qos.logback.classic.encoder.PatternLayoutEncoder" > <pattern > %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n</pattern > </encoder > </appender > <appender name ="FILE" class ="ch.qos.logback.core.rolling.RollingFileAppender" > <rollingPolicy class ="ch.qos.logback.core.rolling.TimeBasedRollingPolicy" > <FileNamePattern > logs/app.%d{yyyy-MM-dd}.log</FileNamePattern > <MaxHistory > 30</MaxHistory > </rollingPolicy > <encoder class ="ch.qos.logback.classic.encoder.PatternLayoutEncoder" > <pattern > %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n</pattern > </encoder > </appender > <root level ="info" > <appender-ref ref ="STDOUT" /> <appender-ref ref ="FILE" /> </root > </configuration >
优化优势
性能更好 :日志框架比System.out.println()性能更优
可配置性强 :可以灵活配置日志级别、输出格式和输出位置
线程安全 :日志框架是线程安全的,而System.out.println()不是
信息丰富 :日志包含时间戳、线程名、类名、行号等信息,便于问题排查
便于管理 :可以统一管理所有日志,便于分析和监控
分页查询具体实现逻辑
员工管理系统的分页查询功能 的完整实现过程
✅ 功能目标 实现一个支持 条件筛选 + 分页展示 的员工列表查询功能:
支持按姓名模糊搜索
支持按性别筛选
支持按入职日期范围筛选
支持分页(当前页码、每页条数)
返回总记录数和当前页数据
🔍 需求分析 1. 前端传给后端的参数
参数名
类型
是否必填
说明
page
Integer
否
当前页码,默认为 1
pageSize
Integer
否
每页显示条数,默认为 10
name
String
否
姓名关键字,模糊匹配
gender
Integer
否
性别(1:男, 2:女)
begin
LocalDate
否
入职开始日期(格式:yyyy-MM-dd)
end
LocalDate
否
入职结束日期(格式:yyyy-MM-dd)
✅ 注意:所有参数都可为空,需做动态判断处理。
2. 后端返回给前端的数据 1 2 3 4 5 6 7 8 9 10 11 { "code" : 1 , "msg" : "success" , "data" : { "total" : 500 , "rows" : [ { "id" : 1 , "name" : "风清扬" , ... } , ... ] } }
total: 符合条件的总记录数
rows: 当前页的数据列表
整体架构设计 1 前端 (Vue/HTML) → HTTP 请求 → Controller → Service → Mapper → 数据库 → 返回结果
实现步骤详解
第一步:定义实体类和查询参数对象 1. Emp.java —— 员工实体类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data public class Emp { private Integer id; private String username; private String password; private String name; private Integer gender; private String phone; private Integer job; private Integer salary; private String image; private LocalDate entryDate; private Integer deptId; private LocalDateTime createTime; private LocalDateTime updateTime; private String deptName; }
✅ entryDate 是 LocalDate 类型,只包含年月日。
2. EmpQueryParam.java —— 查询参数封装类 1 2 3 4 5 6 7 8 9 10 11 12 13 @Data @NoArgsConstructor @AllArgsConstructor public class EmpQueryParam { private Integer page = 1 ; private Integer pageSize = 10 ; private String name; private Integer gender; @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate begin; @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate end; }
✅ 使用 @DateTimeFormat(pattern = "yyyy-MM-dd") 注解指定前端传入日期字符串的格式,避免转换失败。
1 2 3 4 5 6 7 @Data @NoArgsConstructor @AllArgsConstructor public class PageResult <T> { private Long total; private List<T> rows; }
✅ 用于统一返回分页数据结构。
4. Result.java —— 统一响应结果类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Data public class Result { private Integer code; private String msg; private Object data; public static Result success () { return new Result (1 , "success" , null ); } public static Result success (Object data) { return new Result (1 , "success" , data); } public static Result error (String msg) { return new Result (0 , msg, null ); } }
第二步:Controller 层 —— 接收请求并调用服务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController @RequestMapping("/emps") @Slf4j public class EmpController { @Autowired private EmpService empService; @GetMapping public Result page (EmpQueryParam empQueryParam) { log.info("分页查询参数:{}" , empQueryParam); PageResult<Emp> pageResult = empService.page(empQueryParam); return Result.success(pageResult); } }
✅ 将整个查询参数对象作为方法参数接收,简化接口。
第三步:Service 层 —— 核心业务逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Service public class EmpServiceImpl implements EmpService { @Autowired private EmpMapper empMapper; @Override public PageResult<Emp> page (EmpQueryParam empQueryParam) { PageHelper.startPage(empQueryParam.getPage(), empQueryParam.getPageSize()); List<Emp> list = empMapper.list(empQueryParam); Page<Emp> empPage = (Page<Emp>) list; return new PageResult <>(empPage.getTotal(), empPage.getResult()); } }
✅ 关键点:
使用 PageHelper.startPage() 开启分页
调用 Mapper 方法时无需传 start, pageSize
MyBatis 会自动执行两个 SQL:1 2 SELECT COUNT (* ) FROM ... SELECT * FROM ... LIMIT ?,?
第四步:Mapper 层 —— 定义数据库操作接口 1 2 3 4 5 6 7 @Mapper public interface EmpMapper { List<Emp> list (EmpQueryParam empQueryParam) ; }
✅ 不需要手动写 count() 和 list(start, pageSize),因为 PageHelper 会自动处理。
第五步:Mapper XML 文件 —— 动态 SQL 实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <mapper namespace ="com.yx.Mapper.EmpMapper" > <select id ="list" resultType ="com.yx.pojo.Emp" > SELECT emp.*, dept.name as deptName FROM emp LEFT JOIN dept ON emp.dept_id = dept.id <where > <if test ="name != null and name != ''" > AND emp.name LIKE CONCAT('%', #{name}, '%') </if > <if test ="gender != null" > AND emp.gender = #{gender} </if > <if test ="begin != null" > AND emp.entry_date = #{begin} </if > <if test ="end != null" > AND emp.entry_date < = #{end} </if > </where > ORDER BY emp.update_time DESC </select > </mapper >
✅ 关键标签解释:
<where>:自动添加 WHERE 关键字,并去除第一个多余的 AND
<if test="...">:只有当条件成立时才拼接对应的 SQL 片段
#{name}:安全的参数占位符,防止 SQL 注入
第六步:配置 PageHelper 插件(pom.xml 中引入) 1 2 3 4 5 <dependency > <groupId > com.github.pagehelper</groupId > <artifactId > pagehelper-spring-boot-starter</artifactId > <version > 1.4.6</version > </dependency >
✅ Spring Boot 自动配置,无需额外设置。
💡 核心原理总结 ❓ 为什么不用手动写 count() 和 limit? 因为使用了 PageHelper 插件,它会:
在 PageHelper.startPage() 之后,拦截所有 SELECT 语句
自动执行两次 SQL:
第一次:SELECT COUNT(*) FROM ... → 得到总记录数
第二次:SELECT * FROM ... LIMIT ?,? → 得到当前页数据
最终将结果封装成 Page<T> 对象,包含 total 和 result
✅ 优势:减少重复代码,提高开发效率
🔄 流程图(逻辑流程) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 前端发起请求 ↓ Controller 接收 EmpQueryParam ↓ Service 层调用 PageHelper.startPage() ↓ Mapper 执行 list() 方法(触发 PageHelper 分页) ↓ PageHelper 自动执行 count + limit SQL ↓ 返回 Page<Emp> 对象 ↓ 封装为 PageResult 并返回 ↓ 前端渲染分页表格
⚠️ 常见问题与解决方案
问题
原因
解决方案
MethodArgumentNotValidException 报错
日期格式不匹配(如 2025-01-01)
添加 @DateTimeFormat(pattern = "yyyy-MM-dd")
gender=null 查询不到数据
gender = null 在 SQL 中为 false
使用 <if test="gender != null"> 动态判断
name= 传空字符串导致无结果
LIKE '%%' 不生效
使用 name != null and name != '' 判断
分页总数不对
count() 和 list() 条件不一致
使用同一个 EmpQueryParam 对象传递条件
✅ 最佳实践建议
**统一使用 PageHelper**:简化分页逻辑,避免重复写 count() 和 limit
使用对象封装参数 :避免多个 @RequestParam,便于维护
动态 SQL 处理条件 :使用 <where> + <if> 实现灵活查询
日期类型注意格式 :前端传 yyyy-MM-dd,后端用 LocalDate + @DateTimeFormat
日志打印参数 :方便调试,如 log.info("参数:{}", empQueryParam);
🧩 总结一句话
通过 PageHelper + 动态 SQL + 对象封装参数,我们实现了高效、灵活、易维护的员工分页查询功能。
📌 提示 :当你忘记某个细节时,可以回到这个笔记查看:
参数怎么传?
SQL 怎么写?
分页是怎么做的?
日期怎么处理?
事务管理 什么是事务 概念 事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作 要么同时成功,要么同时失败 。
事务的四大特性(ACID) 事务具有以下四个核心特性,通常被称为 ACID 特性:
1. 原子性(Atomicity)
事务是不可分割的最小单元,要么全部成功,要么全部失败
事务中的操作要么全部执行,要么全部不执行,不存在部分执行的情况
2. 一致性(Consistency)
事务完成时,必须使所有的数据都保持一致状态
事务开始前和结束后,数据库的完整性约束没有被破坏
3. 隔离性(Isolation)
数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行
并发执行的事务之间不能互相干扰
4. 持久性(Durability)
事务一旦提交或回滚,它对数据库中的数据的改变就是永久的
即使系统发生故障,事务处理的结果也不会丢失
示例场景 添加员工操作 :
保存员工基本信息
1 insert into emp values (39 , 'Tom' , '123456' , '汤姆' , 1 , '13300001111' , 1 , 4000 , '1.jpg' , '2023-11-01' , 1 , now(), now());
保存员工的工作经历信息
1 2 insert into emp_expr(emp_id, begin , end , company, job) values (39 ,'2019-01-01' , '2020-01-01' , '百度' , '开发' ),(39 ,'2020-01-10' , '2022-02-01' , '阿里' , '架构' );
在这个场景中,如果第一步成功但第二步失败,就会导致数据不一致(有员工基本信息但没有工作经历)。使用事务可以确保这两个操作要么同时成功,要么同时失败。
事务的操作步骤 事务控制主要有三步操作:
开启事务 :start transaction; 或 begin;
执行操作 :执行所有需要在事务中进行的操作
提交/回滚事务 :
全部成功:commit;
有一项失败:rollback;
事务操作示例 1 2 3 4 5 6 7 8 9 10 11 12 start transaction; / begin ;insert into emp values (39 , 'Tom' , '123456' , '汤姆' , 1 , '13300001111' , 1 , 4000 , '1.jpg' , '2023-11-01' , 1 , now(), now());insert into emp_expr(emp_id, begin , end , company, job) values (39 ,'2019-01-01' , '2020-01-01' , '百度' , '开发' ),(39 ,'2020-01-10' , '2022-02-01' , '阿里' , '架构' ); commit ; / rollback ;
事务的应用场景 事务在以下场景中尤为重要:
银行转账 :从一个账户扣款,向另一个账户存款,这两个操作必须同时成功或同时失败
下单扣减库存 :创建订单和扣减库存必须作为一个整体操作
多表关联操作 :如上述的添加员工场景,涉及多个表的操作
批量操作 :批量更新或删除数据时,确保所有操作要么全部成功,要么全部回滚
Spring 事务管理 概述 Spring 提供了声明式事务管理,通过 @Transactional 注解可以轻松实现事务控制,无需手动编写事务开启、提交和回滚的代码。
@Transactional 注解 作用 将当前方法交给 Spring 进行事务管理:
方法执行前,自动开启事务
成功执行完毕,自动提交事务
出现异常,自动回滚事务
位置
业务(Service)层的方法上(推荐)
业务(Service)层的类上
业务(Service)层的接口上
代码示例 Service 层实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Service public class EmpServiceImpl implements EmpService { @Autowired private EmpMapper empMapper; @Autowired private EmpExprMapper empExprMapper; @Transactional @Override public void save (Emp emp) { emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); empMapper.insert(emp); Integer empId = emp.getId(); List<EmpExpr> exprList = emp.getExprList(); if (!CollectionUtils.isEmpty(exprList)) { exprList.forEach(empExpr -> empExpr.setEmpId(empId)); empExprMapper.insertBatch(exprList); } } }
类级别使用 1 2 3 4 5 @Transactional @Service public class EmpServiceImpl implements EmpService { }
接口级别使用 1 2 3 4 5 @Transactional public interface EmpService { void save (Emp emp) ; }
事务的传播行为 概念 事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
示例场景 1 2 3 4 5 6 7 8 9 10 11 @Transactional public void a () { userService.b(); } @Transactional(propagation = Propagation.REQUIRED) public void b () { }
传播行为详解
属性值
含义
REQUIRED
【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW
需要新事务,无论有无,总是创建新事务
SUPPORTS
支持事务,有则加入,无则在无事务状态中运行
NOT_SUPPORTED
不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY
必须有事务,否则抛出异常
NEVER
必须无事务,否则抛出异常
NESTED
如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来执行;如果当前不存在事务,则创建一个新事务
事务的隔离级别 Spring 事务管理也支持不同的事务隔离级别,用于控制事务之间的隔离程度。常用的隔离级别包括:
DEFAULT :默认值,使用底层数据库的默认隔离级别
READ_UNCOMMITTED :读未提交,允许读取其他事务未提交的数据
READ_COMMITTED :读已提交,只允许读取其他事务已提交的数据
REPEATABLE_READ :可重复读,保证同一事务中多次读取同一数据的结果一致
SERIALIZABLE :串行化,最高隔离级别,确保事务串行执行
事务的超时和回滚规则 超时设置 通过 timeout 属性设置事务的超时时间,单位为秒。如果事务执行时间超过该值,事务会被自动回滚。
回滚规则 rollbackFor 属性 rollbackFor 属性用于控制出现何种异常类型时回滚事务。
默认行为 :
Spring 事务管理默认只对 RuntimeException 和 Error 类型的异常进行回滚
对于受检异常(如 IOException、SQLException 等),默认不会回滚事务
使用方式 :
1 2 3 4 5 6 7 8 9 10 11 @Transactional(rollbackFor = {Exception.class}) @Override public void save (Emp emp) { emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); empMapper.insert(emp); }
常见配置 :
rollbackFor = Exception.class:对所有异常都回滚
rollbackFor = {IOException.class, SQLException.class}:对指定异常回滚
noRollbackFor 属性 noRollbackFor 属性用于控制出现何种异常类型时不回滚事务。
使用方式 :
1 @Transactional(noRollbackFor = BusinessException.class)
示例:自定义事务属性 1 2 3 4 5 6 7 8 9 10 @Transactional( propagation = Propagation.REQUIRED, // 传播行为 isolation = Isolation.DEFAULT, // 隔离级别 timeout = 30, // 超时时间 rollbackFor = Exception.class, // 回滚异常 noRollbackFor = BusinessException.class // 不回滚异常 ) public void save (Emp emp) { }
事务管理最佳实践
只在业务层使用事务 :事务应该在业务逻辑层(Service)中管理,而不是在控制器层(Controller)或数据访问层(DAO)中
精确定义事务边界 :尽量将 @Transactional 注解添加到具体的业务方法上,而不是整个类上,以减小事务范围
合理设置事务属性 :根据业务需求,合理设置事务的传播行为、隔离级别、超时时间和回滚规则
注意异常处理 :确保事务能够正确捕获和处理异常,避免异常被吞掉导致事务无法回滚
测试事务 :编写单元测试和集成测试,确保事务在各种场景下都能正常工作
异常处理 1. 全局异常处理器 1.1 概念 全局异常处理器是一种集中处理应用程序中所有异常的机制,它可以捕获并处理从Controller、Service、Mapper等各层抛出的异常,确保应用程序能够优雅地处理错误情况,同时向客户端返回统一的错误响应。
1.2 工作流程
正常流程 :请求经过Controller → Service → Mapper,正常返回结果
异常流程 :当任意层抛出异常时,全局异常处理器会捕获并处理这些异常,然后返回统一的错误响应
1.3 实现方式 使用Spring Boot提供的@RestControllerAdvice和@ExceptionHandler注解来实现全局异常处理器:
1 2 3 4 5 6 7 8 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler public Result handleException (Exception e) { log.error("全局异常处理器,拦截到异常" , e); return Result.error("对不起,服务器异常,请稍后重试" ); } }
1.4 特定异常处理 除了处理通用异常外,还可以针对特定类型的异常进行专门处理,例如处理数据库重复键异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RestControllerAdvice public class ExceptionHandler { @ExceptionHandler public Result handleException (Exception e) { log.error("程序出错了" , e); return Result.error("服务器端出错" ); } @ExceptionHandler public Result handleDuplicateKeyException (DuplicateKeyException e) { log.error("程序出错了" , e); String message = e.getMessage(); int index = message.indexOf("Duplicate entry" ); String substring = message.substring(index); String[] arr = substring.split(" " ); return Result.error(arr[2 ] + "已存在" ); } }
1.5 优势
统一管理 :集中处理所有异常,避免在各个方法中重复编写异常处理代码
统一响应 :向客户端返回统一格式的错误响应,提高API的一致性
简化代码 :业务代码可以专注于核心逻辑,不需要关心异常处理
更好的错误追踪 :可以在全局异常处理器中统一记录异常日志,便于问题排查
1.6 适用场景
RESTful API :为API提供统一的错误响应格式
微服务架构 :在网关层统一处理各服务的异常
任何需要统一异常处理的Spring Boot应用
登录校验技术实现 1. Cookie技术 1.1 基本概念 Cookie 是一种客户端会话跟踪技术,是服务器发送给浏览器的小型文本文件,浏览器会将其存储并在后续请求中携带。
核心特点 :
存储在浏览器端
大小限制(通常4KB左右)
每个域名下的Cookie数量有限
会随请求自动携带
1.2 工作原理
设置Cookie :服务器在响应中通过Set-Cookie头设置Cookie
存储Cookie :浏览器接收并存储Cookie
携带Cookie :浏览器在后续请求中通过Cookie头携带Cookie
读取Cookie :服务器从请求中读取Cookie信息
1.3 代码实现 1.3.1 设置Cookie 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @RestController public class CookieController { @GetMapping("/set-cookie") public Result setCookie (HttpServletResponse response) { Cookie cookie = new Cookie ("login_username" , "itheima" ); cookie.setPath("/" ); cookie.setMaxAge(3600 ); cookie.setHttpOnly(true ); cookie.setSecure(false ); response.addCookie(cookie); return Result.success("Cookie设置成功" ); } }
1.3.2 获取Cookie 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @RestController public class CookieController { @GetMapping("/get-cookie") public Result getCookie (HttpServletRequest request) { Cookie[] cookies = request.getCookies(); if (cookies != null ) { for (Cookie cookie : cookies) { if ("login_username" .equals(cookie.getName())) { String value = cookie.getValue(); System.out.println("获取到Cookie: " + cookie.getName() + " = " + value); return Result.success("获取Cookie成功" , value); } } } return Result.error("未找到指定Cookie" ); } }
1.3.3 Cookie属性说明
属性
方法
说明
名称
setName(String name)
Cookie的名称,一旦设置不可修改
值
setValue(String value)
Cookie的值
路径
setPath(String path)
Cookie的作用路径,默认当前路径
域名
setDomain(String domain)
Cookie的作用域名
过期时间
setMaxAge(int maxAge)
过期时间(秒),0表示删除,负数表示会话Cookie
HttpOnly
setHttpOnly(boolean httpOnly)
防止JavaScript读取,增强安全性
Secure
setSecure(boolean secure)
是否只在HTTPS下传输
1.4 优缺点分析 优点
简单易用 :HTTP协议原生支持
无需服务端存储 :数据存储在客户端
自动携带 :浏览器会自动在请求中携带Cookie
实现简单 :代码量少,易于理解
缺点
安全性差 :Cookie存储在客户端,可被修改
大小限制 :单个Cookie大小限制约4KB
数量限制 :每个域名下Cookie数量有限
移动端支持差 :移动端APP无法直接使用Cookie
跨域限制 :Cookie不能跨域使用
隐私问题 :可能被第三方跟踪
1.5 跨域说明 跨域 是指请求的域名、协议或端口与当前页面不同:
协议跨域 :http:// → https://
域名跨域 :example.com → api.example.com
端口跨域 :8080 → 8081
Cookie默认不支持跨域,需要通过特殊设置(如CORS)才能实现跨域Cookie。
1.6 应用场景
会话管理 :存储用户登录状态
个性化设置 :存储用户偏好设置
跟踪分析 :记录用户行为
购物车 :存储购物车内容
1.7 最佳实践
设置HttpOnly :防止XSS攻击
设置Secure :在HTTPS环境下使用
设置合理的过期时间 :避免Cookie长期存在
加密敏感数据 :不要在Cookie中存储敏感信息
使用路径限制 :减少Cookie的作用范围
1.8 登录校验实现 1.8.1 登录成功设置Cookie 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @PostMapping("/login") public Result login (@RequestBody LoginRequest loginRequest, HttpServletResponse response) { if ("admin" .equals(loginRequest.getUsername()) && "123456" .equals(loginRequest.getPassword())) { Cookie cookie = new Cookie ("login_token" , "valid_token" ); cookie.setPath("/" ); cookie.setMaxAge(3600 ); cookie.setHttpOnly(true ); response.addCookie(cookie); return Result.success("登录成功" ); } return Result.error("用户名或密码错误" ); }
1.8.2 过滤器校验Cookie 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class LoginFilter implements Filter { @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; Cookie[] cookies = req.getCookies(); boolean isLoggedIn = false ; if (cookies != null ) { for (Cookie cookie : cookies) { if ("login_token" .equals(cookie.getName()) && "valid_token" .equals(cookie.getValue())) { isLoggedIn = true ; break ; } } } if (isLoggedIn) { chain.doFilter(request, response); } else { resp.setContentType("application/json" ); resp.getWriter().write("{\"code\":0,\"msg\":\"未登录\",\"data\":null}" ); } } }
2. Session技术 2.1 基本概念 Session 是一种服务端会话跟踪技术,是服务器为每个客户端创建的一个内存空间,用于存储客户端的状态信息。
核心特点 :
存储在服务器端
大小限制较小(受服务器内存限制)
每个客户端对应一个Session
基于Cookie实现会话跟踪
2.2 工作原理
创建Session :服务器为客户端创建Session,生成唯一的Session ID
存储Session ID :服务器将Session ID通过Cookie发送给浏览器
携带Session ID :浏览器在后续请求中携带Session ID
获取Session :服务器通过Session ID获取对应的Session
操作Session :在Session中存储和获取数据
2.3 代码实现 2.3.1 存储数据到Session 1 2 3 4 5 6 7 8 9 10 @Slf4j @RestController public class SessionController { @GetMapping("/s1") public Result session1 (HttpSession session) { log.info("HttpSession-s1: {}" , session.hashCode()); session.setAttribute("loginUser" , "tom" ); return Result.success(); } }
2.3.2 从Session中获取数据 1 2 3 4 5 6 7 8 9 10 11 @Slf4j @RestController public class SessionController { @GetMapping("/s2") public Result session2 (HttpSession session) { log.info("HttpSession-s2: {}" , session.hashCode()); Object loginUser = session.getAttribute("loginUser" ); log.info("loginUser: {}" , loginUser); return Result.success(loginUser); } }
2.4 Session属性说明
方法
说明
setAttribute(String name, Object value)
存储数据到Session
getAttribute(String name)
从Session中获取数据
removeAttribute(String name)
从Session中移除数据
invalidate()
使Session失效
getId()
获取Session ID
getCreationTime()
获取Session创建时间
getLastAccessedTime()
获取Session最后访问时间
setMaxInactiveInterval(int interval)
设置Session最大不活动时间(秒)
getMaxInactiveInterval()
获取Session最大不活动时间
2.5 优缺点分析 优点
安全性高 :数据存储在服务器端,客户端无法直接修改
存储容量大 :相比Cookie,Session可以存储更多数据
类型灵活 :可以存储任意类型的对象
自动管理 :服务器自动管理Session的创建和销毁
缺点
服务器压力 :每个Session都会占用服务器内存
水平扩展困难 :Session存储在单个服务器,不利于集群部署
依赖Cookie :需要Cookie传递Session ID
会话超时 :长时间不活动会导致Session过期
2.6 Session与Cookie的对比
特性
Session
Cookie
存储位置
服务器端
客户端
安全性
高
低
存储容量
大
小(约4KB)
数据类型
任意对象
字符串
生命周期
服务器控制
可设置过期时间
跨域支持
依赖Cookie
不支持
服务器压力
较大
无
2.7 登录校验实现 2.7.1 登录成功存储用户信息到Session 1 2 3 4 5 6 7 8 9 10 11 @PostMapping("/login") public Result login (@RequestBody LoginRequest loginRequest, HttpSession session) { if ("admin" .equals(loginRequest.getUsername()) && "123456" .equals(loginRequest.getPassword())) { session.setAttribute("loginUser" , loginRequest.getUsername()); session.setMaxInactiveInterval(3600 ); return Result.success("登录成功" ); } return Result.error("用户名或密码错误" ); }
2.7.2 过滤器校验Session 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class LoginFilter implements Filter { @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; HttpSession session = req.getSession(false ); boolean isLoggedIn = false ; if (session != null && session.getAttribute("loginUser" ) != null ) { isLoggedIn = true ; } if (isLoggedIn) { chain.doFilter(request, response); } else { resp.setContentType("application/json" ); resp.getWriter().write("{\"code\":0,\"msg\":\"未登录\",\"data\":null}" ); } } }
2.8 最佳实践
合理设置过期时间 :根据业务需求设置Session的过期时间
及时销毁Session :用户登出时调用session.invalidate()
避免存储大对象 :Session中存储的对象不宜过大
考虑集群环境 :在集群环境中使用Session共享方案
结合Cookie使用 :确保Cookie的安全性设置
防止Session固定攻击 :登录成功后重新生成Session ID
3. JWT技术 3.1 基本概念 JWT (JSON Web Token)是一种基于令牌的会话跟踪技术,是一种紧凑的、自包含的令牌格式,用于在各方之间安全地传输信息。
核心特点 :
无状态,便于水平扩展
自包含,包含所有必要信息
安全,使用数字签名保证完整性
跨域支持,适用于前后端分离架构
3.2 JWT令牌结构 JWT令牌由三部分组成,用.分隔:
Header(头部) :记录令牌类型、签名算法
1 2 3 4 { "alg" : "HS256" , "typ" : "JWT" }
Payload(载荷) :携带一些自定义的信息
1 2 3 4 5 { "id" : 1 , "username" : "admin" , "exp" : 1719657600 }
Signature(签名) :防止被篡改,保证安全性
1 2 3 4 5 HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secretKey )
3.3 依赖配置 Maven依赖 :
1 2 3 4 5 6 7 <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > 0.12.6</version > <scope > compile</scope > </dependency >
注意 :0.9.1版本在高版本Java中已不适用,建议使用0.12.6或更高版本。
3.4 代码实现 3.4.1 生成JWT令牌 1 2 3 4 5 6 7 8 9 10 11 12 13 public void testGenerateJwt () { Map<String, Object> dataMap = new HashMap <>(); dataMap.put("id" , 1 ); dataMap.put("username" , "admin" ); String jwt = Jwts.builder() .signWith(SignatureAlgorithm.HS256, "aXRoZWltYQ==" ) .addClaims(dataMap) .setExpiration(new Date (System.currentTimeMillis() + 3600 * 1000 )) .compact(); System.out.println(jwt); }
3.4.2 解析JWT令牌 1 2 3 4 5 6 7 8 9 10 11 12 public void testParseJwt () { String token = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsImV4cCI6MTcxOTY1NzYwMH0.SIGNATURE" ; Claims claims = Jwts.parser() .setSigningKey("aXRoZWltYQ==" ) .parseClaimsJws(token) .getBody(); System.out.println("id: " + claims.get("id" )); System.out.println("username: " + claims.get("username" )); System.out.println("exp: " + claims.getExpiration()); }
3.5 工作原理
生成令牌 :服务器验证用户身份后,生成JWT令牌并返回给客户端
存储令牌 :客户端存储JWT令牌(如localStorage)
携带令牌 :客户端在后续请求中通过请求头(如Authorization)携带令牌
验证令牌 :服务器验证令牌的有效性(签名、过期时间等)
处理请求 :验证通过则处理请求,否则拒绝访问
3.6 优缺点分析 优点
无状态 :服务器不需要存储会话信息,便于水平扩展
自包含 :令牌包含所有必要信息,减少数据库查询
跨域支持 :适用于前后端分离、微服务架构
移动端友好 :移动端APP可以轻松处理令牌
安全性 :使用数字签名保证令牌完整性
缺点
令牌大小 :相比Session ID,JWT令牌较大
撤销困难 :令牌一旦生成,在过期前无法主动撤销
存储敏感信息 :Payload部分是Base64编码,不适合存储敏感信息
性能开销 :每次请求都需要验证签名
3.7 登录校验实现 3.7.1 登录成功生成JWT令牌 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @PostMapping("/login") public Result login (@RequestBody LoginRequest loginRequest) { if ("admin" .equals(loginRequest.getUsername()) && "123456" .equals(loginRequest.getPassword())) { Map<String, Object> claims = new HashMap <>(); claims.put("id" , 1 ); claims.put("username" , loginRequest.getUsername()); String token = Jwts.builder() .signWith(SignatureAlgorithm.HS256, "your-secret-key" ) .addClaims(claims) .setExpiration(new Date (System.currentTimeMillis() + 3600 * 1000 )) .compact(); return Result.success("登录成功" , token); } return Result.error("用户名或密码错误" ); }
3.7.2 过滤器校验JWT令牌 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class JwtFilter implements Filter { @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String token = req.getHeader("Authorization" ); boolean isLoggedIn = false ; if (token != null && token.startsWith("Bearer " )) { try { String jwt = token.substring(7 ); Claims claims = Jwts.parser() .setSigningKey("your-secret-key" ) .parseClaimsJws(jwt) .getBody(); isLoggedIn = true ; req.setAttribute("user" , claims); } catch (Exception e) { System.out.println("JWT验证失败: " + e.getMessage()); } } if (isLoggedIn) { chain.doFilter(request, response); } else { resp.setContentType("application/json" ); resp.getWriter().write("{\"code\":0,\"msg\":\"未登录\",\"data\":null}" ); } } }
3.8 注意事项
密钥管理 :JWT校验时使用的签名密钥,必须和生成JWT令牌时使用的密钥是配套的
令牌过期 :设置合理的过期时间,避免令牌长期有效
敏感信息 :不要在Payload中存储敏感信息
HTTPS :在生产环境中使用HTTPS传输令牌
令牌刷新 :实现令牌刷新机制,避免用户频繁登录
黑名单 :实现令牌黑名单,处理已注销的令牌
3.9 最佳实践
使用强密钥 :使用足够长度和复杂度的密钥
合理设置过期时间 :根据业务需求设置令牌的过期时间
实现令牌刷新 :提供令牌刷新接口,避免用户频繁登录
使用HTTPS :确保令牌传输的安全性
验证令牌有效性 :每次请求都要验证令牌的签名和过期时间
处理令牌异常 :妥善处理令牌验证过程中的异常
Filter 过滤器(Filter)
概念 :Filter过滤器,是JavaWeb三大组件(Servlet、Filter、Listener)之一。
过滤器可以把对资源的请求拦截 下来,从而实现一些特殊的功能。
过滤器一般完成一些通用 的操作,比如:登录校验、统一编码处理、敏感字符处理等。
Filter快速入门
定义Filter :定义一个类,实现 Filter 接口,并实现其所有方法。
配置Filter :Filter类上加@WebFilter注解,配置拦截路径。引导类上加 @ServletComponentScan 开启Servlet组件支持。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class DemoFilter implements Filter { public void init (FilterConfig filterConfig) throws ServletException { System.out.println("init ..." ); } public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws Exception{ System.out.println("拦截到了请求..." ); chain.doFilter(servletRequest, servletResponse); } public void destroy () { System.out.println("destroy ... " ); } }
登录校验Filter 备注说明 用户登录成功后,系统会自动下发JWT令牌,然后在后续的每次请求中,都需要在请求头header中携带到服务端,请求头的名称为 token,值为 登录时下发的JWT令牌。如果检测到用户未登录,则直接响应 401 状态码。
登录校验Filter流程
获取请求url。
判断请求url中是否包含login,如果包含,说明是登录操作,放行。
获取请求头中的令牌(token)。
判断令牌是否存在,如果不存在,响应401。
解析token,如果解析失败,响应401。
放行。
思考
所有的请求,拦截到了之后,都需要校验令牌吗?有一个例外,登录请求
拦截到请求后,什么情况下才可以放行,执行业务操作?有令牌,且令牌校验通过(合法);否则都返回未登录错误结果
完整代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package com.yx.Filter;import com.yx.utils.JwtUtils;import jakarta.servlet.*;import jakarta.servlet.annotation.WebFilter;import jakarta.servlet.http.HttpServlet;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.io.IOException;@Component @Slf4j @WebFilter(urlPatterns = "/*") public class TokenFilter implements Filter { @Autowired JwtUtils jwtUtils; @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; String requestURI = httpServletRequest.getRequestURI(); if (requestURI.contains("/login" )) { log.info("登录操作放行" ); filterChain.doFilter(httpServletRequest, httpServletResponse); return ; } String token = httpServletRequest.getHeader("Token" ); if (token == null || token.isEmpty()) { httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return ; } jwtUtils.parseToken(token); filterChain.doFilter(httpServletRequest, httpServletResponse); } }
Filter执行流程 1 2 3 4 5 6 7 8 public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) { System.out.println("拦截方法执行,拦截到了请求..." ); System.out.println("执行放行前逻辑..." ); chain.doFilter(request, response); System.out.println("执行放行后逻辑..." ); }
执行流程:放行前 → 放行 → 资源 → 放行后
思考问题
问题1 :放行后访问对应资源,资源访问完成后,还会回到Filter中吗?会
问题2 :如果回到Filter中,是重新执行还是执行放行后的逻辑呢?执行放行后逻辑
Filter-拦截路径 Filter 可以根据需求,配置不同的拦截资源路径:
1 2 @WebFilter(urlPatterns = "/*") public class DemoFilter implements Filter {
拦截路径
urlPatterns值
含义
拦截具体路径
/login
只有访问 /login 路径时,才会被拦截
目录拦截
/emps/*
访问/emps下的所有资源,都会被拦截
拦截所有
/*
访问所有资源,都会被拦截
Filter-过滤器链
介绍 :一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链 。
顺序 :注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序。
1 2 3 4 public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) { System.out.println("拦截到了请求..." ); chain.doFilter(servletRequest, servletResponse); }
小结
过滤器的执行流程
配置的过滤器的拦截路径/* 与 /emps/* 分别代表什么意思?
/*:表示拦截所有
/emps/*:表示目录拦截,拦截/emps下的所有资源
什么是过滤器链?
Interceptor 拦截器(Interceptor)
概念 :是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,主要用来动态拦截控制器方法的执行。
作用 :拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。
Interceptor快速入门
定义拦截器 :实现HandlerInterceptor接口,并实现其所有方法。
注册拦截器 :定义一个配置类实现WebMvcConfigurer接口,注册拦截器。
完整代码示例 DemoInterceptor.java :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.yx.Interceptor;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import lombok.extern.slf4j.Slf4j;import org.jspecify.annotations.Nullable;import org.springframework.stereotype.Component;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;@Component @Slf4j public class DemoInterceptor implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("preHandle..." ); return true ; } @Override public void postHandle (HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { log.info("postHandle..." ); } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { log.info("afterCompletion..." ); } }
WebConfig.java :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.yx.config;import com.yx.Interceptor.DemoInterceptor;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 WebConfig implements WebMvcConfigurer { @Autowired private DemoInterceptor demoInterceptor; @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(demoInterceptor).addPathPatterns("/**" ); } }
拦截器使用步骤
定义 :实现HandlerInterceptor接口
preHandle
postHandle
afterCompletion
配置 :定义一个配置类实现WebMvcConfigurer接口,注册拦截器
令牌校验-拦截器 令牌校验拦截器流程
获取请求url。
判断请求url中是否包含login,如果包含,说明是登录操作,放行。
获取请求头中的令牌(token)。
判断令牌是否存在,如果不存在,响应401。
解析token,如果解析失败,响应401。
放行。
完整代码示例 TokenFilter.java (作为拦截器实现):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package com.yx.Filter;import com.yx.utils.JwtUtils;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import org.springframework.web.servlet.HandlerInterceptor;@Slf4j @Component public class TokenFilter implements HandlerInterceptor { @Autowired JwtUtils jwtUtils; @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestURI = request.getRequestURI(); if (requestURI.contains("/login" )) { return true ; } String token = request.getHeader("token" ); if (token == null || token.isEmpty()) { return false ; } jwtUtils.parseToken(token); return true ; } }
WebConfig.java (注册拦截器):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.yx.config;import com.yx.Filter.TokenFilter;import com.yx.Interceptor.DemoInterceptor;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 WebConfig implements WebMvcConfigurer { @Autowired TokenFilter tokenFilter; @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(tokenFilter).addPathPatterns("/**" ).excludePathPatterns("/login" ); } }
拦截器-拦截路径 拦截器可以根据需求,配置不同的拦截路径:
1 2 3 4 @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(demoInterceptor).addPathPatterns("/**" ).excludePathPatterns("/login" ); }
拦截路径
含义
举例
/
一级路径
能匹配/depts, /emps, /login,不能匹配 /depts/1
/**
任意级路径
能匹配/depts, /depts/1, /depts/1/2
/depts/*
/depts下的一级路径
能匹配/depts/1,不能匹配/depts/1/2, /depts
/depts/**
/depts下的任意级路径
能匹配/depts, /depts/1, /depts/1/2,不能匹配/emprs/1
拦截器-执行流程 Filter 与 Interceptor 区别
接口规范不同 :过滤器需要实现Filter接口,而拦截器需要实现HandlerInterceptor接口。
拦截范围不同 :过滤器Filter会拦截所有的资源,而Interceptor只会拦截Spring环境中的资源。
执行流程对比 执行顺序:Filter(放行前) → Interceptor(preHandle) → Controller → Interceptor(postHandle) → Interceptor(afterCompletion) → Filter(放行后)
面向切面编程(AOP) 什么是AOP
AOP : Aspect Oriented Programming(面向切面编程、面向方面编程),可简单理解为就是面向特定方法编程。
场景 :案例中部分业务方法运行较慢,定位执行耗时长的接口,此时需要统计每一个业务方法的执行耗时。
优势 :
减少重复代码
代码无侵入
提高开发效率
维护方便
AOP快速入门 需求:统计所有业务层方法的执行耗时 原始方式 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public List<Dept> list () { long beginTime = System.currentTimeMillis(); List<Dept> deptList = deptMapper.list(); long endTime = System.currentTimeMillis(); log.info("执行耗时:{} ms" , endTime - beginTime); return deptList; } public void delete (Integer id) { long beginTime = System.currentTimeMillis(); deptMapper.delete(id); long endTime = System.currentTimeMillis(); log.info("执行耗时:{} ms" , endTime - beginTime); }
优化后(使用AOP) :
1 2 3 4 5 6 7 8 9 public List<Dept> list () { List<Dept> deptList = deptMapper.list(); return deptList; } public void delete (Integer id) { deptMapper.delete(id); }
SpringAOP快速入门:统计所有业务层方法的执行耗时 步骤
导入依赖 :在pom.xml中引入AOP的依赖
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency >
编写AOP程序 :针对于特定的方法根据业务需要进行编程
1 2 3 4 5 6 7 8 9 10 11 12 @Aspect @Component public class RecordTimeAspect { @Around("execution(* com.itheima.service.impl.*.*(..))") public Object recordTime (ProceedingJoinPoint pjp) throws Throwable { long beginTime = System.currentTimeMillis(); Object result = pjp.proceed(); long endTime = System.currentTimeMillis(); log.info("执行耗时:{} ms" , endTime - beginTime); return result; } }
执行流程
获取方法运行的开始时间
运行原始方法
获取方法运行结束时间,计算执行耗时
AOP核心概念
连接点 : JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
通知 : Advice,指那些重复的逻辑,也就是共性功能(最终体现为一个方法)
切入点 : PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
切面 : Aspect,描述通知与切入点的对应关系(通知+切入点)
目标对象 : Target,通知所应用的对象
代码示例说明 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Aspect @Component public class RecordTimeAspect { @Around("execution(* com.itheima.service.impl.*.*(..))") public Object recordTime (ProceedingJoinPoint pjp) throws Throwable { long begin = System.currentTimeMillis(); Object result = pjp.proceed(); long end = System.currentTimeMillis(); log.info("方法 {} 执行耗时:{}ms" , pjp.getSignature(), end-begin); return result; } }
切面类 : 被 @Aspect 和 @Component 注解修饰的类
通知 : 方法内的代码逻辑
切面 : 通知 + 切入点
目标对象示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Override public List<Dept> list () { List<Dept> deptList = deptMapper.list(); return deptList; } @Override public void delete (Integer id) { deptMapper.delete(id); } @Override public void save (Dept dept) { dept.setCreateTime(LocalDateTime.now()); dept.setUpdateTime(LocalDateTime.now()); deptMapper.save(dept); } @Override public Dept getById (Integer id) { return deptMapper.getById(id); } }
AOP执行流程 动态代理 执行流程:通过动态代理机制实现,实际调用的是代理对象中的方法
1 2 3 4 5 6 7 8 9 10 11 12 @Aspect @Component public class RecordTimeAspect { @Around("execution(* com.itheima.service.impl.*.*(..))") public Object recordTime (ProceedingJoinPoint joinPoint) throws Throwable { long begin = System.currentTimeMillis(); Object result = joinPoint.proceed(); long end = System.currentTimeMillis(); log.info("执行耗时:{} ms" , (end-begin)); return result; } }
目标对象 :
1 2 3 4 5 6 7 8 9 10 public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; @Override public List<Dept> list () { List<Dept> deptList = deptMapper.list(); return deptList; } }
代理对象 :
1 2 3 4 5 6 7 8 9 10 public class DeptServiceProxy implements DeptService { @Override public List<Dept> list () { long begin = System.currentTimeMillis(); List<Dept> deptList = 目标对象.list(); long end = System.currentTimeMillis(); log.info("执行耗时:{} ms" , (end-begin)); return deptList; } }
控制器调用 :
1 2 3 4 5 6 7 8 9 10 public class DeptController { @Autowired private DeptService deptService; @GetMapping public Result list () { List<Dept> deptList = deptService.list(); return Result.success(deptList); } }
通知类型 根据通知方法执行时机的不同,将通知类型分为以下常见的五类:
@Around :环绕通知,此注解标注的通知方法在目标方法前、后都被执行
@Before :前置通知,此注解标注的通知方法在目标方法前被执行
@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@AfterReturning :返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
@AfterThrowing :异常后通知,此注解标注的通知方法发生异常后执行
注意事项
注意 1 :@Around 环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
注意 2 :@Around 环绕通知方法的返回值必须与目标方法的返回值一致
连接点(JoinPoint) 核心概念:连接点 (JoinPoint) 在 Spring AOP 中,连接点 指的是程序执行过程中的一个特定点,比如方法的调用或异常的处理。而 JoinPoint 接口 就是对这些连接点的抽象。
它的主要作用是:让通知方法能够访问到被增强方法(即目标方法)的上下文信息 。通过 JoinPoint 对象,你可以获取到:
目标类名 (getTarget().getClass().getName())
目标方法签名 (getSignature())
目标方法名 (getSignature().getName())
目标方法的参数 (getArgs())
这些信息对于实现日志记录、权限校验、性能监控等横切关注点至关重要。
关键区别:JoinPoint vs ProceedingJoinPoint 1. 对于 @Around (环绕通知)
必须使用 : ProceedingJoinPoint
原因 : @Around 通知的特点是它”环绕”着目标方法执行。它不仅可以在目标方法前后执行逻辑,还拥有控制目标方法是否执行以及何时执行的权力 。
核心方法 : proceed()
这个方法是 ProceedingJoinPoint 接口独有的。
调用 joinPoint.proceed() 才会真正去执行原始的目标方法。
如果你不调用 proceed(),目标方法就不会被执行。这为实现缓存、事务回滚、条件性执行等功能提供了可能。
返回值 : @Around 通知方法的返回值必须是 Object,并且通常需要将 proceed() 方法的返回值返回给调用者,以保证业务逻辑的正常流转。
代码示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Around("execution(* com.itheima.service.DeptService.*(..))") public Object around (ProceedingJoinPoint joinPoint) throws Throwable { String className = joinPoint.getTarget().getClass().getName(); Signature signature = joinPoint.getSignature(); String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); Object res = joinPoint.proceed(); return res; }
2. 对于其他四种通知 (@Before, @After, @AfterReturning, @AfterThrowing)
必须使用 : JoinPoint
原因 : 这四种通知的执行时机是固定的,它们无法控制目标方法的执行 。
@Before: 在目标方法之前执行。
@After: 在目标方法之后执行(无论成功与否)。
@AfterReturning: 在目标方法成功返回后执行。
@AfterThrowing: 在目标方法抛出异常后执行。 因为它们不负责调用目标方法,所以不需要 proceed() 方法。
继承关系 : ProceedingJoinPoint 继承了 JoinPoint 接口,因此它拥有 JoinPoint 的所有功能(如 getArgs(), getSignature()),并额外增加了 proceed() 方法。
代码示例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 @Before("execution(* com.itheima.service.DeptService.*(..))") public void before (JoinPoint joinPoint) { String className = joinPoint.getTarget().getClass().getName(); Signature signature = joinPoint.getSignature(); String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); }
总结与要点
特性
@Around 通知
其他通知 (@Before, @After 等)
所需参数类型
ProceedingJoinPoint
JoinPoint
能否控制目标方法执行
能 ,通过 proceed() 方法
不能 ,目标方法由 AOP 框架自动调用
核心方法
proceed()
无
返回值要求
必须为 Object,并通常返回 proceed() 的结果
void (对于 @Before, @After, @AfterThrowing) 或与目标方法返回值兼容的类型 (对于 @AfterReturning)
灵活性
最高,可以实现所有其他通知的功能
较低,功能单一
简单记忆法 :
看到 @Around,就想到 ProceedingJoinPoint 和 proceed()。
看到其他通知,就用 JoinPoint 来获取信息即可。
@Pointcut (切入点复用) 1. 作用 该注解的作用是将公共的切点表达式抽取出来 。当其他地方需要用到该切点时,只需引用定义的方法名即可,无需重复编写复杂的表达式。
2. 代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))") public void pt () {}@Around("pt()") public Object recordTime (ProceedingJoinPoint joinPoint) throws Throwable { long beginTime = System.currentTimeMillis(); Object result = joinPoint.proceed(); long endTime = System.currentTimeMillis(); log.info("执行耗时:{} ms" , endTime - beginTime); return result; }
3. 使用优势与说明
解耦与维护性 :如果业务层包路径发生变化(例如从 impl 包移到了其他包),只需要修改 @Pointcut 注解中的表达式一次,所有引用了 pt() 的通知都会自动生效,避免了逐个修改通知注解的繁琐。
语法规范 :
被 @Pointcut 修饰的方法必须是 public void 且没有参数。
引用时,直接写方法名加括号,如 "pt()"。
如果在同一个类中引用,直接写方法名;如果在不同类中引用,需要加上类名,如 "com.example.AspectClass.pt()"。
通知顺序 (Advice Order) 当多个切面(Aspect) 的切入点都匹配到同一个目标方法时,这些切面中的通知(Advice)的执行顺序如下:
多切面场景 当一个目标方法被多个切面的切入点表达式匹配到时,所有这些切面中对应的通知方法都会被执行。这就引出了”谁先执行,谁后执行”的问题。
默认执行顺序
前置通知 (@Before)、环绕通知 (@Around 的前半部分):类名字母排名靠前的切面,其通知方法会先执行。
后置通知 (@After, @AfterReturning)、环绕通知 (@Around 的后半部分):类名字母排名靠前的切面,其通知方法会后执行。
简单记忆 :可以把这想象成一个”栈”结构。字母序靠前的切面先进入调用栈(先执行前置逻辑),所以它会最后出来(后执行后置逻辑)。
自定义执行顺序 (@Order 注解) 为了更灵活地控制顺序,可以使用 @Order(数字) 注解添加到切面类上。
规则 :
@Order 注解中的数字越小,优先级越高。
对于前置通知:@Order 值小的切面,其通知方法先执行。
对于后置通知:@Order 值小的切面,其通知方法后执行。
注意 :一旦使用了 @Order 注解,默认的按类名字母排序的规则就会失效。
代码示例解读 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Aspect @Component public class MyAspect2 { @Before("execution(* com.itheima.service.impl.*.*(..))") public void before () { System.out.println("MyAspect2 before..." ); } @After("execution(* com.itheima.service.impl.*.*(..))") public void after () { System.out.println("MyAspect2 after..." ); } } @Aspect @Component public class MyAspect3 { @Before("execution(* com.itheima.service.impl.*.*(..))") public void before () { System.out.println("MyAspect3 before..." ); } @After("execution(* com.itheima.service.impl.*.*(..))") public void after () { System.out.println("MyAspect3 after..." ); } } @Aspect @Component @Order(5) public class RecordTimeAspect { }
根据默认的字母排序规则,MyAspect2 的名字在 MyAspect3 之前,因此:
前置通知:MyAspect2.before() 先于 MyAspect3.before() 执行
后置通知:MyAspect2.after() 后于 MyAspect3.after() 执行
切入点表达式 (Pointcut Expression) 定义与作用
介绍 :切入点表达式是一种用于描述和匹配连接点(通常是方法)的语法。
作用 :它的核心作用是决定项目中的哪些具体方法需要被通知(Advice)所增强。你可以把它理解为 AOP 的”瞄准镜”。
常见形式 1. execution(…) 根据方法的签名来匹配。这是最强大和最常用的一种方式。
基本语法结构 :
1 execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)
带 ? 的部分表示可以省略 。在实际开发中,为了简化表达式,我们通常会省略访问修饰符和 throws 异常部分。
关键组成部分 :一个完整的 execution 表达式主要由 返回值 、包名与类名 、方法名 和 方法参数 这四部分构成。
可省略的部分 :
访问修饰符 :如 public, protected 等,通常省略。
包名.类名 :虽然可以省略,但省略后匹配范围会变得非常大,一般不这么做。
throws 异常 :指方法签名上声明的异常(如 throws SQLException),而不是方法内部实际抛出的异常。这部分也常被省略。
**通配符的使用 (Wildcards)**: 这是 execution 表达式的精髓,它让匹配变得非常灵活。
*** (星号)**:
含义 :匹配单个 任意字符或符号。
用途 :
匹配任意的返回值类型:execution(* ...)
匹配任意的类名或方法名的一部分:...Service* 可以匹配 UserService, OrderService 等。
匹配任意类型的单个参数:...(String, *) 可以匹配第二个参数是任何类型的方法。
示例解读 :1 execution(* com.*.service.*.update*( *))
这个表达式会匹配:
在 com 包下的一级子包(如 com.itheima)中。
任何以 service 结尾的包(如 com.itheima.service)。
任何类(*)。
任何以 update 开头的方法(update*,如 update, updateUser)。
该方法接受且仅接受一个任意类型的参数(( *))。
**.. (双点)**:
含义 :匹配多个连续 的任意符号。
用途 :
在包路径中 :匹配当前包及其所有子包。例如 com.itheima..* 可以匹配 com.itheima.service.DeptService 和 com.itheima.service.impl.DeptServiceImpl。
在参数列表中 :匹配任意数量、任意类型的参数。这是最常用的方式之一,表示”不管这个方法有几个参数,是什么类型,我都匹配”。
示例解读 :1 execution(* com.itheima..DeptService.*(..))
这个表达式会匹配:
在 com.itheima 包及其所有子包下。
名为 DeptService 的类(注意,这里没有 *,所以只匹配这个名字的类)。
该类中的所有方法 (.*)。
无论这些方法的参数是什么(( .. ))。
示例 :
1 2 3 4 @Before("execution(public void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))") public void before (JoinPoint joinPoint) { }
这个表达式精确地匹配了 com.itheima.service.impl.DeptServiceImpl 类中,返回类型为 void,方法名为 delete,且参数为 java.lang.Integer 的那个 public 方法。
2. @annotation(…) 根据注解来匹配。这种方式更加灵活和解耦。
作用 :
@annotation(...) 表达式用于匹配所有被特定注解所标记的方法 。
它的优势在于解耦 。你不需要关心方法在哪个类、哪个包里,也不需要关心方法的签名(返回值、参数等),只要方法上有指定的注解,就会被匹配到。这使得 AOP 的配置更加灵活,业务代码的结构变化(如重构、移动类)不会轻易影响到 AOP 的逻辑。
工作流程 :
第一步:定义切面 。在切面类中,使用 @annotation(自定义注解的全限定名) 作为切入点表达式。
1 2 3 4 5 6 7 8 @Around("@annotation(com.itheima.anno.LogOperation)") public Object logAround (ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); Object result = joinPoint.proceed(); return result; }
第二步:创建自定义注解 。你需要先创建一个自己的注解,例如 @LogOperation。
1 2 3 4 5 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LogOperation {}
第三步:在目标方法上使用注解 。在任何你想要应用 logAround 通知的方法上,加上 @LogOperation 注解即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 @LogOperation @DeleteMapping("/{id}") public Result delete (@PathVariable Integer id) { return Result.success(); } @LogOperation @PostMapping public Result save (@RequestBody Dept dept) { return Result.success(); }
示例 :
1 2 3 4 @Before("@annotation(com.itheima.anno.Log)") public void before () { }
这个表达式会匹配项目中所有 被 @Log (来自 com.itheima.anno 包) 注解标记的方法。无论这个方法在哪个类里,叫什么名字,有什么参数,只要有 @Log 注解,这个 before 通知就会在它执行前运行。
总结对比:
特性
execution 表达式
@annotation 表达式
匹配依据
方法的签名(包、类、方法名、参数等)
方法上的特定注解
灵活性
高,但配置复杂,与代码结构耦合度高
极高 ,配置简单,与代码结构解耦
适用场景
需要对某个包或某类的所有方法进行统一增强
需要精确地、有选择性地对某些特定方法进行增强
维护成本
当项目重构(如移动类、改名)时,可能需要修改表达式
几乎无需维护,只要注解还在,增强就有效
总而言之,execution 适合”广撒网”式的批量增强,而 @annotation 则适合”精准打击”式的按需增强。在实际项目中,两者常常结合使用。
总结
通知顺序 解决了”当有多个增强逻辑时,它们的执行顺序如何控制?”的问题,提供了默认规则和自定义 (@Order) 两种方式。
切入点表达式 解决了”如何精准地选择需要被增强的目标方法?”的问题,介绍了基于方法签名的 execution 和基于注解的 @annotation 两种强大的匹配方式。