实战:实现优雅的数据返回
本节介绍如何让前后台优雅地进行数据交互,正常的数据如何统一数据格式,以及异常情况如何统一处理并返回统一格式的数据。
1.为什么要统一返回值
在项目开发过程中经常会涉及服务端、客户端接口数据传输或前后台分离的系统架构下的数据交互问题。如何确保数据完整、清晰易懂是考验开发者的大难题。定义统一的数据返回格式有利于提高开发效率、降低沟通成本,降低调用方的开发成本。目前比较流行的是基于JSON格式的数据交互。但是JSON只是消息的格式,其中的数据内容还需要重新设计和定义。无论是HTTP接口还是RPC接口,保持返回值格式统一很重要。
在项目中,我们会将响应封装成JSON返回,一般会统一所有接口的数据格式,使前端(iOS、Android、Web)对数据的操作一致、轻松。一般情况下,统一返回数据格式没有固定的规范,只要能描述清楚返回的数据状态以及要返回的具体数据即可,但是一般会包含状态码、消息提示语、具体数据这3部分内容。例如,一般的系统要求返回的基本数据格式如下:
{"code":20000,"message": "成功","data": {"item": [{"id":"1","name":"ysxq","intro":"备注"}]}}
通过上面的示例我们知道,定义的返回值包含4要素:响应结果、响应码、消息、返回数据。
2.统一数据返回
2.1 定义数据格式
定义返回值的基本要素,确保后台执行无论成功还是失败都是返回这些字段,而不会出现其他的字段。定义的返回值包含如下内容:
- Integer code:成功时返回0,失败时返回具体错误码。
- String message:成功时返回null,失败时返回具体错误消息。
- T data:成功时返回具体值,失败时为null。
根据上面的返回数据格式的定义,实际返回的数据模板如下:
{"code":20000,"message": "成功","data": {"item": [{"id":"1","name":"ysxq","intro":"备注"}]}}
其中,data字段为泛型字段,根据实际的业务返回前端需要的数据类型。
2.2 定义状态码
返回的数据中有一个非常重要的字段:状态码。状态码字段能够让服务端、客户端清楚知道操作的结果、业务是否处理成功,如果失败,失败的原因等信息。所以,定义清晰易懂的状态码非常重要。状态码定义如表所示。
以上定义的是通用状态码,其他的业务相关状态码需要根据实际业务定义。
2.3 定义数据处理类
前面定义了返回数据的格式和处理结果的状态码,接下来定义通用的结果处理类。在实际使用时可以根据情况处理。本示例中简单定义如下:
/*** @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)。
2.4 处理数据返回
定义数据处理类后,在控制器中将返回的数据统一加上数据处理。调用如下:
@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);}
2.5 测试
启动helloworld项目,浏览器中访问http://localhost:8080/user/getUser
,页面数据返回如下:
{"code":200,"message": "OK","data": {"name":"ysxq","age": 20,"birthday": "2000-12-18 18:17:15"}}
返回的结果数据在正常的时候能够按照我们的预期结果格式返回。
3.全局异常处理
在项目开发的过程中肯定会碰到异常的情况,出现异常情况时如何处理,如何确保出现异常时程序也能正确地返回数据?总不能所有的方法都加上try catch吧?接下来介绍Spring Boot如何进行全局异常处理,捕获异常后如何按照统一格式返回数据。
3.1 全局异常处理的实现方式
在介绍之前,我们需要先了解Spring中常见的异常处理方式有哪些。一般Spring Boot框架的异常处理有多种方式,从范围来说,包括全局异常捕获处理方式和局部异常捕获处理方式。下面介绍3种比较常用的异常处理解决方案。
(1)使用@ExceptionHandler处理局部异常
在控制器中通过加入@ExceptionHandler注解的方法来实现异常的处理。这种方式非常容易实现,但是只能处理使用@ExceptionHandler注解方法的控制器异常,而无法处理其他控制器的异常,所以不推荐使用。
(2)配置SimpleMappingExceptionResolver类来处理异常
通过配置SimpleMappingExceptionResolver类实现全局异常的处理,但是这种方式不能针对特定的异常进行特殊处理,所有的异常都按照统一的方式处理。
(3)使用ControllerAdvice注解处理全局异常
使用@ControllerAdvice、@ExceptionHandler注解实现全局异常处理,@ControllerAdvice定义全局异常处理类,@ExceptionHandler指定自定义错误处理方法拦截的异常类型。实现全局异常捕获,并针对特定的异常进行特殊处理。
以上三种解决方案,都能实现全局异常处理。但是,推荐使用@ControllerAdvice注解方式处理全局异常,这样可以针对不同的异常分开处理。
3.2 使用@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数据格式统一返回数据。