前后端分离项目中的统一异常处理的使用

本文详细介绍了在前后端分离的项目中,如何处理后端异常并返回合适的HTTP状态码。通过创建自定义异常类、全局异常处理器以及异常枚举,实现了对异常的精细化管理,确保前端能根据不同的响应码进行业务处理。同时,文章讨论了如何避免返回过多异常信息,以优化响应数据的大小和速度。
摘要由CSDN通过智能技术生成

一 问题引入

在前后端分离的项目开发过程中,后端在处理过程需要给前端返回数据结果,这里分两种情况,一种是正常情况(也就是没有发生异常),另一种是发生异常的情况。我们希望不管是正常情况,还是异常情况下,后端都可以给前端不同的响应状态码以及响应内容,这样有利于前端根据不同情况进行业务处理。

我们预设这样一个场景,假如我们模拟新增商品,只传一个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等等)和非业务异常(比如数据库等链接错误)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值