JavaWeb-HttpServletRequest请求域接口

文章目录

  • HttpServletRequest请求域接口
    • HttpServletRequest请求域接口简介
    • 关于请求域和应用域的区别
  • 请求域接口中的相关方法
    • 获取前端请求参数(getParameter系列方法)
    • 存储请求域名参数(Attribute系列方法)
    • 获取客户端的相关地址信息
    • 获取项目的根路径
  • 关于转发和重定向的细致剖析
    • 转发代码实现及相关问题
    • 重定向代码实现及相关问题

HttpServletRequest请求域接口

HttpServletRequest请求域接口简介

其实关于请求域这个词也蛮熟悉的, 因为我们之前学习过 应用域 这一概念, 应用域的生命周期很长, 伴随这服务器的启动和终止, 作用范围也很广, 对所有的处于当前 webapp 也就是 web 应用的所有Servlet对象都生效


  • HttpServletRequest 是位于jakarta.servlet.http.*包下面的一个接口

  • 继承了ServletRequest接口
    public interface HttpServletRequest extends ServletRequest

  • 之前我们学习过HTTP协议的相关内容, 这个对象中封装的其实就是网络传输的时候, 发送的HTTP请求(Request)中封装的相关参数内容信息

  • 实现这个接口是Tomcat服务器实现的, 传递对象封装参数也是Tomcat服务器完成好的内容, 我们作为Java程序员, 只需要学习获取其中封装的相关参数即可


关于请求域和应用域的区别

  • 生命周期不同, 应用域伴随着Tomcat的生命周期 而 请求域 只作用域这一次请求之内, 而且http协议的特点就是, 一次请求一次创建一次请求域对象
  • 而且在进行参数设定的时候, 尽量的去选择请求域的参数而不是应用域的参数, 因为小的域的对象占用的资源比较小

请求域接口中的相关方法

上面都说了, 请求域是封装了相关的http协议的参数信息, 所以必定提供了一些方法来让我们程序员获取到这些参数的信息…


获取前端请求参数(getParameter系列方法)

首先我们思考, 前端传递过来的参数应该采用什么数据结构来组织比较好

我们从下面的前端的页面中获取信息

<!DOCTYPE html>
<html lang='en'>
<head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1.0'><title>个人信息</title>
</head>
<body><h2>个人信息</h2><form action="" method="get">姓名:<input type="text" name="name" value=""><br>年龄:<input type="text" name="age" value=""><br>性别:<input type="radio" name="sex" value=""><input type="radio" name="sex" value=""><br>爱好:<input type="checkbox" name="hobby" value="吃饭"><input type="checkbox" name="hobby" value="睡觉"><input type="checkbox" name="hobby" value="打游戏"><br><input type="submit" value="提交"></form>
</body>
</html>

我们执行 http://127.0.0.1:8080/servlet08/test.html

在这里插入图片描述

我们对URL拆解如下(涉及URLEncoding)

在这里插入图片描述

我们可以发现, 前端向后端提交数据的格式其实并不是单纯的键值对的结构存储的, 因为如果是键值对结构存储的话, 一个key只能对应一个value, 但是复选框这种提交信息的结构, 一个key可以对应多个value信息

所以实际上, 我们的前端发来的数据存储格式是一个特殊的map集合

Map<String, String[]> map = new HashMap<>();

一个String类型的key可以对应一个String[] 数组, 也就是多个value(前端传递参数都是String类型)


上面是一个html页面, 其中包含test类型文本, 单选框, 复选框

在这里插入图片描述

上面的方法的解释:

  • String getParameter(String name): 根据key返回String数组中的第一个参数
  • String[] getParameterValues(String name): 根据key返回完整的String[]数组
  • Enumeration< String > getParameterNames(): 返回一个由所有key组成的集合
  • Map<String, String[]> getParameterMap(): 返回一个key和value组成的完整的集合

还拿我们上面写的那个html页面进行测试(设置一下传递的Servlet路径地址)
(注意, 我们下面的method其实写错了, 实际上是post请求)

在这里插入图片描述

Servlet对象的源码如下

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;import java.io.*;
import java.util.Enumeration;
import java.util.Map;@WebServlet(urlPatterns = "/getparameter")
public class GetParameterServlet extends HttpServlet {// 由于是form表单提交的数据, 我们尽量采用重写doPost的方式进行测试@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {// 最好还是设置一下字符集, 防止出现乱码response.setContentType("text/html;charset=UTF-8");PrintWriter out = response.getWriter();// 1. 使用getParameterMap获取整个的map形式参数集合out.print("<h3>使用getParameterMap获取整个集合</h3>");Map<String, String[]> parameterMap = request.getParameterMap();for(Map.Entry<String, String[]> entry : parameterMap.entrySet()){out.print(entry.getKey() + "=");for(String value : entry.getValue()){out.print(value + " ");}}out.print("<br>");out.print("====================================<br>");// 2. 使用getParameterNames获取整个参数集合的keyout.print("<h3>使用getParameterNames获取整个集合中的key</h3>");Enumeration<String> parameterNames = request.getParameterNames();while(parameterNames.hasMoreElements()){String name = parameterNames.nextElement();out.print(name + " ");}out.print("<br>");out.print("====================================<br>");// 3. 使用getParameterValues, 根据key获取参数集合的value数组String[] hobbys = request.getParameterValues("hobby");out.print("hobby=");for(String hobby : hobbys){out.print(hobby + " ");}out.print("<br>");out.print("====================================<br>");// 4. 使用getParameter, 根据key获取到value数组中的第一个值String name = request.getParameter("name");out.print("name=" + name);out.print("<br>");out.print("====================================<br>");}
}

测试:

下面是form表单中提交的数据信息

在这里插入图片描述


下面是在浏览器中输出的内容

在这里插入图片描述


存储请求域名参数(Attribute系列方法)

Attribute 这个词其实我们很熟悉了, 因为之前学习ServletContext就出现过这个词, 也出现了和下面一模一样的一系列方法, 当时是设置应用域对象, 但是现在是设置请求域对象

在这里插入图片描述
在这里插入图片描述

  • void setAttribute(String name, Object o): 设置请求域参数
  • Object getAttribute(String name): 获取请求域参数
  • Enumeration< String > getAttributeNames(): 获取所有请求域的key组成的集合
  • void removeAttribute(String name): 移除 key 为参数的请求域信息

没啥可说的, 直接上测试代码

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;import java.io.*;
import java.util.Enumeration;// 使用注解来配置Servlet
@WebServlet("/attribute")
public class AttributeInfoServlet extends HttpServlet {// 重写doGet方法@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {response.setContentType("text/html");PrintWriter out = response.getWriter();// 1. 使用 void setAttribute(String name, Object o) 设置请求域参数request.setAttribute("name", "Jack");request.setAttribute("age", 18);// 2. 使用 Object getAttribute(String name) 获取请求域参数Object name = request.getAttribute("name");Object age = request.getAttribute("age");out.print("<h3>" + name + " " + age + "</h3>");out.print("<br>========================================<br>");// 3. 使用 Enumeration< String > getAttributeNames() 获取所有的请求域参数key集合Enumeration<String> attributeNames = request.getAttributeNames();while (attributeNames.hasMoreElements()) {String attributeName = attributeNames.nextElement();out.print("<h3>" + attributeName + "</h3>");}out.print("<br>========================================<br>");// 4. 使用 void removeAttribute(String name) 移除参数request.removeAttribute("name");Object name1 = request.getAttribute("name");out.print("<h3>" + name1 + "</h3>");}
}

测试结果如下

在这里插入图片描述

获取客户端的相关地址信息

我们需要掌握下面的三个获取地址相关信息的方法

  • getRemoteAddr(): 获取客户端主机IP
  • getRemotePort(): 获取客户端的应用端port(端口号)
  • getRemoteHost(): 获取客户端主机名称

下面是关于上面的三个方法的测试代码(我们直接给出)

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;import java.io.*;// 使用注解代替web.xml进行Servlet的配置
@WebServlet(urlPatterns = "/addr")
public class GetAddr extends HttpServlet {// 重写doGet方法@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {// 设置返回的类型以及获取输出流信息response.setContentType("text/html");PrintWriter out = response.getWriter();// 1. 使用getRemoteAddr获取客户端的IP信息String remoteAddr = request.getRemoteAddr();out.print("<h3>客户端的IP地址为</h3><br>");out.print(remoteAddr);out.print("<br>=====================================");// 2. 使用getRemotePort获取客户端的端口号的信息int remotePort = request.getRemotePort();out.print("<h3>客户端的端口号为</h3><br>");out.print(remotePort);out.print("<br>======================================");// 3. 使用getRemoteHost获取客户端的主机名称String remoteHost = request.getRemoteHost();out.print("<h3>客户端的主机名号为</h3><br>");out.print(remoteHost);}
}

在浏览器上面访问这个资源, 可以得到下面的内容

在这里插入图片描述

注意我们的端口号其实不是固定的, 每一次请求的端口号都是在一个范围之内进行随机的, 因为我们的规范建议客户端的端口号设置为变化的, 服务器端的端口号设置为不变的…


获取项目的根路径

这个方法其实用的还是很多的, 因为我们在大量的场景中都需要动态获取根路径, 也就是项目路径, 我们在先前的内容中其实也提到过这个方法…

  • getContextPath(): 获取项目部署的路径…

测试就省略了, 主要是想说这个方法的作用非常的重要, 我们好多地方获取项目的路径都需要这个方法…


关于转发和重定向的细致剖析

首先要了解, 不管是转发和重定向, 其目的都是为了实现资源的跳转

也就是Java中有两种方式实现资源的跳转

  • 转发
  • 重定向

转发代码实现及相关问题

在这里插入图片描述
在这里插入图片描述

  • getRequestDispatch(String servletName): 通过给定的转发的ServletName地址获取一个分发器对象
  • forward(request, response): 把当前Servlet对象的请求响应对象作为参数传递到转发当中去, 从而实现位于同一个请求域的作用…

转发的代码实现

首先创建一个AServlet对象(相关注释都在代码中)

import bean.User;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;import java.io.*;// 使用注解信息简化Servlet配置
// 我们把这个 AServlet 作为资源访问的入口, 然后对 BServlet进行资源的转发(所以二者本质上还是一次请求, 共享同一个请求域)
@WebServlet(urlPatterns = "/a")
public class AServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {// 因为要测试转发是不是在一次请求之内转发(也就是多个Servlet共享同一个request和response对象)// 我们设置相关的请求域参数(我们把用户定义在了另一个包当中, 等会我们复制代码就不展示User类了, 应该可以看懂)request.setAttribute("user", new User("huahua", "19", "zz"));// 获取分发器对象, 调用分发器对象的forward方法对这次请求进行转发RequestDispatcher requestDispatcher = request.getRequestDispatcher("/b");requestDispatcher.forward(request, response);}
}

创建一个BServlet对象作为AServlet的转发请求的地址

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import bean.User;import java.io.*;// 使用注解简化Servlet配置
// 这个BServlet作为转发的接收方, 接收AServlet的转发
@WebServlet(urlPatterns = "/b")
public class BServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {response.setContentType("text/html");PrintWriter out = response.getWriter();// 获取AServlet中的应用域的参数Object user = request.getAttribute("user");// 强制类型转化User us = (User) user;// 输出其中的信息内容out.print(us);}
}

在这里插入图片描述

我们现在在浏览器中访问AServlet的资源, 显然, 跳转到了BServlet中

  • 可以发现, 在A中设置的请求域参数, B中同样可以获取, 所以可以判定二者位于同一个请求域
  • 通过URL可以发现, 虽然资源跳转到了BServlet, 但是URL中的地址还是显示的AServlet的地址, 所以我们可以了解到, 其实转发是一种Tomcat服务器内部进行的资源跳转, 和浏览器无关(和重定向区分的重要依据)
  • 根据上面的提示, 我们可以了解到, 转发是同一次请求的转发, 也就是只能在一种方法当中之间进行转发, 全部都在doGet内部转发, 或者全部都在doPost请求中进行转发…

转发不可以在不同的方法之间完成跳转, 测试如下
假设我们把 BServlet中的 doGet 方法转换为 doPost 方法

在这里插入图片描述

其他代码完全不变, 此时再次向AServlet发送请求

在这里插入图片描述

会发现直接报错, 报错信息是 405 method not allowed

其实针对上述问题, 我们还是有解决方案的, 只需要在doGet方法内部调用doPost就可以避免这种问题

在这里插入图片描述

继续访问AServlet, 会发现程序还是可以正常执行
在这里插入图片描述


重定向代码实现及相关问题

和转发不同, 重定向调用的API位于response对象中

通过 response 对象调用 sendReDirect(/项目路径/Servlet路径)方法进行重定向

代码测试

CServlet如下

import bean.User;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;import java.io.*;// 使用注解简化开发
@WebServlet(urlPatterns = "/c")
public class CServlet extends HttpServlet {// 重写doGet方法@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {// 在CServlet类中设置请求域参数, 然后在DServlet中获取这个请求域参数, 查看是否可以获取得到...request.setAttribute("user", new User("huahua", "19", "zz"));// 调用 sendRedirt 进行重定向操作(重定向要加上项目的地址), 重定向至DServletresponse.sendRedirect(request.getContextPath() + "/d");}
}

DServlet

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;import java.io.*;// 使用注解简化开发
@WebServlet(urlPatterns = "/d")
public class DServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {response.setContentType("text/html");PrintWriter out = response.getWriter();// 尝试接收CServlet中设置的请求域参数, 查看是不是可以获取到(其实本质是查看是不是一次请求)Object user = request.getAttribute("user");out.println(user == null ? "不是一个请求" : "是一个请求");}
}

向CServlet发送请求
在这里插入图片描述

获取响应结果如下

在这里插入图片描述

  • 很明显的看到URL中的资源明显的发生了改变

我们不妨抓个包看一看刚才发生了什么

会发现出现了两次请求…

在这里插入图片描述

第一次向CServlet发送了请求, 这是第一次的请求响应信息

在这里插入图片描述

可以发现, 响应时的状态码是 302 Found

在这里插入图片描述

也就是发生了重定向的操作

第二次请求是直接通过浏览器向DServlet发送了一个请求而不是Tomcat资源内部的跳转, 具体不再演示了


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

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

相关文章

deepseek在pycharm 中的配置和简单应用

对于最常用的调试python脚本开发环境pycharm&#xff0c;如何接入deepseek是我们窥探ai代码编写的第一步&#xff0c;熟悉起来总没坏处。 1、官网安装pycharm社区版&#xff08;免费&#xff09;&#xff0c;如果需要安装专业版&#xff0c;需要另外找破解码。 2、安装Ollama…

AAA协议:从零认识网络的“身份管家”

AAA&#xff08;Authentication, Authorization, Accounting&#xff0c;认证、授权和计费&#xff09;是网络世界的“身份管理员”&#xff0c;负责确认“你是谁”、决定“你能干啥”、记录“你干了啥”。如果你用过华三的交换机或路由器&#xff0c;可能在配置用户管理时见过…

动态规划01背包问题系列一>最后一块石头的重量II

这里写目录标题 题目分析&#xff1a;状态表示&#xff1a;状态转移方程&#xff1a;初始化&#xff1a;填表顺序&#xff1a;返回值&#xff1a;代码呈现&#xff1a;优化版本&#xff1a;代码呈现&#xff1a; 题目分析&#xff1a; 状态表示&#xff1a; 状态转移方程&#…

逐行拆解 C 语言:数据类型、变量

今日&#xff0c;我们即将踏上一段充满趣味与挑战的学习之旅&#xff0c;深度钻研数据类型的多样奥秘&#xff0c;解锁变量创建的实用技巧。不仅如此&#xff0c;还会邂逅两个实用的基础库函数&#xff0c;探索它们在程序中穿针引线的奇妙作用。同时&#xff0c;几个简洁却强大…

【音视频】ffplay简单过滤器

一、ffplay简单过滤器 视频旋转&#xff1a;借助transpose滤镜 ffplay -i 1.mp4 -vf transpose1这里选择不同的数字是不同的方向&#xff1a; 视频翻转&#xff1a;借助hflip/vflip实现水平和垂直翻转&#xff1a; 水平翻转 ffplay 1.mp4 -vf hflip垂直翻转 ffplay 1.mp4 …

springboot中注解有什么用

注解&#xff08;Annotation&#xff09;是 Java 的一个重要特性&#xff0c;我用几个具体例子来解释&#xff1a; 1、标记功能 Service // 告诉Spring这是一个服务类 public class UserService { }Data // 告诉Lombok自动生成getter/setter public class User {private…

Excel中COUNTIF用法解析

COUNTIF 是 Excel 中一个非常实用的函数&#xff0c;用于统计满足某个条件的单元格数量。它的基本语法如下&#xff1a; 基本语法 COUNTIF(范围, 条件) 范围&#xff1a;需要统计的单元格区域&#xff0c;例如 A1:A10 或整列 A:A。 条件&#xff1a;用于判断哪些单元格需要被…

java根据List<Object>中的某个属性排序(数据极少,顺序固定)

public static void main(String[] args) { List<HashMap<String, Object>> dydj new ArrayList<>(); // 模拟原始数据 HashMap<String, Object> map1 new HashMap<>(); map1.put(“city_name”, “张家口”); map1.put(“wjs”, 0); map1.put…

4G工业路由器在公交充电桩中的应用与优势

随着电动公交车的普及&#xff0c;公交充电桩的稳定运行和高效管理是交通营运部门最关心的问题。4G工业路由器凭借其卓越的数据采集和通讯能力&#xff0c;成为实现充电桩智能化管理的关键。 公交充电桩运维管理需求概述&#xff1a; 1.实时性&#xff1a;实时监控充电状态、剩…

利用golang embed特性嵌入前端资源问题解决

embed嵌入前端资源&#xff0c;配置前端路由的代码如下 func StartHttpService(port string, assetsFs embed.FS) error {//r : gin.Default()gin.SetMode(gin.ReleaseMode)r : gin.New()r.Use(CORSMiddleware())// 静态文件服务dist, err : fs.Sub(assetsFs, "assets/di…

【LangChain 数据连接封装】 文档加载器、文档处理器

小结&#xff1a; 文档处理部分&#xff0c;建议在实际应用中详细测试后使用与向量数据库的链接部分本质是接口封装&#xff0c;向量数据库需要自己选型类似 LlamaIndex&#xff0c;LangChain 也提供了丰富的 Document Loaders DocumentLoaders和 Text Splitters Text Splitte…

Springboot集成dubbo完整过程(三)

准备工作 1&#xff0c;准备mysql服务环境2&#xff0c;准备redis服务环境3&#xff0c;准备zookeeper服务环境4&#xff0c;准备逆向生成bean的xml配置文件5&#xff0c;准备slf4j日志xml配置文件6&#xff0c;准备一个sql脚本 1&#xff0c;搭建创建服务工程 1&#xff0c;创…

【DeepSeek 】学习编程的利器:DeepSeek 使用指南

学习编程的利器&#xff1a;DeepSeek 使用指南 如果你正苦于如何开始学习 Python/R/Linux/HTML 语法&#xff0c;这个方法或许是你学习过程中的利器&#xff0c;又不用考虑请教真人&#xff0c;麻烦别人。 学习阶段 第一阶段&#xff1a;通读语法书籍 第一步通读一些相关语法…

【大模型篇】目前主流 AI 大模型体系全解析:架构、特点与应用

大家好,我是大 F,深耕AI算法十余年,互联网大厂技术岗。分享AI算法干货、技术心得。 欢迎关注《大模型理论和实战》、《DeepSeek技术解析和实战》,一起探索技术的无限可能! 阅读完本文,您将知道:目前主流的大模型体系有哪些?及其架构的特点。 前言 在自然语言处理(NL…

电池管理系统(BMS)架构详细解析:原理与器件选型指南

BMS&#xff08;电池管理系统&#xff09;架构详细讲解 从你提供的BMS&#xff08;Battery Management System&#xff09;架构图来看&#xff0c;主要涉及到电池监控模块、通信模块、功率控制模块等部分。下面我将详细讲解该架构的各个功能模块及其工作原理。 1. 电池管理核…

决策树(Decision Tree)基础知识

目录 一、回忆1、*机器学习的三要素&#xff1a;1&#xff09;*函数族2&#xff09;*目标函数2.1&#xff09;*模型的其他复杂度参数 3&#xff09;*优化算法 2、*前处理/后处理1&#xff09;前处理&#xff1a;特征工程2&#xff09;后处理&#xff1a;模型选择和模型评估 3、…

洛谷 P3648 APIO2014 序列分割 题解

写了挺多斜率优化的题目了&#xff0c;这道&#xff08;差点&#xff09;就速切了&#xff0c;原因还是单调队列维护斜率的写法出锅。 题意 题目描述 你正在玩一个关于长度为 n n n 的非负整数序列的游戏。这个游戏中你需要把序列分成 k 1 k 1 k1 个非空的块。为了得到 …

策略模式的C++实现示例

核心思想 策略模式是一种行为型设计模式&#xff0c;它定义了一系列算法&#xff0c;并将每个算法封装在独立的类中&#xff0c;使得它们可以互相替换。策略模式让算法的变化独立于使用它的客户端&#xff0c;从而使得客户端可以根据需要动态切换算法&#xff0c;而不需要修改…

Loki+Promtail+Grafana监控K8s日志

在现代云原生架构中&#xff0c;监控与日志管理对于确保系统稳定性和可靠性至关重要。Kubernetes&#xff08;K8s&#xff09;作为当下流行的容器编排平台&#xff0c;对日志的监控管理需求尤为突出。Loki, Promtail 和 Grafana 构成了一套强大的日志监控解决方案&#xff0c;它…

Git 批量合并 Commit 并且保留之前的 Commit 快速实现的思路

文章目录 需求Rebase / Pick / squashVim 的快速全局字符串替换 需求 我想把如下的提交 commit&#xff0c;变成一个 Commit&#xff0c;并且合并这些 Commit 的消息到一个节点 Rebase / Pick / squash 我合并到 5e59217 这个hash 上&#xff0c;这样合并后会保留两个 Commit…