前言 基于我的上一篇博客:Java 异常处理 中的观点,我编写了一个简单的 Web 案例,包括自定义异常类,在合适的层面处理,以及遇到的由于代码 Bug 抛出运行时异常的情形。示例代码已经上传至 GitHub ,包含基于 Spring Boot 的 Java 后端代码以及基于 Vue.js 的前端代码两个部分。
我们的演示案例有两个功能,获取用户列表以及注册新用户,界面如下所示:
后端实现 先看一下我们领域模型 User,为简化代码未使用数据库,而是模拟数据库的自增主键自动生成 userId。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Data public  class  User      private  static  int  userIdGenerateKey = 1 ;     private  Integer userId;     private  String username;     private  String phone;     private  Integer age;     public  User ()       }     User(String username, String phone, Integer age) {         this .userId = User.userIdGenerateKey++;         this .username = username;         this .phone = phone;         this .age = age;     }     User(User userWithoutId) {         this (userWithoutId.username, userWithoutId.phone, userWithoutId.age);     } } 
在资源库UserRepository一层使用List直接在内存中存储用户,并初始化了两条用户信息。对外提供users()返回所有用户以及addUser()添加一个新用户两个接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Repository public  class  UserRepository      private  List<User> users = new  ArrayList<>();     public  UserRepository ()           users.add(new  User("Jack" , "13500000000" , 18 ));         users.add(new  User("Tom" , "13500000001" , 19 ));     }     public  List<User> users ()           return  users;     }     public  void  addUser (User user)           users.add(user);     } } 
考虑到在业务功能为获取用户列表和注册新用户,而注册功能还可能包括发送验证邮件等之类的功能,所以有必要将其提取至 Service 层提供一个register()接口。目前只是单纯的调用资源库的addUser()添加一个新的用户。
1 2 3 4 5 6 7 8 9 10 @Service public  class  UserService      @Resource      private  UserRepository userRepository;     public  void  register (User user)           userRepository.addUser(new  User(user));     } } 
Controller 层代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @RestController @RequestMapping("/users") public  class  UserController      @Resource      private  UserRepository userRepository;     @Resource      private  UserService userService;     @GetMapping      public  List<User> users ()           return  userRepository.users();     }     @PostMapping      public  void  register (@RequestBody  User user)           userService.register(user);     } } 
添加业务规则进行校验 上述案例还是未抛出任何异常的情况,实际业务必然会对用户输入做一定约束。假设注册新用户有如下业务规则:1. 用户名是必填项,不能为空;2. 年龄和电话号码为可选项,如果填入年龄,必须是大于零的合法数字。
那么根据 DDD 的思想以及信息专家模式,检查用户名和年龄是否合法应该属于领域对象 User。如下所示,这里先埋个坑,也是我未思考全面所留下的隐患,一会儿会提及。
1 2 3 4 5 6 7 8 9 public  boolean  checkAgeIsLegal ()      return  this .age >= 1 ; } public  boolean  checkUsernameIsNotEmpty ()      return  username != null  && !"" .equals(username.trim()); } 
抛出异常 如果客户输入的值不符合要求,我们应当抛出一个异常提醒用户及时修正。自定义RequestArgsIllegalException异常类,意为请求参数非法,重写getMessage()方法。异常类的命名要清晰准确反应问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public  class  RequestArgsIllegalException  extends  Exception      public  RequestArgsIllegalException (String message)           super (message);     }     public  RequestArgsIllegalException (String message, Throwable cause)           super (message, cause);     }     @Override      public  String getMessage ()           return  "Illegal request arguments: "  + super .getMessage();     } } 
在服务层的register()接口,我们使用卫语句 检查参数,如果非法则抛出包含了异常信息的异常。
1 2 3 4 5 6 7 8 9 10 11 12 public  void  register (User user)  throws  RequestArgsIllegalException     if  (!user.checkAgeIsLegal()) {         throw  new  RequestArgsIllegalException("age cannot be less than zero" );     }     if  (!user.checkUsernameIsNotEmpty()) {         throw  new  RequestArgsIllegalException("username can not be empty" );     }     userRepository.addUser(new  User(user)); } 
在 Controller 层捕获异常并将异常信息返回给前端:
1 2 3 4 5 6 7 8 9 10 11 @PostMapping public  void  register (@RequestBody  User user, HttpServletResponse response)  throws  IOException     try  {         userService.register(user);     } catch  (RequestArgsIllegalException e) {         logger.warn("{}. {}" , e.getMessage(), user);         response.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());     } } 
接口调试 使用 Postman 测试后端register()接口,请求体如下,其中年龄违反了业务规则。
1 2 3 4 5 {     "username" : "Halen" ,     "age" : -1 ,     "phone" : "13500000002"  } 
通过 Postman 返回的错误信息,状态吗status为 400,错误信息message为 “Illegal request arguments: age cannot be less than zero” 都已经被包装在返回的消息体中。同理,用户名为空也是一样就不贴出来了。
1 2 3 4 5 6 7 {     "timestamp" : 1532055599002 ,     "status" : 400 ,     "error" : "Bad Request" ,     "message" : "Illegal request arguments: age cannot be less than zero" ,     "path" : "/users"  } 
这里又要提到我之前说到的那个坑了,也是我在使用 Postman 测试接口时发现的 Bug。根据业务规则允许用户不填入年龄和电话号码。
1 2 3 4 5 6 7 8 9 10 11 12 13 {     "username" : "Halen"  } {     "timestamp" : 1532056232417 ,     "status" : 500 ,     "error" : "Internal Server Error" ,     "message" : "No message available" ,     "path" : "/users"  } 
非常可怕的 500 服务器内部错误出现了!根据响应消息完全无法定位错误出处。一般来说,这种情况是后端代码出了 Bug,所幸我们还能查看控制台输出或者日志。果不其然,一个空指针异常被抛出:
1 2 java.lang.NullPointerException: null     at info.s1mple.exceptiondemo.domian.model.user.User.checkAgeIsLegal(User.java:30) 
原来是checkAgeIsLegal()方法出了问题,赶紧去及时修复 Bug,根据业务规则,添加this.age == null认为年龄为空合法。
1 2 3 public  boolean  checkAgeIsLegal ()      return  this .age == null  || this .age >= 1 ; } 
前端实现 前端实现就很简单了,捕获到错误直接 alert 出来。前端使用 axios 发送异步请求,请求后端的注册用户接口,this.user是绑定了用户输入表单值的 User 对象。如果参数符合要求提示成功,如果参数非法则会走入catch分支将错误原因提醒给用户。注意到 Error 对象是做了封装的,我们要获取可以在err.response.data中获取。
1 2 3 4 5 6 7 8 9 10 register (    axios.post('http://localhost:8080/users' , this .user)       .then(() =>  {         alert('Success!' )         this .loadUsers()       })       .catch((err ) =>  {         alert(err.response.data.message)       })   } 
前端捕获了异常将异常出错信息弹框显示:
总结 由此我们整个的前后端异常处理的过程就贯通了,在这个案例中我使用的是受控异常(Checked Exception)并且自定义了异常类。可以看到,相较于运行时异常(RuntimeException),它的好处在于能够很好的将异常分门别类,并且捕获异常返回相应的异常码给前端,也方便前端的报错呈现。最后我们总结一下:对于由用户操作引起的异常,在我们的控制范围之内,应当使用合适的受控异常对其进行捕获并反馈给前端 。