主要解决的问题
- 异常在系统内部的应用和处理
- 异常在服务之间的应用和处理
- 异常在网关和前端交互中的处理
异常定义
异常是高级语言出现的定义,它用于强制程序员在编码中处理它。
这里有个难懂的概念,什么是强制处理。在C语言中没有异常机制,判断一个程序是否执行顺利需要通过方法的返回值来判断,如果是1则表示执行完成,0表示执行错误。在这种机制下,如果程序员在调用方法时没有去判断1/0,而继续执行后续的编码,系统也是会“错误”地运行下去。在java中,遇到异常必须抛出或者catch,否则代码编译不通过,这样就强制程序员去处理它。
异常在系统内部的应用
异常通常表示一个程序无法继续执行,所以在系统内部编写代码时,如果发现插入数量不为1,入参格式不正确,必填入参未填写,都可以直接抛出异常到客户端,因为遇到这些异常表示程序无法继续运行了。
实现这个需求需要定义以下类:
// 通用返回信息实体类
class ApiResult<T>{
private int code;
private String message;
private T data;
private ApiResult(int code, String message, T data){...}
public static ApiResult success(String message){
return new ApiResult(200,message,null);
}
}
// 通用业务端异常
class ServiceException extends RuntimeException{}
// 通用请求异常
class RequestException extends RuntimeException{}
// 全局异常处理
@ControllerAdvice
class GlobalExceptionHandler{
@ExceptionHandler(ServiceException.class)
public ModelAndView exception(ServiceException e, HttpServletRequest request, HttpServletResponse response) {
ModelAndView modelAndView = new ModelAndView(new MappingJackson2JsonView());
modelAndView.addObject("code", 500);
log.error(e.getMessage());
modelAndView.addObject("message", e.getMessage());
return modelAndView;
}
}
// 断言类
class Assert{
public static void isTrue(boolean operation, String message){
if(!operation){
throw new ServiceException(message);
}
}
}
有了以上配置后写业务代码的基础校验会非常简单和清晰
@RequestMapping("car")
class CarController{
private CarService carService;
@PostMapping("save")
public ApiResult save(Car car){
carService.create(car);
return ApiResult.success("保存成功");
}
}
class CarService{
private CarMapper carMapper;
public void create(Car car){
Assert.isTrue(StringUtil.isNotEmpty(car.getNo()),"编号不能为空");
Assert.isTrue(StringUtil.isNotEmpty(car.getName()),"名称不能为空");
Assert.isTrue(this.findByNo(car.getNo())==0,"编号已使用");
int num = carMapper.insert(car);
Assert.isTrue(num==1, "插入失败");
}
}
上述代码如果出现了错误会在全局异常里直接处理,业务代码不需要关注是否有异常情况,除非需要忽略异常的时候,才会catch并无视,其他不需要catch。
在一些老的代码里会出现用map来返回是否处理成功,上游需要取map值判断再继续进行的,这种代码会十分冗长。
异常在服务之间的应用和处理
这里只讨论feign下的设计。
异常在服务之间会被转成json,如ApiResult也会被转成对应的json,然后在上游系统中再从json转成ApiResult。那么这里有个问题就是上游系统调用feign接口的时候都需要判断code是不是200,如果是500的话要把message取出来并抛异常。每一个feign接口都这么做的代码是很冗余也很浪费的。
所以系统设计上可以在feignClient外层包一个切面,在返回时统一判断code是否是200,如果不是就转成Service异常,对于调用端来讲,感觉就是在调系统内部的接口。
public class ExceptionHandleAspect {
@Around(value="@within(feignClient)")
Object aroundMethod(ProceedingJoinPoint joinPoint, FeignClient feignClient){
Object rtn = null;
try {
rtn = joinPoint.proceed();
} catch (Throwable throwable) {
throw new ServiceException("服务调用异常");
}
if(rtn instanceof ApiResult){
ApiResult apiResult = (ApiResult) rtn;
if(apiResult.getCode() != 200){
throw new ServiceException(apiResult.getMessage());
}
}
return rtn;
}
}
异常在网关和前端交互中的处理
我们知道系统里有很多自带的标准编码和异常,如未授权401,服务器错误500,但是我们在ApiResult中又有自己的一套异常。在转给前端前必须要做一次异常的转化,将服务异常转化为我们的ApiResult,这样对于前端或者异构系统来讲,不会出现一个系统多套编码的问题。
这个功能需要改gateway的默认配置,gateway里默认有一个DefaultErrorWebExceptionHandler类,但是这个类不满足需要,需要覆盖原有的类并重写转化方法
public class GlobalErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {
public GlobalErrorWebExceptionHandler(ErrorAttributes errorAttributes, WebProperties.Resources resources, ErrorProperties errorProperties, ApplicationContext applicationContext) {
super(errorAttributes, resources, errorProperties, applicationContext);
}
@Override
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
Map<String, Object> error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
// 取到error后根据系统自定义规则来转成ApiResult并写到response里
}
}