通用异常详细讲解篇
5.通用异常处理
在项目中出现异常是在所难免的,但是出现异常后怎么处理,这就很有学问了。
5.1.场景预设
5.1.1.场景
我们预设这样一个场景,假如我们做新增商品,需要接收下面的参数:
price:价格
name:名称
然后对数据做简单校验:
- 价格不能为空
新增时,自动形成ID,然后随商品对象一起返回
5.1.2.代码
在ly-item-interface中编写实体类:
@Data
public class Item {
private Integer id;
private String name;
private Long price;
}
在ly-item-service中编写业务:
service:
@Service
public class ItemService {
public Item saveItem(Item item){
int id = new Random().nextInt(100);
item.setId(id);
return item;
}
}
- 这里临时使用随机生成id,然后直接返回,没有做数据库操作
controller:
@RestController
public class ItemController {
@Autowired
private ItemService itemService;
@PostMapping("item")
public ResponseEntity<Item> saveItem(Item item){
// 如果价格为空,则抛出异常,返回400状态码,请求参数有误
if(item.getPrice() == null){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
}
Item result = itemService.saveItem(item);
return ResponseEntity.ok(result);
}
}
5.2.统一异常处理
rest客户端:https://insomnia.rest/
5.2.1初步测试
现在我们启动项目,做下测试:
通过RestClient去访问:
发现参数不正确时,返回了400。看起来没问题
5.2.2.问题分析
虽然上面返回结果正确,但是没有任何有效提示,这样是不是不太友好呢?
我们在返回错误信息时,使用了这样的代码:
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
响应体是null,因此返回结果才只有状态码。
如果我们在body中加入错误信息,会出现新的问题:
这个错误显然是返回类型不匹配造成的,我们返回值类型中要的是ResponseEntity<Item>
,因此body中必须是Item对象,不能是字符串。
如何解决?
大家可能会想到:我们把返回值改成String就好了,返回对象的时候,手动进行Json序列化。
这确实能达到目的,但是不是给开发带来了不必要的麻烦呢?
上面的问题,通过ResponseEntity是无法完美解决的,那么我们需要转换一下思考问题的方式,
既然不能统一返回一样的内容,干脆在出现问题的地方就不要返回,而是转为异常,然后再统一处理异常。这样就问题从返回值类型,转移到了异常的处理了。
那么问题来了:具体该怎么操作呢?
5.2.3.统一异常处理
我们先修改controller的代码,把异常抛出:
@PostMapping("item")
public ResponseEntity<Item> saveItem(Item item){
if(item.getPrice() == null){
throw new RuntimeException("价格不能为空");
}
Item result = itemService.saveItem(item);
return ResponseEntity.ok(result);
}
接下来,我们使用SpringMVC提供的统一异常拦截器,因为是统一处理,我们放到ly-common
项目中:
新建一个类,名为:BasicExceptionHandler
然后代码如下:
@ControllerAdvice
@Slf4j
public class BasicExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> handleException(RuntimeException e) {
// 我们暂定返回状态码为400, 然后从异常中获取友好提示信息
return ResponseEntity.status(400).body(e.getMessage());
}
}
解读:
@ControllerAdvice
:默认情况下,会拦截所有加了@Controller
的类
@ExceptionHandler(RuntimeException.class)
:作用在方法上,声明要处理的异常类型,可以有多个,这里指定的是RuntimeException
。被声明的方法可以看做是一个SpringMVC的Handler
:- 参数是要处理的异常,类型必须要匹配
- 返回结果可以是
ModelAndView
、ResponseEntity
等,基本与handler
类似
- 这里等于从新定义了返回结果,我们可以随意指定想要的返回类型。此处使用了String
然后在ly-item-service中引入ly-common:
<dependency>
<groupId>com.leyou.common</groupId>
<artifactId>ly-common</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
重启项目测试:
成功返回了错误信息!
5.2.4.自定义异常
刚才的处理看似完美,但是仔细想想,我们在通用的异常处理中,把返回状态码写死为400了:
这样显然不太合理。
但是,仅凭用户抛出的异常,我们根本无法判断到底该返回怎样的状态码,可能是参数有误、数据库异常、没有权限,等等各种情况。
因此,用户抛出异常时,就必须传递两个内容:
- 异常信息
- 异常状态码
我们定义一个枚举,用于封装这些信息:
@NoArgsConstructor
@AllArgsConstructor
public enum ExceptionEnum {
PRICE_CANNOT_BE_NULL(400, "价格不能为空!");
;
private int value;
private String msg;
public int value() {
return this.value;
}
public String msg() {
return this.msg;
}
}
然后自定义异常,来获取和传递枚举对象。
在ly-common中定义自定义异常类:
代码:
@Getter
public class LyException extends RuntimeException{
private ExceptionEnum exceptionEnum;
public LyException(ExceptionEnum exceptionEnum) {
this.exceptionEnum = exceptionEnum;
}
}
- status:响应状态对象,借用了SpringMVC中提供的
HttpStatus
状态对象 - statusCode:int类型,如果没有传HttpStatus,则使用这个自定义code返回
修改controller代码:
@PostMapping("item")
public ResponseEntity<Item> saveItem(Item item){
if(item.getPrice() == null){
throw new LyException(ExceptionEnum.PRICE_CANNOT_BE_NULL);
}
Item result = itemService.saveItem(item);
return ResponseEntity.ok(result);
}
在ly-common中定义异常结果对象:
@Data
public class ExceptionResult {
private int status;
private String msg;
private long timestamp;
public ExceptionResult(ExceptionEnum em){
this.status = em.value();
this.msg = em.msg();
this.timestamp = System.currentTimeMillis();
}
}
修改异常处理逻辑:
@ControllerAdvice
@Slf4j
public class BasicExceptionHandler {
@ExceptionHandler(LyException.class)
public ResponseEntity<ExceptionResult> handleException(LyException e) {
return ResponseEntity.status(e.getExceptionEnum().value())
.body(new ExceptionResult(e.getExceptionEnum()));
}
}
再次测试:
以后,我们无论controller还是service层的业务处理,出现异常情况,都抛出自定义异常,方便统一处理。
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。