📢 大家好,我是 【战神刘玉栋】,有10多年的研发经验,致力于前后端技术栈的知识沉淀和传播。 💗
🌻 CSDN入驻不久,希望大家多多支持,后续会继续提升文章质量,绝不滥竽充数,欢迎多多交流。👍
写在前面的话
本系列博文已连载到第六篇,通过前五篇博文,我们已完成了教师信息的基础增删改查功能,在介绍其他知识专栏之前,先来谈一谈CURD页面的规范问题。
前后端分离的开发模式中,后端程序猿有必要与前端程序猿约定一个相对于规范的返回格式,如果仅仅返回数据,有点像裸奔。因此,后端项目需要对返回结果进行统一封装返回,前端也需要封装请求后置拦截器对返回结果处理。
按业内约定俗成的规范,返回结果至少包含:code 状态码、data 数据、msg 消息内容、error 错误内容。
上述只是基础部分,实际开发中,可能还包含:timestamp 时间戳、requestId 日志ID等等。
加油,程序猿,保持住Tempo,开干,玩的就是真实!
关联文章:
《程序猿入职必会(1) · 搭建拥有数据交互的 SpringBoot 》
《程序猿入职必会(2) · 搭建具备前端展示效果的 Vue》
《程序猿入职必会(3) · SpringBoot 各层功能完善 》
《程序猿入职必会(4) · Vue 完成 CURD 案例 》
《程序猿入职必会(5) · CURD 页面细节规范 》
返回结果统一封装
定义一个返回值VO类
这个是考虑统一封装的第一步,很简单,仅提供参考。
@Data
public class ResultModel<T> {
/**
* 成功编码
*/
public static final String SUCCESS_CODE = ResponseCodeEnum.SUCCESS.getCode();
/**
* 异常编码
*/
public static final String ERROR_CODE = ResponseCodeEnum.EX_ERROR.getCode();
/**
* 响应编码
*/
private String code = SUCCESS_CODE;
/**
* 响应数据
*/
private T data;
/**
* 响应信息
*/
private String message = "";
/**
* 异常详细信息
*/
private String error = "";
/**
* 返回成功
* @param data
* @param <T>
* @return
*/
public static <T> ResultModel<T> success(T data) {
return success(data, "");
}
/**
* 返回成功
* @param data
* @param message
* @param <T>
* @return
*/
public static <T> ResultModel<T> success(T data, String message) {
return new ResultModel(SUCCESS_CODE, data, message);
}
/**
* 返回失败
* @param code
* @param message
* @param error
* @return
*/
public static ResultModel fail(String code, String message, String error) {
return new ResultModel(code, null, message, error);
}
public static ResultModel fail(ResponseCodeEnum code) {
return new ResultModel(code.getCode(), null, code.getMessage(), code.getMessage());
}
public static ResultModel fail(ResponseCodeEnum code, String error) {
return new ResultModel(code.getCode(), null, code.getMessage(), error);
}
public static ResultModel fail(String error) {
return new ResultModel(ResponseCodeEnum.EX_ERROR.getCode(), null, error, error);
}
public boolean isSuccess() {
return Objects.equals(this.code, ResponseCodeEnum.SUCCESS.getCode());
}
public ResultModel() {
}
public ResultModel(String code, T data, String message) {
this.code = code;
this.data = data;
this.message = message;
}
public ResultModel(String code, T data, String message, String error) {
this.code = code;
this.data = data;
this.message = message;
this.error = error;
}
}
也可以定义一个状态枚举类,非必须:
public enum ResponseCodeEnum {
/**
* 调用成功
*/
SUCCESS("00000", "调用成功"),
/**
* 系统异常
*/
EX_ERROR("EX00000", "系统异常"),
/**
* 参数不合法
*/
EX_PARAM("EX00001", "参数不合法"),
/**
* 接口调用异常
*/
EX_REQUEST("EX00002", "接口调用异常"),
/**
* 接口返回错误
*/
EX_RESULT("EX00003", "接口返回错误"),
/**
* 微信接口异常
*/
EX_WECHAT("EX00004", "微信接口异常"),
/**
* 令牌为空
*/
EX_TOKEN_EMPTY("EX00005", "令牌为空"),
/**
* 令牌无效
*/
EX_TOKEN_INVALID("EX00006", "令牌无效"),
/**
* 网站来源无效
*/
EX_REFERER_INVALID("EX00007", "网站来源无效"),
/**
* 404
*/
EX_PAGE_404("EX404", "页面地址无效");
private String code;
private String message;
ResponseCodeEnum(String code, String message) {
this.code = code;
this.message = message;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public String toString() {
return "[" + this.code + "]" + this.message;
}
}
处理返回值的几种方案
SpringBoot 针对 返回值处理有多种方案,相关关键词诸如 ResponseBodyAdvice、MessageConverters、 HandlerMethodReturnValueHandler。
【三者比较】
1、ResponseBodyAdvice(响应拦截器):
作用:ResponseBodyAdvice 允许你在将响应体写入 HTTP 响应之前拦截和修改它。它提供了一种全局定制响应处理逻辑的方式,适用于 Spring MVC 或 Spring WebFlux 应用程序。
工作原理:ResponseBodyAdvice 接口定义了在响应体写入之前将被调用的方法,你可以在这些方法中检查或修改响应体、方法返回类型、请求和其他上下文信息。这使得你可以根据应用程序的需求对响应进行定制化处理。
示例:你可以使用 ResponseBodyAdvice 添加全局的响应头信息、对返回数据进行统一的格式化等。
2、MessageConverters(消息转换器):
作用:MessageConverters 负责将 Controller 方法的返回值转换为 HTTP 响应的内容,以及将请求的内容转换为 Controller 方法的参数。
工作原理:消息转换器负责将 Java 对象与特定的媒体类型之间进行转换,例如 JSON、XML、HTML 等。它可以根据请求的 Content-Type 头信息和方法的返回值类型,选择适当的转换器来进行转换。
示例:你可以使用 MappingJackson2HttpMessageConverter 将 Java 对象转换为 JSON 格式的响应体,或将请求体中的 JSON 数据转换为 Java 对象。
3、HandlerMethodReturnValueHandler(返回值处理器):
作用:HandlerMethodReturnValueHandler 用于处理方法的返回值,将其转换为合适的响应内容。它负责将方法的返回值转换为 HTTP 响应体的内容。
工作原理:HandlerMethodReturnValueHandler 负责将方法的返回值转换为特定的响应内容,例如对象、字符串、视图等。它可以根据返回值的类型和请求的信息来选择适当的处理方式。
示例:你可以使用 ViewMethodReturnValueHandler 将返回值转换为视图,HttpEntityMethodProcessor 将返回的 HttpEntity 对象转换为 HTTP 响应。
总的来说,ResponseBodyAdvice 允许你在响应体写入之前对其进行全局性的处理,MessageConverters 负责将 Java 对象与特定的媒体类型之间进行转换,而 HandlerMethodReturnValueHandler 用于根据方法的返回值类型和请求信息将其转换为合适的响应内容。
关于顺序,HandlerMethodReturnValueHandler 负责处理方法的返回值,ResponseBodyAdvice 在写入响应体之前提供额外的处理机会,而 MessageConverters 则负责将处理过的结果转换为特定的媒体类型。因此,它们的执行顺序是:先执行 HandlerMethodReturnValueHandler,然后是 ResponseBodyAdvice,最后是 MessageConverters。
【方案点评】
三种处理方案各有千秋,本文选用 HandlerMethodReturnValueHandler 展开介绍,顺便可以介绍一下自定义注解的结合使用。
当然,博主所在公司进行框架封装时,采用 ResponseBodyAdvice,并未采用 HandlerMethodReturnValueHandler,原因是,自定义 HandlerMethodReturnValueHandler 意味着要替换 RequestResponseBodyMethodProcessor, SpringMVC 的若干默认定制功能就消失了,可能导致非意料的情况,具体后续再专栏介绍。
HandlerMethodReturnValueHandler
废话不多说,直接上代码。
Step1、定义两个自定义注解,放着备用
后续需要进行返回值封装处理的控制器,就使用@ResultController 注解即可。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResultModelAnnotation {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@RestController
@ResultModelAnnotation
public @interface ResultController {
}
Step2、自定义 HandlerMethodReturnValueHandler
实现 HandlerMethodReturnValueHandler 接口,实现 supportsReturnType 和 handleReturnValue 方法。
supportsReturnType 代表生效时机,下方意思是当类或者方法包含 ResultModelAnnotation 注解的时生效。
handleReturnValue 代表返回值处理逻辑,其实就是封装成 ResultModel 格式,再 response 出去。
public class ResultModelHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResultModelAnnotation.class) || returnType.hasMethodAnnotation(ResultModelAnnotation.class));
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
ResultModel<Object> resultModel;
ApiOperation methodAnnotation = returnType.getMethodAnnotation(ApiOperation.class);
String message = "";
if (methodAnnotation != null) {
message = methodAnnotation.value() + "成功";
}
if (returnValue instanceof ResultModel) {
resultModel = (ResultModel<Object>) returnValue;
if (!resultModel.isSuccess()) {
resultModel.setMessage(message + "error");
}
} else {
resultModel = ResultModel.success(returnValue, message);
}
mavContainer.setRequestHandled(true);
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
// 设置状态码
response.setStatus(HttpStatus.OK.value());
response.setHeader("result-model", "true");
// 设置ContentType
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
// 避免乱码
response.setCharacterEncoding("UTF-8");
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.write(JSON.toJSONString(resultModel, SerializerFeature.WriteMapNullValue));
writer.flush();
} catch (IOException ex) {
ex.printStackTrace();
} finally {
if (writer != null) {
writer.close();
}
}
}
}
Step3、自定义RequestMappingHandlerAdapter
继承 RequestMappingHandlerAdapter,重写 afterPropertiesSet 方法。
逻辑就是将前面自定义的 ResultModelHandlerMethodReturnValueHandler,放到第一位,首发选手。
public class ResultRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
List<HandlerMethodReturnValueHandler> returnValueHandlers = super.getReturnValueHandlers();
ResultModelHandlerMethodReturnValueHandler handler = new ResultModelHandlerMethodReturnValueHandler();
List<HandlerMethodReturnValueHandler> list = new ArrayList<>();
list.add(handler);
list.addAll(returnValueHandlers);
super.setReturnValueHandlers(list);
}
}
Step4、控制类添加自定义注解
直接用前面博文提到的教师信息控制器,将 @RestController 注解修改为 @ResultController
Tips:若部分接口不需要按这个格式返回,则不需要修改注解。
@ResultController
@Api(value = "ZyTeacherInfoController", tags = {"教师信息表服务"})
@RequestMapping(value = "/zyTeacherInfo")
public class ZyTeacherInfoController extends BaseController {
}
Step5、万事俱备,测试一下
启动服务,访问单个教师的接口:http://localhost:8083/zyTeacherInfo/2
输出信息如下,可以看到其格式了,搞定收工!
{
"code": "00000",
"data": {
"createdTime": "2024-05-16 20:07:21",
"modifiedTime": null,
"sortNo": null,
"stuItem": null,
"teaCode": "2",
"teaConfig": null,
"teaImg": null,
"teaName": "李老师",
"teaPhone": null,
"teaType": null,
"validFlag": "1"
},
"error": "",
"message": "获取教师信息表详细信息成功",
"success": true
}
总结陈词
此篇文章介绍了前后端分离项目中,关于统一返回结果的封装,仅供学习参考。
下一篇文章介绍前端 Axios 插件封装思路,以及对于这一返回封装结果的接受处理。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。