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

作者简介:大家好,我是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

进群,大家一起学习,一起进步,一起对抗互联网寒冬

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

为什么多片DDR菊花链拓扑连接时末端需要接很多的电阻

大家如果做过DDR的设计可能会发现在进行多片DDR连线时&#xff0c;通常在信号的末端会放置很多的电阻(如下图所示)&#xff0c;那么这些电阻都是起什么作用的呢&#xff1f; 通常在DDR末端的电阻是为了防止信号反射的&#xff0c;起阻抗匹配的作用&#xff0c;之前我们介绍过另…

linux修改用户uid和gid并且修改文件所有权(所属用户及所属用户组)(chown命令、chgrp命令)(批量修改查找并修改文件、目录uid和gid)

文章目录 修改Linux用户UID和GID以及文件所有权1. 修改用户的UID和GID1.1 用户UID和GID的概念1.2 修改用户UID1.3 修改用户GID 2. 修改文件所有权2.1 文件所有权的概念2.2 修改文件所有者&#xff08;chown命令&#xff09;2.3 修改文件所属用户组&#xff08;chgrp命令&#x…

Apache CouchDB 垂直权限绕过漏洞 CVE-2017-12635 已亲自复现

Apache CouchDB 垂直权限绕过漏洞 CVE-2017-12635 已亲自复现 漏洞名称影响版本影响版本 漏洞复现环境搭建漏洞利用 总结 漏洞名称 影响版本 Apache CouchDB是一个开源的NoSQL数据库&#xff0c;专注于易用性和成为“完全拥抱web的数据库”。它是一个使用JSON作为数据存储格式…

设计模式——代理模式(Proxy Pattern)

概述 代理模式是指为其他对象提供一种代理&#xff0c;以控制对这个对象的访问。代理对象在访问对象和目标对象之间起到中介作用。代理对象也可以在不修改目标对象的前提下&#xff0c;提供额外的功能操作&#xff0c;拓展目标对象的功能&#xff0c;比如说在目标对象的某个方法…

LeetCode 1901. 寻找峰值 II:二分查找

【LetMeFly】1901.寻找峰值 II&#xff1a;二分查找 力扣题目链接&#xff1a;https://leetcode.cn/problems/find-a-peak-element-ii/ 一个 2D 网格中的 峰值 是指那些 严格大于 其相邻格子(上、下、左、右)的元素。 给你一个 从 0 开始编号 的 m x n 矩阵 mat &#xff0c…

C#调用阿里云接口实现动态域名解析,支持IPv6(Windows系统下载可用)

电信宽带一般能申请到公网IP&#xff0c;但是是动态的&#xff0c;基本上每天都要变&#xff0c;所以想到做一个定时任务&#xff0c;随系统启动&#xff0c;网上看了不少博文很多都支持IPv4&#xff0c;自己动手写了一个。 &#xff08;私信可全程指导&#xff09; 部署步骤…

20231218在Ubuntu18.04下以EXT4格式化HDD

20231218在Ubuntu18.04下以EXT4格式化HDD 2023/12/18 17:24 缘起&#xff1a; 编译一个Android10大概要200GB&#xff0c;编译10个Android10的SDK&#xff0c;3TB的HDD机械硬盘就估计会被填满了&#xff01; 如果使用rm -rf *这个命令将SDK一个一个逐个地删除&#xff0c;估计2…

强大的电子书阅读器:OmniReader Pro for mac

&#x1f50d; OmniReader Pro 是一款专为 Mac 设计的强大阅读工具&#xff0c;它能够帮助你更高效地阅读和处理各种文本内容。无论是电子书、新闻文章、网页文本还是文件资料&#xff0c;OmniReader Pro 都能胜任&#xff01; ✅ OmniReader Pro 提供了丰富的功能&#xff0c…

UE5 C++(六)— 枚举UENUM、结构体USTRUCT和补充属性说明符

文章目录 枚举&#xff08;ENUM&#xff09;第一种方式第二种方式 结构体&#xff08;USTRUCT&#xff09;补充属性说明符&#xff08;ExposeOnSoawn&#xff09;结构体创建数据表格 枚举&#xff08;ENUM&#xff09; 第一种方式 定义枚举 UENUM(BlueprintType) namespace …

java配置+J_IDEA配置+git配置+maven配置+基本语句

当前目录文件夹dir 进入文件夹cd 返回上一级cd.. 创建文件夹&#xff1a;mkdir 文件名删除文件夹&#xff1a;rd 文件夹名&#xff0c; 目录不为空不能直接删 rd /s 带子文件夹一起删 清屏cls 切换d盘才能进入 下载git地址&#xff1a; Git - Downloading Package (g…

Linux网络编程(一):网络基础(上)

参考引用 UNIX 环境高级编程 (第3版)嵌入式Linux C应用编程-正点原子 1. 网络通信概述 网络通信本质上是一种进程间通信&#xff0c;是位于网络中不同主机上的进程之间的通信&#xff0c;属于 IPC 的一种&#xff0c;通常称为 socket IPC&#xff0c;网络通信是为了解决在网络…

德思特EMC RICI测试方案助您对抗电磁设备干扰!

来源&#xff1a;德思特测试测量 德思特方案丨德思特EMC RICI测试方案助您对抗电磁设备干扰&#xff01; 原文链接&#xff1a;https://mp.weixin.qq.com/s/D8wdQr_reaFG-yppT8nzkw 欢迎关注虹科&#xff0c;为您提供最新资讯&#xff01; 方案背景 电磁或射频干扰的敏感性&…

【AIGC重塑教育】AI大模型驱动的教育变革与实践

文章目录 &#x1f354;现状&#x1f6f8;解决方法✨为什么要使用ai&#x1f386;彩蛋 &#x1f354;现状 AI正迅猛地改变着我们的生活。根据高盛发布的一份报告&#xff0c;AI有可能取代3亿个全职工作岗位&#xff0c;影响全球18%的工作岗位。在欧美&#xff0c;或许四分之一…

(八)STM32 USART —— 串口通讯

目录 1. 串口通讯协议简介 1.1 物理层 1.1.1 电平标准 1&#xff09;TTL 电平 2&#xff09;RS-232 电平 3&#xff09;RS-485 电平 4&#xff09;CAN 总线电平 1.1.2 USB 和 串口 的区分 1.1.3 RS-232 信号线 1.2 协议层 1&#xff09;波特率 2&#xff09;通讯…

Arcgis中利用模型构建器统一栅格数据的行列号

1、统一&#xff08;X,Y) 方法&#xff1a;"数据管理工具箱"→"Projections and Transformations"→"Raster"→"Project Raster" 构建模型 这里以行列号最小的栅格&#xff08;X,Y&#xff09;为准&#xff08;其实也就是栅格数据的空…

数据可视化---离群值展示

内容导航 类别内容导航机器学习机器学习算法应用场景与评价指标机器学习算法—分类机器学习算法—回归机器学习算法—聚类机器学习算法—异常检测机器学习算法—时间序列数据可视化数据可视化—折线图数据可视化—箱线图数据可视化—柱状图数据可视化—饼图、环形图、雷达图统…

全球通关第一人,分享阿里云新版ACE认证备考攻略~

2022.3月底阿里云针对老版ACE进行了改版&#xff0c;针对云计算技术的发展趋势&#xff0c;新增了云原生等热门技术&#xff0c;同时新版ACE认证新增了实验和面试&#xff0c;全面考查考生的动手能力和理论知识结构&#xff0c;含金量大大提升。 作为阿里云新版ACE全球通关第一…

【智慧之窗】AI驱动产品探索

一.初识 ChatGPT ChatGPT 是由 OpenAI 开发的自然语言处理&#xff08;NLP&#xff09;模型&#xff0c;基于 GPT&#xff08;Generative Pre-trained Transformer&#xff09;架构。GPT 系列的模型旨在理解和生成自然语言文本。ChatGPT 专注于支持对话性任务&#xff0c;即与…

【remb】twcc 与remb的切换测试

500000bps 70kBps 1 000 000 bps后&#xff0c;图像清晰些了&#xff0c;但在mesh下还是会牺牲了它的及时性&#xff1b;上面的几种情况的延时性很大啊&#xff0c;有流畅度&#xff0c;但延时太大 在twcc策略下&#xff0c;我们看到 220kBps时即大概1.6M时&#xff0c;视频才…

【Spring】09 BeanClassLoaderAware 接口

文章目录 1. 简介2. 作用3. 使用3.1 创建并实现接口3.2 配置 Bean 信息3.3 创建启动类3.4 启动 4. 应用场景总结 Spring 框架为开发者提供了丰富的扩展点&#xff0c;其中之一就是 Bean 生命周期中的回调接口。本文将聚焦于其中的一个接口 BeanClassLoaderAware&#xff0c;介…