优雅地进行全局异常处理、统一返回值封装、自定义异常错误码——Graceful-Response推荐

2023-11-09 17:28:02 浏览数 (2)

1. 简介

Graceful Response是一个Spring Boot体系下的优雅响应处理器,提供一站式统一返回值封装、全局异常处理、自定义异常错误码等功能,使用Graceful Response进行web接口开发不仅可以节省大量的时间,还可以提高代码质量,使代码逻辑更清晰。

强烈推荐你花3分钟学会它!

Graceful Response地址:https://github.com/feiniaojin/graceful-response

本项目案例工程代码:https://github.com/feiniaojin/graceful-response-example.git ,注意选择最新版本的分支。

Spring Boot版本

Graceful Response版本

graceful-response-example分支

2.x

3.2.0-boot2

3.2.0-boot2

3.x

3.2.0-boot3

3.2.0-boot3

2. Java Web API接口数据返回的现状及解决方案

通常我们进行Java Web API接口时,大部分的Controller代码是这样的:

代码语言:java复制
public class Controller {
    
    @GetMapping("/query")
    @ResponseBody
    public Response query(Parameter params) {
        Response res = new Response();
        try {
            //1.校验params参数,非空校验、长度校验
            if (illegal(params)) {
                res.setCode(1);
                res.setMsg("error");
                return res;
            }
            //2.调用Service的一系列操作
            Data data = service.query(params);
            //3.将操作结果设置到res对象中
            res.setData(data);
            res.setCode(0);
            res.setMsg("ok");
            return res;
        } catch (BizException1 e) {
            //4.异常处理:一堆丑陋的try...catch,如果有错误码的,还需要手工填充错误码
            res.setCode(1024);
            res.setMsg("error");
            return res;
        } catch (BizException2 e) {
            //4.异常处理:一堆丑陋的try...catch,如果有错误码的,还需要手工填充错误码
            res.setCode(2048);
            res.setMsg("error");
            return res;
        } catch (Exception e) {
            //4.异常处理:一堆丑陋的try...catch,如果有错误码的,还需要手工填充错误码
            res.setCode(1);
            res.setMsg("error");
            return res;
        }
    }
}

这段代码存在什么问题呢?

  • 真正的业务逻辑被冗余代码淹没,真正执行业务的代码只有
代码语言:java复制
Data data=service.query(params);

其他代码不管是正常执行还是异常处理,都是为了异常封装、把结果封装为特定的格式,例如以下格式:

代码语言:json复制
{
  "code": 0,
  "msg": "ok",
  "data": {
    "id": 1,
    "name": "username"
  }
}

这样的逻辑每个接口都需要处理一遍,都是繁琐的重复劳动。

现在,在引入Graceful Response组件后,我们只要直接返回业务结果,Graceful Response即可自动完成response的格式封装。

3. 快速入门

3.1 引入maven依赖

graceful-response已发布至maven中央仓库,可以直接引入到项目中,maven依赖如下:

代码语言:html复制
<dependency>
    <groupId>com.feiniaojin</groupId>
    <artifactId>graceful-response</artifactId>
    <version>{latest.version}</version>
</dependency>

目前Graceful Response分别对spring boot 2.7版本和3.0以上版本做了适配,其中:

spring boot 2.7版本应使用3.2.0-boot2版本,spring boot 3.0版本以上,应使用3.2.0-boot3版本。

Spring Boot版本

Java版本

Graceful Response版本

graceful-response-example分支

2.x

8

3.2.0-boot2

3.2.0-boot2

3.x

17

3.2.0-boot3

3.2.0-boot3

3.2 在启动类中引入@EnableGracefulResponse注解

代码语言:java复制
@EnableGracefulResponse
@SpringBootApplication
public class ExampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ExampleApplication.class, args);
    }
}

3.3 Controller方法直接返回结果

  • 普通的查询
代码语言:java复制
@Controller
public class Controller {
    @RequestMapping("/get")
    @ResponseBody
    public UserInfoView get(Long id) {
        log.info("id={}", id);
        return UserInfoView.builder().id(id).name("name"   id).build();
    }
}

这个接口直接返回了 UserInfoView的实例对象,调用接口时,Graceful Response将自动封装为以下格式:

代码语言:json复制
{
  "status": {
    "code": "0",
    "msg": "ok"
  },
  "payload": {
    "id": 1,
    "name": "name1"
  }
}

UserInfoView被自动封装到payload字段中。

返回结果的格式是可以自定义的,Graceful

Response提供了两种风格的Response,可以通过配置的方式进行切换,如果这两种风格也不能满足需要,我们还可以根据自己的需要进行自定义返回的Response格式。

  • 返回值为空的场景
代码语言:java复制
public class Controller {
    @RequestMapping("/void")
    @ResponseBody
    public void testVoidResponse() {
        //业务操作
    }
}

testVoidResponse方法的返回时void,调用这个接口时,将返回:

代码语言:json复制
{
  "status": {
    "code": "200",
    "msg": "success"
  },
  "payload": {}
}

3.4 Service方法业务处理

在引入Graceful Response后,Service将:

  • 接口直接返回业务数据类型,而不是Response
代码语言:java复制
public interface ExampleService {
    UserInfoView query1(Query query);
}
  • Service接口实现类中,直接抛业务异常,接口调用异常时将直接返回错误码和错误提示
代码语言:java复制
public class ExampleServiceImpl implements ExampleService {
    @Resource
    private UserInfoMapper mapper;

    UserInfoView query1(Query query) {
        UserInfo userInfo = mapper.findOne(query.getId());
        if (Objects.isNull(userInfo)) {
            //这里直接抛自定义异常
            throw new NotFoundException();
        }
        //……后续业务操作
    }
}
代码语言:java复制
/**
 * NotFoundException的定义,使用@ExceptionMapper注解修饰
 * code:代表接口的异常码
 * msg:代表接口的异常提示
 */
@ExceptionMapper(code = "1404", msg = "找不到对象")
public class NotFoundException extends RuntimeException {

}

当Service方法抛出NotFoundException异常时,接口将直接返回错误码,不需要手工set。

代码语言:json复制
{
  "status": {
    "code": "1404",
    "msg": "找不到对象"
  },
  "payload": {}
}

验证:启动example工程后,请求http://localhost:9090/example/notfound

3.5 通用异常类和通用工具类

@ExceptionMapper设计的初衷,是将异常与错误码关联起来,用户只需要抛异常,不需要再关注异常与错误码的对应关系。

部分用户反馈,希望在不自定义新异常类的情况下,也能可以按照预期返回错误码和异常信息,因此从2.1版本开始,新增了GracefulResponseException异常类,用户只需要抛出该异常即可。

代码语言:java复制
public class Service {
  
  public void method() {
    throw new GracefulResponseException("自定义的错误码","自定义的错误信息");
  }
}

为简化使用,从2.1版本开始提供了GracefulResponse通用工具类,在需要抛出GracefulResponseException时,只需要调用raiseException方法即可。 这样做的目的是将用户的关注点从异常转移到错误码。

示例如下:

代码语言:java复制
public class Service {

    public void method() {
        //当condition==true时,抛出GracefulResponseException异常,返回自定义的错误码和错误信息
        if (condition) {
            GracefulResponse.raiseException("自定义的错误码", "自定义的错误信息");
        }
    }
}

3.6 参数校验异常以及错误码

在3.0版本以前,如果validation发生了校验异常,Graceful Response在默认情况下会捕获并返回code=1,参数校验发生的异常信息会丢失;如果使用异常别名功能,可以对大的校验异常返回统一的错误码,但是不够灵活并且依旧没有解决参数异常提示的问题。

Graceful Response从3.0版本开始,引入@ValidationStatusCode注解,可以非常方便地支持validation校验异常。

@ValidationStatusCode注解目前只有一个code属性,用于指定参数校验异常时的错误码,错误提示则取自validation校验框架。

  • 对入参类进行参数校验
代码语言:java复制
@Data
public class UserInfoQuery {

    @NotNull(message = "userName is null !")
    @Length(min = 6, max = 12)
    @ValidationStatusCode(code = "520")
    private String userName;
}

userName字段任意一项校验不通过时,接口将会返回异常码520和校验注解中的message

代码语言:json复制
{
  "status": {
    "code": "520",
    "msg": "userName is null !"
  },
  "payload": {}
}

详细见example工程ExampleController的validateDto方法

http://localhost:9090/example/validateDto

注意:@ValidationStatusCode校验参数对象字段的情况,code取值顺序为:会先取字段上的注解,再去该属性所在对象的类(即UserInfoQuery类)上的注解,再取全局配置的参数异常码gr.defaultValidateErrorCode,最后取默认的全局默认的错误码(默认code=1)

  • 直接在Controller中校验方法入参

直接在Controller方法中进行参数校验:

代码语言:java复制
public class ExampleController {

  @RequestMapping("/validateMethodParam")
  @ResponseBody
  @ValidationStatusCode(code = "1314")
  public void validateMethodParam(@NotNull(message = "userId不能为空") Long userId,
                                  @NotNull(message = "userName不能为空") Long userName{
      //省略业务逻辑
  }
}

当userId、或者userName校验不通过时,将会返回code=1314,msg为对应的校验信息。

代码语言:json复制
{
  "status": {
    "code": "1314",
    "msg": "userId不能为空"
  },
  "payload": {}
}

详细见example工程ExampleController的validateMethodParam方法

http://localhost:9090/example/validateMethodParam

注意:@ValidationStatusCode校验Controller方法参数字段的情况,code取值顺序为:会先取当前方法上的注解,再去该方法所在类(即ExampleController类)上的注解,再取全局配置的参数异常码gr.defaultValidateErrorCode,最后取默认的全局默认的错误码(默认code=1)

4. 进阶用法

4.1 Graceful Response异常错误码处理

以下是使用Graceful Response进行异常、错误码处理的开发步骤。

创建自定义异常,采用 @ExceptionMapper注解修饰,注解的 code属性为返回码,msg属性为错误提示信息

代码语言:java复制
@ExceptionMapper(code = 1007, msg = "有内鬼,终止交易")
public static final class RatException extends RuntimeException {

}

Service执行具体逻辑,需要抛异常的时候直接抛出去即可,不需要再关心异常与错误码关联的问题

代码语言:java复制
public class Service {
    public void illegalTransaction() {
        //需要抛异常的时候直接抛
        if (hasRat()) {
            logger.error("有内鬼终止交易");
            throw new RatException();
        }
        doIllegalTransaction();
    }
}

Controller调用Service

代码语言:java复制
public class Controller {
    @RequestMapping("/test3")
    public void test3() {
        logger.info("test3: RuntimeException");
        //Controller中不会进行异常处理,也不会手工set错误码,只关心核心操作,其他的通通交给Graceful Response
        exampleService.illegalTransaction();
    }
}

在浏览器中请求controller的/test3方法,有异常时将会返回:

代码语言:json复制
{
  "status": {
    "code": 1007,
    "msg": "有内鬼,终止交易"
  },
  "payload": {
  }
}

4.2 外部异常别名

案例工程( https://github.com/feiniaojin/graceful-response-example.git )启动后,

通过浏览器访问一个不存在的接口,例如 http://localhost:9090/example/get2?id=1

如果没开启Graceful Response,将会跳转到404页面页面,主要原因是应用内部产生了 NoHandlerFoundException异常。如果开启了Graceful

Response,默认会返回code=1的错误码。

这类非自定义的异常,如果需要自定义一个错误码返回,将不得不对每个异常编写Advice逻辑,在Advice中设置错误码和提示信息,这样做非常繁琐。

Graceful Response可以非常轻松地解决给这类外部异常定义错误码和提示信息的问题。

以下为操作步骤:

  • 创建异常别名,并用 @ExceptionAliasFor注解修饰
代码语言:java复制
@ExceptionAliasFor(code = "1404", msg = "not found", aliasFor = NoHandlerFoundException.class)
public class NotFoundException extends RuntimeException {
}

code:捕获异常时返回的错误码

msg:为提示信息

aliasFor:表示将成为哪个异常的别名,通过这个属性关联到对应异常。

  • 注册异常别名

创建一个继承了AbstractExceptionAliasRegisterConfig的配置类,在实现的registerAlias方法中进行注册。

代码语言:java复制
@Configuration
public class GracefulResponseConfig extends AbstractExceptionAliasRegisterConfig {

    @Override
    protected void registerAlias(ExceptionAliasRegister aliasRegister) {
        aliasRegister.doRegisterExceptionAlias(NotFoundException.class);
    }
}
  • 浏览器访问不存在的URL

再次访问 http://localhost:9090/example/get2?id=1 ,服务端将返回以下json,正是在ExceptionAliasFor中定义的内容

代码语言:json复制
{
  "code": "1404",
  "msg": "not found",
  "data": {
  }
}

4.3 自定义Response格式

Graceful Response内置了两种风格的响应格式,并通过graceful-response.response-style进行配置

  • graceful-response.response-style=0,或者不配置(默认情况)

将以以下的格式进行返回:

代码语言:json复制
{
  "status": {
    "code": 1007,
    "msg": "有内鬼,终止交易"
  },
  "payload": {
  }
}
  • graceful-response.response-style=1

将以以下的格式进行返回:

代码语言:json复制
{
  "code": "1404",
  "msg": "not found",
  "data": {
  }
}
  • 自定义响应格式 如果以上两种格式均不能满足业务需要,可以通过自定义。

例如以下响应:

代码语言:java复制
public class CustomResponseImpl implements Response {

    private String code;

    private Long timestamp = System.currentTimeMillis();

    private String msg;

    private Object data = Collections.EMPTY_MAP;

    @Override
    public void setStatus(ResponseStatus statusLine) {
        this.code = statusLine.getCode();
        this.msg = statusLine.getMsg();
    }

    @Override
    @JsonIgnore
    public ResponseStatus getStatus() {
        return null;
    }

    @Override
    public void setPayload(Object payload) {
        this.data = payload;
    }

    @Override
    @JsonIgnore
    public Object getPayload() {
        return null;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public Long getTimestamp() {
        return timestamp;
    }
}

注意,不需要返回的属性可以返回null或者加上@JsonIgnore注解

  • 配置graceful-response.response-class-full-name

将CustomResponseImpl的全限定名配置到graceful-response.response-class-full-name属性。

代码语言:yaml复制
graceful-response:
  response-class-full-name: com.feiniaojin.gracefuresponse.example.config.CustomResponseImpl

注意,graceful-response.response-class-full-name后,graceful-response.responseStyle将不再生效。

4.4 例外处理

有用户反馈引入Graceful Response后,所有的controller方法均被处理了,他们希望能配置一些例外的情况。

Graceful Response从 3.2.0版本开始,提供了两种方式实现controller方法例外排除。

4.4.1 单个方法例外排除

针对某个Controller方法,我们可以添加@ExcludeFromGracefulResponse注解,声明该方法不需要进行统一的包装。

代码语言:java复制
/**
 * @author lihao3
 * @date 2023/6/30 10:10
 */
@Api("用户相关接口")
@Slf4j
@RestController
@RequestMapping("system/user")
@RequiredArgsConstructor
public class SysUserController {

  private final SysUserService service;

  @ApiOperation("删除")
  @DeleteMapping("{id}")
  @ExcludeFromGracefulResponse
  public String delete(@PathVariable Long id) {
    service.delete(id);
    return "删除成功";
  }

}

这样配置就会直接返回"删除成功",不再进行统一返回值的封装。

4.4.2 包级别的例外处理

用户可以通过配置graceful-response.exclude-packages,声明某些包需要跳过不进行处理。

该配置项支持*和**,例如

代码语言:yaml复制
graceful-response:
  exclude-packages:
    - com.lizhiadmin.pro.module.*

该配置表明com.lizhiadmin.pro.module包下的所有controller均不会被Graceful Response进行自动处理。

详细案例见example工程的ExcludeController类,该类下的test方法由于在application.yaml文件中配置了graceful-response.exclude-packages,因此Graceful Response将不会对其进行统一结果封装。

代码语言:txt复制
https://github.com/feiniaojin/graceful-response-example/blob/3.2.0-boot2/src/main/java/com/feiniaojin/gracefuresponse/example/controller/exclude/ExcludeController.java

5. 常用配置

代码语言:yaml复制
graceful-response:
  # 自定义Response类的全限定名,默认为空。 配置response-class-full-name后,response-style将不再生效
  response-class-full-name:
  # 是否打印异常日志,默认为false
  print-exception-in-global-advice: 
  # Response风格,不配置默认为0
  response-style: 
  # 自定义的成功响应码,不配置则为0
  default-success-code: 
  # 自定义的成功提示,默认为ok
  default-success-msg: 
  # 自定义的失败响应码,默认为1
  default-error-code: 
  # 自定义的失败提示,默认为error
  default-error-msg: 
  # 全局的参数校验错误码,默认等于default-error-code
  default-validate-error-code: 
  # 例外包路径(支持数字, *和**通配符匹配),该包路径下的controller将被忽略处理
  exclude-packages:
    - com.lizhiadmin.pro.module.*.controller

0 人点赞