你这代码写得真丑,满屏的try-catch,全局异常处理不会吗?下

311 阅读6分钟

统一返回结果

在验证统一异常处理器之前,顺便说一下统一返回结果。说白了,其实是统一一下返回结果的数据结构。code、message 是所有返回结果中必有的字段,而当需要返回数据时,则需要另一个字段 data 来表示。

所以首先定义一个 BaseResponse 来作为所有返回结果的基类;

然后定义一个通用返回结果类CommonResponse,继承 BaseResponse,而且多了字段data;

为了区分成功和失败返回结果,于是再定义一个 ErrorResponse;

最后还有一种常见的返回结果,即返回的数据带有分页信息,因为这种接口比较常见,所以有必要单独定义一个返回结果类 QueryDataResponse,该类继承自CommonResponse,只是把 data 字段的类型限制为 QueryDdata,QueryDdata中定义了分页信息相应的字段,即totalCount、pageNo、 pageSize、records。

其中比较常用的只有 CommonResponse 和 QueryDataResponse,但是名字又贼鬼死长,何不定义2个名字超简单的类来替代呢?于是 R 和 QR 诞生了,以后返回结果的时候只需这样写:new R<>(data)、new QR<>(queryData)。

所有的返回结果类的定义这里就不贴出来了

验证统一异常处理

因为这一套统一异常处理可以说是通用的,所有可以设计成一个 common包,以后每一个新项目/模块只需引入该包即可。所以为了验证,需要新建一个项目,并引入该 common包。项目结构如下:

项目结构 以后只需这样引入即可

引入common包

主要代码

下面是用于验证的主要源码: @Service public class LicenceService extends ServiceImpl<LicenceMapper, Licence> {

@Autowired
private OrganizationClient organizationClient;

/**
 * 查询{@link Licence} 详情
 * @param licenceId
 * @return
 */
public LicenceDTO queryDetail(Long licenceId) {
    Licence licence = this.getById(licenceId);
    checkNotNull(licence);

    OrganizationDTO org = ClientUtil.execute(() -> organizationClient.getOrganization(licence.getOrganizationId()));
    return toLicenceDTO(licence, org);
}

/**
 * 分页获取
 * @param licenceParam 分页查询参数
 * @return
 */
public QueryData<SimpleLicenceDTO> getLicences(LicenceParam licenceParam) {
    String licenceType = licenceParam.getLicenceType();
    LicenceTypeEnum licenceTypeEnum = LicenceTypeEnum.parseOfNullable(licenceType);
    // 断言, 非空
    ResponseEnum.BAD_LICENCE_TYPE.assertNotNull(licenceTypeEnum);

    LambdaQueryWrapper<Licence> wrapper = new LambdaQueryWrapper<>();
    wrapper.eq(Licence::getLicenceType, licenceType);
    IPage<Licence> page = this.page(new QueryPage<>(licenceParam), wrapper);
    return new QueryData<>(page, this::toSimpleLicenceDTO);
}

/**
 * 新增{@link Licence}
 * @param request 请求体
 * @return
 */
@Transactional(rollbackFor = Throwable.class)
public LicenceAddRespData addLicence(LicenceAddRequest request) {
    Licence licence = new Licence();
    licence.setOrganizationId(request.getOrganizationId());
    licence.setLicenceType(request.getLicenceType());
    licence.setProductName(request.getProductName());
    licence.setLicenceMax(request.getLicenceMax());
    licence.setLicenceAllocated(request.getLicenceAllocated());
    licence.setComment(request.getComment());
    this.save(licence);

    return new LicenceAddRespData(licence.getLicenceId());
}

/**
 * entity -> simple dto
 * @param licence {@link Licence} entity
 * @return {@link SimpleLicenceDTO}
 */
private SimpleLicenceDTO toSimpleLicenceDTO(Licence licence) {
    // 省略
}

/**
 * entity -> dto
 * @param licence {@link Licence} entity
 * @param org {@link OrganizationDTO}
 * @return {@link LicenceDTO}
 */
private LicenceDTO toLicenceDTO(Licence licence, OrganizationDTO org) {
    // 省略
}

/**
 * 校验{@link Licence}存在
 * @param licence
 */
private void checkNotNull(Licence licence) {
    ResponseEnum.LICENCE_NOT_FOUND.assertNotNull(licence);
}

} ps: 这里使用的DAO框架是mybatis-plus。 启动时,自动插入的数据为: -- licence INSERT INTO licence (licence_id, organization_id, licence_type, product_name, licence_max, licence_allocated) VALUES (1, 1, 'user','CustomerPro', 100,5); INSERT INTO licence (licence_id, organization_id, licence_type, product_name, licence_max, licence_allocated) VALUES (2, 1, 'user','suitability-plus', 200,189); INSERT INTO licence (licence_id, organization_id, licence_type, product_name, licence_max, licence_allocated) VALUES (3, 2, 'user','HR-PowerSuite', 100,4); INSERT INTO licence (licence_id, organization_id, licence_type, product_name, licence_max, licence_allocated) VALUES (4, 2, 'core-prod','WildCat Application Gateway', 16,16);

-- organizations INSERT INTO organization (id, name, contact_name, contact_email, contact_phone) VALUES (1, 'customer-crm-co', 'Mark Balster', 'mark.balster@custcrmco.com', '823-555-1212'); INSERT INTO organization (id, name, contact_name, contact_email, contact_phone) VALUES (2, 'HR-PowerSuite', 'Doug Drewry','doug.drewry@hr.com', '920-555-1212');

开始验证

捕获自定义异常

获取不存在的 licence 详情:http://localhost:10000/licence/5。成功响应的请求:licenceId=1

校验非空

捕获 Licence not found 异常

   Licence not found

2.根据不存在的 licence type 获取 licence 列表:http://localhost:10000/licence/list?licenceType=ddd。可选的 licence type 为:user、core-prod 。

校验非空

捕获 Bad licence type 异常

Bad licence type

捕获进入 Controller 前的异常

访问不存在的接口:http://localhost:10000/licence/list/ddd

捕获404异常 http 方法不支持:http://localhost:10000/licence

PostMapping

捕获 Request method not supported 异常

  Request method not supported

3 校验异常1:http://localhost:10000/licence/list?licenceType=

getLicences

LicenceParam

捕获参数绑定校验异常

licence type cannot be empty

4.校验异常2:post 请求,这里使用postman模拟。

addLicence

LicenceAddRequest

请求url即结果

捕获参数绑定校验异常 注:因为参数绑定校验异常的异常信息的获取方式与其它异常不一样,所以才把这2种情况的异常从 进入 Controller 前的异常 单独拆出来,下面是异常信息的收集逻辑:

异常信息的收集

捕获未知异常

假设我们现在随便对 Licence 新增一个字段 test,但不修改数据库表结构,然后访问:http://localhost:10000/licence/1。

增加test字段

捕获数据库异常

Error querying database

小结

可以看到,测试的异常都能够被捕获,然后以 code、message 的形式返回。每一个项目/模块,在定义业务异常的时候,只需定义一个枚举类,然后实现接口BusinessExceptionAssert,最后为每一种业务异常定义对应的枚举实例即可,而不用定义许多异常类。使用的时候也很方便,用法类似断言。

扩展

在生产环境,若捕获到 未知异常 或者 ServletException,因为都是一长串的异常信息,若直接展示给用户看,显得不够专业,于是,我们可以这样做:当检测到当前环境是生产环境,那么直接返回 "网络异常"。

生产环境返回“网络异常”

可以通过以下方式修改当前环境:

修改当前环境为生产环境

总结

使用 断言 和 枚举类 相结合的方式,再配合统一异常处理,基本大部分的异常都能够被捕获。为什么说大部分异常,因为当引入 spring cloud security 后,还会有认证/授权异常,网关的服务降级异常、跨模块调用异常、远程调用第三方服务异常等,这些异常的捕获方式与本文介绍的不太一样,不过限于篇幅,这里不做详细说明,以后会有单独的文章介绍。

另外,当需要考虑国际化的时候,捕获异常后的异常信息一般不能直接返回,需要转换成对应的语言,不过本文已考虑到了这个,获取消息的时候已经做了国际化映射,逻辑如下:

获取国际化消息

最后总结,全局异常属于老生长谈的话题,希望这次通过手机的项目对大家有点指导性的学习。大家根据实际情况自行修改。

也可以采用以下的jsonResult对象的方式进行处理,也贴出来代码.

@Slf4j @RestControllerAdvice public class GlobalExceptionHandler {

/**
 * 没有登录
 * @param request
 * @param response
 * @param e
 * @return
 */
@ExceptionHandler(NoLoginException.class)
public Object noLoginExceptionHandler(HttpServletRequest request,HttpServletResponse response,Exception e)

{ log.error("[GlobalExceptionHandler][noLoginExceptionHandler] exception",e); JsonResult jsonResult = new JsonResult(); jsonResult.setCode(JsonResultCode.NO_LOGIN); jsonResult.setMessage("用户登录失效或者登录超时,请先登录"); return jsonResult; }

/**
 *  业务异常
 * @param request
 * @param response
 * @param e
 * @return
 */
@ExceptionHandler(ServiceException.class)
public Object businessExceptionHandler(HttpServletRequest request,HttpServletResponse response,Exception e)

{ log.error("[GlobalExceptionHandler][businessExceptionHandler] exception",e); JsonResult jsonResult = new JsonResult(); jsonResult.setCode(JsonResultCode.FAILURE); jsonResult.setMessage("业务异常,请联系管理员"); return jsonResult; }

/**
 * 全局异常处理
 * @param request
 * @param response
 * @param e
 * @return
 */
@ExceptionHandler(Exception.class)
public Object exceptionHandler(HttpServletRequest request,HttpServletResponse response,Exception e)

{ log.error("[GlobalExceptionHandler][exceptionHandler] exception",e); JsonResult jsonResult = new JsonResult(); jsonResult.setCode(JsonResultCode.FAILURE); jsonResult.setMessage("系统错误,请联系管理员"); return jsonResult; } }