Fork me on GitHub

servlet入门

Servlet: server applet

运行在服务器端的小程序

  • Servlet就是一个接口,定义了Java类被浏览器访问到(tomcat识别)的规则。
  • 自定义一个类,实现Servlet接口,复写方法。

快速入门

  1. 创建JavaEE项目
  2. 定义一个类,实现Servlet接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    package cn.web;

    import javax.servlet.*;
    import java.io.IOException;

    public class ServletDemo1 implements Servlet {
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
    return null;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {

    }

    @Override
    public String getServletInfo() {
    return null;
    }

    @Override
    public void destroy() {

    }
    }
  3. 实现接口中的抽象方法

  4. 配置Servlet
    web.xml中配置, 写在</web-app>标签里面
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!--配置Servlet -->
    <servlet>
    <servlet-name>demo1</servlet-name>
    <servlet-class>cn.web.ServletDemo1</servlet-class>
    </servlet>

    <servlet-mapping>
    <servlet-name>demo1</servlet-name>
    <url-pattern>/demo1</url-pattern>
    </servlet-mapping>

执行原理

  1. 当服务器接受到客户端浏览器的请求后,会解析请求URL路径,获取访问的Servlet的资源路径
  2. 查找web.xml文件,是否有对应的<url-pattern>标签体内容。
  3. 如果有,则在找到对应的<servlet-class>全类名
  4. tomcat会将字节码文件加载进内存,并且创建其对象
  5. 调用service方法

生命周期

  1. 被创建:执行·方法,只执行一次

    • Servlet什么时候被创建?

      • 默认情况下,第一次被访问时,Servlet被创建
      • 可以配置执行Servlet的创建时机。
        • 标签下配置
          1. 第一次被访问时,创建
            • 的值为负数
          2. 在服务器启动时,创建
            • 的值为0或正整数
    • Servlet的init方法,只执行一次,说明一个Servlet在内存中只存在一个对象,Servlet是单例的

      • 多个用户同时访问时,可能存在线程安全问题。
      • 解决:尽量不要在Servlet中定义成员变量。即使定义了成员变量,也不要对修改值
  2. 提供服务:执行service方法,执行多次
    • 每次访问Servlet时,Service方法都会被调用一次
  3. 被销毁:执行destroy方法,只执行一次
    • Servlet被销毁时执行。服务器关闭时,Servlet被销毁
    • 只有服务器正常关闭时,才会执行destroy方法。
    • destroy方法在Servlet被销毁之前执行,一般用于释放资源

Servlet3.0

  • 好处:

    • 支持注解配置。可以不需要web.xml了。
  • 步骤:

    1. 创建JavaEE项目,选择Servlet的版本3.0以上,可以不创建web.xml
    2. 定义一个类,实现Servlet接口
    3. 复写方法
    4. 在类上使用@WebServlet注解,进行配置
      @WebServlet("资源路径")
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      @Target({ElementType.TYPE})
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      public @interface WebServlet {
      String name() default "";//相当于<Servlet-name>

      String[] value() default {};//代表urlPatterns()属性配置

      String[] urlPatterns() default {};//相当于<url-pattern>

      int loadOnStartup() default -1;//相当于<load-on-startup>

      WebInitParam[] initParams() default {};

      boolean asyncSupported() default false;

      String smallIcon() default "";

      String largeIcon() default "";

      String description() default "";

      String displayName() default "";
      }

urlpartten:Servlet访问路径

  1. 一个Servlet可以定义多个访问路径 : @WebServlet({"/d4","/dd4","/ddd4"})
  2. 路径定义规则:
    1. /xxx:路径匹配
    2. /xxx/xxx:多层路径,目录结构
    3. *.do:扩展名匹配

HttpServlet

Servlet的体系结构

  • GenericServlet:将Servlet接口中其他的方法做了默认空实现,只将service()方法作为抽象, 将来定义Servlet类时, 可以继承GenericServlet,实现service()方法即可
  • HttpServlet:对http协议的一种封装,简化操作
    1. 定义类继承HttpServlet
    2. 复写doGet/doPost方法

Request

request对象和response对象的原理

1. `request`和`response`对象是由服务器创建的。我们来使用它们
2. `request`对象是来获取请求消息,`response`对象是来设置响应消息

request对象继承体系结构

1
2
3
4
5
ServletRequest	-- 接口
| 继承
HttpServletRequest -- 接口
| 实现
org.apache.catalina.connector.RequestFacade 类(tomcat)

request功能

获取请求消息数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@WebServlet("/demo2")
public class ServletDemo2 extends HttpServlet {
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

}

@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String contextPath = request.getContextPath();
System.out.println("contextPath = " + contextPath);

String servletPath = request.getServletPath();
System.out.println("servletPath = " + servletPath);

String method = request.getMethod();
System.out.println("method = " + method);

String queryString = request.getQueryString();
System.out.println("queryString = " + queryString);

String requestURI = request.getRequestURI();
System.out.println("requestURI = " + requestURI);

StringBuffer requestURL = request.getRequestURL();
System.out.println("requestURL = " + requestURL);

String remoteAddr = request.getRemoteAddr();
System.out.println("remoteAddr = " + remoteAddr);
}
}

contextPath = /UserDemo_war_exploded
servletPath = /demo2
method = GET
queryString = name=IIce
requestURI = /UserDemo_war_exploded/demo2
requestURL = http://localhost:8080/UserDemo_war_exploded/demo2
remoteAddr = 0:0:0:0:0:0:0:1

其他功能

  1. 获取请求参数通用方式:不论get还是post请求方式都可以使用下列方法来获取请求参数

    1. String getParameter(String name):根据参数名称获取参数值 username=zs&password=123
    2. String[] getParameterValues(String name):根据参数名称获取参数值的数组 hobby=xx&hobby=game
    3. Enumeration<String> getParameterNames():获取所有请求的参数名称
    4. Map<String,String[]> getParameterMap():获取所有参数的map集合
    5. 中文乱码问题
      • 解决:在获取参数前,设置request的编码request.setCharacterEncoding("utf-8");
  2. 请求转发:一种在服务器内部的资源跳转方式

    1. 步骤:

      1. 通过request对象获取请求转发器对象:RequestDispatcher getRequestDispatcher(String path)
      2. 使用RequestDispatcher对象来进行转发:forward(ServletRequest request, ServletResponse response)
    2. 特点:

      1. 浏览器地址栏路径不发生变化
      2. 只能转发到当前服务器内部资源中。
      3. 转发是一次请求
  3. 共享数据:

    • 域对象:一个有作用范围的对象,可以在范围内共享数据
    • request域:代表一次请求的范围,一般用于请求转发的多个资源中共享数据
    • 方法:
      1. void setAttribute(String name,Object obj):存储数据
      2. Object getAttitude(String name):通过键获取值
      3. void removeAttribute(String name):通过键移除键值对
  4. 获取ServletContext

    • ServletContext getServletContext()

JavaBean

用于封装JavaBean的

  1. JavaBean:标准的Java类

    1. 要求:
      1. 类必须被public修饰
      2. 必须提供空参的构造器
      3. 成员变量必须使用private修饰
      4. 提供公共settergetter方法
    2. 功能:封装数据
  2. 概念:
    成员变量:
    属性:settergetter方法截取后的产物, 例如:getUsername() --> Username--> username

  3. 方法:

    1. setProperty()
    2. getProperty()
    3. populate(Object obj , Map map):将map集合的键值对信息,封装到对应的JavaBean对象中

HTTP

响应头

常见的响应头:

  1. Content-Type:服务器告诉客户端本次响应体数据格式以及编码格式
  2. Content-disposition:服务器告诉客户端以什么格式打开响应体数据
    • 值:
      • in-line:默认值,在当前页面内打开
      • attachment;filename=xxx:以附件形式打开响应体。文件下载

Response对象

设置响应消息

设置响应行

  1. 格式:HTTP/1.1 200 ok
  2. 设置状态码:setStatus(int sc)

设置响应头

setHeader(String name, String value)

设置响应体

  1. 获取输出流
    • 字符输出流:PrintWriter getWriter()
    • 字节输出流:ServletOutputStream getOutputStream()
  2. 使用输出流,将数据输出到客户端浏览器

重定向

1
2
//简单的重定向方法
response.sendRedirect("/responseDemo2");
  • 重定向的特点:redirect
    1. 地址栏发生变化
    2. 重定向可以访问其他站点(服务器)的资源
    3. 重定向是两次请求。不能使用request对象来共享数据
  • 转发的特点:forward
    1. 转发地址栏路径不变
    2. 转发只能访问当前服务器下的资源
    3. 转发是一次请求,可以使用request对象来共享数据

路径问题

  1. 相对路径:通过相对路径不可以确定唯一资源

    • 如:./index.html
    • 不以/开头,以.开头路径

    • 规则:找到当前资源和目标资源之间的相对位置关系

      • ./:当前目录
      • ../:后退一级目录
  2. 绝对路径:通过绝对路径可以确定唯一资源
    • 如:http://localhost/responseDemo2 /responseDemo2
    • /开头的路径
    • 规则:判断定义的路径是给谁用的?判断请求将来从哪儿发出
      • 给客户端浏览器使用:需要加虚拟目录(项目的访问路径)
        • 建议虚拟目录动态获取:request.getContextPath()
        • <a> , <form> 重定向…
      • 给服务器使用:不需要加虚拟目录
        • 转发路径

服务器输出字符数据到浏览器

  • 步骤:

    1. 获取字符输出流
    2. 输出数据
  • 乱码问题:

    1. PrintWriter pw = response.getWriter();获取的流的默认编码是ISO-8859-1
    2. 设置该流的默认编码
    3. 告诉浏览器响应体使用的编码
      1
      2
      //简单的形式,设置编码,是在获取流之前设置
      response.setContentType("text/html;charset=utf-8");

ServletContext对象

概念

代表整个web应用,可以和程序的容器(服务器)来通信

获取

  1. 通过request对象获取
    request.getServletContext();
  2. 通过HttpServlet获取
    this.getServletContext();

功能

获取MIME类型

  • MIME类型:在互联网通信过程中定义的一种文件数据类型

    格式: 大类型/小类型 text/html image/jpeg

  • 获取:String getMimeType(String file)

域对象: 共享数据

  1. setAttribute(String name,Object value)
  2. getAttribute(String name)
  3. removeAttribute(String name)

    ServletContext对象范围:所有用户所有请求的数据

获取文件的真实(服务器)路径

1
2
3
4
5
6
7
8
9
10
11
String getRealPath(String path)  


String b = context.getRealPath("/b.txt");//web目录下资源访问
System.out.println(b);

String c = context.getRealPath("/WEB-INF/c.txt");//WEB-INF目录下的资源访问
System.out.println(c);

String a = context.getRealPath("/WEB-INF/classes/a.txt");//src目录下的资源访问
System.out.println(a);

文件下载

文件下载需求

  1. 页面显示超链接
  2. 点击超链接后弹出下载提示框
  3. 完成图片文件下载

分析

  1. 超链接指向的资源如果能够被浏览器解析,则在浏览器中展示,如果不能解析,则弹出下载提示框。不满足需求
  2. 任何资源都必须弹出下载提示框
  3. 使用响应头设置资源的打开方式
    • content-disposition:attachment;filename=xxx

步骤

  1. 定义页面,编辑超链接href属性,指向Servlet,传递资源名称filename
  2. 定义Servlet
    1. 获取文件名称
    2. 使用字节输入流加载文件进内存
    3. 指定response的响应头: content-disposition:attachment;filename=xxx
    4. 将数据写出到response输出流

Cookie

保存在客户端

  1. 创建Cookie对象,绑定数据
    new Cookie(String name, String value)
  2. 发送Cookie对象
    response.addCookie(Cookie cookie)
  3. 获取Cookie,拿到数据
    Cookie[] request.getCookies()

cookie的细节

  1. 一次可不可以发送多个cookie?

    • 可以
    • 可以创建多个Cookie对象,使用response调用多次addCookie方法发送cookie即可。
  2. cookie在浏览器中保存多长时间?

    1. 默认情况下,当浏览器关闭后,Cookie数据被销毁
    2. 持久化存储:
      • setMaxAge(int seconds)
        1. 正数:将Cookie数据写到硬盘的文件中。持久化存储。并指定cookie存活时间,时间到后,cookie文件自动失效
        2. 负数:默认值
        3. 零:删除cookie信息
  3. cookie能不能存中文?

    • 在tomcat 8 之前 cookie中不能直接存储中文数据。
      • 需要将中文数据转码—一般采用URL编码(%E3)
    • 在tomcat 8 之后,cookie支持中文数据。特殊字符还是不支持,建议使用URL编码存储,URL解码解析
  4. cookie共享问题?

    1. 假设在一个tomcat服务器中,部署了多个web项目,那么在这些web项目中cookie能不能共享?

      • 默认情况下cookie不能共享
      • setPath(String path):设置cookie的获取范围。默认情况下,设置当前的虚拟目录
        • 如果要共享,则可以将path设置为"/"
    2. 不同的tomcat服务器间cookie共享问题?

      • setDomain(String path):如果设置一级域名相同,那么多个服务器之间cookie可以共享
        • setDomain(".baidu.com"),那么tieba.baidu.comnews.baidu.com中cookie可以共享

Cookie的特点和作用

  1. cookie存储数据在客户端浏览器
  2. 浏览器对于单个cookie 的大小有限制(4kb) 以及 对同一个域名下的总cookie数量也有限制(20个)
  • 作用:
    1. cookie一般用于存出少量的不太敏感的数据
    2. 在不登录的情况下,完成服务器对客户端的身份识别

Session

依赖于Cookie, 保存在服务器端

  1. 获取HttpSession对象:
    HttpSession session = request.getSession();
  2. 使用HttpSession对象:
    1
    2
    3
    Object getAttribute(String name)  
    void setAttribute(String name, Object value)
    void removeAttribute(String name)

细节

  1. 当客户端关闭后,服务器不关闭,两次获取session是否为同一个?

    • 默认情况下。不是。
    • 如果需要相同,则可以创建Cookie,键为JSESSIONID,设置最大存活时间,让cookie持久化保存。
      1
      2
      3
      Cookie c = new Cookie("JSESSIONID",session.getId());
      c.setMaxAge(60*60);
      response.addCookie(c);
  2. 客户端不关闭,服务器关闭后,两次获取的session是同一个吗?

    • 不是同一个,但是要确保数据不丢失。tomcat自动完成以下工作
      • session的钝化: 在服务器正常关闭之前, 将session对象系列化到硬盘上
      • session的活化: 在服务器启动后, 将session文件转化为内存中的session对象即可。
  3. session什么时候被销毁?

    1. 服务器关闭
    2. session对象调用invalidate()
    3. session默认失效时间 30分钟
      选择性配置修改
      1
      2
      3
      <session-config>
      <session-timeout>30</session-timeout>
      </session-config>
  4. session的特点

    1. session用于存储一次会话的多次请求的数据,存在服务器端
    2. session可以存储任意类型,任意大小的数据

session与Cookie的区别

  1. session存储数据在服务器端,Cookie在客户端
  2. session没有数据大小限制,Cookie有
  3. session数据安全,Cookie相对于不安全

JSP(Java Server Pages)

JSP本质上就是一个Servlet

JSP的脚本: JSP定义Java代码的方式

  1. <% 代码 %>: 定义的java代码,在service方法中。service方法中可以定义什么,该脚本中就可以定义什么。
  2. <%! 代码 %>: 定义的java代码,在jsp转换后的java类的成员位置。
  3. <%= 代码 %>: 定义的java代码,会输出到页面上。输出语句中可以定义什么,该脚本中就可以定义什么。

JSP的内置对象

  • 在jsp页面中不需要获取和创建,可以直接使用的对象
  • jsp一共有9个内置对象。
变量名真实类型作用
pageContextPageContext当前页面共享数据,还可以获取其他八个内置对象
requestHttpServletRequest一次请求访问的多个资源(转发)
sessionHttpSession一次会话的多个请求间
applicationServletContext所有用户间共享数据
responseHttpServletResponse响应对象
pageObject当前页面(Servlet)的对象,this
outJspWriter输出对象,数据输出到页面上
configServletConfigServlet的配置对象
exceptionThrowable异常对象

response.getWriter()out.write()的区别:

  • 在tomcat服务器真正给客户端做出响应之前,会先找response缓冲区数据,再找out缓冲区数据。
    • response.getWriter()数据输出永远在out.write()之前

El表达式

  1. 概念:Expression Language 表达式语言
  2. 作用:替换和简化jsp页面中java代码的编写
  3. 语法:${表达式}
  4. 注意:
    • jsp默认支持el表达式的。如果要忽略el表达式
      1. 设置jsp中page指令中:isELIgnored="true" 忽略当前jsp页面中所有的el表达式
      2. \${表达式} :忽略当前这个el表达式

使用

运算符

  1. 算数运算符: + - * /(div) %(mod)
  2. 比较运算符: > < >= <= == !=
  3. 逻辑运算符: &&(and) ||(or) !(not)
  4. 空运算符: empty
    • 功能:用于判断字符串、集合、数组对象是否为null或者长度是否为0
    • ${empty list}:判断字符串、集合、数组对象是否为null或者长度为0
    • ${not empty str}:表示判断字符串、集合、数组对象是否不为null 并且 长度>0

获取值

  1. el表达式只能从域对象中获取值
  2. 语法:

    1. ${域名称.键名}:从指定域中获取指定键的值

      • 域名称:
        1. pageScope –> pageContext
        2. requestScope –> request
        3. sessionScope –> session
        4. applicationScope –> application(ServletContext)
      • 举例:在request域中存储了name=张三
      • 获取:${requestScope.name}
    2. ${键名}:表示依次从最小的域中查找是否有该键对应的值,直到找到为止。

    3. 获取对象、List集合、Map集合的值

      1. 对象:${域名称.键名.属性名}

        • 本质上会去调用对象的getter方法
      2. List集合:${域名称.键名[索引]}

      3. Map集合:

        • ${域名称.键名.key名称}
        • ${域名称.键名["key名称"]}

隐式对象

  • el表达式中有11个隐式对象
  • pageContext
    • 获取jsp其他八个内置对象
      • ${pageContext.request.contextPath}:动态获取虚拟目录

JSTL

概念:JavaServer Pages Tag Library JSP标准标签库

是由Apache组织提供的开源的免费的jsp标签 <标签>

作用:用于简化和替换jsp页面上的java代码

使用步骤:

  1. 导入jstl相关jar包
  2. 引入标签库:taglib指令: <%@ taglib %> 放在jsp头部
  3. 使用标签

常用的JSTL标签

  1. if:相当于java代码的if语句

    1. 属性:
      • test 必须属性,接受boolean表达式
        • 如果表达式为true,则显示if标签体内容,如果为false,则不显示标签体内容
        • 一般情况下,test属性值会结合el表达式一起使用
      1. 注意:
        • c:if标签没有else情况,想要else情况,则可以在定义一个c:if标签
  2. choose:相当于java代码的switch语句

    1. 使用choose标签声明 相当于switch声明
    2. 使用when标签做判断 相当于case
    3. 使用otherwise标签做其他情况的声明 相当于default
  3. foreach:相当于java代码的for语句

三层架构:软件设计架构

  1. 界面层(表示层):用户看的得界面。用户可以通过界面上的组件和服务器进行交互
  2. 业务逻辑层:处理业务逻辑的。
  3. 数据访问层:操作数据存储文件。

MVC
model: dao(data access object) 操作数据库 包名xx.xx.dao
view: jsp 包名xx.xx.web, 接受用户参数, 封装数据完成显示
controller: service 包名xx.xx.service 业务逻辑操作

Filter: 过滤器

概念

  • 生活中的过滤器:净水器,空气净化器,土匪、
  • web中的过滤器:当访问服务器的资源时,过滤器可以将请求拦截下来,完成一些特殊的功能。
  • 过滤器的作用:
    • 一般用于完成通用的操作。如:登录验证、统一编码处理、敏感字符过滤…

快速入门

步骤

  1. 定义一个类,实现接口Filter
  2. 复写方法
  3. 配置拦截路径
    1. web.xml
    2. 注解

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@WebFilter("/*")//访问所有资源之前,都会执行该过滤器
public class FilterDemo1 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("filterDemo1被执行了....");

//放行
filterChain.doFilter(servletRequest,servletResponse);

}

@Override
public void destroy() {

}
}

过滤器细节

web.xml配置

1
2
3
4
5
6
7
8
9
<filter>
<filter-name>demo1</filter-name>
<filter-class>cn.itcast.web.filter.FilterDemo1</filter-class>
</filter>
<filter-mapping>
<filter-name>demo1</filter-name>
<!-- 拦截路径 -->
<url-pattern>/*</url-pattern>
</filter-mapping>

过滤器执行流程

  1. 执行过滤器
  2. 执行放行后的资源
  3. 回来执行过滤器放行代码下边的代码

过滤器生命周期方法

  1. init:在服务器启动后,会创建Filter对象,然后调用init方法。只执行一次。用于加载资源
  2. doFilter:每一次请求被拦截资源时,会执行。执行多次
  3. destroy:在服务器关闭后, Filter对象被销毁。如果服务器是正常关闭,则会执行destroy方法。只执行一次。用于释放资源

过滤器配置详解

  • 拦截路径配置:

    1. 具体资源路径: /index.jsp 只有访问index.jsp资源时,过滤器才会被执行
    2. 拦截目录: /user/* 访问/user下的所有资源时,过滤器都会被执行
    3. 后缀名拦截: *.jsp 访问所有后缀名为jsp资源时,过滤器都会被执行
    4. 拦截所有资源:/* 访问所有资源时,过滤器都会被执行
  • 拦截方式配置:资源被访问的方式

    • 注解配置:
      • 设置dispatcherTypes属性
        1. REQUEST:默认值。浏览器直接请求资源
        2. FORWARD:转发访问资源
        3. INCLUDE:包含访问资源
        4. ERROR:错误跳转资源
        5. ASYNC:异步访问资源
    • web.xml配置
      • 设置<dispatcher></dispatcher>标签即可

过滤器链(配置多个过滤器)

执行顺序:如果有两个过滤器:过滤器1和过滤器2(入栈出栈)

  1. 过滤器1
  2. 过滤器2
  3. 资源执行
  4. 过滤器2
  5. 过滤器1
  • 过滤器先后顺序问题:
    1. 注解配置:按照类名的字符串比较规则比较,值小的先执行
      • 如: AFilterBFilter, AFilter就先执行了。
    2. web.xml配置: <filter-mapping>谁定义在上边,谁先执行

Listener:监听器

概念:web的三大组件之一。

  • 事件监听机制
    • 事件 :一件事情
    • 事件源 :事件发生的地方
    • 监听器 :一个对象
    • 注册监听:将事件、事件源、监听器绑定在一起。 当事件源上发生某个事件后,执行监听器代码

ServletContextListener:监听ServletContext对象的创建和销毁

  • 方法:
    • void contextDestroyed(ServletContextEvent sce)ServletContext对象被销毁之前会调用该方法
    • void contextInitialized(ServletContextEvent sce)ServletContext对象创建后会调用该方法
  • 步骤:

    1. 定义一个类,实现ServletContextListener接口
    2. 复写方法
    3. 配置

      1. web.xml

        1
        2
        3
        4
        5
        <listener>
        <listener-class>cn.web.listener.ContextLoaderListener</listener-class>
        </listener>

        * 指定初始化参数<context-param>
      2. 注解:

        • @WebListener
-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!
0%