Springboot 使用【过滤器】实现在请求到达 Controller 之前修改请求体参数和在结果返回之前修改响应体
文章目录
- 前情提要
- 解决方案
- 自定义 HttpServletRequest 包装类 RequestWrapper
- 自定义 HttpServletResponse 包装类 ResponseWrapper
- 自定义过滤器 MiddlewareFilter
- 配置过滤器
- 注解
- 配置类
- 编写 Controller 测试
前情提要
在项目中需要使用过滤器 在请求调用 Controller 方法前修改请求参数和在结果返回之前修改返回结果。
在 Controller 中定义如下接口:
@PostMapping("/hello")
public JSONObject hello(@RequestBody Map<String, Object> params) {return JSONObject.parseObject(JSON.toJSONString(params));
}
定义的过滤器如下:
public class ServNoFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {// 获取请求体内容String requestBody = getRequestBody(httpServletRequest);// 业务处理......// 放行filterChain.doFilter(httpServletRequest, httpServletResponse);}private String getRequestBody(HttpServletRequest request) throws IOException {BufferedReader reader = new BufferedReader(request.getReader());StringBuilder sb = new StringBuilder();String line;while ((line = reader.readLine()) != null) {sb.append(line);}return sb.toString();}
}
此时启动项目,访问接口,则会在控制台打印如下异常信息:Request processing failed; nested exception is java.lang.IllegalStateException: getReader() has already been called for this request
。
表示在过滤器中已经通过 request.getReader()
方法将请求流读取。
如果在过滤器中将 getReader()
换成 getInputStream()
就会报请求体为空异常:org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing
。
这是因为在 Servlet 中,请求对象的输入流只能被读取一次。而在第一次读取请求体时,Servlet 容器会将请求体保存在内存中,并将其解析成相应的请求参数和请求头信息。如果在后续的处理中再次读取请求体,就可能会导致数据错误或异常。
解决方案
自定义 HttpServletRequest 包装类 RequestWrapper
在 Servlet 中,原始的 HttpServletRequest
对象中的请求流(即请求体)只能读取一次。这是因为 HTTP 协议是基于流的协议,服务器在读取请求流时会将其消耗掉,一旦读取完毕,就无法再次读取
当 Servlet 容器读取完请求流后,会将请求的内容解析并储存在相应的属性中,如请求参数、请求头信息等。在后续的处理过程中,Servlet 可以从这些属性中获取请求内容,而不必再次读取请求流。
因此,我们需要自定义 RequestWrapper
将请求流保存下来,并提供方法来多次读取请求体的内容。
自定义 HttpServletRequest
包装类 RequestWrapper
如下:
/*** HttpServletRequest 包装类,允许在 Servlet 中多次读取请求体内容* 重写了 getInputStream()方法和 getReader() 方法,返回可以多次读取的流。*/
public class RequestWrapper extends HttpServletRequestWrapper {private final byte[] body;/*** 构造 RequestWrapper 对象** @param request 原始 HttpServletRequest 对象* @param context 请求体内容*/public RequestWrapper(HttpServletRequest request, String context) {super(request);this.body = context.getBytes(StandardCharsets.UTF_8);}/*** 重写 getInputStream 方法,返回经过包装后的 ServletInputStream 对象** @return 经过包装后的 ServletInputStream 对象*/@Overridepublic ServletInputStream getInputStream() {return new ServletInputStreamWrapper(new ByteArrayInputStream(body));}/*** 重写 getReader 方法,返回经过包装后的 BufferedReader 对象** @return 经过包装后的 BufferedReader 对象*/@Overridepublic BufferedReader getReader() {return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));}/*** 私有内部类,用于包装 ServletInputStream 对象*/private static class ServletInputStreamWrapper extends ServletInputStream {private final ByteArrayInputStream inputStream;/*** 构造函数,传入待包装的 ByteArrayInputStream 对象** @param inputStream 待包装的 ByteArrayInputStream 对象*/public ServletInputStreamWrapper(ByteArrayInputStream inputStream) {this.inputStream = inputStream;}/*** 重写 read 方法,读取流中的下一个字节** @return 读取到的下一个字节,如果已达到流的末尾,则返回-1*/@Overridepublic int read() {return inputStream.read();}/*** 覆盖 isFinished 方法,指示流是否已完成读取数据** @return 始终返回 false,表示流未完成读取数据*/@Overridepublic boolean isFinished() {return false;}/*** 重写 isReady 方法,指示流是否准备好进行读取操作** @return 始终返回 false,表示流未准备好进行读取操作*/@Overridepublic boolean isReady() {return false;}/*** 重写 setReadListener 方法,设置读取监听器** @param readListener 读取监听器*/@Overridepublic void setReadListener(ReadListener readListener) {}}
}
自定义 HttpServletResponse 包装类 ResponseWrapper
与请求流(即请求体)一样,原始的 HttpServletResponse
对象中的响应流(即响应体)只能写入一次。当服务器在向客户端发送响应时,会将响应流写入到网络传输通道中,一旦写入完毕,就无法再次修改或写入。
因此我们需要通过自定义 ResponseWrapper
包装原始的 HttpServletResponse
对象并重写其输出流或者输出写方法,从而实现对响应流的修改和控制。
自定义 HttpServletResponse
包装类 ResponseWrapper
如下:
/*** HttpServletResponse 包装类对,提供对响应数据的处理和操作。*/
public class ResponseWrapper extends HttpServletResponseWrapper {private final ByteArrayOutputStream outputStream;private ServletOutputStream servletOutputStream;private PrintWriter writer;/*** 构造函数,传入原始的 HttpServletResponse 对象** @param response 原始的 HttpServletResponse 对象*/public ResponseWrapper(HttpServletResponse response) {super(response);this.outputStream = new ByteArrayOutputStream();}/*** 重写 getOutputStream 方法,返回经过包装后的 ServletOutputStream 对象** @return 经过包装后的 ServletOutputStream 对象*/@Overridepublic ServletOutputStream getOutputStream() {if (servletOutputStream == null) {servletOutputStream = new ServletOutputStreamWrapper(outputStream);}return servletOutputStream;}/*** 重写 getWriter 方法,返回经过包装后的 PrintWriter 对象** @return 经过包装后的 PrintWriter 对象*/@Overridepublic PrintWriter getWriter() {if (writer == null) {writer = new PrintWriter(getOutputStream());}return writer;}/*** 获取响应数据,并指定字符集** @param charsetName 字符集名称* @return 响应数据字符串*/public String getResponseData(String charsetName) {Charset charset = Charset.forName(charsetName);byte[] bytes = outputStream.toByteArray();return new String(bytes, charset);}/*** 设置响应数据,并指定字符集** @param responseData 响应数据字符串* @param charsetName 字符集名称*/public void setResponseData(String responseData, String charsetName) {Charset charset = Charset.forName(charsetName);byte[] bytes = responseData.getBytes(charset);outputStream.reset();try {outputStream.write(bytes);} catch (IOException e) {// 处理异常}setCharacterEncoding(charsetName);}/*** 私有内部类,用于包装 ServletOutputStream 对象*/private static class ServletOutputStreamWrapper extends ServletOutputStream {private final ByteArrayOutputStream outputStream;/*** 构造函数,传入待包装的 ByteArrayOutputStream 对象** @param outputStream 待包装的 ByteArrayOutputStream 对象*/public ServletOutputStreamWrapper(ByteArrayOutputStream outputStream) {this.outputStream = outputStream;}/*** 重写 write 方法,将指定字节写入输出流** @param b 字节*/@Overridepublic void write(int b) {outputStream.write(b);}/*** 重写 isReady 方法,指示输出流是否准备好接收写入操作** @return 始终返回 false,表示输出流未准备好接收写入操作*/@Overridepublic boolean isReady() {return false;}/*** 重写 setWriteListener 方法,设置写入监听器** @param writeListener 写入监听器*/@Overridepublic void setWriteListener(WriteListener writeListener) {}}
}
自定义过滤器 MiddlewareFilter
我们的需求是:在请求到达服务器之前,对请求参数进行修改;在响应返回之前,对响应结果进行处理。
对于这样的需求,我们可以通过自定义过滤器来实现。大致实现思路如下:
-
修改请求参数(请求体),我们可以:
- 获取请求体内容。
- 修改请求体内容。
- 将修改后的请求对象替换原来的请求对象,以便后续获取修改后的参数。
-
修改响应结果(响应体),我们可以:
- 获取响应数据。
- 对响应数据进行处理。
- 将修改后的数据作为最终结果返回。
同时为了确保每个请求在请求时只会被过滤一次,我们可以继承 OncePerRequestFilter
来定义自己的过滤器。
最终,自定义过滤器如下:
public class MiddlewareFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {// 1. 从 HttpServletRequest 对象中获取请求体内容String requestBody = getRequestBody(httpServletRequest);// 2. 解析请求体内容为JSON对象JSONObject jsonBody = JSONObject.parseObject(requestBody);// 3. 修改请求体内容jsonBody.put("paramKey","paramValue");// 4. 包装 HttpServletRequest 对象为自定义的 RequestWrapper 对象,以便后续的处理RequestWrapper requestWrapper = new RequestWrapper(httpServletRequest, jsonBody.toJSONString());// 5. 包装 HttpServletResponse 对象为自定义的 ResponseWrapper 对象,以便后续的处理ResponseWrapper responseWrapper = new ResponseWrapper(httpServletResponse);// 6. 调用下一个过滤器或 ServletfilterChain.doFilter(requestWrapper, responseWrapper);// 7. 获取响应数据String responseData = responseWrapper.getResponseData(StandardCharsets.UTF_8.name());// 8. 解析响应数据为JSON对象JSONObject jsonData = JSONObject.parseObject(responseData);// 9. 在这里可以对响应数据进行处理jsonData.put("responseKey", "responseValue");// 10. 将修改后的 JSON 对象转换为字符串responseData = jsonData.toJSONString();// 11. 将修改后的 JSON 对象设置为最终的响应数据responseWrapper.setResponseData(responseData, StandardCharsets.UTF_8.name());// 12. 将响应数据写入原始的响应对象,解决响应数据无法被多个过滤器处理问题OutputStream outputStream = httpServletResponse.getOutputStream();outputStream.write(responseData.getBytes(StandardCharsets.UTF_8));outputStream.flush();}/*** 获取请求体内容。** @param request HttpServletRequest对象* @return 请求体内容* @throws IOException 如果读取请求体内容时发生I/O异常*/private String getRequestBody(HttpServletRequest request) throws IOException {BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));StringBuilder sb = new StringBuilder();String line;while ((line = reader.readLine()) != null) {sb.append(line);}return sb.toString();}
}
配置过滤器
注解
通过 Java Servlet 3.0 规范中引入的 @WebFilter
注解配置过滤器。
@WebFilter
注解可以应用在实现了 Filter
接口或继承自 OncePerRequestFilter
的类上,标识该类为过滤器,并指定过滤器的相关配置,包括拦截的 URL 路径、执行顺序以及初始化参数等。
我们可以在 MiddlewareFilter
过滤器上使用 @WebFilter
注解注册该过滤器并指定执行该过滤器执行的顺序和拦截的 URL:
@WebFilter(value = "1000", urlPatterns = "/hello")
public class MiddlewareFilter extends OncePerRequestFilter {......
}
value
:设置过滤器的执行顺序,数字越小,优先级越高。urlPatterns
:指定要拦截的 URL 路径,允许指定多个 URL 路径urlPatterns = {"/hello","/hello1"}
。
还需要再启动类上使用@ServletComponentScan
注解扫描和注册带有 @WebServlet
、@WebFilter
和 @WebListener
注解的组件:
@ServletComponentScan
@SpringBootApplication
public class Demo1Application {public static void main(String[] args) {SpringApplication.run(Demo1Application.class, args);}}
配置类
除了注解的形式配置过滤器,我们还可以通过配置类的形式进行配置。
创建 FilterConfig
类用于配置需要注册的过滤器,同时在类上添加 @Configuration
注解,标识该类为配置类,在项目启动时 Spring 会自动扫描该类中的 Bean 定义,并将其加载到容器中:
@Configuration
public class FilterConfig {@Beanpublic FilterRegistrationBean<MiddlewareFilter> middlewareFilter() {FilterRegistrationBean<MiddlewareFilter> registration = new FilterRegistrationBean<>();registration.setFilter(new MiddlewareFilter()); // 设置过滤器实例registration.addUrlPatterns("/hello"); // 拦截的 URL 路径registration.setOrder(1000); // 设置过滤器执行顺序(数字越小,越先执行)return registration;}}
在类中我们定义了名为 middlewareFilter
的方法,用于注册我们自定义的 MiddlewareFilter
过滤器。
在 方法中,创建了一个 FilterRegistrationBean
对象用于注册和配置过滤器,并设置 MiddlewareFilter
对象作为过滤器实例,指定了过滤器要拦截的 URL 路径,滤器执行顺序。
最后将 FilterRegistrationBean
对象返回,以便 Spring 自动进行注册和管理。
编写 Controller 测试
创建两个接口,同样的逻辑,接收一个请求体参数 params
,再将接收的参数以 JSON 格式返回:
@RestController
public class BasicController {/*** 处理 /hello 请求的方法* @param params 请求体参数,以键值对的形式传递* @return 经过转换后的 JSONObject 对象*/@PostMapping("/hello")public JSONObject hello(@RequestBody Map<String, Object> params) {return JSONObject.parseObject(JSON.toJSONString(params));}@PostMapping("/hello1")public JSONObject hello1(@RequestBody Map<String,Object> params) {return JSONObject.parseObject(JSON.toJSONString(params));}
}
启动项目,在 ApiFox 中分别以同样的请求参数发送 POST
请求调用 /hello
、/hello1
接口:
-
请求参数:
{"name": "hello","age": 20 }
-
/hello
接口返回结果:{"paramKey": "paramValue","responseKey": "responseValue","name": "hello","age": 20 }
-
/hello1
接口返回结果:{"name": "hello","age": 20 }
复制多个 MiddlewareFilter
过滤器模拟多层过滤器修改请求体参数和返回结果,测试结果如下:
{"paramKey": "paramValue", //过滤器1"responseKey2": "responseValue2", //过滤器2"responseKey": "responseValue", //过滤器2"paramKey2": "paramValue2", //过滤器1"name": "hello","age": 20
}
相关文章:

Springboot 使用【过滤器】实现在请求到达 Controller 之前修改请求体参数和在结果返回之前修改响应体
文章目录 前情提要解决方案自定义 HttpServletRequest 包装类 RequestWrapper自定义 HttpServletResponse 包装类 ResponseWrapper自定义过滤器 MiddlewareFilter配置过滤器注解配置类 编写 Controller 测试 前情提要 在项目中需要使用过滤器 在请求调用 Controller 方法前修改…...

Unity 3D常用的数据结构
目录 数组使用场景 ArrayList数组ArrayList的缺点 List\<T\>数组List\<T\>有以下3点好处 链表链表与数组的不同之处链表的优势数组和链表的应用场景 LinkedList\<T\>C#中内置的双向链表LinkedList使用场景 队列(Queue\<T\>)和栈…...

h5唤起微信小程序
wx-open-launch-weapp 就用这个 开放标签属于自定义标签,Vue会给予未知标签的警告,可通过配置Vue.config.ignoredElements [wx-open-launch-weapp] 来忽略Vue对开放标签的检查。 sdk授权。 调试打开时iOS会弹窗 noPermissionJsApi: [],confi…...

面向对象(精髓)变继承关系为组和关系(_Decorator模式)
在软件开发中,设计模式是解决常见问题的可重用解决方案。在面向对象编程中,继承和组合是两种常用的代码复用方式。然而,随着软件需求的不断变化,我们需要更灵活的设计方式来应对不断变化的需求。在本文中,我们将讨论从…...

MES系统在智能生产中的重要作用
在未来智能制造的发展趋势中,制造执行系统(MES)作为关键技术和工具,扮演着至关重要的角色。随着科技的不断进步和制造业的数字化转型,MES的地位将愈发凸显,对于企业实现智能化生产、提高效率、降低成本具有…...

2024.3.13每日一题
LeetCode 最大二进制奇数 题目链接:2864. 最大二进制奇数 - 力扣(LeetCode) 题目描述 给你一个 二进制 字符串 s ,其中至少包含一个 1 。 你必须按某种方式 重新排列 字符串中的位,使得到的二进制数字是可以由该组…...

YOLOv5 | 涨点复现!YOLOv5添加BiFPN有效提升目标检测精度
目录 🚀🚀🚀订阅专栏,更新及时查看不迷路🚀🚀🚀 介绍: BiFPN 代码实现 ⭐欢迎大家订阅我的专栏一起学习⭐ 🚀🚀🚀订阅专栏,更新及…...

【Nut3】nuxt.config.ts项目nuxt配置文件介绍
简言 记录下nuxt3的nuxt.config.ts文件的介绍和使用。 Nuxt Configuration nuxt.config.ts Nuxt可以通过一个单独的nuxt.config文件进行简单配置。 配置文件创建 nuxt.config文件的扩展名可以是.js、.ts或.mjs。 然后默认导出全局函数defineNuxtConfig的返回值,…...

区块链技术的革命性影响
1. 区块链技术的基本原理: 区块链是一种去中心化的分布式数据库技术,通过不断增长的记录(块)构成一个链式结构。每个区块包含了交易数据的加密信息以及上一个区块的哈希值,从而形成了不可篡改的交易记录。这种去中心化…...

多线程(volatile)
volatile的功能 保证内存可见性禁止指令重排序 内存可见性 简单的理解 两(多)个线程同时针对一个变量进行操作, 一个线程读, 一个线程修改, 此时读到的值不一定是修改过后的值 即读线程没有感知到变量的变化 (其实是 编译器/JVM 对于代码在多线程情况下的优化进行了误判) 从 J…...

蓝桥杯 填空 卡片
蓝桥杯 填空题 卡片 解题思路: 我们只需要消耗完卡片的个数即可。 代码示例: #include<bits/stdc.h> using namespace std; int a[10]; bool isEnd(){for(int i0;i<10;i){if(a[i]-1)return false;}return true; } bool getN(int x){while(x){i…...

ELK介绍使用
文章目录 一、ELK介绍二、Elasticsearch1. ElasticSearch简介:2. Elasticsearch核心概念3. Elasticsearch安装4. Elasticsearch基本操作1. 字段类型介绍2. 索引3. 映射4. 文档 5. Elasticsearch 复杂查询 三、LogStash1. LogStash简介2. LogStash安装 四、kibana1. …...

【UE5】非持枪状态蹲姿移动的动画混合空间
项目资源文末百度网盘自取 在BlendSpace文件夹中单击右键选择动画(Animation)中的混合空间(Blend Space) ,选择SK_Female_Skeleton,命名为BS_NormalCrouch 打开BS_NormalCrouch 水平轴表示角色的方向,命名为Direction,方向的最…...

Windows C++ TCP开发(使用select函数以及设置非阻塞/Reuse属性)
1、select官方函数说明: 语法 C int WSAAPI select([in] int nfds,[in, out] fd_set *readfds,[in, out] fd_set *writefds,[in, out] fd_set *exceptfds,[in] const timeval *timeout );参数 [in] nfds 已忽略。 包含 nf…...

ARM TrustZone技术解析:构建嵌入式系统的安全扩展基石
🌈个人主页: Aileen_0v0 🔥热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法|MySQL| 💫个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-LOdvohfCEnd8eKyd {font-family:"trebuchet ms",verdana,arial,sans-serif;f…...

初识Python语言-课堂练习【pyhton123题库】
初识Python语言-课堂练习【pyhton123题库】 一、单项选择题 1、Guido van Rossum正式对外发布Python版本的年份是: A 2008B 1998C 1991D 2002 【答案】C 【解析】暂无解析2、下面不是Python语言特点的是:…...

chrome高内存占用问题
chrome号称内存杀手不是盖的,不设设置的话,经常被它内存耗尽死机是常事。以下自用方法 1 自带的memory saver chrome://settings/performance PerformanceMemory Saver When on, Chromium frees up memory from inactive tabs. This gives active tab…...

【C语言】文件操作篇-----程序文件和数据文件,文件的打开和关闭,二进制文件和文本文件,fopen,fclose【图文详解】
欢迎来CILMY23的博客喔,本篇为【C语言】文件操作篇-----程序文件和数据文件,文件的打开和关闭,二进制文件和文本文件【图文详解】,感谢观看,支持的可以给个一键三连,点赞关注收藏。 前言 在了解完动态内存管…...

知识碎片收集
目录 1. 如何计算两点经纬度之间的距离2. 加权随机采样3.什么时LLDB和GDB 1. 如何计算两点经纬度之间的距离 1.知乎-如何计算两点经纬度间距离 2.根据两点经纬度坐标计算距离 3.根据经纬度计算两点之间的距离的公式推导过程以及google.maps的测距函数 4.根据经纬度点计算经…...

不可不知!用例图的绘制与应用全指南深度解析
在软件开发领域中,用例图是一种强大的工具,用于描述系统的功能需求以及系统与外部实体之间的交互。无论是在需求分析阶段还是在系统设计过程中,用例图都扮演着至关重要的角色。本文将全面介绍用例图的绘制方法和其在软件开发中的应用…...

【数据结构七】堆与PriorityQueue详解
堆 在Java中有一种数据结构基于队列,并保证操作的数据带有优先级,该数据结构应该提供了两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。它的底层使用了堆这种数据结…...

uniapp写支付的操作
支付的时候一般需要几个参数: ‘timeStamp’: 时间戳,‘nonceStr’: 随机字符串,不超过32位‘package’: 下单后接口返回的prepauid‘signType’: 签名的算法‘paySign’: 后端会给前端一个签名sign: data.sign // 根据签名算法生成签名 <template&…...

微信小程序开发系列(二十四)·wxml语法·列表渲染·wx:for-item 和 wx:for-index
目录 1. 如果需要对默认的变量名和下标进行修改,可以使用wx:for-item 和 wx:for-index 2. 将 wx:for 用在 标签上,以渲染一个包含多个节点的结构块 方法一 方法二 3. 总结 3.1 wx:for-item 和 wx:for-index总结 3.2 总结 1. 如果需要对默…...

下载无水印抖音视频
在抖音看到某些视频想下载,却出现无法保存在本地【显示"作品暂时无法保存,链接已复制"】。或者下载的视频有水印。 而某些微信小程序下载可能需要付费或者有水印。其实我们可以直接使用电脑浏览器直接下载。 举个例子: 这是来自王道官方账号的一条视频链…...

L1-039 古风排版(C++)
中国的古人写文字,是从右向左竖向排版的。本题就请你编写程序,把一段文字按古风排版。 输入格式: 输入在第一行给出一个正整数N(<100),是每一列的字符数。第二行给出一个长度不超过1000的非空字符串&a…...

springboot项目docker分层构建
一、需求场景 在使用dockerfile构建springboot项目时,速度较慢,用时比较长,为了加快构建docker镜像的速度,采用分层构建的方式 二、构建配置 1、pom.xml配置 <properties><project.build.sourceEncoding>UTF-8<…...

深入理解SPA、CSR与SSR的区别及应用
随着Web技术的快速发展,前端开发架构也在不断演进。在现代Web应用中,单页面应用(SPA)、客户端渲染(CSR)和服务器端渲染(SSR)是三种常见的实现方式,它们各自拥有独特的特性…...

基于电鳗觅食优化算法(Electric eel foraging optimization,EEFO)的无人机三维路径规划(提供MATLAB代码)
一、无人机路径规划模型介绍 无人机三维路径规划是指在三维空间中为无人机规划一条合理的飞行路径,使其能够安全、高效地完成任务。路径规划是无人机自主飞行的关键技术之一,它可以通过算法和模型来确定无人机的航迹,以避开障碍物、优化飞行…...

将SQL数据库转换为Mysql数据库
一、准备工作 1、SQL server安装包与已经有数据的mdf、ldf数据库文件; 2、.net Framework安装包;(用于支持SQL Server安装的组件) 3、MySql安装包;(用于目标数据库的环境安装) 4、navicat安装包…...

Java集合进阶
双列集合 单列集合的特点:一次添加一个。 双列集合的特点:一次添加一对/键值对/键值对对象/Entry。 左键(不可重复)右值(可重复),一一对应。 Map是双列集合的顶层接口,他的功能是…...