背景
我们在开发中,如果涉及到双端的交互,最好能统一调用的格式。确实很多人是这样做的,但是如果完全依赖开发者各自去维持这个规则,随着业务的增长,接口的增多,会带来额外的开发量,而且很大可能最终也没法保证完全遵守对应的规则。所以,如果能实现一个统一的门面,将数据返回和异常都封装起来,让业务开发者不关心数据格式和异常,只关心自己的业务问题,想必对开发效率会有比较好的提升。
很幸运的是依赖SpringBoot的组件,我们是可以统一封装返回或者异常的,下面做个具体的介绍。
原理简单介绍
依赖的SpringBoot组件主要是HttpMessageConverter
和RestControllerAdvice
或者ControllerAdvice
,前者是Spring的对象转换器,可以将业务数据按照需要的类型来返回,比如返回json,返回byte,返回html等。内部已有定义了如jackson
,gson
,string
等多种的转换器,有兴趣的可以通过HttpMessageConverter
的具体实现来了解。
RestControllerAdvice
, ControllerAdvice
是注解,可以通过实现ResponseBodyAdvice
接口的beforeBodyWrite()
方法来控制最终返回的数据结构和状态,也可以结合@ExceptionHandler
和@ResponseStatus
注解来定义方法统一处理包装异常的返回结构。下面会已实际的例子来做说明。
统一的返回结构定义
首先定义一个统一的数据返回结构,包括状态码,返回信息,返回数据三个部分。
@Data
public class BaseResult<T> {
private Integer code;
private String msg;
private T data;
public static <T> BaseResult success(T data) {
BaseResult<T> result = new BaseResult<>();
result.setData(data);
result.setCode(HttpStatus.OK.value());
return result;
}
public static BaseResult success() {
return success(null);
}
public static BaseResult fail(String msg) {
BaseResult result = new BaseResult<>();
result.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
result.setMsg(msg);
return result;
}
}
数据的统一返回
我们先处理数据的返回。定义一个ControllerAdvice
实现ResponseBodyAdvice
接口
@RestControllerAdvice
public class CommonResultAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (null == o) {
//这里包含特殊情况,后面会做介绍
return BaseResult.success();
}
if (o instanceof BaseResult) {
return o;
}
return BaseResult.success(o);
}
}
这里说下当直接返回null,即null == o
的情况,在使用默认的jackson
的情况下,如果不处理,是没有contentBody
返回的。文章这里使用了gson来处理。原因是在org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse)
中处理的时候,jackson和gson实现不同,jackson会直接返回空的content。
要默认改成gson,可以在配置中添加
spring.http.converters.preferred-json-mapper=gson
但是直接如上使用还是会有问题,因为我们定义的基本返回对象有泛型,所以在直接返回List<T>
, Map<T>
等泛型的时候,默认的writeInternal()
方法写入会有问题,进入下面代码的第一个分支,会抛出java.lang.ClassCastException
。
@Override
protected void writeInternal(Object o, @Nullable Type type, Writer writer) throws Exception {
// In Gson, toJson with a type argument will exclusively use that given type,
// ignoring the actual type of the object... which might be more specific,
// e.g. a subclass of the specified type which includes additional fields.
// As a consequence, we're only passing in parameterized type declarations
// which might contain extra generics that the object instance doesn't retain.
if (type instanceof ParameterizedType) {
getGson().toJson(o, type, writer);
}
else {
getGson().toJson(o, writer);
}
}
因此,这里自定义GsonMessageConverter
。
@Bean
public GenericHttpMessageConverter<Object> httpMessageConverter() {
return new CusHttpMessageConverter();
}
class CusHttpMessageConverter extends GsonHttpMessageConverter {
@Override
protected void writeInternal(Object o, Type type, Writer writer) throws Exception {
//BaseResult 也是泛型
//List<T> 也是泛型导致报错:java.lang.ClassCastException
if (type instanceof ParameterizedType && TypeUtils.isAssignable(type, o.getClass())) {
getGson().toJson(o, type, writer);
} else {
getGson().toJson(o, writer);
}
}
@Override
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
return true;
}
}
效果和验证
可以用如下的代码来进行验证,String, Object , List , null 都是支持的。
@RestController
public class TestController {
@GetMapping("/test")
public String test() throws Exception {
throw new Exception("测试拦截");
}
@GetMapping("/str")
public String string() {
return "OK";
}
@GetMapping("/list")
public List<Integer> list() {
return Arrays.stream(new Integer[] {1, 2, 3, 4, 5}).collect(Collectors.toList());
}
@GetMapping("/fail")
public Object fail() {
return BaseResult.fail("错误消息");
}
@GetMapping("/param")
public Object paramTest(@RequestParam(value = "str") String str) {
return str;
}
}
这里目前有个问题:在请求返回string的时候,默认如果请求头没有带
accept: applicaion/json
,返回content-Type
还是text/html
,需要注意下。
异常的统一返回
对于异常,如上所说使用ControllerAdvice
和ExceptionHandler
结合的方式,代码如下:
@Slf4j
@RestControllerAdvice
public class ControllerExpAdvice {
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public BaseResult handleGlobalException(Exception e) {
BaseResult result = handleBaseException(e);
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
result.setCode(status.value());
return result;
}
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public BaseResult handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
BaseResult<?> baseResponse = handleBaseException(e);
baseResponse.setCode(HttpStatus.BAD_REQUEST.value());
baseResponse.setMsg("缺失请求主体");
return baseResponse;
}
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public BaseResult handleNoHandlerFoundException(NoHandlerFoundException e) {
BaseResult<?> baseResponse = handleBaseException(e);
HttpStatus status = HttpStatus.NOT_FOUND;
baseResponse.setCode(status.value());
return baseResponse;
}
private <T> BaseResult<T> handleBaseException(Throwable t) {
Assert.notNull(t, "Throwable must not be null");
log.error("Captured an exception", t);
BaseResult<T> result = new BaseResult<>();
result.setMsg(t.getMessage());
return result;
}
}
特殊处理404
在错误处理时,对于返回404
的请求,会直接返回一个少量错误信息的页面,而这里我们希望当404的时候也是返回json
结构,所以这里需要加个配置,在application.properties
中加如下俩行:
spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false
最终的效果是:
{
"code": 404,
"msg": "No handler found for GET /listsdgag"
}
总结
以上就是本期文章的全部内容,其实关于SpringBoot统一返回结构和异常处理相关的也比较多,缺点是很多都抄来抄去,极少有优秀的实践文章。希望这篇能给看到的大家带来帮助。
网友评论