参数校验放在controller还是service_极致优雅,零侵入的全局异常处理、参数校验、错误码、统一响应格式解决方案...

baea5037aa64de4cfe09a958b7d6b77b.png

1. 需求背景

在前后端分离的开发场景下,通常的Controller伪代码是这样的:

@GetMapping
@ResponseBody
public Result GeneralPageQuery(Parameter params) {

    Result result = new Result();

    //1. 校验params参数,非空校验、长度校验

    try{

        //2. 调用Service的一系列操作
    }catch(Exception e){

        //3. 异常处理:一堆丑陋的try...catch,如果有错误码的,还需要手工填充错误码
    }

    return result;
}

或者这样:

@RequestMapping(value = "/content", method = RequestMethod.GET)
@ResponseBody
public CommonResult content() {
    HomeContentResult contentResult = homeService.content();
    return CommonResult.success(contentResult);
}

以上代码均来自github高星流行的项目,通常存在以下几种问题:

  • 手工进行封装统一响应格式

    为方便移动端处理接口返回值,通常要求将结果按照约定的格式封装起来。这个封装的操作大部分开发人员都是自己完成的。例如上文中的

    Result result = new Result();
    CommonResult.success(contentResult);

    每一个接口都需要开发人员手工创建一个Result、CommonResult,属于低效的重复劳动。

    一般的,通常需要将处理结果封装到类似如下格式的Java Bean中

    public class ResponseBean{
        private int code ;
        private String msg ;
        private Object data ;
    }

    每次有返回结果时,执行

    ResponseBean bean=new ResponseBean();
    bean.setData(data);
    return bean;
  • 混乱的异常处理体系

    虽然@ExceptionHandler已经存在了很多年,但是很多程序员还是习惯在每个接口进行异常捕获,然后根据不同的异常,封装异常码到上述的Result、CommonResult中,满篇的try……catch既显得代码冗余,也影响开发效率,还影响代码的可读性。

    对外提供的API,处理业务逻辑时发生了异常,不能直接给调用方返回异常堆栈信息,而是要返回这个异常对应的错误码,便于双方开发人员对接。

    异常类并没有和异常错误码进行关联,在抛出异常时,需要到枚举类、到定义类中找到异常对应的异常码,既容易出错,也非常低效。

  • 接口参数校验及错误提示

    接口参数的某些校验属于重复体力劳动,例如非空校验、长度校验等,可以交给类似Hibernate Validator 这样的Bean Validation框架进行处理。

    Validator校验出来的异常,还需要跟匹配成开发者自定义的异常,方便返回错误码。

  • Http异常转换

    在进行REST微服务开发时,Http协议本身会返回各种状态码,我们希望统一转成200,并返回自定义的异常码。

以上所有问题,global-result-handler-starter一次性解决。

2. global-result-handler-starter案例代码

以下是使用global-result-handler-starter进行开发的代码:

@PostMapping
public Long add(@RequestBody TaskDTO.Info info) {
    if(log.isDebugEnabled()){
        log.debug("TaskDTO.Info=[{}]",gson.toJson(info));
    }
    return taskService.add(info);
}

@GetMapping
public TaskDTO.Page pageList(TaskDTO.Query query) {

    if(log.isDebugEnabled()){
        log.debug("TaskDTO.Query=[{}]",gson.toJson(query));
    }
   TaskDTO.Page page= taskService.pageList(query);
   if(log.isDebugEnabled()){
        log.debug("TaskDTO.Page=[{}]",gson.toJson(page));
    }
   return page;
}

@GetMapping("/{id}")
public TaskDTO.DetailInfo detail(@PathVariable Long id) {

    if(log.isDebugEnabled()){
        log.debug("Getting Task detail,id=[{}]",id);
    }
    TaskDTO.DetailInfo detailInfo = taskService.getById(id);
    if(log.isDebugEnabled()){
        log.debug("Task detail=[{}]",gson.toJson(detailInfo));
    }
    return detailInfo;
}

@PutMapping("/{id}")
public void update(@PathVariable String id, TaskDTO.Info info) {
    if(log.isDebugEnabled()){
        log.debug("TaskDTO.Info=[{}]",gson.toJson(info));
    }
    taskService.updateById(info);
}

@DeleteMapping("/{id}")
public void delete(@PathVariable Long id) {
    if(log.isDebugEnabled()){
        log.debug("Deleting Task ,id=[{}]",id);
    }
    taskService.deleteById(id);
}

以上代码具有以下特点

  • 代码优雅、简洁、直观

  • 在DTO中结合Validator进行校验

  • 直接返回所需要的结果,不需要每个接口都new ResponseBean()

  • 统一异常处理,业务异常结合异常码,也是阿里巴巴《Java开发手册》所推荐的

    异常的抛出主要在service层,例如

    public TaskDTO.Info getInfoById(Long id) {
      Task info = taskExMapper.selectByPrimaryKey(id);
      if(info == null){
          throw new CommonException.NotFoundException();
      }
      return entity2Info(info);
    }

    而CommonException.NotFoundException异常在抛出时,已经定义了异常码

    @ExceptionMapper(code = 1404,msg = "Not found!")
    public static class NotFoundException extends RuntimeException {
    }

3. global-result-handler-starter使用教程

3.1 引入starter

global-result-handler-starter目前已发布到Maven中央仓库,可以直接在项目的pom文件中引入。

<dependency>
  <groupId>com.feiniaojin.grhgroupId>
  <artifactId>global-result-handler-starterartifactId>
  <version>0.3version>
dependency>
  • 目前可用的版本是0.3

3.2 在启动类上通过注解开启统一处理

@EnableGlobalResultHandler
@SpringBootApplication
public class ExampleApplication {
  public static void main(String[] args) {
    SpringApplication.run(ExampleApplication.class, args);
  }
}

3.3 在DTO中设置校验

grh的接口参数校验,采用的是Hibernate Validator原生的方法,并未提供任何新增的校验方法,保证了代码的可移植性,只要是Hibernate Validator支持的校验方式,grh即兼容。

如果要扩展Hibernate Validator的校验功能,只要Hibernate Validator支持,则grh也会支持。

@Data
public class RequestDTO {

    @NotNull(message = "userId is null ")
    private Long userId;

    @NotNull(message = "userName is null ")
    @Length(min = 6, max = 12)
    private String userName;

    @NotNull(message = "age is null ")
    @Range(min = 18, max = 50)
    private Integer age;
}

3.4 创建业务异常时加上@ExceptionMapper注解

在创建业务异常时,使用@ExceptionMapper进行注解,并设置异常错误码和提示信息。

推荐实践:同一个模块的业务异常,在同一个"XxxxExceptions"类中创建静态内部类,便于分组和维护,同模块的错误码拥有相同的前缀,例如用户中心相关的异常采用10xx~20xx码段。

public class ExampleExceptions {

    @ExceptionMapper(code = 1024, msg = "UnCheckedException")
    public static class UnCheckedException extends RuntimeException {

    }

    @ExceptionMapper(code = 2048, msg = "CheckedException")
    public static class CheckedException extends Exception {

    }
}

3.5 关闭spring mvc的自动匹配

spring:
  mvc:
    throw-exception-if-no-handler-found: true
  resources:
    add-mappings: false

3.6 实现具体业务逻辑

以下代码来自example工程的ExampleController

/**
 * class {@code ExampleController} 使用案例的Controller.
 *
 * @author feiniaojin
 */
@Controller
@RequestMapping("/example")
@Slf4j
@Validated
@Api("ExampleController")
public class ExampleController {

  @Resource
  private ExampleService exampleService;

  /**
   * 测试空返回值.
   */
  @RequestMapping("/void")
  @ResponseBody
  @ApiOperation(value = "测试返回空值", notes = "")
  public void testVoidResponse() {

  }

  @RequestMapping("/validate")
  @ResponseBody
  public void testValidateException(@Validated RequestDto dto) {
    log.info(dto.toString());
  }

  @RequestMapping("/success")
  @ResponseBody
  public RequestDto testSuccess(@Validated RequestDto dto) {
    log.info(dto.toString());
    return dto;
  }

  @RequestMapping("/get")
  @ResponseBody
  public ResponseDto get(Long id) {
    log.info("id=" + id);
    return exampleService.getById(id);
  }

  /**
   * 测试抛出运行时异常的处理.
   *
   * @param dto 入参
   * @return 直接返回,未处理
   */
  @RequestMapping("/runtime")
  @ResponseBody
  public RequestDto testRuntimeException(RequestDto dto) {
    log.info(dto.toString());
    exampleService.testUnCheckedException();
    return dto;
  }

  /**
   * 测试受检异常的情形.
   *
   * @param dto 入参
   * @return 未处理,直接将入参返回
   * @throws Exception 首检异常
   */
  @RequestMapping("/checked")
  @ResponseBody
  public RequestDto testCheckedException(RequestDto dto) throws Exception {
    log.info(dto.toString());
    exampleService.testCheckedException();
    return dto;
  }

  /**
   * 测试抛出{@code Throwable} 的情形.
   *
   * @param dto 入参
   * @return 未处理,直接返回
   * @throws Throwable 抛出Throwable异常
   */
  @RequestMapping("/throwable")
  @ResponseBody
  public RequestDto testThrowable(RequestDto dto) throws Throwable {
    log.info(dto.toString());
    throw new Throwable();
  }

  /**
   * 测试Controller中方法对参数进行校验的情形.
   *
   * @param userId 非空
   */
  @RequestMapping("/method")
  @ResponseBody
  public void testMethod(@NotNull Long userId) {
    log.info("" + userId);

  }

  /**
   * 不支持的http方法调用.
   * POST接口,使用GET进行请求
   *
   * @param userId
   */
  @RequestMapping(value = "/methodPost", method = RequestMethod.POST)
  @ResponseBody
  public void testMethodNotSupport(Long userId) {
    log.info("" + userId);

  }

  /**
   * 测试Controller中方法对参数进行校验的情形.
   */
  @RequestMapping("/jsonStr")
  @ResponseBody
  public String jsonStr() {
    log.info("");
    return "jsonStr";
  }

  /**
   * 测试Controller中方法对参数进行校验的情形.
   */
  @RequestMapping("/str")
  public String str() {
    log.info("");
    return "view";
  }
}

3.7 demo工程的结构

ae3a57ab9ed09653e4ca155876ee67fd.png

3.8 Swagger2支持

从0.2版本开始,添加对Swagger2的支持。

  • 添加Swagger2的依赖,目前只测试了2.6.0版本的

    <dependency>
      <groupId>io.springfoxgroupId>
      <artifactId>springfox-swagger-uiartifactId>
      <version>2.6.0version>
    dependency>
    <dependency>
      <groupId>io.springfoxgroupId>
      <artifactId>springfox-swagger2artifactId>
      <version>2.6.0version>
    dependency>
  • 在工程中添加springmvc的静态路径

    spring:
    mvc:
      throw-exception-if-no-handler-found: true
      static-path-pattern: "*.html"
      add-mappings: false

    如果不添加,grh将会拦截html

3.9 自定义返回格式和默认异常码

从0.3版本开始,支持自定义默认异常码和统一返回的格式。

要支持自定义默认异常码和统一返回的格式,可以参考global-result-handler-defaults,该模块提供了很多默认的实现。

具体步骤如下:

3.9.1 增加maven依赖

增加global-result-handler-def的依赖

<dependency>
    <groupId>com.feiniaojin.grhgroupId>
    <artifactId>global-result-handler-defartifactId>
    <version>0.3version>
dependency>

global-result-handler-def只有一些定义的interface注解,不包含任何实现,也不包含任何第三方库,可以直接在api接口定义中引入。

3.9.2 提供自定义实现

参考global-result-handler-defaults,对需要自定义的内容进行实现。

3.9.3 Spring Boot中配置实例化

将自定义的组件配置到Spring Boot中,即可替换默认的实现。

4. 版本历史

0.1

基本功能的实现,用于作者个人项目,实现的功能有:

  • 统一异常处理

  • 参数校验并返回

  • 统一返回格式

  • 异常与错误码关联

0.2

  • 增加对swagger的支持

0.3

  • 支持自定义默认错误码

  • 支持自定义返回格式

  • 支持自定义http状态码与异常关联

  • 支持自定义参数校验异常与异常关联

  • example工程中测试spring boot跳转静态页面

5. 源码地址

以上的所有实现,是基于笔者在实际开发中的需求进行抽象设计的,未必适合所有的情形。

该项目的源码已上传至github,有需要的同学可以clone下来进行二次开发,欢迎提交PR。

github:

starter: https://github.com/feiniaojin/global-result-handler.git

example: https://github.com/feiniaojin/global-result-handler-starter-example.git

global-result-handler-starter-example需要搭配对应版本的global-result-handler-starter,二者的版本号一致。

联系方式:

微信:qyj000100

邮箱:943868899@qq.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值