本节介绍如何让前后台优雅地进行数据交互,正常的数据如何统一数据格式,以及异常情况如何统一处理并返回统一格式的数据。
在项目开发过程中经常会涉及服务端、客户端接口数据传输或前后台分离的系统架构下的数据交互问题。如何确保数据完整、清晰易懂是考验开发者的大难题。定义统一的数据返回格式有利于提高开发效率、降低沟通成本,降低调用方的开发成本。目前比较流行的是基于JSON格式的数据交互。但是JSON只是消息的格式,其中的数据内容还需要重新设计和定义。无论是HTTP接口还是RPC接口,保持返回值格式统一很重要。
在项目中,我们会将响应封装成JSON返回,一般会统一所有接口的数据格式,使前端(iOS、Android、Web)对数据的操作一致、轻松。一般情况下,统一返回数据格式没有固定的规范,只要能描述清楚返回的数据状态以及要返回的具体数据即可,但是一般会包含状态码、消息提示语、具体数据这3部分内容。例如,一般的系统要求返回的基本数据格式如下:
{
"code":20000,
"message": "成功",
"data": {
"item": [
{
"id":"1",
"name":"ysxq",
"intro":"备注"
}
]
}
}
通过上面的示例我们知道,定义的返回值包含4要素:响应结果、响应码、消息、返回数据。
定义返回值的基本要素,确保后台执行无论成功还是失败都是返回这些字段,而不会出现其他的字段。定义的返回值包含如下内容:
根据上面的返回数据格式的定义,实际返回的数据模板如下:
{
"code":20000,
"message": "成功",
"data": {
"item": [
{
"id":"1",
"name":"ysxq",
"intro":"备注"
}
]
}
}
其中,data字段为泛型字段,根据实际的业务返回前端需要的数据类型。
返回的数据中有一个非常重要的字段:状态码。状态码字段能够让服务端、客户端清楚知道操作的结果、业务是否处理成功,如果失败,失败的原因等信息。所以,定义清晰易懂的状态码非常重要。状态码定义如表所示。
以上定义的是通用状态码,其他的业务相关状态码需要根据实际业务定义。
前面定义了返回数据的格式和处理结果的状态码,接下来定义通用的结果处理类。在实际使用时可以根据情况处理。本示例中简单定义如下:
/**
* @Title: JSONResult.java
* @Package com.example.demo
* @Description: 自定义响应数据结构
200:表示成功
500:表示错误,错误信息在msg字段中
501: bean验证错误,无论多少个错误都以map形式返回
502:拦截器拦截到用户token出错
555:异常抛出信息
*/
public class JSONResult {
//定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();
//响应业务状态
private Integer status;
//响应消息
private String msg;
//响应中的数据
private Object data;
public static JSONResult build(Integer status, String msg, Object data) {
return new JSONResult(status, msg, data);
}
public static JSONResult ok(Object data) {
return new JSONResult(data);
}
public static JSONResult ok() {
return new JSONResult(null);
}
public static JSONResult errorMsg(String msg) {
return new JSONResult(500, msg, null);
}
public static JSONResult errorMap(Object data) {
return new JSONResult(501, "error", data);
}
public static JSONResult errorTokenMsg(String msg) {
return new JSONResult(502, msg, null);
}
public static JSONResult errorException(String msg) {
return new JSONResult(555, msg, null);
}
public JSONResult() {
}
public JSONResult (Integer status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}
public JSONResult(Object data) {
this.status = 200;
this.msg = "OK";
this.data = data;
}
public Boolean isOK() {
return this.status == 200;
}
/**
* @Description: 将json结果集转化为JSONResult对象,需要转换的对象是一个类
* @param jsonData
* @param clazz
* @return
*/
public static JSONResult formatToPojo(String jsonData, Class<?> clazz) {
try {
if (clazz == null) {
return MAPPER.readValue(jsonData, JSONResult.class);
}
JsonNode jsonNode = MAPPER.readTree(jsonData);
JsonNode data = jsonNode.get("data");
Object obj = null;
if (clazz != null) {
if (data.isObject()) {
obj = MAPPER.readValue(data.traverse(), clazz);
}else if (data.isTextual()) {
obj = MAPPER.readValue(data.asText(), clazz);
}
}
return build(jsonNode.get("status").intValue(), jsonNode.get("msg").asText(), obj);
} catch (Exception e) {
return null;
}
}
/**
*
* @Description: 没有object对象的转换
* @param json
* @return
*/
public static JSONResult format(String json) {
try {
return MAPPER.readValue(json, JSONResult.class);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
*
* @Description: Object是集合转换,需要转换的对象是一个list
* @param jsonData
* @param clazz
* @return
*/
public static JSONResult formatToList(String jsonData, Class<?> clazz) {
try {
JsonNode jsonNode = MAPPER.readTree(jsonData);
JsonNode data = jsonNode.get("data");
Object obj = null;
if (data.isArray() && data.size() > 0) {
obj = MAPPER.readValue(data.traverse(), MAPPER.getTypeFactory().constructCollectionType(List.class, clazz));
}
return build(jsonNode.get("status").intValue(), jsonNode.get("msg").asText(), obj);
} catch (Exception e) {
return null;
}
}
public String getOK() {
return ok;
}
public void setOK(String ok) {
this.ok = ok;
}
}
上面定义了数据返回处理类,定义了响应数据结构,所有接口的数据返回统一通过此类处理,接收此类数据后,需要使用本类的方法转换成对应的数据类型格式(类或者list)。
定义数据处理类后,在控制器中将返回的数据统一加上数据处理。调用如下:
@RequestMapping("/getUser")
public JSONResult getUserJson() {
User u = new User();
u.setName( "ysxq");
u.setAge(20);
u.setBirthday(new Date());
u.setPassword("ysxq123456");
return JSONResult.ok(u);
}
启动helloworld项目,浏览器中访问http://localhost:8080/user/getUser
,页面数据返回如下:
{
"code":200,
"message": "OK",
"data": {
"name":"ysxq",
"age": 20,
"birthday": "2000-12-18 18:17:15"
}
}
返回的结果数据在正常的时候能够按照我们的预期结果格式返回。
在项目开发的过程中肯定会碰到异常的情况,出现异常情况时如何处理,如何确保出现异常时程序也能正确地返回数据?总不能所有的方法都加上try catch吧?接下来介绍Spring Boot如何进行全局异常处理,捕获异常后如何按照统一格式返回数据。
在介绍之前,我们需要先了解Spring中常见的异常处理方式有哪些。一般Spring Boot框架的异常处理有多种方式,从范围来说,包括全局异常捕获处理方式和局部异常捕获处理方式。下面介绍3种比较常用的异常处理解决方案。
(1)使用@ExceptionHandler处理局部异常
在控制器中通过加入@ExceptionHandler注解的方法来实现异常的处理。这种方式非常容易实现,但是只能处理使用@ExceptionHandler注解方法的控制器异常,而无法处理其他控制器的异常,所以不推荐使用。
(2)配置SimpleMappingExceptionResolver类来处理异常
通过配置SimpleMappingExceptionResolver类实现全局异常的处理,但是这种方式不能针对特定的异常进行特殊处理,所有的异常都按照统一的方式处理。
(3)使用ControllerAdvice注解处理全局异常
使用@ControllerAdvice、@ExceptionHandler注解实现全局异常处理,@ControllerAdvice定义全局异常处理类,@ExceptionHandler指定自定义错误处理方法拦截的异常类型。实现全局异常捕获,并针对特定的异常进行特殊处理。
以上三种解决方案,都能实现全局异常处理。但是,推荐使用@ControllerAdvice注解方式处理全局异常,这样可以针对不同的异常分开处理。
下面通过示例演示@ControllerAdvice注解实现全局统一异常处理。定义一个自定义的异常处理类GlobalExceptionHandler,具体示例代码如下:
@ControllerAdvice
public class GlobalExceptionHandler {
public static final String ERROR_VIEW = "error";
Logger logger = LoggerFactory.getLogger(getClass());
@ExceptionHandler(value = {Exception.class})
public Object errorHandler(HttpServletRequest request, HttpServletResponse response, Exception e) throws Exception {
//e.printStackTrace();
//记录日志
logger.error(ExceptionUtils.getMessage(e));
//是否Ajax请求
if (isAjax(request)) {
return JSONResult.errorException(e.getMessage());
} else {
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", request.getRequestURL());
mav.setViewName(ERROR_VIEW);
return mav;
}
}
/**
* @Title: GlobalExceptionHandler.java
* @Package com.example.demo
* @Description: 判断是否是Aiax请求
*/
public static boolean isAjax(HttpServletRequest httprequest) {
return (httprequest.getHeader("X-Requested-With") !=null && "XMLHttpRequest".equals(httprequest.getHeader("X-Requested-With")));
}
}
上面的示例,处理全部Exception的异常,如果需要处理其他异常,例如NullPointerException异常,则只需要在GlobalException类中使用@ExceptionHandler(value = {NullPointerException.class})注解重新定义一个异常处理的方法即可。
启动项目,在浏览器中输入http://localhost:8088/err/error,结果如图所示。
处理异常之后页面自动调整到统一的错误页面,如果是Ajax请求出错,则会按照定义的JSON数据格式统一返回数据。