首页>>后端>>SpringBoot->SpringBoot实战:一招实现结果的优雅响应

SpringBoot实战:一招实现结果的优雅响应

时间:2023-11-30 本站 点击:1

今天说一下 Spring Boot 如何实现优雅的数据响应:统一的结果响应格式、简单的数据封装。

前提

无论系统规模大小,大部分 Spring Boot 项目是提供 Restful + json 接口,供前端或其他服务调用,格式统一规范,是程序猿彼此善待彼此的象征,也是减少联调挨骂的基本保障。

通常响应结果中需要包含业务状态码、响应描述、响应时间戳、响应内容,比如:

{"code":200,"desc":"查询成功","timestamp":"2020-08-1214:37:11","data":{"uid":"1597242780874","name":"测试1"}}

对于业务状态码分为两个派系:一个是推荐使用 HTTP 响应码作为接口业务返回;另一种是 HTTP 响应码全部返回 200,在响应体中通过单独的字段表示响应状态。两种方式各有优劣,个人推荐使用第二种,因为很多 Web 服务器对 HTTP 状态码有拦截处理功能,而且状态码数量有限,不够灵活。比如返回 200 表示接口处理成功且正常响应,现在需要有一个状态码表示接口处理成功且正常响应,但是请求数据状态不对,可以返回 2001 表示。

自定义响应体

定义一个数据响应体是返回统一响应格式的第一步,无论接口正常返回,还是发生异常,返回给调用方的结构格式都应该不变。给出一个示例:

@ApiModel@DatapublicclassResponse<T>{@ApiModelProperty(value="返回码",example="200")privateIntegercode;@ApiModelProperty(value="返回码描述",example="ok")privateStringdesc;@ApiModelProperty(value="响应时间戳",example="2020-08-1214:37:11")privateDatetimestamp=newDate();@ApiModelProperty(value="返回结果")privateTdata;}

这样,只要在 Controller 的方法返回Response就可以了,接口响应就一致了,但是这样会形成很多格式固定的代码模板,比如下面这种写法:

@RequestMapping("hello1")publicResponse<String>hello1(){finalResponse<String>response=newResponse<>();response.setCode(200);response.setDesc("返回成功");response.setData("Hello,World!");returnresponse;}

调用接口响应结果为:

{"code":200,"desc":"返回成功","timestamp":"2020-08-1214:37:11","data":"Hello,World!"}

这种重复且没有技术含量的代码,怎么能配得上程序猿这种优(lan)雅(duo)的生物呢?最好能在返回响应结果的前提下,减去那些重复的代码,比如:

@RequestMapping("hello2")publicStringhello2(){return"Hello,World!";}

这就需要借助 Spring 提供的ResponseBodyAdvice来实现了。

全局处理响应数据

先上代码:

/***<br>createdat2020/8/12**@authorwww.howardliu.cn*@since1.0.0*/@RestControllerAdvicepublicclassResultResponseAdviceimplementsResponseBodyAdvice<Object>{@Overridepublicbooleansupports(finalMethodParameterreturnType,finalClass<?extendsHttpMessageConverter<?>>converterType){return!returnType.getGenericParameterType().equals(Response.class);//1}@OverridepublicObjectbeforeBodyWrite(finalObjectbody,finalMethodParameterreturnType,finalMediaTypeselectedContentType,finalClass<?extendsHttpMessageConverter<?>>selectedConverterType,finalServerHttpRequestrequest,finalServerHttpResponseresponse){if(body==null||bodyinstanceofResponse){returnbody;}finalResponse<Object>result=newResponse<>();result.setCode(200);result.setDesc("查询成功");result.setData(body);if(returnType.getGenericParameterType().equals(String.class)){//2ObjectMapperobjectMapper=newObjectMapper();try{returnobjectMapper.writeValueAsString(result);}catch(JsonProcessingExceptione){thrownewRuntimeException("将Response对象序列化为json字符串时发生异常",e);}}returnresult;}}/***<br>createdat2020/8/12**@authorwww.howardliu.cn*@since1.0.0*/@RestControllerpublicclassHelloWorldController{@RequestMapping("hello2")publicStringhello2(){return"Hello,World!";}@RequestMapping("user1")publicUseruser1(){Useru=newUser();u.setUid(System.currentTimeMillis()+"");u.setName("测试1");returnu;}}

上面代码是实现了 Spring ResponseBodyAdvice类的模板方式,按照 Spring 的要求实现就行。只有两个需要特别注意的地方,也就是代码中标注 1 和 2 的地方。

首先说 1 这一行,也就是supports方法,这个方法是校验是否需要调用beforeBodyWrite方法的前置判断,返回true则执行beforeBodyWrite方法,这里根据 Controller 方法返回类型来判断是否需要执行beforeBodyWrite,也可以一律返回true,在后面判断是否需要进行类型转换。

然后重点说下 2 这一行,这行是坑,是大坑,如果对 Spring 结构不熟悉的,绝对会在这徘徊许久,不得妙法。

代码 2 这一行是判断Controller的方法是否返回的是String类型的结果,如果是,将返回的对象序列化之后返回。

这是因为SpringString类型的响应类型单独处理了,使用StringHttpMessageConverter类进行数据转换。在处理响应结果的时候,会在方法getContentLength中计算响应体大小,其父类方法定义是protected Long getContentLength(T t, @Nullable MediaType contentType),而StringHttpMessageConverter将方法重写为protected Long getContentLength(String str, @Nullable MediaType contentType),第一个参数是响应对象,固定写死是String类型,如果我们强制返回Response对象,就会报ClassCastException

当然,直接返回String的场景不多,这个坑可能会在某天特殊接口中突然出现。

补充说明

上面只是展示了ResponseBodyAdvice类最简单的应用,我们还可以实现更多的扩展使用。比如:

返回请求ID:这个需要与与RequestBodyAdvice联动,获取到请求ID后,在响应是放在响应体中;

结果数据加密:通过ResponseBodyAdvice实现响应数据加密,不会侵入业务代码,而且可以通过注解方式灵活处理接口的加密等级;

有选择的包装响应体:比如定义注解IgnoreResponseWrap,在不需要包装响应体的接口上定义,然后在supports方法上判断方法的注解即可,比如:

@Overridepublicbooleansupports(finalMethodParameterreturnType,finalClass<?extendsHttpMessageConverter<?>>converterType){finalIgnoreResponseWrap[]declaredAnnotationsByType=returnType.getExecutable().getDeclaredAnnotationsByType(IgnoreResponseWrap.class);return!(declaredAnnotationsByType.length>0||returnType.getGenericParameterType().equals(Response.class));}

很多其他玩法就不一一列举了。

总结

上面说了正常响应的数据,只做到了一点优雅,想要完整,还需要考虑接口异常情况,总不能来个大大的try/catch/finally包住业务逻辑吧,那也太丑了。后面会再来一篇,重点说说接口如何在出现异常时,也能返回统一的结果响应。


作者:看山。

游于码界,戏享人生。

如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/SpringBoot/4347.html