原创,转载请注明出处
背景
前后端分离的开发模式已成为业界的事实标准:后端开发服务接口,提供业务需要的数据;前端拿到数据后,执行数据渲染的逻辑。
随着微服务架构的普及,大量的巨石(单体)应用,按照不同的领域,拆分成很多的领域集群,每个领域由不同的开发团队来负责,最大化产品迭代的速度,践行cicd,devops。
多个领域团队产出的服务数据,在格式上不统一,给前端同学的解析带来了很大的困扰;最普遍的场景是,分别解析异构的数据格式,给代码的整洁规范带来了很大的挑战。同时,后端服务在抛出异常时,跟前端的交互,展示也因为没有规范,会导致大量的无意义劳动。
混乱的现状
愿景与期望
格式规范化,并预留必要的扩展性
- 对于不同的领域团队,透传出的数据格式在系统层面做到一致。
- 对于正常的业务请求,和异常发生时,在系统层面也做到一致。
- 在服务接口格式基本一致的基础上,留出必要的扩展性
a. 让用户自定义,是否需要输出系统统一的格式,预留出个性化结果的扩展点(比如:检查系统是否健康的结果响应(return 200;)已然存在)
b. 输出格式的扩展性,以哪种格式来输出.
规范化之后的模型。
解决规划
返回格式的定义(ResultDTO)
@Data
public class ResultDTO {
/**
* 是否成功
*/
private Boolean success;
/**
* 返回的对象
*/
private Object result;
/**
* 错误码
*/
private String errorCode;
/**
* 错误信息
*/
private String errorMsg;
public ResultDTO() {
}
public ResultDTO(Boolean success) {
this.success = success;
}
public ResultDTO(String code, String msg) {
this.success = false;
this.errorCode = code;
this.errorMsg = msg;
}
public ResultDTO(Object result) {
this.success = true;
this.result = result;
}
}
正常业务响应时展示示例
{
"result": "world",
"success": true
}
业务异常时展示示例
{
"errorCode": "1001",
"errorMsg": "系统处理失败",
"success": false
}
正常业务响应流程处理
两个思路
-
添加ResponseBodyAdvice(推荐 @since spring 4.1)
-
添加 ReturnValueHandler
业务异常时统一处理
ControllerAdvice
落地方案
自定义annotation(WithApiResult),支持类级别和方法级别的格式输出
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface WithApiResult {
Class<ResultDTO> result() default ResultDTO.class;
}
实现自定义的ResponseBodyAdvice
@Slf4j
@ControllerAdvice
public class GlobalControllerAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return retrieveWithApiResultAnnotation(returnType) != null;
}
private WithApiResult retrieveWithApiResultAnnotation(MethodParameter returnType) {
Class<?> declaringClass = returnType.getDeclaringClass();
WithApiResult annotation = AnnotationUtils.findAnnotation(declaringClass, WithApiResult.class);
if (annotation == null) {
annotation = returnType.getMethodAnnotation(WithApiResult.class);
}
return annotation;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof ResultDTO) {
return body;
}
ResultDTO wrapper = new ResultDTO();
wrapper.setResult(body);
wrapper.setSuccess(true);
return wrapper;
}
}
或者:实现自定义的HandlerMethodReturnValueHandler并加载进springmvc(ResponseBodyAdvice两种方案选一即可)
public class WithApiResultMethodProcessor implements HandlerMethodReturnValueHandler {
private RequestResponseBodyMethodProcessor target;
public WithApiResultMethodProcessor(RequestResponseBodyMethodProcessor target) {
this.target = target;
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return target.supportsReturnType(returnType);
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
WithApiResult annotation = retrieveWithApiResultAnnotation(returnType);
if (annotation != null) {
Class<ResultDTO> result = annotation.result();
ResultDTO wrapper = result.newInstance();
wrapper.setResult(returnValue);
wrapper.setSuccess(true);
returnValue = wrapper;
}
target.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
private WithApiResult retrieveWithApiResultAnnotation(MethodParameter returnType) {
Class<?> declaringClass = returnType.getDeclaringClass();
WithApiResult annotation = AnnotationUtils.findAnnotation(declaringClass, WithApiResult.class);
if (annotation == null) {
annotation = returnType.getMethodAnnotation(WithApiResult.class);
}
return annotation;
}
}
自定义的ReturnValueHandler加载到springmvc的执行流程
@Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
@PostConstruct
public void postConstruct() {
List<HandlerMethodReturnValueHandler> original = requestMappingHandlerAdapter.getReturnValueHandlers();
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>();
for (HandlerMethodReturnValueHandler each : original) {
if (each instanceof RequestResponseBodyMethodProcessor) {
WithApiResultMethodProcessor wrapper = new WithApiResultMethodProcessor((RequestResponseBodyMethodProcessor) each);
log.info("WithApiResultMethodProcessor added.");
handlers.add(wrapper);
}
handlers.add(each);
}
requestMappingHandlerAdapter.setReturnValueHandlers(handlers);
}
异常流程的统一处理
@ExceptionHandler
@ResponseBody
public ResultDTO handleException(HttpServletRequest request, Exception ex) {
log.error("system error", ex);
ResultDTO result = null;
if (ex instanceof IllegalArgumentException) {
result = new ResultDTO(CommonErrorEnum.PARAM_ERROR.getErrorCode(), ex.getMessage());
} else if (ex instanceof BizException) {
BizException bizEx = (BizException) ex;
result = new ResultDTO(bizEx.getErrorCode(), ex.getMessage());
} else if (ex instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException validException = (MethodArgumentNotValidException) ex;
BindingResult bindingResult = validException.getBindingResult();
List<ObjectError> allErrors = bindingResult.getAllErrors();
StringBuilder builder = new StringBuilder();
allErrors.forEach(each -> {
builder.append(each.getDefaultMessage()).append(";");
});
result = new ResultDTO(CommonErrorEnum.PARAM_ERROR.getErrorCode(), builder.toString());
} else {
result = new ResultDTO(CommonErrorEnum.SYSTEM_ERROR.getErrorCode(), CommonErrorEnum.SYSTEM_ERROR.getErrorMsg());
}
return result;
}
过程记录
-
添加ReturnValueHandler的时机(详细查看落地方案)
-
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#addReturnValueHandlers的设计用意
添加的ReturnValueHandlers作为customed handlers会放到default handlers的后面
-
业务异常的处理,建议封装成ErrorEnum,统一处理 链接待补充。
-
分页结果的格式定义(PagedDTO)
对于分页查询的场景,PagedDTO赋值给result,示例如下:
{
"result": {
"pageNum": 1,
"pageSize": 10,
"totalRecord": 100,
"list": [{}]
},
"success": true
}
PagedDTO模型
@Data
public class PagedDTO<T> implements Serializable {
private static final long serialVersionUID = -3884577158754259731L;
/**
* 第几页
*/
private Long pageNum = 1L;
/**
* 页面size
*/
private Long pageSize = 20L;
/**
* count记录数
*/
private Long totalRecord = 0L;
/**
* 模型列表
*/
private List<T> list;
public Long getTotalPage() {
return (totalRecord - 1) / pageSize + 1;
}
}