使用 Spring Boot 开发微服务的过程中,我们会使用别人提供的接口,也会设计接口给别人使用,这时候微服务应用之间的协作就需要有一定的规范。
- 基于 rpc 协议,我们一般有两种思路:(1)提供服务的应用统一将异常包起来,然后用错误码交互;(2)提供服务的应用将运行时异常抛出,抛出自定义的业务异常,服务的调用者通过异常 catch 来处理异常情况。
- 基于 HTTP 协议,那么最流行的就是 RESTful 协议,服务提供方会自己处理所有异常,并且返回的结果中会跟 HTTP 的状态码相结合,这篇文章我们就用一个例子来说明 RESTful 接口的错误处理如何做。
首先我们需要新建一个简单的 Controller,代码如下:
@RestController
class GreetingController {@RequestMapping("/greet")
String sayHello(@RequestParam("name") String name) {if (name == null || name.isEmpty()) {throw new IllegalArgumentException("The'name'parameter must not be null or empty");
}
return String.format("Hello %s!", name);
}
}
通过 http 请求客户端——httpie 发送 HTTP 请求,这个工具比 curl 的好处是:返回值信息有语法高亮、对返回的 JSON 字符串自动格式化。启动服务器,使用命令 http http://127.0.0.1:8080/greet?name=duqi
发起请求,结果如下:
HTTP/1.1 200 OK
Content-Length: 11
Content-Type: text/plain;charset=UTF-8
Date: Sat, 05 Dec 2015 05:45:03 GMT
Server: Apache-Coyote/1.1
X-Application-Context: application
现在我们制造一个错误的请求,@RequestParam 是获取 URL 中的参数,如果这个参数不提供则会出错。因此,我们发送一个命令http http://127.0.0.1:8080
,看结果如何。
HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Sat, 05 Dec 2015 05:54:06 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: application
{
"error": "Bad Request",
"exception": "org.springframework.web.bind.MissingServletRequestParameterException",
"message": "Required String parameter'name'is not present",
"path": "/greet",
"status": 400,
"timestamp": 1449294846060
}
可以看到,由于没有提供 name 参数,服务器返回的状态码是 400:错误的请求。在响应体中的内容依次如下:
- error : 错误信息;
- exception:异常的类型,MissingServletRequestParameterExeption,见名知意,说明是缺少了某个请求参数;
- message:对异常的说明
- path:显示请求的 URL 路径;
- status:表示返回的错误码
- timestamp:错误发生的时间戳,调用 System.currentMills()
如果我们给定 name 参数,却不给它赋值,又会如何?发送命令http http:127.0.0.1:8080/greet?name
,则服务器的返回值如下:
HTTP/1.1 500 Internal Server Error
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Sat, 05 Dec 2015 06:01:24 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: application
{
"error": "Internal Server Error",
"exception": "java.lang.IllegalArgumentException",
"message": "The'name'parameter must not be null or empty",
"path": "/greet",
"status": 500,
"timestamp": 1449295284160
}
对比上面,可以看出,这次返回的错误码是 500,表示服务器内部错误;返回的异常类型是 java.lang.IllegalArgumentException,表示参数不合法。服务器内部错误表示服务器抛出了异常缺没有处理,我们更愿意 API 返回 400,告诉调用者自己哪里做错了。如何实现呢?利用 @ExceptionHandler 注解即可。
在 GreetingController 控制器中加入如下处理函数,用于捕获这个控制器的异常。
@ExceptionHandler
void handleIllegalArgumentException(IllegalArgumentException e,
HttpServletResponse response) throws IOException {response.sendError(HttpStatus.BAD_REQUEST.value());
}
再次发送命令http http:127.0.0.1:8080/greet?name
, 则返回下面的结果:
HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Sat, 05 Dec 2015 06:08:50 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: application
{
"error": "Bad Request",
"exception": "java.lang.IllegalArgumentException",
"message": "The'name'parameter must not be null or empty",
"path": "/greet",
"status": 400,
"timestamp": 1449295729978
}
说明我们在服务器端捕获了 IllegalArgumentException 这个异常,并设置 response 的返回码为 400。如果你想对多个异常都进行一样的处理,则上述异常处理代码可以修改为下面这样(给 @ExceptionHandler 传入参数):
@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class})
void handleIllegalArgumentException(HttpServletResponse response) throws IOException {response.sendError(HttpStatus.BAD_REQUEST.value());
}
现在这个异常处理代码是加在当前的这个控制器中,因此它只处理属于这个控制器的响应,如果我们新建一个类,并用注解 @ControllerAdvice 修饰,并在这个类中定义上述的异常处理代码,则它会负责处理所有的请求。
Spring Boot 1.2.0 以后,还支持在 response 修改对应的 message,只要将对应的 message 信息传入 sendError 函数即可,例如:
@ExceptionHandler({IllegalArgumentException.class, NullPointerException.class})
void handleIllegalArgumentException(HttpServletResponse response) throws IOException {response.sendError(HttpStatus.BAD_REQUEST.value(),
"Please try again and with a non empty string as'name'");
}
再次执行同样的命令,会收到下列反馈:
HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Sat, 05 Dec 2015 06:21:05 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked
X-Application-Context: application
{
"error": "Bad Request",
"exception": "java.lang.IllegalArgumentException",
"message": "Please try again and with a non empty string as'name'","path":"/greet","status": 400,"timestamp": 1449296465060
}
如果希望验证请求的参数,可以使用 JSR-303 Bean Validation API,并参考 Spring Validation。在 spring.io 上还有一个验证表单输入的例子 Validating Form Input。
参考资料
- 模拟 GET/POST 请求的工具
- Spring Boot Error Response
Spring Boot 1.x 系列
- Spring Boot 的自动配置、Command-line-Runner
- 了解 Spring Boot 的自动配置
- Spring Boot 的 @PropertySource 注解在整合 Redis 中的使用
- Spring Boot 项目中如何定制 HTTP 消息转换器
- Spring Boot 整合 Mongodb 提供 Restful 接口
- Spring 中 bean 的 scope
- Spring Boot 项目中使用事件派发器模式
本号专注于后端技术、JVM 问题排查和优化、Java 面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。