当前位置: 首页 > 科技观察

SpringBoot是如何统一后端返回格式的?老鸟就是这么干的!

时间:2023-03-17 21:48:54 科技观察

大家好,我是飘渺。今天我们就来说说在SpringBoot基于前后端分离的开发模式下,如何友好的返回一个统一的标准格式,如何优雅的处理全局异常。首先我们来看一下为什么要返回一个统一的标准格式?为什么要给SpringBoot返回一个统一的标准格式?默认情况下,SpringBoot常见的返回格式有三种:第一种:returnString@GetMapping("/hello")publicStringgetStr(){return"hello,javadaily";}此时调用接口获取的返回值如下:hello,javadaily第二种:返回自定义对象@GetMapping("/aniaml")publicAniamlgetAniaml(){Aniamlaniaml=newAniaml(1,"pig");returnaniaml;}这时候通过获取的返回值调用接口如下:{"id":1,"name":"pig"}第三种:接口异常@GetMapping("/error")publicintererror(){inti=9/0;returni;}此时调用接口得到的返回值如下:{"timestamp":"2021-07-08T08:05:15.423+00:00","status":500,"error":"InternalServerError","path":"/wrong"}基于以上情况,如果你和前端开发人员共同调试界面,他们会很迷茫,因为我们没有给他一个统一的格式,前端人员不知道如何处理返回值UE。更有什者,有些同学,比如小张,喜欢把成绩包装起来。他用的是Result对象,小王也喜欢对结果进行封装,但是他用的是Response对象。遇到这种情况,相信前端人员一定是疯了。因此,我们需要在我们的项目中定义一个统一的标准返回格式。定义返回标准格式一个标准的返回格式至少包括3部分:status状态值:后台统一定义各种返回结果的状态码messagedescription:本次接口调用的结果描述datadata:本次返回的数据.{"status":"100","message":"操作成功","data":"hello,javadaily"}当然也可以根据需要添加其他扩展值,比如我们添加的接口返回对象中的调用时间4.timestamp:接口调用时间定义返回对象@DatapublicclassResultData{/**结果状态,见ResultData.java*/privateintstatus;privateStringmessage;privateTdata;privatelongtimestamp;publicResultData(){this.timestamp=System.currentTimeMillis获取特定状态码();}publicstaticResultDatasuccess(Tdata){ResultDataresultData=newResultData<>();resultData.setStatus(ReturnCode.RC100.getCode());resultData.setMessage(ReturnCode.RC100.getMessage());resultData.setData(data);returnresultData;}publicstaticResultDatafail(intcode,Stringmessage){ResultDataresultData=newResultData<>();resultData.setStatus(code);resultData.setMessage(message);returnresultData;}}定义状态码publicenumReturnCode{/**操作成功**/RC100(100,"操作成功ded"),/**操作失败**/RC999(999,"操作失败"),/**服务限流**/RC200(200,"服务已开启限流保护,请稍后再试!"),/**服务降级**/RC201(201,"服务已开启降级保护,请稍后再试!"),/**热点参数currentlimiting**/RC202(202,"热点参数限流,请稍后再试!"),/**系统规则不符合要求**/RC203(203,"系统规则不符合要求requirements,pleasetryagainlater!"),/**授权规则失败**/RC204(204,"授权规则失败,请稍后重试!"),/**access_denied**/RC403(403,"无法访问权限,请联系管理管理员授予的权限"),/**access_denied**/RC401(401,"匿名用户未经许可访问资源时异常"),/**服务异常**/RC500(500,"系统异常,请稍后再试Try"),INVALID_TOKEN(2001,"访问令牌无效"),ACCESS_DENIED(2003,"您无权访问此资源"),CLIENT_AUTHENTICATION_FAILED(1001,"客户端认证失败"),USERNAME_OR_PASSWORD_ERROR(1002,"用户名或密码错误"),UNSUPPORTED_GRANT_TYPE(1003,"不支持的认证方式");/**自定义状态码**/privatefinalintcode;/**自定义说明**/privatefinalStringmessage;ReturnCode(intcode,Stringmessage){this.code=code;this.message=message;}publicintgetCode(){returncode;}publicStringgetMessage(){returnmessage;}}统一返回格式@GetMapping("/hello")publicResultDatagetStr(){returnResultData.success("你好,javadaily");}此时调用接口得到的返回值如下:{"status":100,"message":"hello,javadaily","data":null,"timestamp":1625736481648,"httpStatus":0}这样确实达到了我们想要的结果,在很多项目中看到过这种写法,在Controller层,通过ResultData.success()将返回的结果打包返回给前端,看这里我们不妨停下来想一想,这样做的缺点是什么?最大的缺点就是我们每次写接口都需要调用ResultData.success()这行代码来打包结果,重复劳动和浪费精力;而且很容易被其他人利用老鸟笑。所以我们需要优化代码代码,我们只需要使用SpringBoot提供的ResponseBodyAdvice即可。vice功能:拦截Controller方法的返回值,对返回值/响应体进行统一处理,一般用于统一返回格式、加解密、签名等。我们先看ResponseBodyAdvice的源码:publicinterfaceResponseBodyAdvice{/***advice功能是否支持*true支持,false不支持*/booleansupports(MethodParametervar1,Class>var2);/***对返回的数据进行处理*/@NullableTbeforeBodyWrite(@NullableTvar1,MethodParametervar2,MediaTypevar3,Class>var4,ServerHttpRequestvar5,ServerHttpResponsevar6);}我们只需要写一个具体的实现类即可/***@authorjam*@date2021/7/810:10上午*/@RestControllerAdvicepublicclassResponseAdviceimplementsResponseBodyAdvice{@AutowiredprivateObjectMapperobjectMapper;@Overridepublicbooleansupports(MethodParametermethodParameter,Class>aClass){returntrue;}@SneakyThrows@OverridepublicObjectbeforeBodyWrite(Objecto,MethodParametermethodParameter,MediaTypemediaType,Class>aClass,ServerHttpRequestserverHttpRequest,ServerHttpResponseserverHttpResponse){if(oinstanceofString){returnobjectMapper.writeValueAsString(ResultData.success(o));}returnResultData.success(o);}}有两点需要注意:@RestControllerAdvice注解@RestControllerAdvice是@RestController注解的增强,可以实现三个功能:全局异常处理全局数据绑定全局数据预处理String类型判断if(oinstanceofString){returnobjectMapper.writeValueAsString(ResultData.success(o));}这段代码必须加上。如果Controller直接返回String,SpringBoot是直接Return,所以需要我们手动转成json。经过上面的处理,我们不再需要通过ResultData.success()进行转换,直接返回原始数据格式。SpringBoot会自动帮我们实现封装类的封装。@GetMapping("/hello")publicStringgetStr(){return"hello,javadaily";}此时我们调用接口返回的数据为:@GetMapping("/hello")publicStringgetStr(){return"hello,javadaily";}感觉完美吗?别着急,还有一个问题等着你。这个时候接口异常问题有问题。由于我们没有处理Controller的异常,一旦我们调用的方法出现异常,就会出现问题。比如下面的接口@GetMapping("/wrong")publicintererror(){inti=9/0;returni;}返回的结果是:这显然不是我们想要的结果。接口报错并返回运行成功的响应码。前端看了会打人的。别着急,我们进入第二个话题,如何优雅地处理全局异常。为什么SpringBoot需要一个全局的异常处理器而不是手写try...catch?如前所述,默认情况下,SpringBoot在发生异常时返回的结果是这样的:{"timestamp":"2021-07-08T08:05:15.423+00:00","status":500,"error":"InternalServerError","path":"/wrong"}这种数据格式返回给前端,前端无法理解,所以这时候我们一般使用try...catch来处理异常@GetMapping("/wrong")publicintererror(){inti;try{i=9/0;}catch(Exceptione){log.error("error:{}",e);i=0;}returni;}我们追求的目标肯定是不用手动写try...catch,而是想通过全局异常处理器来处理。对于自定义异常,只能通过全局异常处理函数@GetMapping("error1")publicvoidempty(){thrownewRuntimeException("Customexception");}当我们引入Validator参数校验器时,参数校验是不会传递的抛出异常,此时try...catch无法捕获,只能使用全局异常处理器。SpringBoot集成参数校验可参考这篇文章SpringBoot开发秘籍-集成参数校验及进阶技巧如何实现全局异常处理@Slf4j@RestControllerAdvicepublicclassRestExceptionHandler{/***默认全局异常处理。*@paramethee*@returnResultData*/@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)publicResultDataexception(Exceptione){log.error("全局异常信息ex={}",e.getMessage(),e);returnResultData.fail(ReturnCode.RC500.getCode(),e.getMessage());}}需要说明三个细节:@RestControllerAdvice,RestController的增强类,可以用来实现全局异常处理器@ExceptionHandler,统一处理某类异常,从而减少代码重复和复杂度。比如获取自定义异常,@ExceptionHandler(BusinessException.class)@ResponseStatus指定客户端收到的http状态码,体验效果。这时我们调用如下接口:@GetMapping("error1")publicvoidempty(){thrownewRuntimeException("CustomException");}返回结果如下:{"status":500,"message":"自定义Exception","data":null,"timestamp":1625795902556}基本满足我们的需求。但是当我们同时启用统一标准格式封装函数ResponseAdvice和RestExceptionHandler全局异常处理器时,新的问题又出现了:{"status":100,"message":"operationsuccessful","data":{"status":500,"message":"customexception","data":null,"timestamp":1625796167986},"timestamp":1625796168008}此时返回的结果是这样的,统一格式增强函数会给再次封装返回异常结果,接下来需要修复。全局异常访问返回的标准格式应该让全局异常访问的标准格式变得非常简单,因为全局异常处理器已经为我们封装好了标准格式,我们只需要直接返回给客户端即可。@SneakyThrows@OverridepublicObjectbeforeBodyWrite(Objecto,MethodParametermethodParameter,MediaTypemediaType,Class>aClass,ServerHttpRequestserverHttpRequest,ServerHttpResponseserverHttpResponse){if(oinstanceofString){returnobjectMapper.writeValueAsResultofString(数据)};成功;}returnResultData.success(o);}关键代码:if(oinstanceofResultData){returno;}如果返回结果是ResultData对象,直接返回即可。这时候我们再次调用上面的error方法,返回的结果符合我们的要求。{"status":500,"message":"Customexception","data":null,"timestamp":1625796580778}好了,今天的文章就到这里,希望大家能够掌握如何在您的项目返回并优雅地处理全局异常。github地址:https://github.com/jianzh5/cloud-blog/