5.通用异常处理

在项目中出现异常是在所难免的,但是出现异常后怎么处理,这就很有学问了。

5.1.场景预设

5.1.1.场景

我们预设这样一个场景,假如我们做新增商品,需要接收下面的参数:

price:价格
name:名称

然后对数据做简单校验:

  • 价格不能为空

新增时,自动形成ID,然后随商品对象一起返回

5.1.2.代码

在ly-item-interface中编写实体类:

@Data
public class Item {
    private Integer id;
    private String name;
    private Long price;
}

在ly-item-service中编写业务:

service:

@Service
public class ItemService {
    
    public Item saveItem(Item item){
        int id = new Random().nextInt(100);
        item.setId(id);
        return item;
    }
}
  • 这里临时使用随机生成id,然后直接返回,没有做数据库操作

controller:

@RestController
public class ItemController {

    @Autowired
    private ItemService itemService;

    @PostMapping("item")
    public ResponseEntity<Item> saveItem(Item item){
        // 如果价格为空,则抛出异常,返回400状态码,请求参数有误
        if(item.getPrice() == null){
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);
        }
        Item result = itemService.saveItem(item);
        return ResponseEntity.ok(result);
    }
}

5.2.统一异常处理

rest客户端:https://insomnia.rest/

5.2.1初步测试

现在我们启动项目,做下测试:

通过RestClient去访问:

1534202307259

发现参数不正确时,返回了400。看起来没问题

5.2.2.问题分析

虽然上面返回结果正确,但是没有任何有效提示,这样是不是不太友好呢?

我们在返回错误信息时,使用了这样的代码:

return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(null);

响应体是null,因此返回结果才只有状态码。

如果我们在body中加入错误信息,会出现新的问题:

1534202544199

这个错误显然是返回类型不匹配造成的,我们返回值类型中要的是ResponseEntity<Item>,因此body中必须是Item对象,不能是字符串。

如何解决?

大家可能会想到:我们把返回值改成String就好了,返回对象的时候,手动进行Json序列化。

这确实能达到目的,但是不是给开发带来了不必要的麻烦呢?

上面的问题,通过ResponseEntity是无法完美解决的,那么我们需要转换一下思考问题的方式,

既然不能统一返回一样的内容,干脆在出现问题的地方就不要返回,而是转为异常,然后再统一处理异常。这样就问题从返回值类型,转移到了异常的处理了。

那么问题来了:具体该怎么操作呢?

5.2.3.统一异常处理

我们先修改controller的代码,把异常抛出:

@PostMapping("item")
public ResponseEntity<Item> saveItem(Item item){
    if(item.getPrice() == null){
        throw new RuntimeException("价格不能为空");
    }
    Item result = itemService.saveItem(item);
    return ResponseEntity.ok(result);
}

接下来,我们使用SpringMVC提供的统一异常拦截器,因为是统一处理,我们放到ly-common项目中:

新建一个类,名为:BasicExceptionHandler

1534203441335

然后代码如下:

@ControllerAdvice
@Slf4j
public class BasicExceptionHandler {

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<String> handleException(RuntimeException e) {
        // 我们暂定返回状态码为400, 然后从异常中获取友好提示信息
        return ResponseEntity.status(400).body(e.getMessage());
    }
}

解读:

  • @ControllerAdvice:默认情况下,会拦截所有加了@Controller的类

1534203615380

  • @ExceptionHandler(RuntimeException.class):作用在方法上,声明要处理的异常类型,可以有多个,这里指定的是RuntimeException。被声明的方法可以看做是一个SpringMVC的Handler

    • 参数是要处理的异常,类型必须要匹配
    • 返回结果可以是ModelAndViewResponseEntity等,基本与handler类似
  • 这里等于从新定义了返回结果,我们可以随意指定想要的返回类型。此处使用了String

然后在ly-item-service中引入ly-common:

<dependency>
    <groupId>com.leyou.common</groupId>
    <artifactId>ly-common</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

重启项目测试:

1534204142605

成功返回了错误信息!

5.2.4.自定义异常

刚才的处理看似完美,但是仔细想想,我们在通用的异常处理中,把返回状态码写死为400了:

1534204244712

这样显然不太合理。

但是,仅凭用户抛出的异常,我们根本无法判断到底该返回怎样的状态码,可能是参数有误、数据库异常、没有权限,等等各种情况。

因此,用户抛出异常时,就必须传递两个内容:

  • 异常信息
  • 异常状态码

我们定义一个枚举,用于封装这些信息:

image-20180913135824077

@NoArgsConstructor
@AllArgsConstructor
public enum ExceptionEnum {
    PRICE_CANNOT_BE_NULL(400, "价格不能为空!");
    ;
    private int value;
    private String msg;

    public int value() {
        return this.value;
    }

    public String msg() {
        return this.msg;
    }
}

然后自定义异常,来获取和传递枚举对象。

在ly-common中定义自定义异常类:

1534204572495

代码:

@Getter
public class LyException extends RuntimeException{

    private ExceptionEnum exceptionEnum;

    public LyException(ExceptionEnum exceptionEnum) {
        this.exceptionEnum = exceptionEnum;
    }
}
  • status:响应状态对象,借用了SpringMVC中提供的HttpStatus状态对象
  • statusCode:int类型,如果没有传HttpStatus,则使用这个自定义code返回

修改controller代码:

@PostMapping("item")
public ResponseEntity<Item> saveItem(Item item){
    if(item.getPrice() == null){
        throw new LyException(ExceptionEnum.PRICE_CANNOT_BE_NULL);
    }
    Item result = itemService.saveItem(item);
    return ResponseEntity.ok(result);
}

在ly-common中定义异常结果对象:

image-20180913140843232

@Data
public class ExceptionResult {
    private int status;
    private String msg;
    private long timestamp;

    public ExceptionResult(ExceptionEnum em){
        this.status = em.value();
        this.msg = em.msg();
        this.timestamp = System.currentTimeMillis();
    }
}

修改异常处理逻辑:

@ControllerAdvice
@Slf4j
public class BasicExceptionHandler {

    @ExceptionHandler(LyException.class)
    public ResponseEntity<ExceptionResult> handleException(LyException e) {
        return ResponseEntity.status(e.getExceptionEnum().value())
                .body(new ExceptionResult(e.getExceptionEnum()));
    }
}

再次测试:

image-20180913140749602

以后,我们无论controller还是service层的业务处理,出现异常情况,都抛出自定义异常,方便统一处理。