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

news2024/11/24 14:36:35
作者简介:大家好,我是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

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【办公软件】C# NPOI 操作Excel 案例

文章目录 1、加入NPOI 程序集&#xff0c;使用nuget添加程序集2、引用NPOI程序集3、设置表格样式4、excel加载图片5、导出excel 1、加入NPOI 程序集&#xff0c;使用nuget添加程序集 2、引用NPOI程序集 private IWorkbook ExportExcel(PrintQuotationOrderViewModel model){//…

算法通关村第十关—快速排序(青铜)

快速排序 快排的基本过程 快速排序是将分治法运用到排序问题的典型例子  快速排序基本思想是&#xff1a;通过一个标记pivot元素将n个元素的序列划分为左右两个子序列left和right,.其中left中的元素都比pivot小&#xff0c;right的都比pivot的大&#xff0c;然后再次对Ieft和r…

企业办公加密系统中——全透明加密和半透明加密的区别

PC端访问地址&#xff1a; www.drhchina.com 天锐绿盾数据防泄密系统中的全透明加密和半透明加密的区别如下&#xff1a; 全透明加密是采用驱动层动态加解密技术&#xff0c;对企业内部所有涉密文档进行强制加密处理&#xff0c;从文件创建开始即可自动加密保护。加密文档在加…

【思扬赠书 | 第1期】教你如何一站式解决OpenCV工程化开发痛点

⛳️ 写在前面参与规则 ✅参与方式&#xff1a;关注博主、点赞、收藏、评论&#xff0c;任意评论&#xff08;每人最多评论三次&#xff09;⛳️本次送书1~3本【取决于阅读量&#xff0c;阅读量越多&#xff0c;送的越多】思扬赠书 | 第1期活动开始了&#xff01;&#xff01;…

十问ByteHouse:如何基于ClickHouse玩转向量检索?

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 向量检索被广泛使用于以图搜图、内容推荐以及大模型推理等场景。随着业务升级与 AI 技术的广泛使用&#xff0c;用户期望处理的向量数据规模越来越大&#xff0c;对…

【SpringCloudAlibaba】Sentinel熔断限流工具的使用

一、前言 随着微服务的流行&#xff0c;服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式、多语言异构化服务架构的流量治理组件&#xff0c;主要以流量为切入点&#xff0c;从流量路由、流量控制、流量整形、熔断降级、系统自适应过载保护、热点流量防护等多个维…

增强的对象文字

首先看一下我们之前的一个餐厅的对象 const restaurant {name: Classico Italiano,location: Via Angelo Tavanti 23, Firenze, Italy,categories: [Italian, Pizzeria, Vegetarian, Organic],starterMenu: [Focaccia, Bruschetta, Garlic Bread, Caprese Salad],mainMenu: […

element组件库的日期选择器如何限制?

本次项目中涉及到根据日期查找出来的数据进行调整,所以修改的数据必须是查找范围内的数据.需要对调整数据的日期进行限制,效果如下: 首先我们使用了element 组件库的日期选择器,其中灌完介绍, picker-options中函数disabledDate可以设置禁用状态,代码如下: <el-date-pickerv…

Oracle11g登录方法

刚部署完Oracle11g可以使用如下方法登录 方法1 sqlplus sys/oracle as sysdba; 用系统用户登录 create user xy identified by test123; 创建用户 grant connect, resource,dba to xy; 授予权限 之后退出执行sqlplus登录命令 使用创建的用户登录 方法2 sqlplus ---登录…

网络工程师常用协议之ICMP协议原理与应用

Internet控制消息协议ICMP&#xff08;Internet Control Message Protocol&#xff09;是网络层的一个重要协议。ICMP协议用来在网络设备间传递各种差错和控制信息&#xff0c;并对于收集各种网络信息、诊断和排除各种网络故障等方面起着至关重要的作用。使用基于ICMP的应用时&…

java SSM教师业绩管理系统myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

一、源码特点 java SSM教师业绩管理系统是一套完善的web设计系统&#xff08;系统采用SSM框架进行设计开发&#xff0c;springspringMVCmybatis&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代 码和数据库&#xff0c;系统主要采…

HTML有哪些列表以及具体的使用!!!

文章目录 一、HTML列表二、列表的应用1、无序列表2、有序列表3、自定义列表 三、总结 一、HTML列表 html的列表有三种&#xff0c;一种是无序列表&#xff0c;一种是有序列表&#xff0c;还有一种为自定义列表。 二、列表的应用 1、无序列表 <ul> <li>无序列表…

基于CNN+数据增强+残差网络Resnet50的少样本高准确度猫咪种类识别—深度学习算法应用(含全部工程源码)+数据集+模型(二)

系列文章目录 基于CNN数据增强残差网络Resnet50的少样本高准确度猫咪种类识别—深度学习算法应用(含全部工程源码)数据集模型&#xff08;一&#xff09; 基于CNN数据增强残差网络Resnet50的少样本高准确度猫咪种类识别—深度学习算法应用(含全部工程源码)数据集模型&#xf…

社交心理学视角下的TikTok:用户是如何被吸引的?

TikTok作为一款风靡全球的短视频应用&#xff0c;成功吸引了数以亿计的用户。这引发了一个问题&#xff0c;TikTok到底具有何种魅力&#xff0c;是如何在社交心理学层面吸引用户的呢&#xff1f;本文将从社交心理学的角度出发&#xff0c;深入剖析TikTok的吸引力因素&#xff0…

理性消费成主流!国台国标酒高性价比火热出圈

近日&#xff0c;国台酒业集团获得2023财联社第六届投资年会“年度最具价值品牌奖”。2023年国台品牌价值达2062.68亿元&#xff0c;稳居中国白酒第十名、贵州白酒第三名。 随着酱酒龙头企业官宣提高出厂价&#xff0c;高品质酱酒进一步被市场关注&#xff0c;同样源于茅台镇核…

某音上很火的圣诞树分享

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 效果截图&#xff08;这里不给动态了&#xff0c;某音到处都是了&#xff09;&#xff1a; 源代码&#xff1a; <script src"…

hive企业级调优策略之Join优化

测试所用到的数据参考&#xff1a; 原文链接&#xff1a;https://blog.csdn.net/m0_52606060/article/details/135080511 本教程的计算环境为Hive on MR。计算资源的调整主要包括Yarn和MR。 Join算法概述 Hive拥有多种join算法&#xff0c;包括Common Join&#xff0c;Map …

【离散数学】——期末刷题题库(树其一)

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

【微服务】springboot整合minio详解

目录 一、前言 二、Minio 概述 2.1 Minio简介 2.1 Minio特点 三、Minio 环境搭建 3.1 部署过程 3.1.1 拉取镜像 3.1.2 启动容器 3.1.3 访问web页面 四、Minio基本使用 4.1 基本概念 4.2 上传文件演示 4.3 用户管理 4.4 Java操作Minio 4.4.1 导入依赖 4.4.2 上传…

算法专题二:滑动窗口

算法专题二&#xff1a;滑动窗口 一.长度最小的子数组&#xff1a;1.思路一&#xff1a;暴力解法2.思路二&#xff1a;滑动窗口双指针3.GIF题目解析&#xff1a;思路一&#xff1a;思路二&#xff1a; 二.无重复字符的最长子串&#xff1a;1.思路一&#xff1a;滑动窗口2.GIF题…