servlet
Servlet 是什么
Servlet 是一种实现动态页面的技术. 是一组 Tomcat 提供给程序猿的 API, 帮助程序猿简单高效的开发一个 web app
动态页面 vs 静态页面
静态页面也就是内容始终固定的页面. 即使 用户不同/时间不同/输入的参数不同 , 页面内容也不会发生变化. (除非网站的开发人员修改源代码, 否则页面内容始终不变)
动态页面指的就是 用户不同/时间不同/输入的参数不同, 页面内容会发生变化
构建动态页面的技术有很多, 每种语言都有一些相关的库/框架来做这件事
Servlet 就是 Tomcat 这个 HTTP 服务器提供给 Java 的一组 API, 来完成构建动态页面这个任务
Servlet 主要做的工作
- 允许程序猿注册一个类, 在 Tomcat 收到某个特定的 HTTP 请求的时候, 执行这个类中的一些代码
- 帮助程序猿解析 HTTP 请求, 把 HTTP 请求从一个字符串解析成一个 HttpRequest 对象
- 帮助程序猿构造 HTTP 响应. 程序猿只要给指定的 HttpResponse 对象填写一些属性字段, Servlet 就会自动的安装 HTTP 协议的方式构造出一个 HTTP 响应字符串, 并通过 Socket 写回给客户端
简而言之, Servlet 是一组 Tomcat 提供的 API, 让程序猿自己写的代码能很好的和 Tomcat 配合起来, 从而更简单的实现一个 web app
第一个 Servlet 程序
1. 创建项目
使用 IDEA 创建一个 Maven 项目
2. 引入依赖
Maven 项目创建完毕后, 会自动生成一个 pom.xml 文件
我们需要在 pom.xml 中引入 Servlet API 依赖的 jar 包
在中央仓库 https://mvnrepository.com/ 中搜索 “servlet”, 一般第一个结果就是
选择版本. 一般我们使用 3.1.0 版本
把中央仓库中提供的 xml 复制到项目的 pom.xml 中
3. 创建目录
当项目创建好了之后, IDEA 会帮我们自动创建出一些目录
- src 表示源代码所在的目录
- main/java 表示源代码的根目录. 后续创建 .java 文件就放到这个目录中
- main/resources 表示项目的一些资源文件所在的目录. 此处暂时不关注
- test/java 表示测试代码的根目录. 此处暂时不关注
1) 创建 webapp 目录
在 main 目录下, 和 java 目录并列, 创建一个 webapp 目录
webapp 目录就是未来部署到 Tomcat 中的一个重要的目录. 当前我们可以往 webapp 中放一些静态资源, 比如 html , css 等
在这个目录中还有一个重要的文件 web.xml. Tomcat 找到这个文件才能正确处理 webapp 中的动态资源
2) 创建 web.xml
然后在 webapp 目录内部创建一个 WEB-INF 目录, 并创建一个 web.xml 文件
3) 编写 web.xml
往 web.xml 中拷贝以下代码
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>
当前的servlet的程序没有main方法,main方法可以被视为汽车的发动机,有了发动机,才能启动;在程序中同样如此,这里的servlet程序相等于是车厢,只需要将其连接上车头tomcat即可,也就是将程序放到webapps目录下即可
tomcat只需要通过识别那些文件下存在WEB-INF/web.xml便可知道该文件是需要连接上的车厢
4. 编写代码
在 java 目录中创建一个类 HelloServlet, 代码如下
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//控制台打印
System.out.println("hello servlet");
//写入resp的body中
resp.getWriter().write("hello servlet");
}
}
- 创建一个类 HelloServlet , 继承自 HttpServlet
- 在这个类上方加上 @WebServlet(“/hello”) 注解, 表示 Tomcat 收到的请求中, 路径为 /hello的请求才会调用 HelloServlet 这个类的代码. (这个路径未包含 Context Path)
- 重写 doGet 方法. doGet 的参数有两个, 分别表示收到的 HTTP 请求 和要构造的 HTTP 响应. 这个方法会在 Tomcat 收到 GET 请求时触发
- HttpServletRequest 表示 HTTP 请求. Tomcat 按照 HTTP 请求的格式把 字符串 格式的请求转成了一个 HttpServletRequest 对象. 后续想获取请求中的信息(方法, url, header, body 等) 都是通过这个对象来获取
- HttpServletResponse 表示 HTTP 响应. 代码中把响应对象构造好(构造响应的状态码, header, body 等)
- resp.getWriter() 会获取到一个流对象, 通过这个流对象就可以写入一些数据, 写入的数据会被构造成一个 HTTP 响应的 body 部分, Tomcat 会把整个响应转成字符串, 通过 socket 写回给浏览器
5. 打包程序
使用 maven 进行打包. 打开 maven 窗口
然后展开 生命周期 , 双击 package 即可进行打包
打包成功之后, 能够看到 SUCCESS 这样的字样
打包成功后, 可以看到在 target 目录下, 生成了一个 jar 包
war包是tomcat专属的用于描述webapp的程序
6. 部署程序
把 war 包拷贝到 Tomcat 的 webapps 目录下
启动 Tomcat , Tomcat 就会自动把 war 包解压缩
7. 验证程序
此时通过浏览器访问 http://127.0.0.1:8080/hello_servlet-1.0-SNAPSHOT/hello
URL 中的 PATH 分成两个部分, 其中 HelloServlet 为 Context Path, hello 为 Servlet Path
总结
上述在浏览器地址栏中输入URL之后,浏览器立即构造了一个对应的HTTP GET请求,发给了tomcat;tomcat根据第一级路径,确定具体的webapp,根据第二级路径,确定调用哪个类;再通过GET/POST方法确定调用HelloServlet的哪个方法
Servlet 运行原理
Tomcat 的定位
我们自己的实现是在 Tomcat 基础上运行的
当浏览器给服务器发送请求的时候, Tomcat 作为 HTTP 服务器, 就可以接收到这个请求
HTTP 协议作为一个应用层协议, 需要底层协议栈来支持工作. 如下图所示
更详细的交互过程可以参考下图
1. 接收请求
- 用户在浏览器输入一个 URL, 此时浏览器就会构造一个 HTTP 请求
- 这个 HTTP 请求会经过网络协议栈逐层进行 封装 成二进制的 bit 流, 最终通过物理层的硬件设备转换成光信号/电信号传输出去
- 这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达目标主机
- 服务器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成HTTP 请求. 并交给 Tomcat 进程进行处理
- Tomcat 通过 Socket 读取到这个请求(一个字符串), 并按照 HTTP 请求的格式来解析这个请求, 根据请求中的 Context Path 确定一个 webapp, 再通过 Servlet Path 确定一个具体的 类. 再根据当前请求的方法 (GET/POST/…), 决定调用这个类的 doGet 或者 doPost 等方法. 此时我们的代码中的doGet / doPost 方法的第一个参数 HttpServletRequest 就包含了这个 HTTP 请求的详细信息
2. 根据请求计算响应
- 在我们的 doGet / doPost 方法中, 就执行到了我们自己的代码. 我们自己的代码会根据请求中的一些信息, 来给 HttpServletResponse 对象设置一些属性. 例如状态码, header, body 等
3. 返回响应
- 我们的 doGet / doPost 执行完毕后, Tomcat 就会自动把HttpServletResponse 这个我们刚设置好的对象转换成一个符合 HTTP 协议的字符串, 通过 Socket 把这个响应发送出去
- 此时响应数据在服务器的主机上通过网络协议栈层层 封装, 最终又得到一个二进制的 bit 流, 通过物理层硬件设备转换成光信号/电信号传输出去
- 这些承载信息的光信号/电信号通过互联网上的一系列网络设备, 最终到达浏览器所在的主机
- 浏览器主机收到这些光信号/电信号, 又会通过网络协议栈逐层进行 分用, 层层解析, 最终还原成HTTP 响应, 并交给浏览器处理
- 浏览器也通过 Socket 读到这个响应(一个字符串), 按照 HTTP 响应的格式来解析这个响应. 并且把body 中的数据按照一定的格式显示在浏览器的界面上
Servlet API 详解
HttpServlet
核心方法
方法名称 | 调用时机 |
---|---|
init | 在 HttpServlet 实例化之后被调用一次 |
destory | 在 HttpServlet 实例不再使用的时候调用一次 |
service | 收到 HTTP 请求的时候调用 |
doGet | 收到 GET 请求的时候调用(由 service 方法调用) |
doPost | 收到 POST 请求的时候调用(由 service 方法调用) |
doPut/doDelete/doOptions/… | 收到其他请求的时候调用(由 service 方法调用) |
写代码的时候主要重写 doXXX 方法, 很少会重写 init / destory / service
这些方法的调用时机, 就称为 “Servlet 生命周期”. (也就是描述了一个 Servlet 实例从生到死的过程)
HttpServlet 的实例只是在程序启动时创建一次. 而不是每次收到 HTTP 请求都重新创建实例
代码示例: 处理 各种 请求
创建 MethodServlet.java, 创建 四种 方法
@WebServlet("/method")
public class MethodServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("hello doget");
resp.getWriter().write("doget");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("hello dopost");
resp.getWriter().write("dopost");
}
@Override
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("hello doput");
resp.getWriter().write("doput");
}
@Override
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("hello dodelete");
resp.getWriter().write("dodelete");
}
}
通过postman进行检测
剩余方法类似,这里不再赘述
HttpServletRequest
当 Tomcat 通过 Socket API 读取 HTTP 请求(字符串), 并且按照 HTTP 协议的格式把字符串解析成HttpServletRequest 对象
核心方法
方法 | 描述 |
---|---|
String getProtocol() | 返回请求协议的名称和版本 |
String getMethod() | 返回请求的 HTTP 方法的名称,例如,GET、POST 或 PUT |
String getRequestURI() | 从协议名称直到 HTTP 请求的第一行的查询字符串中,返回该请求的 URL 的一部分 |
String getContextPath() | 返回指示请求上下文的请求 URI 部分 |
String getQueryString() | 返回包含在路径后的请求 URL 中的查询字符串 |
Enumeration getParameterNames() | 返回一个 String 对象的枚举,包含在该请求中包含的参数的名称 |
String getParameter(String name) | 以字符串形式返回请求参数的值,或者如果参数不存在则返回null |
Enumeration getHeaderNames() | 返回一个枚举,包含在该请求中包含的所有的头名 |
String getHeader(String name) | 以字符串形式返回指定的请求头的值 |
int getContentLength() | 以字节为单位返回请求主体的长度,并提供输入流,或者如果长度未知则返回 -1 |
InputStream getInputStream() | 用于读取请求的 body 内容. 返回一个 InputStream 对象 |
通过这些方法可以获取到一个请求中的各个方面的信息
请求对象是服务器收到的内容, 不应该修改. 因此上面的方法也都只是 “读” 方法, 而不是 “写” 方法
代码示例: 打印请求信息
创建 RequestServlet 类
@WebServlet("/request")
public class RequestServlet extends HelloServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 这里是设置响应的 content-type. 告诉浏览器, 响应 body 里的数据格式是啥样的.
resp.setContentType("text/html");
// 搞个 StringBuilder, 把这些 api 的结果拼起来, 统一写回到响应中.
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(req.getProtocol());
stringBuilder.append("<br>");
stringBuilder.append(req.getMethod());
stringBuilder.append("<br>");
stringBuilder.append(req.getRequestURI());
stringBuilder.append("<br>");
stringBuilder.append(req.getContextPath());
stringBuilder.append("<br>");
stringBuilder.append(req.getQueryString());
stringBuilder.append("<br>");
stringBuilder.append("<br>");
stringBuilder.append("<br>");
stringBuilder.append("<br>");
// 获取到 header 中所有的键值对
Enumeration<String> headerNames = req.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
stringBuilder.append(headerName + ": " + req.getHeader(headerName));
stringBuilder.append("<br>");
}
resp.getWriter().write(stringBuilder.toString());
}
}
部署程序,观察结果如下
代码示例: 获取 GET 请求中的参数
GET 请求中的参数一般都是通过 query string 传递给服务器的
创建 GetParameterServlet 类
public class GetParameterServlet extends HelloServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 预期浏览器会发一个形如 /getParameter?studentId=10&classId=20 请求.
// getParameter 方法就能拿到 query string 中的键值对内容了.
// getParameter 得到的是 String 类型的结果.
String studentId = req.getParameter("studentId");
String classId = req.getParameter("classId");
resp.setContentType("text/html; charset=utf8");
// resp.setCharacterEncoding("utf8");
resp.getWriter().write("学生id = " + studentId + " 班级id = " + classId);
}
}
重新部署程序,观察结果如下
当没有 query string的时候, getParameter 获取的值为 null
当手动添加 query string的时候
代码示例: 获取 POST 请求中的参数(form表单)
POST 请求的参数一般通过 body 传递给服务器. body 中的数据格式有很多种. 如果是采用 form 表单的形式, 仍然可以通过 getParameter 获取参数的值
创建类 PostParameterServlet
@WebServlet("/postparameter")
public class PostParameterServlet extends HelloServlet{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String studentId = req.getParameter("studentId");
String classId = req.getParameter("classId");
resp.setContentType("text/html");
resp.getWriter().write("studentId = " + studentId + " classId = " + classId);
}
}
创建 test.html, 放到 webapp 目录中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="postparameter" method="post">
<input type="text" name="studentId">
<input type="text" name="classId">
<input type="submit" value="提交">
</form>
</body>
</html>
重新部署程序,输入URL,填入数值观察结果如下
点击提交后,根据form表单中的请求方式和URL,页面发生跳转,最后也是获取到POST请求的参数
详细过程
代码示例: 获取 POST 请求中的参数(JSON)
如果 POST 请求中的 body 是按照 JSON 的格式来传递, 那么获取参数的代码就要发生调整
创建 PostParameter2Servlet 类
@WebServlet("/postparameter2")
public class PostParameter2Servlet extends HelloServlet{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//在流对象中读多少个字节? 取决于 Content-Length
int length = req.getContentLength();
byte[] buffer = new byte[length];
InputStream inputStream = req.getInputStream();
inputStream.read(buffer);
// 把这个字节数组构造成 String, 打印出来.
String body = new String(buffer, 0, length, "utf8");
System.out.println("body = " + body);
resp.getWriter().write(body);
}
}
这里使用postman进行模拟
通过结果可以看出,服务器拿到的 JSON 数据仍然是一个整体的 String 类型, 如果要想获取到 userId 和classId 的具体值, 还需要搭配 JSON 库进一步解析
代码示例: 获取 POST 请求中的参数(JSON 库)
引入 Jackson 这个库, 进行 JSON 解析
-
在中央仓库中搜索 Jackson, 选择 JackSon Databind
-
把中央仓库中的依赖配置添加到 pom.xml 中
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.14.1</version>
</dependency>
- 在 PostParameter2Servlet 类中修改代码
class Student{
public int studentId;
public int classId;
@Override
public String toString() {
return "studentId:"+studentId+" "+"classId:"+classId;
}
}
@WebServlet("/postparameter2")
public class PostParameter2Servlet extends HelloServlet{
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//在流对象中读多少个字节? 取决于 Content-Length
// int length = req.getContentLength();
// byte[] buffer = new byte[length];
// InputStream inputStream = req.getInputStream();
// inputStream.read(buffer);
// // 把这个字节数组构造成 String, 打印出来.
// String body = new String(buffer, 0, length, "utf8");
// System.out.println("body = " + body);
// resp.getWriter().write(body);
ObjectMapper objectMapper = new ObjectMapper();
// readValue 就是把一个 json 格式的字符串转成 Java 对象.
Student student = objectMapper.readValue(req.getInputStream(), Student.class);
System.out.println(student.studentId + ", " + student.classId);
resp.getWriter().write(student.toString());
}
}
重新部署,观察结果
- Jackson 库的核心类为 ObjectMapper. 其中的 readValue 方法把一个 JSON 字符串转成 Java 对象. 其中的 writeValue 方法把一个 Java 对象转成 JSON 格式字符串
- readValue 的第二个参数为 JsonData 的 类对象. 通过这个类对象, 在 readValue 的内部就可以借助反射机制来构造出 JsonData 对象, 并且根据 JSON 中key 的名字, 把对应的 value 赋值给 JsonData 的对应字段
HttpServletResponse
Servlet 中的 doXXX 方法的目的就是根据请求计算得到相应, 然后把响应的数据设置到HttpServletResponse 对象中
然后 Tomcat 就会把这个 HttpServletResponse 对象按照 HTTP 协议的格式, 转成一个字符串, 并通过Socket 写回给浏览器
核心方法
方法 | 描述 |
---|---|
void setStatus(int sc) | 为该响应设置状态码 |
void setHeader(String name,String value) | 设置一个带有给定的名称和值的 header. 如果 name 已经存在,则覆盖旧的值 |
void addHeader(String name, String value) | 添加一个带有给定的名称和值的 header. 如果 name 已经存在,不覆盖旧的值, 并列添加新的键值对 |
void setContentType(String type) | 设置被发送到客户端的响应的内容类型 |
void setCharacterEncoding(String charset) | 设置被发送到客户端的响应的字符编码(MIME 字符集)例如,UTF-8 |
void sendRedirect(String location) | 使用指定的重定向位置 URL 发送临时重定向响应到客户端 |
PrintWriter getWriter() | 用于往 body 中写入文本格式数据 |
OutputStream getOutputStream() | 用于往 body 中写入二进制格式数据 |
响应对象是服务器要返回给浏览器的内容, 这里的重要信息都是程序猿设置的. 因此上面的方法都是 “写” 方法
代码示例: 重定向
实现一个程序, 返回一个重定向 HTTP 响应, 自动跳转到另外一个页面
创建 RedirectServlet 类
@WebServlet("/redirect")
public class RedirectServlet extends HelloServlet{
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(302);
resp.setHeader("Location", "https://www.baidu.com");
}
}
重新部署,观察结果
输入URL之后,直接跳转至百度首页
抓包结果
Cookie 和 Session
回顾 Cookie
HTTP 协议自身是属于 “无状态” 协议;默认情况下 HTTP 协议的客户端和服务器之间的这次通信, 和下次通信之间没有直接的联系
什么是cookie
浏览器提供的持久化存储数据的机制
cookie的来源
cookie是服务器返回给浏览器的;服务器代码中由开发者决定要什么样的信息保存到客户端(浏览器)中,通过HTTP响应的Set-Cookie字段,把键值对写回到浏览器中
cookie的去向
cookie在后续浏览器访问服务器的时候顺带在请求的header中发送给服务器;如此操作的原因是服务器只有一个,但是客服端却有很多,服务器便可以通过cookie中的值来识别当前客户端的身份
cookie的存储
cookie存储在浏览器(客户端)所在主机的硬盘中;浏览器根据域名和存储类似键值对
理解会话机制 (Session)
用户登录时服务器并不知道用户的身份;针对登录操作,B站会查询数据库,验证用户的用户名和密码是否正确,如果正确,则登录成功;B站会把当前用户的身份信息在内存中也保存一份,同时也会给当前用户分配一个标识身份的序号,也成为sessionId;服务器使用像hash这样的结构将序号作为key,身份信息作为value存储起来,服务器把生成的这些键值对称为session
Cookie 和 Session 的区别
关联:网站的登录功能中,配合使用
区别:cookie是客户端的存储机制;session是服务器的存储机制
cookie中可以存储各种键值对;session专门用来保存用户的身份信息
cookie可以单独使用,不配合session;session也可以不搭配cookie使用
cookie和浏览器是强相关的;cookie则是属于HTTP协议的一分部
代码示例: 实现用户登陆
实现简单的用户登陆逻辑
主要涉及两个页面:登录页面,主页面;涉及两个servlet:处理登录的loginservlet判定用户和密码,构造主页面的indexservlet
编写登录页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form action="login" method="post">
<input type="text" name="username">
<br>
<input type="password" name="password">
<br>
<input type="submit" value="提交">
</form>
</body>
</html>
loginservlet处理登录请求
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException, IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
// 验证用户名密码是否正确.
if (!username.equals("zhangsan") && !username.equals("lisi")) {
// 登陆失败!!
// 重定向到 登陆页面
System.out.println("登陆失败, 用户名错误!");
resp.sendRedirect("login.html");
return;
}
if (!password.equals("123")) {
// 登陆失败!!
System.out.println("登陆失败, 密码错误!");
resp.sendRedirect("login.html");
return;
}
// 登陆成功
// 1. 创建一个会话.
HttpSession session = req.getSession(true);
// 2. 把当前的用户名保存到会话中. 此处 HttpSession 又可以当成一个 map 使用.
session.setAttribute("username", username);
// 3. 重定向到主页
resp.sendRedirect("index");
}
}
// 1. 创建一个会话.
HttpSession session = req.getSession(true);
// 2. 把当前的用户名保存到会话中. 此处 HttpSession 又可以当成一个 map 使用.
session.setAttribute("username", username);
用户登录成功,创建一个会话,getSession(true)判定当前请求是否有对应的会话;如果sessionId不存在,创建会话,放入哈希表中
创建过程:
- 构造一个HTTPsession对象
- 构造唯一的sessionid
- 将这个键值对插入哈希表
- 将sessionid设置到响应报文Set-Cookie中
indexservlet生成主页
@WebServlet("/index")
public class IndexServlet extends HttpServlet {
// 通过 重定向, 浏览器发起的是 GET .
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException, IOException {
// 先判定用户的登陆状态.
// 如果用户还没登陆, 要求先登陆.
// 已经登陆了, 则根据 会话 中的用户名, 来显示到页面上.
// 这个操作不会触发会话的创建.
HttpSession session = req.getSession(false);
if (session == null) {
// 未登录状态
System.out.println("用户未登录!");
resp.sendRedirect("login.html");
return;
}
// 已经登陆
String username = (String) session.getAttribute("username");
// 构造页面.
resp.setContentType("text/html;charset=utf8");
resp.getWriter().write("欢迎 " + username + "回来!");
}
}
交互过程
第一次交互,进行登录操作
请求
响应
第二次交互
请求
响应
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!