我从阿里云学到的返回值处理技巧

2023-12-19

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

阿里云@CosmoController

来思考一下前面两篇都做了什么。

一开始,我们发现前后端交互没有统一的数据格式,于是封装了Result/PageResult等工具类,统一JSON格式:

{
    "data": {},
    "success": true,
    "message": "success"
}

随后,我们又发现出现异常时SpringBoot默认返回的JSON和正常响应时的JSON仍旧不统一,于是尝试使用Result处理异常,将自定义异常转为Result输出,并让@RestControllerAdvice对抛出的异常进行兜底处理。

@PostMapping("insertUser")
public Result<Boolean> insertUser(@RequestBody User user) {
    if (user == null) {
        // 常见处理1:只传入定义好的错误
        return Result.error(ExceptionCodeEnum.EMPTY_PARAM)
    }
    if (user.getUserType() == null) {
        // 常见处理2:抛出自定义的错误信息
        return Result.error(ExceptionCodeEnum.ERROR_PARAM, "userType不能为空");
    }
    if (user.getAge() < 18) {
        // 常见处理3:抛出自定义的错误信息
        return Result.error("年龄不能小于18");
    }

    return Result.success(userService.save(user));
}
/**
 * 全局异常处理
 *
 * @author mx
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 业务异常
     *
     * @param
     * @return
     */
    @ExceptionHandler(BizException.class)
    public Result<ExceptionCodeEnum> handleBizException(BizException bizException) {
        log.error("业务异常:{}", bizException.getMessage(), bizException);
        return Result.error(bizException.getError());
    }

    /**
     * 运行时异常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(RuntimeException.class)
    public Result<ExceptionCodeEnum> handleRunTimeException(RuntimeException e) {
        log.error("运行时异常: {}", e.getMessage(), e);
        return Result.error(ExceptionCodeEnum.ERROR);
    }

}

但我曾见过阿里云的代码类似这样:

你会发现,人家返回的是CourseDTO,而不是Result.success(courseDTO)。但是,前端得到的JSON却是这样的:

还是做了统一结果封装!

于是你感到很困惑:我靠,怎么搞的?

秘密就在@CosmoController这个阿里云自定义的注解上!

认识ResponseBodyAdvice

我们直接看代码,后面再解释ResponseBodyAdvice是什么。

最简单的一个Controller是这样的:

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("getUser")
    public User getUser(Long id) {
        return userService.getById(id);
    }
}

得到的JSON是这样的:

{
    "id": 1,
    "name": "测试1",
    "age": 18,
    "userType": 1,
    "createTime": "2021-01-13T19:18:20",
    "updateTime": "2021-01-13T19:18:20",
    "deleted": false,
    "version": 0
}

我们加一个ResponseBodyAdvice:

@RestControllerAdvice
public class CommonResponseDataAdvice implements ResponseBodyAdvice<Object> {


    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return false;
    }

    @Override
    public Object beforeBodyWrite(Object o, 
                                  MethodParameter methodParameter, 
                                  MediaType mediaType, 
                                  Class<? extends HttpMessageConverter<?>> aClass, 
                                  ServerHttpRequest serverHttpRequest, 
                                  ServerHttpResponse serverHttpResponse) {

        return "mock result";
    }
}

重新请求,你会发现!没什么变化...

不好意思,忘了把上面的CommonResponseDataAdvice#supports()返回值改成true了,重新请求:

怎么返回值变成了"mock result"了,JSON呢?打个断点观察一下:

哦,原来这个Object就是原先Controller的返回值。

整理一下ResponseBodyAdvice:

  • Spring提供的一个接口,和AOP一样的,XxxAdvice都是用来增强的
  • 配合@RestControllerAdvice注解,可以“拦截”返回值
  • 通过supports()方法判断是否需要“拦截”

模拟阿里云@CosmoController

有了ResponseBodyAdvice,我们很容易想到:只要在beforeBodyWrite()方法内对返回值进行统一结果封装,就能达到@CosmoController一样的效果!

只需改一行代码:

@RestControllerAdvice
public class CommonResponseDataAdvice 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) {
		// 改一行代码即可:把Object返回值用Result封装
        return Result.success(o);
    }
}

有@CosmoController的味道了,但我们用的是@RestController,而阿里云用的是自定义@CosmoController,逼格高一些。

怎么改成一样的呢?

分两步走:

  • 定义@CosmoController注解
  • 在CommonResponseDataAdvice中判断:如果使用了@CosmoController,就对该类所有返回值进行包装

定义@CosmoController

要明确一点,SpringBoot其实只会处理@Controller/@RestController,包括Controller Bean的实例化及返回值处理。@CosmoController哪位?没听过。

但我们可以学习@RestController的逆袭之路:

看到没,SpringBoot准确来说只认@Controller+@ResponseBody,但@RestController为了让SpringBoot承认自己,直接把两位大哥带在身边了(注解上面加注解,并不是什么新鲜事,你看@Target)。

所以,我们可以在@CosmoController上面套一个@RestController:

@RestController
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface CosmoController {
}

这样的好处是,原先@RestController有的功能@CosmoController都“继承”了(让你模仿,也希望你超越)。

ResponseBodyAdvice统一结果封装

我们的目标是:

  • 如果使用了@CosmoController,就在CommonResponseDataAdvice中使用Result封装结果
  • 如果使用了原生的@RestController,就原样返回,不做任何处理
@RestControllerAdvice
public class CommonResponseDataAdvice implements ResponseBodyAdvice<Object> {


    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        // 对标注了@CosmoController注解的Controller返回值进行处理。methodParameter.getDeclaringClass()表示得到方法所在的类。
        return methodParameter.getDeclaringClass().isAnnotationPresent(CosmoController.class);
    }

    @Override
    public Object beforeBodyWrite(Object o,
                                  MethodParameter methodParameter,
                                  MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> aClass,
                                  ServerHttpRequest serverHttpRequest,
                                  ServerHttpResponse serverHttpResponse) {

        return Result.success(o);
    }
}

对UserController分别使用@RestController和@CosmoController,发现已经达到预期效果。

优化

上面的代码还不够健壮,有些情况没考虑到:

  • 如果Controller返回值已经用Result封装过了呢,此时会造成重复嵌套!
  • 标注了@CosmoController后,内部个别方法不希望用Result封装该怎么做?
  • 诸如参数校验失败等情况怎么处理呢?

如果Controller中的返回值已经用Result封装过,应该直接返回,否则会出现重复嵌套:

{
    "code": 200,
    "message": "成功",
    "data": {
        "code": 200,
        "message": "成功",
        "data": {
            "id": 1,
            "name": "测试1",
            "age": 18,
            "userType": 1,
            "createTime": "2021-01-13T19:18:20",
            "updateTime": "2021-01-13T19:18:20",
            "deleted": false,
            "version": 0
        }
    }
}

解决办法是,在beforeBodyWrite()里判断并排除:

@Override
public Object beforeBodyWrite(Object o,
                              MethodParameter methodParameter,
                              MediaType mediaType,
                              Class<? extends HttpMessageConverter<?>> aClass,
                              ServerHttpRequest serverHttpRequest,
                              ServerHttpResponse serverHttpResponse) {
	// 已经包装过的,不再重复包装
    if (o instanceof Result) {
        return o;
    }

    return Result.success(o);
}

如果个别方法希望忽略Result封装,可以单独再定一个注解:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface IgnoreCosmoResult {
}
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
    // 标注了@CosmoController,且类及方法上都没有标注@IgnoreCosmoResult的方法才进行包装
    return methodParameter.getDeclaringClass().isAnnotationPresent(CosmoController.class)
            && !methodParameter.getDeclaringClass().isAnnotationPresent(IgnoreCosmoResult.class)
            && !methodParameter.getMethod().isAnnotationPresent(IgnoreCosmoResult.class);
}
@Slf4j
@CosmoController
public class UserController {

    @IgnoreCosmoResult
    @GetMapping("getUser")
    public User getUser(Long id) {
        return null;
    }

    @GetMapping("getUser2")
    public User getUser2(Long id) {
        return null;
    }
}

完整的代码:

@RestControllerAdvice
public class CommonResponseDataAdvice implements ResponseBodyAdvice<Object> {


    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        // 标注了@CosmoController,且类及方法上都没有标注@IgnoreCosmoResult的方法才进行包装
        return methodParameter.getDeclaringClass().isAnnotationPresent(CosmoController.class)
                && !methodParameter.getDeclaringClass().isAnnotationPresent(IgnoreCosmoResult.class)
                && !methodParameter.getMethod().isAnnotationPresent(IgnoreCosmoResult.class);
    }

    @Override
    public Object beforeBodyWrite(Object o,
                                  MethodParameter methodParameter,
                                  MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> aClass,
                                  ServerHttpRequest serverHttpRequest,
                                  ServerHttpResponse serverHttpResponse) {
		// 已经包装过的,不再重复包装
        if (o instanceof Result) {
            return o;
        }

        return Result.success(o);
    }
}

第三个问题,你仔细想想,其实解决第一个问题时顺便搞定了。如果参数校验错误,处理方式大致有两种:

  • 转为自定义异常抛出,由@RestControllerAdvice兜底处理
  • 在当前方法中用Result.error()封装错误信息返回

ResponseBodyAdvice对第一种策略没有影响,异常仍旧会被@RestControllerAdvice全局异常捕获,而第二种策略由于已经用Result封装,会被ResponseBodyAdvice忽略,不再重复包装,所以前端收到的是正确的格式:

{
  "code": -1
  "message": "用户不存在",
  "data": null
}

最后我想说,这种封装意义好像也不大~后面介绍一些其它用法吧。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

我从阿里云学到的返回值处理技巧 的相关文章

  • OneNote笔记使用记录

    1 快捷键 2 快速设置行距 Ctrl 1 设置一倍行距 Ctrl 2 两倍行距 Ctrl 5 1 5 倍行距 3 切换样式标题 Ctrl Alt 1 标题 1 Ctrl Alt 2 标题 2 Ctrl Alt 3 标题 3 Ctrl Sh
  • vtk用户指南 第十一章 随时间变化的数据

    11 1时序支持简介 创建可视化工具包的目的是允许人们可视化 从而探索具有空间范围的数据中的特征 它允许人们回答一些问题 比如 在这些数据中 最大价值的区域在哪里 它们有什么形状和价值 以及 这些形状是如何分布的 VTK提供了大量的技术来显
  • 阿里云一二级域名配置

    一级域名配置 二级域名配置
  • 2023中国品牌节金谱奖荣誉发布 酷开科技获颁OTT行业科技创新奖

    11月17日 19日 以 复苏与腾飞 为主题的2023第十七届中国品牌节 在杭州市云栖小镇国际会展中心成功举行 在18日晚间的荣耀盛典上 TopBrand 2023中国品牌节金谱奖 荣誉发布 酷开科技斩获OTT行业科技创新奖 酷开科技作为O
  • 面试150-13(Leetcode238除自身以外数组的乘积)

    代码 class Solution public int productExceptSelf int nums int n nums length int res new int n int product 1 int zerocnt 0
  • 再看参数校验

    作者简介 大家好 我是smart哥 前中兴通讯 美团架构师 现某互联网公司CTO 联系qq 184480602 加我进群 大家一起学习 一起进步 一起对抗互联网寒冬 写一个接口 大致就几个步骤 参数校验 编写Service Dao SQL

随机推荐

  • 如何开发一个免费的App

    开发一个免费App意味着能够在项目启动初期 以更低成本的方式进行业务的迭代和市场化验证 互联网发展到2023年 尤其在生成式AI及大模型技术 跃进式 增长的背景下 一个创新式商业模式的起步变得异常艰难 但如果用好工具 那么不仅能事半功倍 还
  • 网络安全设备概念的熟悉和学习

    什么是网络安全 网络安全技术有哪些 Web应用防火墙 WAF 为什么需要WAF 什么是WAF 与传统防火墙的区别 WAF不是全能的 入侵检测系统 IDS 什么是IDS 跟防火墙的比较 部署位置选择
  • 【网络安全】—Shell编程入门(1)

    文章目录 基础 变量概念介绍 特殊变量进阶 数值计算实践 条件测试比较 条件判断语句 流程控制语句 循环语句应用
  • threejs 解析外部模型关键帧动画

    参考资料 threejs中文网 threejs qq交流群 814702116 解析外部模型关键帧动画 前面几节课 用到的关键帧动画 是借助threejs提供的两个类 KeyframeTrack AnimationClip 自己写代码实现
  • SQL Server 大数据量分页

    1 ROW NUMBER OVER 方式 SQL2012以下推荐使用 SELECT FROM SELECT ROW NUMBER OVER ORDER BY menuId AS RowId FROM sys menu AS r WHERE
  • “jar“:CreateProcess error = 2,系统找不到指定的文件

    起因 系统配置的java环境为jar 或java环境不完整导致 修复 将jar exe拷贝至java环境的bin目录中即可
  • c语言:指针作为参数传递

    探究实参与形参它们相互独立 由于主调函数的变量 a b 与被调函数的形参 x y 它们相互独立 函数 swap 可以修改变量 x y 但是却无法影响到主调函数中的 a b 现在利用取地址运算符 分别打印它们的首地址 让我们从内存的角度 来分
  • 使用小程序实现App灰度测试的好处

    灰度测试 Gray Testing 是一种软件测试策略 也被称为渐进性测试或部分上线测试 在灰度测试中 新的软件版本或功能并非一次性推送给所有用户 而是仅在一小部分用户中进行测试 这可以帮助开发团队逐步暴露新功能或版本 以便及时发现和修复问
  • 网络安全基础知识

    1 什么是防火墙 什么是堡垒主机 什么是DMZ 防火墙是在两个网络之间强制实施访问控制策略的一个系统或一组系统 堡垒主机是一种配置了安全防范措施的网络上的计算机 堡垒主机为网络之间的通信提供了一个阻塞点 也可以说 如果没有堡垒主机 网络间将
  • Matlab-绘图及其位置摆放

    一 绘图函数 1 绘制二维图形 1 1 plot 函数的应用格式 1 plot x 当x 为一向量时 以x 元素的值为纵坐标 x 的序号为横坐标值绘制曲线 当x 为一实矩阵时 则以其序号为横坐标 按列绘制每列元素值相对于其序号的曲线 例如
  • threejs 机械虚拟装配案例(播放)

    参考资料 threejs中文网 threejs qq交流群 814702116 机械虚拟装配案例 播放 如果你想做一个产品 机械 建筑的虚拟装配动画 可以美术先在建模软件中生成关键帧动画的数据 然后通过threejs加载模型 播放动画数据即
  • 使用AI大模型无损放大照片

    在线体验 点击 图像处理 即可使用 private static final String IMAGE QUALITY ENHANCE https aip baidubce com rest 2 0 image process v1 ima
  • 多线程案例:购买车票

    购票案例 多线程同步 多线程的并发执行虽然可以提高程序的效率 但是 当多个线程去访问同一个资源时 也会引发一些安全问题 并发 同一个对象被多个线程同时操作 处理多线程问题时 多个线程访问同一个对象 并且某些线程还想修改这个对象 这时候我们就
  • 网络安全自学入门:(超详细)从入门到精通学习路线&规划,学完即可就业_网络安全自学从哪里入手

    很多人上来就说想学习黑客 但是连方向都没搞清楚就开始学习 最终也只是会无疾而终 黑客是一个大的概念 里面包含了许多方向 不同的方向需要学习的内容也不一样 算上从学校开始学习 已经在网安这条路上走了10年了 无论是以前在学校做安全研究 还是毕
  • 开源一个超好用的接口Mock工具——Msw-Tools

    作为一名前端开发 是不是总有这样的体验 基础功能逻辑和页面UI开发很快速 本来可以提前完成 但是接口数据联调很费劲 耗时又耗力 有时为了保证进度还不得不加加班 为了摆脱这种痛苦 经过一周的努力 从零开发了一个灵活无依赖 且集成简单的数据接口
  • threejs动画播放(暂停、倍速、循环)AnimationAction

    参考资料 threejs中文网 threejs qq交流群 814702116 动画播放 暂停 倍速 循环 上节课对关键帧动画如何 创建 如何 播放 做了整体介绍 下面进一步介绍关键帧动画播放的知识 比如关键帧动画停止播放 暂停播放 倍速播
  • 3DM/OFF格式在线转换

    3D模型在线转换 https 3dconvert nsdt cloud 是一个可以进行3D模型格式转换的在线工具 支持多种3D模型格式进行在线预览和互相转换 3DM与OFF格式简介 3DM是一种常用的三维模型文件格式 具有多种几何体和材质
  • 【网络安全】—Shell编程入门(2)

    文章目录 循环控制语句 函数知识精讲 数组知识精讲 开发环境规范 调试优化实践 自动化实战项目 在前面的章节中 我们已经介绍了Shell编程的基础知识 包括变量 特殊变量 数值计算 条件测试 条件判断和基本的循环语句 接下来 我
  • ssm+mysql应急指挥平台-计算机毕业设计源码13263

    摘 要 科技进步的飞速发展引起人们日常生活的巨大变化 电子信息技术的飞速发展使得电子信息技术的各个领域的应用水平得到普及和应用 信息时代的到来已成为不可阻挡的时尚潮流 人类发展的历史正进入一个新时代 在现实运用中 应用软件的工作规则和开发步
  • 我从阿里云学到的返回值处理技巧

    作者简介 大家好 我是smart哥 前中兴通讯 美团架构师 现某互联网公司CTO 联系qq 184480602 加我进群 大家一起学习 一起进步 一起对抗互联网寒冬 阿里云 CosmoController 来思考一下前面两篇都做了什么 一开