在一个月黑风高的晚上(
麻烦大家假装没看到时间),我的leader突然给我指派了一个需求,如下所示
首先说明一下,【金融服务】是我们其中一个后台管理系统的功能菜单,【积分商城】则是我们另外一个系统。 然后来看下这个需求:很明显是要在我们系统维护一个【银行网点】的功能,然后提供一些接口给【积分商城】使用,于是我设计了如下的两张表
然后我又写下了这样的一个接口给积分商城的同事进行对接
/**
* 积分商城银行网点信息Controller
*
* @author hcq
* @date 2020/5/9 16:32
*/
@RestController
@RequestMapping("/bank/service")
@AllArgsConstructor
public class BankServiceController {
private final BankServiceMapper bankServiceMapper;
/**
* 通过银行网点业务范围关键字 获取银行网点列表
* @param scope 业务范围 关键字
*/
@PostMapping("/getAllBankService")
public List<BankService> getAllBankService(String scope){
QueryWrapper<BankService> wrapper=new QueryWrapper<>();
wrapper.lambda().like(BankService::getScope,scope);
return bankServiceMapper.selectList(wrapper);
}
}
过了一会,我的同事忽然找到我跟我说:“你的接口是不是有问题啊?我怎么拿不到数据呀?” 我跟他说:“你别乱说话,这么简单的接口,怎么可能会有问题?是不是你请求方式不对?” 随后他扔了张截图给我。。。
我一看就知道了是怎么回事,于是对他说:“这不是没问题吗?没查到数据就返回了一个空集合,这不是正常的逻辑吗,你竟然还怀疑我的接口有问题。”
同事一脸不忿的辩解道:“你返回这个给我,我怎么会知道是什么意思,我还以为请求失败了呢!”
一阵激烈的探讨之下(
我被吊打了一顿),最后我妥协了,然后决定返回一个请求状态码给他,这样他就可以通过这个请求状态码来判断自己的请求是否是成功的。于是这个接口变成了下面这样
@RestController
@RequestMapping("/bank/service")
@AllArgsConstructor
public class BankServiceController {
private final BankServiceMapper bankServiceMapper;
/**
* 通过银行网点业务范围关键字 获取银行网点列表
* @param scope 业务范围 关键字
*/
@PostMapping("/getAllBankService")
public Map<String,Object> getAllBankService(String scope){
Map<String,Object> result=new HashMap<>(2);
QueryWrapper<BankService> wrapper=new QueryWrapper<>();
wrapper.lambda().like(BankService::getScope,scope);
result.put("resultCode","00");
result.put("data",bankServiceMapper.selectList(wrapper));
return result;
}
}
返回数据也变成了这个样子
过了一会,同事又找到我跟我说还需要一个查询业务员信息的接口,于是我又开发了一个接口
@RestController
@RequestMapping("/bank/service")
@AllArgsConstructor
public class BankServiceController {
private final BankServiceMapper bankServiceMapper;
private final BankServiceSaleMapper bankServiceSaleMapper;
/**
* 通过银行网点业务范围关键字 获取银行网点列表
* @param scope 业务范围 关键字
*/
@PostMapping("/getAllBankService")
public Map<String,Object> getAllBankService(String scope){
Map<String,Object> result=new HashMap<>(2);
QueryWrapper<BankService> wrapper=new QueryWrapper<>();
wrapper.lambda().like(BankService::getScope,scope);
result.put("resultCode","00");
result.put("data",bankServiceMapper.selectList(wrapper));
return result;
}
/**
* 通过网点id获取网点信息及业务员信息
* @param bankId 服务网点id
*/
@PostMapping("/getBankServiceDetail")
public Map<String,Object> getDetail(String bankId){
// 查询网点信息
BankServiceVo bankServiceVo=new BankServiceVo();
BankService bankService = bankServiceMapper.selectById(bankId);
BeanUtils.copyProperties(bankService,bankServiceVo);
// 查询业务员信息
QueryWrapper<BankServiceSale> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(BankServiceSale::getBankServiceId,bankId);
bankServiceVo.setBankServiceSaleList(bankServiceSaleMapper.selectList(wrapper));
// 返回数据
Map<String,Object> result=new HashMap<>(2);
result.put("code","00");
result.put("data",bankServiceVo);
return result;
}
}
这次我学聪明了,在他问我之前就已经把状态码返回了,但是我觉得之前的状态码叫resultCode不如直接叫code简洁,于是我就把这个接口的状态码改为了code字段
不料,这位同事竟然不乐意了,他说:“你这两个接口的状态码字段都不一样,难道我还要针对每次请求都做一个单独的判断不成???”。
原来是这个原因。于是我找到我的leader,向他说明了这个问题。leader发话了:“为避免以后再出现类似的情况,所有开发人员在写接口时,必须返回同一套状态码。小何,这件事情既然是你提出的,那就由你来负责吧。” 我认真考虑了一下,既然要约束字段名,那么就不能继续使用Map了,先不说可读性差的问题,就是Map的约束力也不够啊,自己都还有手抖写错的风险呢,又怎么去严格要求其他人呢。经过再三考虑我封装了一个统一的响应对象如下
/**
* 接口统一响应对象
*/
@Data
public class ResultVo<T> {
/**
* 状态码
*/
private String code;
/**
* 响应数据
*/
private T data;
private ResultVo(String code, T data) {
super();
this.code = code;
this.data = data;
}
public static <T> ResultVo<T> buildSuccess(T data){
return new ResultVo<>("00", data);
}
}
然后将原先的两个接口也修改如下
@RestController
@RequestMapping("/bank/service")
@AllArgsConstructor
public class BankServiceController {
private final BankServiceMapper bankServiceMapper;
private final BankServiceSaleMapper bankServiceSaleMapper;
/**
* 通过网点业务范围获取网点信息
* @param scope 业务范围 关键字
*/
@PostMapping("/getAllBankService")
public ResultVo<List<BankService>> getAllBankService(String scope){
QueryWrapper<BankService> wrapper=new QueryWrapper<>();
wrapper.lambda().like(BankService::getScope,scope);
return ResultVo.buildSuccess(bankServiceMapper.selectList(wrapper));
}
/**
* 通过网点id获取网点信息及业务员信息
* @param bankId 服务网点id
*/
@PostMapping("/getBankServiceDetail")
public ResultVo<BankServiceVo> getDetail(String bankId){
// 查询网点信息
BankServiceVo bankServiceVo=new BankServiceVo();
BankService bankService = bankServiceMapper.selectById(bankId);
BeanUtils.copyProperties(bankService,bankServiceVo);
// 查询业务员信息
QueryWrapper<BankServiceSale> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(BankServiceSale::getBankServiceId,bankId);
bankServiceVo.setBankServiceSaleList(bankServiceSaleMapper.selectList(wrapper));
return ResultVo.buildSuccess(bankServiceVo);
}
}
至此,此事算是告一段落了。
过了两天、这位同事又找到我跟我说:“你的接口还是有问题啊。接口报错时没有返回状态码,所以无法判断请求是否成功”
我看了一眼这个截图,然后又看了看代码,知道自己写出了BUG(
默默的扛起了这口大锅)。 于是我们又讨论了一番(好在这次认错态度良好,没有被吊打),增加了两条接口规则
基于这两点,我在接口统一响应对象这个类中增加了一个message字段用于提示异常信息。
/**
* 接口统一响应对象
*/
@Data
public class ResultVo<T> {
/**
* 状态码
*/
private String code;
/**
* 提示信息
*/
private String message;
/**
* 响应数据
*/
private T data;
private ResultVo(String code, String message,T data) {
super();
this.code = code;
this.message=message;
this.data = data;
}
public static <T> ResultVo<T> buildSuccess(T data){
return new ResultVo<>("00", "SUCCESS", data);
}
public static <T> ResultVo<T> buildException(String code,String message){
return new ResultVo<>(code, message, null);
}
}
然后原本的两个接口就变成了下面的这个样子
/**
* 积分商城网点信息
*
* @author hcq
* @date 2020/5/6 16:32
*/
@RestController
@RequestMapping("/bank/service")
@AllArgsConstructor
public class BankServiceController {
private final BankServiceMapper bankServiceMapper;
private final BankServiceSaleMapper bankServiceSaleMapper;
/**
* 通过网点业务范围获取网点信息
* @param scope 业务范围 关键字
*/
@PostMapping("/getAllBankService")
public ResultVo<List<BankService>> getAllBankService(String scope){
ResultVo resultVo;
try {
QueryWrapper<BankService> wrapper=new QueryWrapper<>();
wrapper.lambda().like(BankService::getScope,scope);
return ResultVo.buildSuccess(bankServiceMapper.selectList(wrapper));
}catch (Exception e){
e.printStackTrace();
resultVo=resultVo.buildException("99","系统异常");
}
return resultVo;
}
@PostMapping("/getBankServiceDetail")
public ResultVo<BankServiceVo> getDetail(String bankId){
ResultVo resultVo;
try {
// 查询网点信息
BankServiceVo bankServiceVo=new BankServiceVo();
BankService bankService = bankServiceMapper.selectById(bankId);
if(bankService==null){
return resultVo.buildException("10000",String.format("网点%s不存在",bankId));
}
BeanUtils.copyProperties(bankService,bankServiceVo);
// 查询业务员信息
QueryWrapper<BankServiceSale> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(BankServiceSale::getBankServiceId,bankId);
bankServiceVo.setBankServiceSaleList(bankServiceSaleMapper.selectList(wrapper));
return ResultVo.buildSuccess(bankServiceVo);
}catch (Exception e){
e.printStackTrace();
resultVo=resultVo.buildException("99","系统异常");
}
return resultVo;
}
}
看到这一坨代码,不禁使我陷入了沉思。。。本来很简单的一个业务逻辑为什么现在看起来如此复杂,自己写的代码自己读起来都略感吃力。 这个时候我明白了:实际开发过程中,异常的业务逻辑场景往往比正常的业务逻辑场景要多的多,而业务的复杂度也一般体现在对异常业务情况的处理上。假如正常的业务逻辑与异常的业务逻辑混合在一起,那么开发人员在写代码、读代码时不免要分散一部分精力用于处理异常逻辑,这样一来对正常业务逻辑处理的关注就会被分散,从而增加了编码的难度,编码的难度提高了,那么出现bug的风险也将大大增加。那么到底有没有一种可能,可以将正常的业务逻辑与异常的业务逻辑区分开呢?
Spring帮助我们解决了这个难题:
Spring
在3.2版本增加了两个注解@ControllerAdvice
@ExceptionHandler
,通过这两个注解我们可以实现一个全局的异常处理器,当接口抛出异常时,全局异常处理器可以捕获到异常信息并做出处理。
首先,要定义一个全局异常的处理器,其中@ExceptionHandler(Exception.class)指明需要处理的异常类型
/**
* 全局异常处理器
* @author hcq
* @date 2020/5/9 11:35
*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* Exception异常统一处理
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public ResultVo handlerException(Exception e) {
log.error("系统异常",e);
return ResultVo.buildException("99","系统异常");
}
}
这样一来我们的接口就可以简化为下面的这个样子
/**
* 积分商城网点信息
*
* @author hcq
* @date 2020/5/6 16:32
*/
@RestController
@RequestMapping("/bank/service")
@AllArgsConstructor
public class BankServiceController {
private final BankServiceMapper bankServiceMapper;
private final BankServiceSaleMapper bankServiceSaleMapper;
/**
* 通过网点业务范围获取网点信息
*
* @param scope 业务范围 关键字
*/
@PostMapping("/getAllBankService")
public ResultVo<List<BankService>> getAllBankService(String scope) {
QueryWrapper<BankService> wrapper = new QueryWrapper<>();
wrapper.lambda().like(BankService::getScope, scope);
return ResultVo.buildSuccess(bankServiceMapper.selectList(wrapper));
}
/**
* 通过网点id获取网点信息及业务员信息
* @param bankId 服务网点id
*/
@PostMapping("/getBankServiceDetail")
public ResultVo<BankServiceVo> getDetail(String bankId) {
// 查询网点信息
BankServiceVo bankServiceVo = new BankServiceVo();
BankService bankService = bankServiceMapper.selectById(bankId);
if (bankService == null) {
return ResultVo.buildException("10000",String.format("网点%s不存在", bankId));
}
BeanUtils.copyProperties(bankService, bankServiceVo);
// 查询业务员信息
QueryWrapper<BankServiceSale> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(BankServiceSale::getBankServiceId, bankId);
bankServiceVo.setBankServiceSaleList(bankServiceSaleMapper.selectList(wrapper));
return ResultVo.buildSuccess(bankServiceVo);
}
}
然后再次尝试发起一个get请求
可以看到bank/service/getAllBankService这个接口仅支持post请求,所以服务器内部会抛出HttpRequestMethodNotSupportedException异常,然后可以看到,此异常已经被全局异常处理器捕获到并且返回了我们统一的响应对象
此时我们又会发现一个问题:因为全局异常处理器中处理的是Exception异常,所以无论系统内部发生任何异常,这个接口都会返回99、系统错误这个信息。而且Spring虽然已经帮我们解决了所有的try-catch,但是不知你有没有注意到,即使没有try-catch,接口中依然还是可能会有异常逻辑包含在内的。
if (bankService == null) {
return ResultVo.buildException("10000",String.format("网点%s不存在", bankId));
}
这也算???这当然算。。。这类异常的不同之处是不需要通过try-catch处理,而且还可以返回明确的提示信息。但是说到底,它也还是一种用于处理异常的逻辑 既然如此,那我可以把异常进行细分,使每个异常都对应一个异常的业务场景,异常内部维护着对应的状态码和提示信息。当抛出此异常时,由全局异常处理器将对应的状态码及提示信息封装成统一响应对象进行返回
/**
* 业务异常
* @author hcq
* @date 2020/5/8 12:02
*/
@Getter
@Setter
public class BusinessException extends RuntimeException{
/**
* 具体异常码
*/
protected String code;
/**
* 异常信息
*/
protected String message;
BusinessException(String code, String message){
super(message);
this.code=code;
this.message=message;
}
}
/**
* 业务异常:网点%s不存在
* @author hcq
* @date 2020/5/10 14:38
*/
public class NotFindBankException extends BusinessException {
public NotFindBankException(String bankId){
super(10000,String.format("网点%s不存在",bankId));
}
}
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 业务异常处理器
* @param e 业务异常
* @return 统一响应对象
*/
@ExceptionHandler(BusinessException.class)
@ResponseBody
public ResultVo handlerException(BusinessException e) {
log.error("业务异常",e);
return ResultVo.buildException(e.getCode(),e.getMessage())
}
/**
* Exception异常统一处理
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public ResultVo handlerException(Exception e) {
log.error("系统异常",e);
return ResultVo.buildException("99","系统异常");
}
}
@PostMapping("/getBankServiceDetail")
public ResultVo<BankServiceVo> getDetail(String bankId) {
// 查询网点信息
BankServiceVo bankServiceVo = new BankServiceVo();
BankService bankService = bankServiceMapper.selectById(bankId);
if (bankService == null) {
throw new NotFindBankException(bankId);
}
BeanUtils.copyProperties(bankService, bankServiceVo);
// 查询业务员信息
QueryWrapper<BankServiceSale> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(BankServiceSale::getBankServiceId, bankId);
bankServiceVo.setBankServiceSaleList(bankServiceSaleMapper.selectList(wrapper));
return ResultVo.buildSuccess(bankServiceVo);
}
至此,我们便可以通过Spring的全局异常处理器,将异常逻辑完全剥离,开发人员只需要关注正常的业务逻辑,异常的业务逻辑只需要抛出异常即可
采用Spring全局异常处理器+自定义异常后,虽然功能已经很强大了,但是其存在的弊端也是一目了然:随着业务的不断增加、自定义异常类也会变得十分繁多。并且可以发现这些自定义异常类通常只有状态码和提示信息不一样而已,因此可以用一个枚举类来维护所有异常状态码及提示信息,然后再将这些状态码和提示信息统一封装为一个业务异常。
/**
* 业务异常
* @author hcq
* @date 2020/5/8 12:02
*/
@Getter
@Setter
public class BusinessException extends RuntimeException{
/**
* 具体异常码
*/
protected String code;
/**
* 异常信息
*/
protected String message;
BusinessException(String code, String message){
super(message);
this.code=code;
this.message=message;
}
}
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 业务异常处理器
* @param e 业务异常
* @return 统一响应对象
*/
@ExceptionHandler(BusinessException.class)
@ResponseBody
public ResultVo handlerException(BusinessException e) {
log.error("业务异常",e);
return ResultVo.buildException(e.getCode(),e.getMessage())
}
/**
* Exception异常统一处理
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public ResultVo handlerException(Exception e) {
log.error("系统异常",e);
return ResultVo.buildException("99","系统异常");
}
}
/**
* 异常枚举
* @author hcq
* @date 2020/5/8 12:03
*/
@AllArgsConstructor
@Getter
public enum ExceptionAssertEnum implements ExceptionAssert {
/*---------------- 支付系统异常 ----------------*/
NOT_FIND_BANK_EXCEPTION(10000,"网点%s不存在");
private final Integer code;
private final String message;
}
这里你肯定会很好奇,为什么这个枚举类要叫这个名字,而且还要实现一个接口? 这里其实是源于我偶然间看到的一篇文章:https://www.jianshu.com/p/3f3d9e8d1efa 我惊奇的发现虽然这篇文章中虽然也是通过异常枚举+自定义异常+全局异常处理器来处理异常逻辑的,但是抛异常的方式却与我迥然不同,我发现我每次抛异常前总是会有一个if判断,而这篇文章中类似于Assert抛异常的方式令我十分震惊,于是我也借鉴了一下,然后将枚举对象抽象出了两个接口,一个用于控制枚举对象的属性访问,一个用于控制枚举对象的行为。
/**
* 异常行为+异常属性
* @author hcq
* @date 2020/5/8 12:00
*/
public interface ExceptionAssert extends ExceptionStatus {
/**
* 创建异常
* @param args 提示信息
* @return Exception
*/
default BusinessException newException(Object... args){
String message=String.format(getMessage(),args);
return new BusinessException(getCode(),message);
}
/**
* 非空断言
* @param obj 断言对象
* @param args 提示信息
* @throws BusinessException obj == null
*/
default void notNull(Object obj,Object... args) {
if (obj == null) {
throw newException(args);
}
}
/**
* 断言一个boolean值
* @param obj 断言对象
* @param args 提示信息
* @throws BusinessException obj为false时
*/
default void isTrue(boolean obj,Object... args) {
if (!obj) {
throw newException(args);
}
}
}
/**
* 异常属性
* @author hcq
* @date 2020/5/8 11:50
*/
public interface ExceptionStatus {
/**
* 获取异常状态码
* @return code
*/
Integer getCode();
/**
* 获取异常提示信息
* @return message
*/
String getMessage();
}
/**
* 通过网点id获取网点信息及业务员信息
* @param bankId 服务网点id
*/
@PostMapping("/getBankServiceDetail")
public ResultVo<BankServiceVo> getDetail(String bankId) {
// 查询网点信息
BankServiceVo bankServiceVo = new BankServiceVo();
BankService bankService = bankServiceMapper.selectById(bankId);
// 异常处理
ExceptionAssertEnum.NOT_FIND_BANK_EXCEPTION.notNull(bankService,bankId);
// 查询业务员信息
BeanUtils.copyProperties(bankService, bankServiceVo);
QueryWrapper<BankServiceSale> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(BankServiceSale::getBankServiceId, bankId);
bankServiceVo.setBankServiceSaleList(bankServiceSaleMapper.selectList(wrapper));
return ResultVo.buildSuccess(bankServiceVo);
}
}
可以看到抛异常的方式变为了更为简洁的:
ExceptionAssertEnum.NOT_FIND_BANK_EXCEPTION.notNull(bankService,bankId)
最后忍不住夸自己一下