SSM速通4
PS:在我的项目中时不时会出现lombok依赖未知原因不可用的问题,有时候可以成功,有时候死活用不了,大家可通过修改版本或者删除lombok新增插件来进行解决,以下给出其他大佬的一个有效方法:
六、常用功能
补充:如何复制一个项目并且使其被maven识别
- 复制该项目,修改其中的pom.xml中的GAVP
- 点击右侧栏的maven选择添加maven项目,将新复制并完成修改的项目添加进即可被识别
1、拦截器
拦截器(Interceptor)是一种在方法调用前后或请求处理前后执行特定逻辑的机制,常用于日志记录、权限验证、事务管理等场景。
以下都是基于Spring MVC拦截器做例子
单个拦截器执行顺序:preHandle------>目标方法------>postHandle------>afterCompletion
多个拦截器执行顺序:
preHandle0------>preHandle1------>preHandle2------>目标方法------>postHandle2------>postHandle1------>postHandle0------>afterCompletion2------>afterCompletion1------>afterCompletion0
可见preHandle是顺序执行,**当preHandle全部放行且目标方法正确执行才开始postHandle,**postHandle和afterCompletion都是倒序执行,已经执行过的preHandle即使目标方法未执行,其afterCompletion也会执行
比如我在preHandle1设置拦截,即最终只有preHandle0------>preHandle1(拦截)------>afterCompletion1

1.1、创建拦截器
java
package org.example.spring05bestpracitce.interceptor;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Component
public class MyHandlerInterceptor implements HandlerInterceptor {
// 注意每个重写的方法内都可以添加自己的业务逻辑,比如判断用户是否登录、记录日志等
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 在控制器方法执行前调用
System.out.println("Pre-handle method is calling");
return true; // 返回true继续执行,false则中断
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
// 在控制器方法执行后,视图渲染前调用
System.out.println("Post-handle method is calling");
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
// 在整个请求完成后调用
System.out.println("After completion method is calling");
}
}
创建的拦截器需要实现HandlerInterceptor接口,该接口中包含3个方法,其中我们主要使用的是前两个方法:preHandle(在方法执行前调用)和postHandle(在方法执行后调用)
1.2、注册拦截器
java
package org.example.spring05bestpracitce.config;
import org.example.spring05bestpracitce.interceptor.MyHandlerInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 注册我们的拦截器需要一个组件:WebMvcConfigurer
* 方法1、使用 @Bean 将该组件注入到容器中,然后重写其中的 addInterceptors 方法
* 方法2、使当前类实现 WebMvcConfigurer 接口,重写 addInterceptors 方法
*/
// 方法1
//@Configuration
//public class MySpringMVCConfig{
// @Bean
// public WebMvcConfigurer webMvcConfigurer(){
// return new WebMvcConfigurer(){
// @Override
// public void addInterceptors(InterceptorRegistry registry) {
// // 路径规则
// registry.addInterceptor(new MyHandlerInterceptor())
// .addPathPatterns("/api/**") // 拦截路径
// .excludePathPatterns("/api/public/**"); // 排除路径
// }
// };
// }
//}
// 方法2
@Configuration
// WebMvcConfigurer配置类用于注册我们的拦截器,专门对 SpringMVC 底层进行配置
public class MySpringMVCConfig implements WebMvcConfigurer {
@Autowired
private MyHandlerInterceptor myHandlerInterceptor; // 注入拦截器组件
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myHandlerInterceptor)
.addPathPatterns("/api/**") // 拦截路径
.excludePathPatterns("/api/public/**"); // 排除路径
}
}
其中拦截路径表示不准通行的路径,排除路径就是允许通过的路径
1.3、过滤器
拦截器和过滤器对比:
| 特性 | 过滤器(Filter) | 拦截器(Interceptor) |
|---|---|---|
| 所属规范 | Java Servlet规范 | 各框架自有实现(Spring MVC, Struts2等) |
| 作用范围 | Web容器层面 | 框架层面 |
| 执行位置 | Servlet容器与Servlet/JSP之间 | Controller/Action方法前后 |
| 依赖关系 | 不依赖任何框架 | 依赖特定MVC框架 |
| 配置方式 | web.xml或@WebFilter注解 | 框架配置文件或Java配置类 |
过滤器可过滤的范围更广,可过滤web应用所有请求,过滤器属于原生组件
在Spring MVC应用中,推荐使用拦截器;脱离了Spring MVC则使用过滤器
使用过滤器(Filter)当:
- 需要处理Servlet层面的原始请求/响应
- 功能与业务逻辑无关(如编码、压缩)
- 需要应用于所有请求(包括静态资源)
使用拦截器(Interceptor)当:
- 需要访问Controller处理信息
- 功能与业务逻辑相关(如权限检查)
- 需要精细的路径匹配控制
- 需要处理方法返回值或异常
2、异常处理
异常处理是Java编程中保证程序健壮性的重要机制
异常处理优先级:
本类的定义的处理方法 > 全局的精确处理方法 > 全局的模糊处理方法 > Spring Boot的兜底自适应机制
Spring Boot的兜底自适应机制,即该异常我们没有设置处理方法,如果我们是网页则会返回错误页面,如果我们是通过类似 postman 发送的是 http 请求则会返回 json格式的错误信息
2.1、编程式异常处理
编程式的异常处理,即通过大量代码,经过try-catch-finally 结构处理异常
java
try {
// 可能抛出异常的代码
FileInputStream fis = new FileInputStream("file.txt");
} catch (FileNotFoundException e) {
// 处理特定异常
System.err.println("文件未找到: " + e.getMessage());
} catch (IOException e) {
// 处理更一般的异常
System.err.println("IO错误: " + e.getMessage());
} finally {
// 无论是否发生异常都会执行
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
System.err.println("关闭文件流失败");
}
}
}
2.2、声明式异常处理
声明式异常处理即通过Spring中的注解实现异常处理,本方法因为编程式异常编码过于繁琐而产生的,为了减少要编码的量
java
package org.example.spring05bestpracitce.controller;
import org.example.spring05bestpracitce.common.R;
import org.example.spring05bestpracitce.entity.Employee;
import org.example.spring05bestpracitce.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.io.FileNotFoundException;
@RestController
public class EmployeeRestController {
@Autowired
private EmployeeService employeeService;
@GetMapping("/employee/{id}")
// 当然也可以写成 @RequestMapping(value = "/employee/{id}",method = RequestMethod.GET)
// 只有当请求方法为 GET 时,才会调用该方法
public R getEmployeeById(@PathVariable int id) {
return R.ok(employeeService.getEmployeeById(id));
}
@PostMapping("/employee")
// 只有当请求方法为 POST 时,才会调用该方法
// 其请求体中包含的 JSON 数据会被自动映射到 Employee 对象中
public R addEmployee(@RequestBody Employee employee) {
employeeService.addEmployee(employee);
return R.ok();
}
@PutMapping("/employee/{id}")
// 只有当请求方法为 PUT 时,才会调用该方法
// 其请求体中包含的 JSON 数据会被自动映射到 Employee 对象中
// 同时,其路径变量 id 也会被自动映射到 id 参数中
public R updateEmployee(@RequestBody Employee employee, @PathVariable int id) {
employeeService.updateEmployee(employee,id);
return R.ok();
}
@DeleteMapping("/employee/{id}")
// 当然也可以写成 @RequestMapping(value = "/employee/{id}",method = RequestMethod.DELETE)
// 只有当请求方法为 DELETE 时,才会调用该方法
public R deleteEmployeeById(@PathVariable int id) {
employeeService.deleteEmployee(id);
return R.ok();
}
@GetMapping("/employee")
public String getAllEmployees() {
employeeService.getAllEmployees();
return "ok";
}
// 如果当前 controller 类中进行请求时,出现以下异常,则会调用对应的异常处理方法,出了该类不会调用本类中的异常处理方法
// 处理文件未找到异常
@ExceptionHandler(FileNotFoundException.class)
public R handleFileNotFoundException(FileNotFoundException ex) {
return R.error(ex.getMessage());
}
// 处理数学算术异常
@ExceptionHandler(ArithmeticException.class)
public R handleArithmeticException(ArithmeticException ex) {
return R.error("Arithmetic exception: " + ex.getMessage());
}
// 处理其它异常
@ExceptionHandler(Exception.class) // 当然也可以用所有错误/异常的顶级父类 Throwable(范围更广)
public R handleException(Exception ex) {
return R.error("Exception: " + ex.getMessage());
}
}
以上给出的是只在本类中实现,即只有在本类中出现异常才会使用本类中的异常处理方法,出了该类就不会使用,我们可以通过创建一个全局的异常处理包来实现全局的异常处理,即不光本类可使用,其他类也可以使用
2.3、全局异常处理
核心注解是 @ControllerAdvice ,如果返回 json 格式文件则再加上 @RsponseBody,当然二者可以直接融合为: @RestControllerAdvice,在该类中再继续进行我们的异常处理方法的编写即可
java
@RestControllerAdvice // 相当于 @ControllerAdvice + @ResponseBody
public class MyExceptionAdivce {
// 处理文件未找到异常
@ExceptionHandler(FileNotFoundException.class)
public R handleFileNotFoundException(FileNotFoundException ex) {
return R.error(ex.getMessage());
}
// 处理数学算术异常
@ExceptionHandler(ArithmeticException.class)
public R handleArithmeticException(ArithmeticException ex) {
return R.error("Arithmetic exception: " + ex.getMessage());
}
// 处理其它异常
@ExceptionHandler(Exception.class) // 当然也可以用所有错误/异常的顶级父类 Throwable(范围更广)
public R handleException(Exception ex) {
return R.error("Exception: " + ex.getMessage());
}
}
2.4、异常处理最终方法
除了我们上面处理的系统异常,还有我们业务中遇到的业务异常,我们通过封装一个业务异常类和业务异常枚举类配合已有的全局异常处理器来实现最终方法
注意:@Data 虽然"能用",但对异常类来说太重且不安全 ,容易忽略父类字段和语义,建议用 @Getter/@Setter + 手动 toString() 更可控。
最佳实践:异常类慎用 @Data
- 很多团队会只用 @Getter + @Setter ,甚至完全不用 Lombok,以避免隐藏行为。
- 或者手动实现 toString() ,确保打印出有用信息(如
code,msg,getMessage())
①、创建业务异常类
java
package org.example.spring05bestpracitce.exception;
import lombok.Getter;
/**
* 业务异常类,当我们的业务系统庞大时可能遇到各种各样的异常
* 1、订单异常
* 1001 订单不存在
* 1002 订单状态异常
* 1003 订单金额异常
* ......
* 2、库存异常
* 2001 库存不足
* 2002 库存异常
* ......
* 3、用户异常
* 3001 用户不存在
* 3002 用户状态异常
* 3003 用户已登录
* 3004 用户信息不完整
* 3005 用户年龄不合规
* ......
* 4、支付异常
* 4001 支付失败
* 4002 支付超时
* ......
* 5、其他业务异常
* 5001 其他业务异常
* ......
*/
// 异常除了我们的系统异常,还有我们的业务异常
// 业务异常的出现通常是因为我们的业务逻辑错误导致的,比如用户年龄不合规、用户信息不完整等
// 最终的业务异常根据我们的实际业务进行修改,这里只是一个简单的示例
@Getter
public class BusinessException extends RuntimeException {
private Integer code; // 业务异常码
private String message; // 业务异常信息
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
// 特别定义一个构造方法,用于直接传入业务异常枚举类
// 这样我们在抛出业务异常时就可以直接传入业务异常枚举类,而不需要手动传入异常码和异常信息
public BusinessException(BusinessExceptionEnum businessExceptionEnum) {
super(businessExceptionEnum.getMessage());
this.code = businessExceptionEnum.getCode();
this.message = businessExceptionEnum.getMessage();
}
}
②、创建业务异常枚举类
java
package org.example.spring05bestpracitce.exception;
import lombok.Getter;
/**
* 业务异常枚举类,为了将我们的业务异常码和异常信息定义在一个地方,方便我们的使用
* 简单来说,就是固化我们的业务异常码和异常信息,将我们业务所有可能遇到的异常一个一个枚举出来
*/
public enum BusinessExceptionEnum {
// 以下具体的异常种类根据我们的实际业务进行修改
// 订单异常
ORDER_NOT_FOUND(1001, "订单不存在"),
ORDER_STATUS_ERROR(1002, "订单状态异常"),
ORDER_AMOUNT_ERROR(1003, "订单金额异常"),
// 库存异常
INVENTORY_NOT_ENOUGH(2001, "库存不足"),
INVENTORY_ERROR(2002, "库存异常"),
// 用户异常
USER_NOT_FOUND(3001, "用户不存在"),
USER_STATUS_ERROR(3002, "用户状态异常"),
USER_ALREADY_LOGGED_IN(3003, "用户已登录"),
USER_INFO_INCOMPLETE(3004, "用户信息不完整"),
USER_AGE_ERROR(3005, "用户年龄不合规"),
// 支付异常
PAYMENT_FAILED(4001, "支付失败"),
PAYMENT_TIMEOUT(4002, "支付超时"),
// 其他业务异常
OTHER_BUSINESS_ERROR(5001, "其他业务异常");
// 我们的异常状态码和异常信息只允许get获取,不允许set设置
@Getter
private Integer code;
@Getter
private String message;
private BusinessExceptionEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
}
③、统一结果和全局异常处理器的完善
只展示新增的部分
java
public class R<T> {
// 重载,根据异常码返回不同的异常信息
public static <T> R<T> error(String msg, int code) {
return new R<>(code,msg,null);
}
// 定义一个方法,根据异常枚举返回异常信息
public static <T> R<T> error(BusinessExceptionEnum exceptionEnum) {
return R.error(exceptionEnum.getMessage(), exceptionEnum.getCode());
}
}
java
// 全局异常处理类
@RestControllerAdvice // 相当于 @ControllerAdvice + @ResponseBody
public class MyExceptionAdivce {
// 新增:处理业务异常
@ExceptionHandler(BusinessException.class)
public R handleBusinessException(BusinessException ex) {
return R.error(ex.getMessage(), ex.getCode());
}
}
④、service实现类的返回结果完善
只展示部分
java
@Service // 本质是对 dao 层的再度包装,补充验证、拦截等功能
// 即我们的 controller 层是调用 service 层的方法,service 层再调用 dao 层的方法并且补充功能,最后返回结果给 controller 层
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeDao employeeDao;
@Override
public Employee getEmployeeById(int id) {
if(id <= 0){
throw new BusinessException(BusinessExceptionEnum.USER_NOT_FOUND);
}
// 调用 dao 层的方法查询员工
Employee employee = employeeDao.getEmployeeById(id);
// 如果员工不存在,抛出业务异常
if(employee == null){
throw new BusinessException(BusinessExceptionEnum.USER_NOT_FOUND);
}
return employee;
}
@Override
public void addEmployee(Employee employee) {
// 验证员工信息是否完整
// (在我们的建表语句中是使用 not null 约束 name,unique 约束 email,所以这里需要验证)
// 当然如果后来业务要求我们的邮箱有命名规范等等,我们可以在这里添加正则表达式等等来进行验证
if(employee.getName() == null || employee.getEmail() == null){
throw new BusinessException(BusinessExceptionEnum.USER_INFO_INCOMPLETE);
}
// (在我们的建表语句中是要求员工年龄在 18 到 65 岁之间,所以这里需要验证)
if(employee.getAge() < 18 || employee.getAge() > 65){
throw new BusinessException(BusinessExceptionEnum.USER_AGE_ERROR);
}
// 当以上约束均满足,调用 dao 层的方法新增员工
employeeDao.addEmployee(employee);
}
@Override
public void updateEmployee(Employee employee, int id) {
// 验证员工 id 是否存在
if(id <= 0){
throw new BusinessException(BusinessExceptionEnum.USER_NOT_FOUND);
}
// 调用 dao 层的方法查询员工,如果员工不存在,抛出业务异常
if(employeeDao.getEmployeeById(id) == null){
throw new BusinessException(BusinessExceptionEnum.USER_NOT_FOUND);
}
// 验证员工信息是否完整
// (在我们的建表语句中是使用 not null 约束 name,unique 约束 email,所以这里需要对要更新的员工信息进行验证)
// 当然如果后来业务要求我们的邮箱有命名规范等等,我们可以在这里添加正则表达式等等来进行验证
if(employee.getName() == null || employee.getEmail() == null){
throw new BusinessException(BusinessExceptionEnum.USER_INFO_INCOMPLETE);
}
// (在我们的建表语句中是要求员工年龄在 18 到 65 岁之间,所以这里需要对要更新的员工年龄进行验证)
if(employee.getAge() < 18 || employee.getAge() > 65){
throw new BusinessException(BusinessExceptionEnum.USER_AGE_ERROR);
}
// 当以上约束均满足,调用 dao 层的方法修改员工
employeeDao.updateEmployee(employee, id);
}
}
以下给出我的理解:

3、数据校验
3.1、为何需要JSR校验
业务校验(如用户ID是否存在、业务规则验证等)应该在Service层进行,而JSR(Java Specification Requests)校验(如Bean Validation)主要处理基础数据格式校验。
| 维度 | JSR校验 (Bean Validation) | Service层业务校验 |
|---|---|---|
| 校验目的 | 数据格式、基本规则 | 业务规则、业务状态 |
| 校验内容 | 字段格式、基本约束 | 业务逻辑、数据关联性 |
| 校验时机 | 数据进入系统时 | 业务操作执行前 |
| 复用性 | 跨系统通用 | 业务特定 |
| 示例 | 邮箱格式、非空检查、长度限制 | 用户是否存在、库存是否充足 |
| 技术实现 | 注解驱动 | 手动编码 |
| 错误类型 | 输入错误 | 业务规则违反 |
①、分层防御原则
- Controller层:使用JSR校验拦截明显不合规的请求,避免无效请求进入业务逻辑
- Service层:进行真正的业务规则校验
java
// Controller层
@PostMapping("/orders")
public ResponseEntity createOrder(@Valid @RequestBody OrderCreateDTO dto) {
// 如果dto基础校验失败,根本不会进入这个方法
orderService.createOrder(dto);
}
// Service层
public void createOrder(OrderCreateDTO dto) {
// 业务校验1:用户是否存在
User user = userRepository.findById(dto.getUserId());
if (user == null) {
throw new BusinessException("用户不存在");
}
// 业务校验2:库存检查
if (!inventoryService.hasStock(dto.getProductId(), dto.getQuantity())) {
throw new BusinessException("库存不足");
}
// ...其他业务逻辑
}
②、关注点分离
- JSR校验关注数据形态是否正确
- 业务校验关注业务状态是否允许
③、性能优化
在请求进入业务逻辑前就过滤掉格式错误的请求,避免不必要的数据库查询和业务处理
3.2、数据校验的实现
①、导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<scope>test</scope>
</dependency>
②、编写校验注解
Java 中数据校验主要使用 Bean Validation (JSR 380) 规范,最常用的实现是 Hibernate Validator。以下是常见的校验注解:
Ⅰ、基本校验注解
- @NotNull - 验证对象不为 null
- @Null - 验证对象必须为 null
- @AssertTrue - 验证 Boolean 对象必须为 true
- @AssertFalse - 验证 Boolean 对象必须为 false
Ⅱ、数值校验
- @Min(value) - 验证数字必须大于等于指定值
- @Max(value) - 验证数字必须小于等于指定值
- @DecimalMin(value) - 验证数字必须大于等于指定值(字符串形式)
- @DecimalMax(value) - 验证数字必须小于等于指定值(字符串形式)
- @Digits(integer, fraction) - 验证数字的整数位和小数位精度
- @Positive - 验证数字必须是正数
- @PositiveOrZero - 验证数字必须是正数或零
- @Negative - 验证数字必须是负数
- @NegativeOrZero - 验证数字必须是负数或零
Ⅲ、字符串校验
- @Size(min, max) - 验证集合/数组/字符串长度在指定范围内
- @NotEmpty - 验证字符串/集合不为 null 且不为空
- @NotBlank - 验证字符串不为 null 且至少包含一个非空白字符
- @Pattern(regexp) - 验证字符串必须匹配正则表达式
Ⅳ、日期校验
- @Past - 验证日期必须在当前时间之前
- @PastOrPresent - 验证日期必须在当前时间或之前
- @Future - 验证日期必须在当前时间之后
- @FutureOrPresent - 验证日期必须在当前时间或之后
Ⅴ、其他校验
- @Email - 验证必须是有效的电子邮件地址
- @Valid - 用于级联验证对象中的属性
- @CreditCardNumber - 验证信用卡号码(需要额外依赖)
- @URL - 验证必须是有效的URL
Ⅵ、自定义校验
详情见下方的3.3、自定义校验器
通过以上注解,将其标注在对应的成员变量上即可代表要对该成员变量的输入值进行数据校验,以下给出例子:
(每个注解上都可以编写默认的报错信息,方便接下来的 BindingResult 的对象获取使用)
java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
private Integer id;
@NotBlank(message = "姓名不能为空")
private String name;
@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄不能小于18岁")
@Max(value = 65, message = "年龄不能大于65岁")
private Integer age;
@Email(message = "邮箱格式错误")
private String email;
private String gender;
private String address;
private BigDecimal salary;
}
③、使用 @Valid 开始校验
以下以 controller 层的添加员工的方法为例进行测试:
java
@RestController
public class EmployeeRestController {
@Autowired
private EmployeeService employeeService;
@PostMapping("/employee")
// 只有当请求方法为 POST 时,才会调用该方法
// 其请求体中包含的 JSON 数据会被自动映射到 Employee 对象中
public R addEmployee(@RequestBody @Valid Employee employee) {
// 添加 @Valid 注解,对请求体中的数据进行 JSR 校验
employeeService.addEmployee(employee);
return R.ok();
}
}




如果以上的JSR校验不通过,则其下的业务层面的方法旧不继续执行,也就是添加员工的方法不执行
④、优化返回json
Ⅰ、初步优化
即返回具体的简洁的报错信息,而不是如上面的截图中所显示那样一大串
要想实现此效果,需要我们在参数后添加 BindingResult 参数,利用其中的方法进行获取操作
java
@RestController
public class EmployeeRestController {
@Autowired
private EmployeeService employeeService;
@PostMapping("/employee")
// 只有当请求方法为 POST 时,才会调用该方法
// 其请求体中包含的 JSON 数据会被自动映射到 Employee 对象中
public R addEmployee(@RequestBody @Valid Employee employee, BindingResult bindingResult) { // 添加 @Valid 注解,对请求体中的数据进行 JSR 校验
// 将校验结果转换为 Map 类型,键为字段名,值为错误信息
Map<String,String> errorsMap = new HashMap<>();
// 调用 BindingResult 的 hasErrors 方法,判断是否有校验错误
if (bindingResult.hasErrors()) {
// 如果有错则将错误信息添加到 errorsMap 中
// 调用 BindingResult 的 getFieldErrors() 方法,获取所有字段的校验错误信息
for(FieldError fieldError : bindingResult.getFieldErrors()) {
// getField() 方法获取字段名,getDefaultMessage() 方法获取错误信息
errorsMap.put(fieldError.getField(), fieldError.getDefaultMessage());
}
return R.error(errorsMap);
}
employeeService.addEmployee(employee);
return R.ok();
}
}
// 同时你的 R 类中也要定义一个重载的 error 方法
public static R error(Map<String,String> errorsMap) {
return new R<>(500,"fail",errorsMap);
}
最终你的错误信息返回的结果如下:

Ⅱ、进一步优化
初步优化中是实现了返回错误的json优化,但是如果每个方法前我们都使用 BindingResult bindingResult 去接收错误信息并且使用其中的方法,获取字段名、获取错误信息、返回错误信息,不免太过麻烦冗长,为了简化这一步,我们可以使用全局异常处理器,这样我们就可以不用每次都使用BindingResult bindingResult,而是每次出错,直接调用全局异常处理器之中已经定义好的逻辑(即初步优化中的代码)
校验异常抛出的异常类是 MethodArgumentNotValidException,参数必须是"需要 MessageConverter 的实体",而不是单个普通类型,只有下面两种场景会抛 MethodArgumentNotValidException:
@RequestBody @Valid XXX dto(JSON → 对象)@ModelAttribute @Valid XXX dto(表单 → 对象)
注意异常处理的优先级,如果你的controller层有处理其他异常的处理方法应先注释掉

以下给出我的全局异常处理类
java
// 全局异常处理类
@RestControllerAdvice // 相当于 @ControllerAdvice + @ResponseBody
public class MyExceptionAdivce {
// 处理文件未找到异常
@ExceptionHandler(FileNotFoundException.class)
public R handleFileNotFoundException(FileNotFoundException ex) {
return R.error(ex.getMessage());
}
// 处理数学算术异常
@ExceptionHandler(ArithmeticException.class)
public R handleArithmeticException(ArithmeticException ex) {
return R.error("Arithmetic exception: " + ex.getMessage());
}
// 处理其它异常
@ExceptionHandler(Exception.class) // 当然也可以用所有错误/异常的顶级父类 Throwable(范围更广)
public R handleException(Exception ex) {
System.out.println(ex.getMessage());
return R.error("Exception: " + ex.getMessage(),111);
}
// 处理数据校验异常,当出现异常显示的是 MethodArgumentNotValidException(方法参数校验异常)
@ExceptionHandler(MethodArgumentNotValidException.class)
public R handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
// 从异常中获取校验结果对象 BindingResult
BindingResult bindingResult = ex.getBindingResult();
// 将校验结果转换为 Map 类型,键为字段名,值为错误信息
Map<String, String> errorsMap = new HashMap<>();
// 如果有错则将错误信息添加到 errorsMap 中
// 调用 BindingResult 的 getFieldErrors() 方法,获取所有字段的校验错误信息
for (FieldError fieldError : bindingResult.getFieldErrors()) {
// getField() 方法获取字段名,getDefaultMessage() 方法获取错误信息
errorsMap.put(fieldError.getField(), fieldError.getDefaultMessage());
}
return R.error(errorsMap);
}
// 处理业务异常
@ExceptionHandler(BusinessException.class)
public R handleBusinessException(BusinessException ex) {
return R.error(ex.getMessage(), ex.getCode());
}
}

3.3、自定义校验器
自定义校验器多配合自定义校验注解生效,其核心是模仿已写过的校验注解,实现自定义的校验注解,该校验注解是通过自定义的校验器实现校验功能。
①、自定义校验注解
java
package org.example.spring05bestpracitce.customvalidator;
// 此类用于标记自定义校验注解
// 该接口名应该是你要实现的自定义的校验注解的名称,如 @MyAnnotation 等等
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
// 以下四个注解是自定义校验注解的固定格式,不能缺少
@Target({ElementType.FIELD, ElementType.PARAMETER}) // 可以标注在字段或参数上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
@Constraint(validatedBy = {MyValidator.class}) // 指定校验器是 MyValidator 类
@Documented
// 此处使用的是 @interface 关键字来定义一个注解
public @interface MyAnnotation {
// 以下三个是必须的属性,不能缺少
// 1、默认错误消息
String message() default "不符合自定义校验规则"; // 这其中的 default 内容是可以修改的,根据你的校验规则来自定义错误消息
// 2、分组
Class<?>[] groups() default {};
// 3、负载
Class<? extends Payload>[] payload() default {};
}
②、自定义校验器
自定义校验器是和自定义校验注解进行绑定的
java
package org.example.spring05bestpracitce.customvalidator;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
// 该类是实现自定义校验注解的校验器
// 要想成为一个校验器,必须实现 ConstraintValidator 接口
// 该接口有两个类型参数:第一个是自定义校验注解的类型,第二个是校验的目标类型
public class MyValidator implements ConstraintValidator<MyAnnotation, String> {
// 在该重写方法中实现自定义校验逻辑,比如我决定通过 @MyAnnotation 注解来校验用户是否输入了正确的性别(只能是男或女)
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value.equals("男") || value.equals("女");
}
}
③、实现测试
java
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
private Integer id;
@NotBlank(message = "姓名不能为空")
private String name;
@NotNull(message = "年龄不能为空")
@Min(value = 18, message = "年龄不能小于18岁")
@Max(value = 65, message = "年龄不能大于65岁")
private Integer age;
@Email(message = "邮箱格式错误")
private String email;
@MyAnnotation(message = "性别只能是男或女") // 使用自定义的校验注解来校验性别
private String gender;
private String address;
private BigDecimal salary;
}

④、补充@interface和interface的区别
| 特性 | interface |
@interface |
|---|---|---|
| 关键字 | interface |
@interface |
| 用途 | 定义行为契约 | 定义元数据注解 |
| 包含内容 | 方法签名、常量 | 注解元素(看起来像方法) |
| 继承 | 可以继承其他接口 | 隐式继承 java.lang.annotation.Annotation |
| 实现/使用 | 类通过 implements 实现 |
通过 @AnnotationName 使用 |
| 运行时保留 | 总是可用 | 需要 @Retention 指定 |
| 处理方式 | 编译器和JVM直接支持 | 通常需要注解处理器或反射处理 |
3.4、o层结构
无非就是由于我们对接的两头对象不同而导致同一个传输对象的不同表现方式,比如dao层(entity包)是封装了数据库中所有字段的所有信息,但是前端在获取时可能就是通过vo,作为对象进行数据库和前端之间进行传输,这个vo只包含了部分信息,对于敏感信息,比如数据库中有用户的身份证号或者手机号等等,则进行部分信息隐藏以保证数据的安全性。
简单实现
Ⅰ、创建vo层对象

Ⅱ、使用vo层对象作为参数并进行转化拷贝
java
@RestController
public class EmployeeRestController {
@Autowired
private EmployeeService employeeService;
@PostMapping("/employee")
// 只有当请求方法为 POST 时,才会调用该方法
// 其请求体中包含的 JSON 数据会被自动映射到 Employee 对象中
public R addEmployee(@RequestBody @Valid AddEmployeeVo addEmployeeVo) {
// 将 AddEmployeeVo 转为 Employee 对象
Employee employee = new Employee();
// 利用 BeanUtils 包中的方法进行属性对拷贝,将 addEmployeeVo 中的属性值拷贝到 employee 中
BeanUtils.copyProperties(addEmployeeVo, employee);
employeeService.addEmployee(employee);
return R.ok();
}
注意:不是在数据库的entity对象标注解,而是新建一个对应的vo,在该vo的对象上进行标注
4、接口文档
接口文档是开发者和使用者之间的重要沟通桥梁,它详细描述了API的功能、参数、返回值和使用方法。良好的接口文档应该:
- 清晰说明接口用途
- 详细列出所有参数及其要求
- 提供请求和响应示例
- 包含错误代码和可能的异常情况
- 说明认证和授权要求
以下是使用knife4j进行自动的文档编辑,具体详情可见:https://doc.xiaominfo.com/docs/quick-start
4.1、引入依赖
yaml
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>4.5.0</version> # 具体版本具体看
</dependency>
4.2、导入配置
在resource中创建yaml文件,文件中导入配置即可
yaml
# springdoc-openapi项目配置
springdoc:
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha
api-docs:
path: /v3/api-docs
group-configs:
- group: 'default'
paths-to-match: '/**'
packages-to-scan: com.xiaominfo.knife4j.demo.web # 此处改为你自己的controller的位置
# knife4j的增强配置,不需要增强可以不配
knife4j:
enable: true
setting:
language: zh_cn
4.3、使用注解
①、类/接口级注解
| 注解 | 作用 | 示例 |
|---|---|---|
@Tag |
定义 API 分组(原 Swagger 的 @Api) |
@Tag(name = "用户管理", description = "用户相关接口") |
@Hidden |
隐藏接口/类/字段 | @Hidden(不显示在文档中) |
②、方法级注解
| 注解 | 作用 | 示例 |
|---|---|---|
@Operation |
描述单个操作(原 @ApiOperation) |
java<br>@Operation(<br> summary = "创建用户",<br> description = "根据User对象创建用户"<br>) |
@Parameter |
描述参数(原 @ApiParam) |
java<br>@Parameter(name = "id", description = "用户ID", required = true) |
@Parameters |
组合多个 @Parameter |
- |
③、模型注解(DTO/VO)
| 注解 | 作用 | 示例 |
|---|---|---|
@Schema |
描述字段/类(原 @ApiModel/@ApiModelProperty) |
java<br>@Schema(description = "用户名", example = "admin")<br>private String username; |
④、接口增强
| 注解 | 作用 | 示例 |
|---|---|---|
@ApiOperationSupport |
扩展接口行为 | java<br>@ApiOperationSupport(<br> author = "开发者A",<br> params = @DynamicParameters(...)<br>) |
@DynamicParameters |
动态参数(如Map接收参数时) | java<br>@DynamicParameters(<br> name = "userMap",<br> properties = {<br> @DynamicParameter(name = "name", value = "用户名")<br> }<br>) |
⑤、分组增强
| 注解 | 作用 | 示例 |
|---|---|---|
@ApiGroup |
定义分组(Knife4j 扩展) | @ApiGroup("管理后台") |
@ApiSort |
接口排序(值越小越靠前) | @ApiSort(1) |
4.4、进行访问
访问你的ip加端口+doc.html即可,如localhost:8080/doc.html,输出结果类似下方
注意:大多数无法显示的错误来源都是因为你的springboot和knife4不兼容,二者版本不匹配

5、数据转换
即通过@JsonFormat进行日期的转化(序列化和反序列化之间的转化),@JsonFormat(pattern="你自己指定的日期格式",timezone="时区")
注意:不是在数据库的entity对象标注,而是新建一个对应的vo,在该vo的对象上进行标注