设计一个优秀的异常处理机制
异常处理的乱象例举
乱象一:捕获异常后只输出到控制台
前端js-ajax代码
$.ajax({
type: "GET",
url: "/user/add",
dataType: "json",
success: function(data){
alert("添加成功");
}
});
后端业务代码
try {
} catch (XyyyyException e) {
e.printStackTrace();
}
问题:
乱象二:混乱的返回方式
前端代码
$.ajax({
type: "GET",
url: "/goods/add",
dataType: "json",
success: function(data) {
if (data.flag) {
alert("添加成功");
} else {
alert(data.message);
}
},
error: function(data){
alert("添加失败");
}
});
后端代码
@RequestMapping("/goods/add")
@ResponseBody
public Map add(Goods goods) {
Map map = new HashMap();
try {
map.put(flag, true);
} catch (Exception e) {
e.printStackTrace();
map.put("flag", false);
map.put("message", e.getMessage());
}
reutrn map;
}
问题:
该如何设计异常处理
面向相关方友好
为什么要将系统运行时异常捕获,转换为自定义异常抛出?
答:因为用户不认识ConnectionTimeOutException类似这种异常是什么东西,但是转换为自定义异常就要求程序员对运行时异常进行一个翻译,比如:自定义异常里面应该有message字段,后端程序员应该明确的在message字段里面用面向用户的友好语言,说明服务端发生了什么。
开发规范
自定义异常和相关数据结构
该如何设计数据结构
枚举异常的类型
为了防止开发人员大脑发散,每个开发人员都不断的发明自己的异常类型,我们需要规定好异常的类型(枚举)。比如:系统异常、用户(输入)操作导致的异常、其他异常等。
public enum CustomExceptionType {
USER_INPUT_ERROR(400,"您输入的数据错误或您没有权限访问资源!"),
SYSTEM_ERROR (500,"系统出现异常,请您稍后再试或联系管理员!"),
OTHER_ERROR(999,"系统出现未知异常,请联系管理员!");
CustomExceptionType(int code, String desc) {
this.code = code;
this.desc = desc;
}
private String desc;
private int code;
public String getDesc() {
return desc;
}
public int getCode() {
return code;
}
}
自定义异常
public class CustomException extends RuntimeException {
private int code ;
private String message;
private CustomException(){}
public CustomException(CustomExceptionType exceptionTypeEnum) {
this.code = exceptionTypeEnum.getCode();
this.message = exceptionTypeEnum.getDesc();
}
public CustomException(CustomExceptionType exceptionTypeEnum,
String message) {
this.code = exceptionTypeEnum.getCode();
this.message = message;
}
public int getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
请求接口统一响应数据结构
为了解决不同的开发人员使用不同的结构来响应给前端,导致规范不统一,开发混乱的问题。我们使用如下代码定义统一数据响应结构
@Data
public class AjaxResponse {
private boolean isok;
private int code;
private String message;
private Object data;
private AjaxResponse(){}
public static AjaxResponse error(CustomException e) {
AjaxResponse resultBean = new AjaxResponse();
resultBean.setIsok(false);
resultBean.setCode(e.getCode());
resultBean.setMessage(e.getMessage());
return resultBean;
}
public static AjaxResponse error(CustomExceptionType customExceptionType,
String errorMessage) {
AjaxResponse resultBean = new AjaxResponse();
resultBean.setIsok(false);
resultBean.setCode(customExceptionType.getCode());
resultBean.setMessage(errorMessage);
return resultBean;
}
public static AjaxResponse success(){
AjaxResponse ajaxResponse = new AjaxResponse();
ajaxResponse.setIsok(true);
ajaxResponse.setCode(200);
ajaxResponse.setMessage("请求响应成功!");
return ajaxResponse;
}
public static AjaxResponse success(Object obj){
AjaxResponse ajaxResponse = new AjaxResponse();
ajaxResponse.setIsok(true);
ajaxResponse.setCode(200);
ajaxResponse.setMessage("请求响应成功!");
ajaxResponse.setData(obj);
return ajaxResponse;
}
public static AjaxResponse success(Object obj,String message){
AjaxResponse ajaxResponse = new AjaxResponse();
ajaxResponse.setIsok(true);
ajaxResponse.setCode(200);
ajaxResponse.setMessage(message);
ajaxResponse.setData(obj);
return ajaxResponse;
}
}
对于不同的场景,提供了四种构建AjaxResponse 的方法。
使用示例如下
例如:更新操作,Controller无需返回额外的数据
return AjaxResponse.success();
例如:查询接口,Controller需返回结果数据(data可以是任何类型数据)
return AjaxResponse.success(data);
通用全局异常处理逻辑
通用异常处理逻辑
程序员的异常处理逻辑要十分的单一:无论在Controller层、Service层还是什么其他位置,程序员只负责一件事:那就是捕获异常,并将异常转换为自定义异常。使用用户友好的信息去填充CustomException的message,并将CustomException抛出去。
@Service
public class ExceptionService {
public void systemBizError() {
try {
Class.forName("com.mysql.jdbc.xxxx.Driver");
} catch (ClassNotFoundException e) {
throw new CustomException(
CustomExceptionType.SYSTEM_ERROR,
"在XXX业务,myBiz()方法内,出现ClassNotFoundException,请将该信息告知管理员");
}
}
public void userBizError(int input) {
if(input < 0){
throw new CustomException(
CustomExceptionType.USER_INPUT_ERROR,
"您输入的数据不符合业务逻辑,请确认后重新输入!");
}
}
}
全局异常处理器
通过团队内的编码规范的要求,我们已经知道了:不允许程序员截留处理Exception,必须把异常转换为自定义异常CustomException全都抛出去。那么程序员把异常跑出去之后由谁来处理?那就是ControllerAdvice。
ControllerAdvice注解的作用就是监听所有的Controller,一旦Controller抛出CustomException,就会在@ExceptionHandler(CustomException.class)注解的方法里面对该异常进行处理。
处理方法很简单就是使用AjaxResponse.error(e)包装为通用的接口数据结构返回给前端。
@ControllerAdvice
public class WebExceptionHandler {
@ExceptionHandler(CustomException.class)
@ResponseBody
public AjaxResponse customerException(CustomException e) {
if(e.getCode() == CustomExceptionType.SYSTEM_ERROR.getCode()){
}
return AjaxResponse.error(e);
}
@ExceptionHandler(Exception.class)
@ResponseBody
public AjaxResponse exception(Exception e) {
return AjaxResponse.error(new CustomException(
CustomExceptionType.OTHER_ERROR));
}
}
业务状态与HTTP协议状态一致
不知道大家有没有注意到一个问题(看上图)?这个问题就是我们的AjaxResponse的code是400,但是真正的HTTP协议状态码是200? 通说的说,目前
在很多的公司开发RESTful服务时,要求HTTP状态码能够体现业务的最终执行状态,所以说:我们有必要让业务状态与HTTP协议Response状态码一致。
@Component
@ControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
if(selectedContentType.equalsTypeAndSubtype(
MediaType.APPLICATION_JSON)){
response.setStatusCode(
HttpStatus.valueOf(((AjaxResponse) body).getCode())
);
return body;
}
return body;
}
}
进一步优化
我们已经知道了,ResponseBodyAdvice 接口的作用是:在将数据返回给用户之前,做最后一步的处理。将上文的GlobalResponseAdvice 中beforeBodyWrite方法代码优化如下。
所以,我们之前的代码是这样写的,比如:某个controller方法返回值
return AjaxResponse.success(objList);
现在就可以这样写了,因为在GlobalResponseAdvice 里面会统一再封装为AjaxResponse。
return objList;
最终代码如下:
@Component
@ControllerAdvice
public class GlobalResponseAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter methodParameter, Class aClass) {
return true;
}
@Override
public Object beforeBodyWrite(Object body,
MethodParameter methodParameter,
MediaType mediaType,
Class aClass,
ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse) {
if(mediaType.equalsTypeAndSubtype(
MediaType.APPLICATION_JSON)){
if(body instanceof AjaxResponse){
AjaxResponse ajaxResponse = (AjaxResponse)body;
if(ajaxResponse.getCode() != 999){
serverHttpResponse.setStatusCode(HttpStatus.valueOf(
ajaxResponse.getCode()
));
}
return body;
}else{
serverHttpResponse.setStatusCode(HttpStatus.OK);
return AjaxResponse.success(body);
}
}
return body;
}
}
服务端数据校验异常处理逻辑
异常校验的规范及常用注解
在web开发时,对于请求参数,一般上都需要进行参数合法性校验的,原先的写法时一个个字段一个个去判断,这种方式太不通用了,所以java的JSR 303: Bean Validation规范就是解决这个问题的。
JSR 303只是个规范,并没有具体的实现,目前通常都是才有hibernate-validator进行统一参数校验。
JSR303定义的校验类
Hibernate Validator 附加的 constraint
用法:把以上注解加在ArticleVO的属性字段上,然后在参数校验的方法上加@Valid注解 如:
当用户输入参数不符合注解给出的校验规则的时候,会抛出BindException或MethodArgumentNotValidException。
参考
Assert断言与IllegalArgumentException
之前给大家讲通用异常处理的时候,用户输入异常判断是这样处理的。这种方法也是可以用的,但是我们学了这么多的知识,可以优化一下
public void userBizError(int input) {
if(input < 0){
throw new CustomException(
CustomExceptionType.USER_INPUT_ERROR,
"您输入的数据不符合业务逻辑,请确认后重新输入!");
}
}
更好的写法是下面这样的,使用org.springframework.util.Assert断言input >= 0,如果不满足条件就抛出IllegalArgumentException,参数不合法的异常。
public void userBizError(int input) {
Assert.isTrue(input >= 0,"您输入的数据不符合业务逻辑,请确认后重新输入!");
}
org.springframework.util.Assert断言提供了大量的断言方法,针对各种数据类型进行数据合法性校验,使用它我们编写代码更方便。
友好的数据校验异常处理(用户输入异常的全局处理)
我们已知当数据校验失败的时候,会抛出异常BindException或MethodArgumentNotValidException。所以我们对这两种异常做全局处理,防止程序员重复编码带来困扰。
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public AjaxResponse handleBindException(MethodArgumentNotValidException ex) {
FieldError fieldError = ex.getBindingResult().getFieldError();
return AjaxResponse.error(new CustomException(CustomExceptionType.USER_INPUT_ERROR,
fieldError.getDefaultMessage()));
}
@ExceptionHandler(BindException.class)
@ResponseBody
public AjaxResponse handleBindException(BindException ex) {
FieldError fieldError = ex.getBindingResult().getFieldError();
return AjaxResponse.error(new CustomException(CustomExceptionType.USER_INPUT_ERROR,
fieldError.getDefaultMessage()));
}
我们已知使用org.springframework.util.Assert断言,如果不满足条件就抛出IllegalArgumentException。可以使用下面的全局异常处理函数
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public AjaxResponse handleIllegalArgumentException(IllegalArgumentException e) {
return AjaxResponse.error(
new CustomException(CustomExceptionType.USER_INPUT_ERROR,
e.getMessage())
);
}
AOP完美处理页面跳转异常
页面跳转异常处理
之前章节给大家讲的都是JSON接口类的异常处理,那假如我们做页面模板开发时(非前后端分离的应用),Controller发生异常我们该怎么办?应该统一跳转到error.html页面,并且不能影响JSON数据接口的全局统一异常处理。
程序员抛出自定义异常CustomException(职责单一),全局异常处理截获之后返回@ResponseBody AjaxResponse,不是ModelAndView,所以我们无法跳转到error.html页面,那我们该如何做页面跳转error.html方式的全局的异常处理?
以下是我给出答案:
使用这种方法处理页面类异常,程序员只需要涉及到页面跳转的Controller方法上加@ModelView注解即可。 当该方法抛出异常的时候就会自动跳转到error页面。
错误的写法
@GetMapping("/freemarker")
public String index(Model model) {
try{
List<ArticleVO> articles = articleRestService.getAll();
model.addAttribute("articles", articles);
}catch (Exception e){
return "error";
}
return "fremarkertemp";
}
正确的写法
@ModelView
@GetMapping("/freemarker")
public String index(Model model) {
List<ArticleVO> articles = articleRestService.getAll();
model.addAttribute("articles", articles);
return "fremarkertemp";
}
用面向切面的方法处理页面全局异常
因为用到了面向切面编程,所以引入maven依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
ModelView 注解,只起到标注的作用
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface ModelView {
}
以@ModelView注解为切入点,面向切面编程,将所有捕获到的Exception转换为ModelViewException抛出。
@Aspect
@Component
@Slf4j
public class ModelViewAspect {
@Pointcut("@annotation(com.dhy.boot.launch.exception.ModelView)")
public void pointcut() { }
@AfterThrowing(pointcut="pointcut()",throwing="e")
public void afterThrowable(Throwable e) {
throw ModelViewException.transfer(e);
}
}
新定义一个异常类ModelViewException,将捕获到的异常Exception转化为ModelViewException
public class ModelViewException extends RuntimeException{
public static ModelViewException transfer(Throwable cause) {
return new ModelViewException(cause);
}
private ModelViewException(Throwable cause) {
super(cause);
}
}
全局异常处理器处理ModelViewException,将异常页面定位到error.html:
@ExceptionHandler(ModelViewException.class)
public ModelAndView viewExceptionHandler(HttpServletRequest req, ModelViewException e) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("exception", e);
modelAndView.addObject("url", req.getRequestURL());
modelAndView.setViewName("error");
return modelAndView;
}
访问测试
写一个error页面,因为我使用了freemarker模板,所以是errot.ftl(如果没有模板引擎,就error.html就可以)。
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<title>error.html</title>
</head>
<body>
<h1>exception.toString()</h1>
<div>${exception.toString()}</div>
<h1>exception.message</h1>
<div>${exception.message}</div>
<h1>url</h1>
<div>${url}</div>
</body>
</html>
随便找一个页面跳转的controller方法,我访问的是之前开发的 http://localhost:8888/template/freemarker 进行测试,访问之前人为的制造一个异常。重要的是不要忘了加@ModelView注解
访问结果如下,跳转到error.html页面(我的error页面做的比较简陋,大家可以自定义样式):
|