>[info]来源:https://juejin.cn/post/7123091045071454238
案例代码:https://gitee.com/flymini/codes03/tree/master/learn-contrl
Controller 层主要有如下职责:
1. 接收请求并解析参数。
2. 调用 Service 执行具体的业务逻辑。
3. 捕获业务逻辑异常并做出反馈。
4. 业务逻辑执行成功后做出响应。
```java
@RestController
@RequiredArgsConstructor
public class StudentController {
final StudentService studentService;
@GetMapping("/student/getById")
public Student getById(@RequestParam("id") String id) {
//参数校验
if (!StringUtils.hasText(id)) {
throw new RuntimeException("id不能为空");
}
//异常处理
try {
Student student = studentService.getById(id);
return student;
} catch (RuntimeException ex) {
throw new RuntimeException(ex);
}
}
}
```
如果按照上面这样来封装 controller 层,就有如下问题:
1. 参数校验过多耦合了业务代码,违背单一职责。
2. 可能会在多个地方抛出同一个异常,导致代码重复。
3. 各种异常反馈和成功时的响应返回的数据格式不一致,接口对接不友好。
我们可以如下封装 controller 层逻辑,避免上面的问题。
[TOC]
# 1. 统一返回结构
>[info]让 controller 层都返回统一的数据结构。
**1. 根据需要封装结果枚举**
```java
public enum ResponseEnum {
OK(20000, "操作成功"),
FAIL(30000, "操作失败"),
FAIL_VALITED(30000, "参数校验失败"),
FORBIDDEN(30000, "没有权限");
private Integer code;
private String message;
ResponseEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
```
**2. 封装统一返回体**
```java
@Setter
@Getter
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> ok(T data) {
Result<T> r = new Result<>();
r.setCode(ResponseEnum.OK.getCode());
r.setMessage(ResponseEnum.OK.getMessage());
r.setData(data);
return r;
}
public static <T> Result<T> fail(ResponseEnum resEnum) {
Result<T> r = new Result<>();
r.setCode(resEnum.getCode());
r.setMessage(resEnum.getMessage());
return r;
}
}
```
**3. 在 controller 层统一返回 Result**
```java
@RestController
@RequiredArgsConstructor
public class StudentController {
final StudentService studentService;
@GetMapping("/student/findById")
public Result findById(@RequestParam("id") String id) {
return Result.ok(studentService.getById(id));
}
}
```
<br/>
# 2. 参数校验
>[info]参数校验可以借助 Hibernate Validator 框架来完成,参考 https://www.kancloud.cn/king_om/springboot3x/3208554
```java
@PathVariable、@RequestParam 参数校验不通过时抛出异常 ConstraintViolationException。
@RequestBody 参数校验不通过时抛出异常 MethodArgumentNotValidException。
```
<br/>
# 3. 统一拦截异常
>[info]在代码任何地方只要异常没有被捕获,而是被抛出,统一异常拦截器就能够拦截到这个异常,拦截到这个异常后就可以做些自定义处理,然后再响应数据。
**1. 根据需要可以自定义一些异常**
```java
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
public class ForbiddenException extends RuntimeException {
public ForbiddenException(String message) {
super(message);
}
}
```
**2. 封装统一异常拦截器**
```java
@Slf4j
@RestControllerAdvice
public class ExceptionAdvice {
/**
* 捕获BusinessException异常
*/
@ExceptionHandler({BusinessException.class})
public Result<?> handleBusinessException(BusinessException ex) {
log.error("[handleBusinessException]: {}", ex.getMessage(), ex);
return Result.fail(ResponseEnum.FAIL);
}
/**
* 捕获ForbiddenException异常
*/
@ExceptionHandler({ForbiddenException.class})
public Result<?> handleForbiddenException(ForbiddenException ex) {
log.error("[handleForbiddenException]: {}", ex.getMessage(), ex);
return Result.fail(ResponseEnum.FORBIDDEN);
}
/**
* 捕获@RequestBody参数校验不通过时抛出的异常处理
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
log.error("[argumentException]: {}", ex.getMessage(), ex);
return Result.fail(ResponseEnum.FAIL_VALITED);
}
/**
* 捕获@PathVariable和@RequestParam参数校验不通过时抛出的异常处理
*/
@ExceptionHandler({ConstraintViolationException.class})
public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
log.error("[constraintException]: {}", ex.getMessage(), ex);
return Result.fail(ResponseEnum.FAIL_VALITED);
}
/**
* 当其他异常无法处理时就由这个方法捕获
*/
@ExceptionHandler({Exception.class})
public Result<?> handleException(Exception ex) {
log.error("[handleException]: {}", ex.getMessage(), ex);
return Result.fail(ResponseEnum.FAIL);
}
}
```
**3. 测试**
```java
@Validated
@RestController
@RequiredArgsConstructor
public class StudentController {
final StudentService studentService;
@GetMapping("/student/findById")
public Result findById(@RequestParam("id") @NotBlank(message = "id不能为空") String id) {
Student student = studentService.getById(id);
return Result.ok(student);
}
}
```
在访问时故意不传入参数`id`,后台就会抛出如下异常:
```
jakarta.validation.ConstraintViolationException: findById.id: id不能为空
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:138) ~[spring-context-6.0.13.jar:6.0.13]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed
...
```
客户端得到如下响应结构:
```json
{"code":30000,"message":"参数校验失败","data":null}
```
- Spring
- Spring是什么
- Spring与EJB对比
- Spring的组成
- 首个Spring程序
- IoC控制反转
- 什么是IoC
- IoC编程
- 依赖注入方式
- 不同变量注入
- AOP面向切面编程
- AOP思想
- AOP实现原理
- AOP关键术语
- AOP编程
- 5种增强方式
- 切入点规则
- 自动装配
- Spring注解开发
- Bean注解
- AOP注解
- 完全注解
- 配置文件拆分
- SpringBean
- Bean常用属性
- Bean作用域
- Bean生命周期
- SpringBoot
- SpringBoot是什么
- 项目创建
- 配置文件
- 配置类型
- 读取配置
- 占位符
- 多环境配置
- 配置优先级
- 更改配置文件
- 自定义IoC容器
- 常用组件
- ApplicationContextAware
- CommandLineRunner
- Boot[Web]
- 引入模板引擎
- 静态资源访问
- 指定首页
- JSP支持
- 注册拦截器
- 注册Servlet组件
- 注册Servlet
- 注册过滤器
- 注册监听器
- 拦截器与过滤器区别
- 文件上传
- 文件下载
- 变更服务器
- Controller层封装
- HttpServletRequest
- 获取请求行
- 获取请求头
- 获取请求体
- Boot[自动配置]
- 自动配置是什么
- 自动配置报告
- 关闭自动配置
- 条件注解
- Boot[场景启动器]
- 场景启动器是什么
- 自定义场景启动器
- Boot[日志]
- 日志框架
- 日志级别
- 日志配置
- 配置文件
- 切换日志
- Boot[邮件任务]
- Boot[定时任务]
- cron表达式
- 起步
- 任务并行
- 注解Scheduled参数
- Boot[异步任务]
- 起步
- 注意事项与原理
- 自定义线程池
- Boot[缓存]
- JSR107缓存技术
- Spring缓存抽象
- 缓存注解
- SpEL表达式
- 起步
- 自定义key生成器
- 工作原理
- Boot[Redis]
- 起步
- 序列化机制
- Boot[Jdbc]
- 起步
- 两个模板类
- JdbcTemplate
- 增删改
- 查询
- NamedParameterJdbcTemplate
- 增删改
- 查询
- 自定义JdbcTemplate
- Boot[JPA]
- SpringDataJPA是什么
- 与JPA、Hibernate的关系
- 起步
- SpringDataJPA原理
- 查询方式
- 方法命名规则查询
- 限制查询结果查询
- 注解Query查询
- 命名参数查询
- SpEL表达式查询
- 原生查询
- 更新与删除
- 查询指定字段
- Specification动态查询
- 分页查询与排序
- 多表查询
- 一对一查询
- 一对多查询
- 多对多查询
- Specification查询
- Query注解查询
- 主键策略
- 单独主键
- 联合主键
- 级联操作
- 加载规则
- 审计功能
- 常用注解
- 避坑指南
- Boot[JSR303]
- JSR303是什么
- 常用约束
- 起步
- 简单校验
- 嵌套校验
- 分组校验
- 自定义约束注解
- 自定义校验工具
- Spring事务
- 事务的作用
- 起步
- 事务参数
- SpringDoc文档
- SpringDoc是什么
- 起步
- 自定义配置
- 常用Doc注解
- JSR303文档
- knife4j文档
- 常用配置
- Boot[RabbitMQ]
- 起步
- Fanout交换机类型
- Direct交换机类型
- Topic交换机类型
- 延迟队列插件
- RabbitListener监听方法
- JWT认证
- 认证流程
- 起步
- 密码加密
- JWT认证实现