一 问题引入
在前后端分离的项目开发过程中,后端在处理过程需要给前端返回数据结果,这里分两种情况,一种是正常情况(也就是没有发生异常),另一种是发生异常的情况。我们希望不管是正常情况,还是异常情况下,后端都可以给前端不同的响应状态码以及响应内容,这样有利于前端根据不同情况进行业务处理。
我们预设这样一个场景,假如我们模拟新增商品,只传一个id,如果id为1则抛出异常。
service:
@Service
public class ItemService {
public Long saveItem(Long id){
// 模拟添加操作,如果id为1抛异常
if(id.equals(1L)){
throw new RuntimeException("Id不能为1!");
}
return id;
}
}
controller:
@RestController
public class ItemController {
@Autowired
private ItemService itemService;
@PostMapping("/save")
public Long saveItem(@RequestParam("id") Long id){
return itemService.saveItem(id);
}
}
此时我们经过测试(postman),id=2 返回的是状态200 ;
id=1 返回状态500
也就是成功即200 失败即500 此时的区分不够细化,不利于前端根据不同情况进行业务处理。
二 正常情况数据返回(不发生异常)
在controller调用过程不发生异常的情况下,我们对成功状态码的设置一般可以划分为两种(基于SpringBoot)
使用HttpServletResponse对象
使用ResponseEntity
方式一:HttpServletResponse
@RestController
public class ItemController {
@Autowired
private ItemService itemService;
@PostMapping("/save")
public Long saveItem(@RequestParam("id") Long id, HttpServletResponse response){
response.setStatus(201);
return itemService.saveItem(id);
}
}
方式二:ResponseEntity
@RestController
public class ItemController {
@Autowired
private ItemService itemService;
@PostMapping("/save")
public ResponseEntity<Long> saveItem(@RequestParam("id") Long id){
return ResponseEntity.status(201).body(itemService.saveItem(id));
}
}
注意:开发常见的状态码枚举和对应的数字
HttpStatus.CREATED -> 201 新增,插入成功
HttpStatus.ok -> 200 查询成功
HttpStatus.NO_CONTENT ->204 删除,修改成功
三 异常情况数据返回
- 情况1:只是抛出一个运行时异常RunTimeException
public Long saveItem(Long id){
// 模拟添加操作,如果id为1抛异常
if(id.equals(1L)){
throw new RuntimeException("Id不能为1! ");
}
return id;
}
此刻是spring给我们封装了一个异常对象,并返回给了客户端,但是这个数据一般企业都希望自己来封装。因为这里所有异常的状态码都是500,我们希望错误的状态码更细致一些,换句话来说,就是我们希望能够自己指定异常时的状态码。而RuntimeException异常又无法自定义状态码,只有自己定义一个异常类了
自定义异常类:
/**
* 自定义异常类
*/
@Getter
public class LyException extends RuntimeException{
private Integer status;
public LyException(Integer status,String message){
super(message);
this.status = status;
}
}
改造service:
@Service
public class ItemService {
public Long saveItem(Long id){
// 模拟添加操作,如果id为1抛异常
if(id.equals(1L)){
throw new LyException(501, "Id不能为1!");
}
return id;
}
}
经过测试:
问题发现:
此时,我们发现状态码是没有改变的,因为spring作者在写框架时不可能预知我们会去自定义什么异常类(继承Exception或RunTimeException),换句话就是spring并不认识我们自定义的异常类LyException,它依旧把它当成RuntimeException去默认的拦截处理,我们的做法是,不需要spring自行拦截异常,我们自定义拦截异常的处理器类
补充:这里的LyException我们使用的super(message),来存入异常信息,但是其实定义一个成员变量private String message 然后在构造方法中使用this.message=message也可以存入异常信息,但是我们进入到异常的最高父类Throwable类发现它的变量是detailMessage 而这里spring把LyException当成RunTimeException 而Throwable的每个子类都是使用的super(message)此时 LyException使用的是私有变量message替代不了Throwable变量detailMessage,但是通过测试我们可以知道message是可以存进去的 这个时候就是java基础中的成员变量和属性的区别了 一个类的属性取决于Get和Set方法中去掉get和set的单词后的首字母小写的才是属性 刚好Throwable中的detailMessage的getset方法是getMessage也就是Thowable的属性是message而不是detailMessage 所以LyException中的message是可以存进父类的message属性的。
总结 父类Thowable的变量是detailMessage但是由于getset是getMessage所以属性是message。
子类LyException的属性是message 子类继承父类实现属性覆盖这里作者比较懒就没有贴代码 只是作为一个基础回顾 不必深究
自定义拦截异常处理器:
/**
* 自定义异常拦截器
*/
@RestControllerAdvice
public class LyExceptionController {
/**
* 声明具体拦截器什么异常类型
*/
@ExceptionHandler(LyException.class)
public ResponseEntity<LyException> handlerException(LyException e){
return ResponseEntity.status(e.getStatus()).body(e);
}
}
- @RestControllerAdvice指定该类为异常拦截器类并且以json格式返回异常
- @ExceptionHandler(LyException.class):声明具体拦截器拦截什么异常类型,可以同时指定多个异常类型。
再测试:
此时我们自行拦截了异常,虽然我们要的信息都有了,但是返回的异常信息太多太多,因为我们自行拦截的时候返回的是一个Exception子类,它会输出很多父类的属性信息而spring拦截的时候是会做处理,核心就是我们body中不要放入Exception类或其子类,而只需要一个包含状态码和异常信息的对象,前端根本用不到这些信息(只需要status和message),如果我们将这些信息返回,那么响应的数据多了,一定程度上响应了数据的响应速度,所以我们需要进一步优化。
我们的做法是:自定义异常结果类:
/**
* 封装异常信息类
*/
@Getter
public class ExceptionResult {
private Integer status;
private String message;
public ExceptionResult(Integer status,String message){
this.status = status;
this.message = message;
}
public ExceptionResult(LyException e){
this.status = e.getStatus();
this.message = e.getMessage();
}
}
改造全局异常处理类(异常拦截类):
/**
* 全局异常处理类
*/
@ControllerAdvice
public class LyExceptionController {
/**
* 异常处理方法
*/
@ExceptionHandler(value = LyException.class) // 捕获LyException异常
public ResponseEntity<ExceptionResult> resolveException(LyException e){
return ResponseEntity.status(e.getStatus()).body(new ExceptionResult(e));
}
}
再测试:
此时body中返回的不再是一个RunTimeException的子类LyException,这样不会打印多余的信息,我们的全局异常问题已经解决了,要的响应码和响应内容都是我们自定义的了。
难道这样就可以了咩?
在实际开发中,我们的异常是很多的,而且这些异常是和前端约定好的,但现在异常响应码和异常响应内容都是写死在代码中,如果某一个响应码或者响应内容需要修改,那么所有用到这个响应码和响应内容的地方都要跟着改,很不方便,会造成很多重复的工作,所以还需要进一步优化。
自定义异常枚举类:
@Getter
public enum ExceptionEnum {
INVALID_FILE_TYPE(400, "无效的文件类型!"),
INVALID_PARAM_ERROR(400, "无效的请求参数!"),
INVALID_PHONE_NUMBER(400, "无效的手机号码"),
INVALID_VERIFY_CODE(400, "验证码错误!"),
INVALID_USERNAME_PASSWORD(400, "无效的用户名和密码!"),
INVALID_SERVER_ID_SECRET(400, "无效的服务id和密钥!"),
INVALID_NOTIFY_PARAM(400, "回调参数有误!"),
INVALID_NOTIFY_SIGN(400, "回调签名有误!"),
CATEGORY_NOT_FOUND(404, "商品分类不存在!"),
BRAND_NOT_FOUND(404, "品牌不存在!"),
SPEC_NOT_FOUND(404, "规格不存在!"),
GOODS_NOT_FOUND(404, "商品不存在!"),
CARTS_NOT_FOUND(404, "购物车不存在!"),
APPLICATION_NOT_FOUND(404, "应用不存在!"),
ORDER_NOT_FOUND(404, "订单不存在!"),
ORDER_DETAIL_NOT_FOUND(404, "订单数据不存在!"),
DATA_TRANSFER_ERROR(500, "数据转换异常!"),
INSERT_OPERATION_FAIL(500, "新增操作失败!"),
UPDATE_OPERATION_FAIL(500, "更新操作失败!"),
DELETE_OPERATION_FAIL(500, "删除操作失败!"),
FILE_UPLOAD_ERROR(500, "文件上传失败!"),
DIRECTORY_WRITER_ERROR(500, "目录写入失败!"),
FILE_WRITER_ERROR(500, "文件写入失败!"),
SEND_MESSAGE_ERROR(500, "短信发送失败!"),
INVALID_ORDER_STATUS(500, "订单状态不正确!"),
STOCK_NOT_ENOUGH_ERROR(500, "库存不足!"),
UNAUTHORIZED(401, "登录失效或未登录!");
private int status;
private String message;
ExceptionEnum(int status, String message) {
this.status = status;
this.message = message;
}
}
改造LyException类
@Getter
public class LyException extends RuntimeException{
private Integer status;
public LyException(Integer status,String message){
super(message);
this.status = status;
}
public LyException(ExceptionEnum exceptionEnum){
super(exceptionEnum.getMessage());
this.status = exceptionEnum.getStatus();
}
}
改造service
@Service
public class ItemService {
public Long saveItem(Long id){
// 模拟添加操作,如果id为1抛异常
if(id.equals(1L)){
//throw new RuntimeException("Id不能为1!");
//throw new LyException(501,"自定义:Id不能为1!");
throw new LyException(ExceptionEnum.FILE_UPLOAD_ERROR);
}
return id;
}
}
到此,问题全部解决了,其实可以拦截多种异常,可以在全局异常处理中使用Exception e接拦截的异常,然后判断异常类型进行处理。主要划分为业务异常(curd等等)和非业务异常(比如数据库等链接错误)。