JavaWeb②Servlet 那些事

Java Web 三大组件:Servlet、Filter、Listener

Servlet(Server Applet)

服务器端程序

  1. 交互式处理客户端请求并完成响应。
  2. Java Web 的核心,实现动态网页技术。

1、Servlet 开发

回顾 Tomcat 服务器

  • 以原生方式搭建项目,缺点十分明显,即不方便。
  • 接下来通过 IDE 集成 Tomcat 开发。

1.1、原生方式开发

原生方式开发步骤

  1. 导入依赖tomcat/lib/servlet-api
  2. 编写 Servlet:实现 Servlet 接口的 5 个方法
  3. 部署 Servlet
    1. 使用 javac 编译为 class 文件
    2. 将 class 文件复制到 WEB-INF/classes
    3. web.xml 中配置 Servlet
  4. 重启 Tomcat,访问 Servlet。

1.2、IDE 集成开发

基于 Maven 开发

1.2.1、web 工程

建议使用第一种方式

方式一:添加 Web Application 框架支持

  1. 创建空 Maven 项目

  2. 右键项目,添加框架支持

    image-20220319171342895

方式二:webapp 模板

  1. 选择 maven-archetype-webapp

    image-20220319171022033
  2. 更新 web.xml 文件

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
             version="4.0">
    </web-app>
    

1.2.2、项目结构

image-20220319172413090

1.2.3、集成 Tomcat

步骤

  1. 设置 IDEA 的应用服务器

    image-20220319173228676
  2. 配置项目的 Tomcat 服务器

    • 添加配置

      image-20220319173626202
    • 配置模块

      image-20220319173918367

说明

  1. 应用上下文:默认与 Artifact 同名,表示项目的 URL

    image-20220319174512773
  2. 服务器信息

    image-20220322001811587

1.3、开发步骤(❗)

  1. 导入依赖:导入 Maven 依赖即可,而无需手动导入 jar 包
  2. 编写 Servlet:实现 Servlet 接口的 5 个方法
  3. 部署 Servlet:在 web.xml 中配置 Servlet
  4. 启动 Tomcat:IDEA 自动编译并将 class 文件输出到 WEB-INF/classes
  5. 访问 Servlet

1.3.1、导入依赖

在父工程导入依赖并刷新 Maven,所有子模块都会生效。

<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
</dependency>

1.3.2、编写 Servlet

重点关注 service() 方法

public class HelloServlet implements Servlet {

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {...}

    @Override
    public ServletConfig getServletConfig() {...}

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("Hello, Servlet!");
    }

    @Override
    public String getServletInfo() {...}

    @Override
    public void destroy() {...}
}

1.3.3、部署 Servlet

  • servlet-name:Servlet 的名称(别名)

  • servlet-class:Servlet 所在类

  • url-pattern:Servlet 访问路径

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
             version="4.0">
        <!-- Servlet节点 -->
        <servlet>
            <servlet-name>helloServlet</servlet-name>
            <servlet-class>indi.jaywee.intro.HelloServlet</servlet-class>
        </servlet>
        <!-- Servlet映射节点 -->
        <servlet-mapping>
            <servlet-name>helloServlet</servlet-name>
            <url-pattern>/hello</url-pattern>
        </servlet-mapping>
    </web-app>
    

1.4、打包

1.4.1、常见打包方式

项目开发完成后,就需要进行打包发布

常见的打包方式

全称 包含资源 适用容器
jar Java Application Archive Java类的普通库、资源(resources)、辅助文件(auxiliary files) 应用服务器
(application servers)
war Web Application Archive web 应用程序。即所有 Java 类、配置信息、静态资源 小型服务程序容器
(servlet containers)
ear Enterprise Application Archive 企业应用程序。即所有 jar 文件、资源、Java 类和 Web 应用程序 EJB 容器
(EJB containers)

1.4.2、Tomcat 打包

Tomcat 以 war 包方式进行打包。

  1. 在项目结构中,选择要打包的模块

    image-20220319181133755
  2. 构建项目,项目自动打包输出到 out\artifacts 目录

    image-20220319181413549

1.4.3、Maven 打包

Maven 以 jar 包方式进行打包。

  1. 选择要打包的模块

  2. 先 clean 再 package,项目打包输出模块的 target 目录下

    image-20220320145439179

2、HTTP(❗)

了解 HTTP协议 相关知识,建议全文阅读

HTTP 协议 是 TCP/IP 协议族的一个子集。

  • 用于 C/S 通信,通过请求和响应的交换达成通信。
  • 客户端发出请求,服务器端响应请求并返回。

从 JavaWeb 开发的角度来看,至少要掌握以下概念。

2.1、基础知识

  1. URI 和 URL
  2. HTTP 版本:HTTP/1.0、HTTP/1.1
  3. 相关协议:DNS、TCP、IP

2.2、特点

  1. 无状态协议、Cookie
  2. 持久连接、管线化
  3. 内容协商(涉及的首部)

2.3、HTTP 报文

HTTP 报文:用于 HTTP 协议交互的信息。

  1. 分类:请求报文、响应报文

  2. 结构

    • 报文首部:请求行 / 响应行、首部字段(通用、请求/响应、实体、其它)
    • 报文主体:传输实体主体,如 Web 表单
  3. 常用请求方法GET、POST

    image-20220303171854893
  4. 常见响应状态码

示例

3、聊聊 Servlet(❗)

3.1、Servlet API

Servlet 体系

image-20220319190841407

  1. Servlet 接口
  2. GenericServlet 抽象类
  3. HttpServlet 抽象类

3.1.1、说明

Servlet 接口

  • 所有 Servlet 都会(直接或间接)实现该接口,接口方法如下

  • 以实现 Servlet 接口方式创建 Servlet,需要实现所有方法

    public interface Servlet {
        void init(ServletConfig var1) throws ServletException;
        ServletConfig getServletConfig();
        void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
        String getServletInfo();
        void destroy();
    }
    

GenericServlet 抽象类

  • 实现 Servlet 接口:实现了除 service() 以外的其它方法。
  • 以继承 GenericServlet 方式创建 Servlet,只需实现 service() 方法

HttpServlet 类

  • 继承 GenericServlet 抽象类:在抽象类的基础上扩展,以适用 HTTP。
  • 以继承 HttpServlet 方式创建 Servlet,需要实现处理对应请求的方法(doGet/ doPost/ doPut/ doDelete)

3.1.2、HttpServlet

Java Web 中推荐以继承 HttpServlet 方式创建 Servlet

HttpServlet 中有两个 service() 方法

service(ServletRequest,ServletResponse) service(HttpServletRequest,HttpServletResponse)
来源 重写自父类 重载
权限 public protected
说明 接收客户端请求,并分发给受保护的 service() 接收 HTTP 请求,并分发给特定 doXxx() 方法
作用 接收请求 处理请求

接收请求的 service()

判断客户端请求(响应)是否为 HTTP 请求(响应)。

  • :调用重载的 service()

  • :抛出异常

    @Override
    public void service(ServletRequest req, ServletResponse res)
        throws ServletException, IOException {
        HttpServletRequest  request;
        HttpServletResponse response;
        
        if (!(req instanceof HttpServletRequest &&
                res instanceof HttpServletResponse)) {
            throw new ServletException("non-HTTP request or response");
        }
    
        request = (HttpServletRequest) req;
        response = (HttpServletResponse) res;
    
        service(request, response);
    }
    

处理请求的 service()

判断请求类型,委托给对应的处理方法。

  1. 模板方法模式 的体现

  2. 请求首部 If-Modified-Since:指定时间,只有服务器资源在指定时间之后被修改过才返回。

    protcted void service(HttpServletRequest req, HttpServletResponse resp)
        throws ServletException, IOException {
        String method = req.getMethod();
    
        if (method.equals(METHOD_GET)) {
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                doGet(req, resp);
            } else {
                long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
                // 最近修改时间 > 指定时间,符合条件
                if (ifModifiedSince < lastModified) {
                    maybeSetLastModified(resp, lastModified);
                    doGet(req, resp);
                } else {
                    resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
                }
            }
    
        } else if (method.equals(METHOD_HEAD)) {
            long lastModified = getLastModified(req);
            maybeSetLastModified(resp, lastModified);
            doHead(req, resp);
    
        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);
            
        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);
            
        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);
            
        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);
            
        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);
            
        } else {
            //
            // Note that this means NO servlet supports whatever method was requested, anywhere on this server.
            //
    
            String errMsg = lStrings.getString("http.method_not_implemented");
            Object[] errArgs = new Object[1];
            errArgs[0] = method;
            errMsg = MessageFormat.format(errMsg, errArgs);
            
            resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
        }
    }
    

3.2、配置方式

3.2.1、web.xml

基于 XML 配置文件

之前的 Servlet 开发中使用的就是这种方式。

  • servlet

    • servlet-name:Servlet 的名称(别名)
    • servlet-class:Servlet 所在类(全限类名)
    • load-on-startup:启动时加载
  • servlet-mapping

    • servlet-name
    • url-pattern:Servlet 访问路径
  • welcome-file-list:欢迎页(首页)

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
             version="4.0">
        <!-- Servlet配置 -->
        <servlet>
            <servlet-name>helloServlet</servlet-name>
            <servlet-class>indi.jaywee.intro.HelloServlet</servlet-class>
            <load-on-startup>0</load-on-startup>
        </servlet>
        <!-- 映射配置 -->
        <servlet-mapping>
            <servlet-name>helloServlet</servlet-name>
            <url-pattern>/hello</url-pattern>
        </servlet-mapping>
    
        <!-- 首页 -->
        <welcome-file-list>
            <welcome-file>hello.jsp</welcome-file>
        </welcome-file-list>
    </web-app>
    

启动时加载

标记该 Servlet 是否在容器启动时加载

值必须是整数,越小优先级越高。

  • 非负数:容器在应用启动时加载并初始化这个 Servlet,值越小优先级越高。
  • 负数或没有设置:当 Servlet 被请求时才加载。

URL 匹配规则

匹配顺序:精确匹配 > 后缀匹配 > 通配

含义 说明
/具体URL 精确匹配 通过指定 URL 访问 Servlet
*.xxx 后缀匹配 通过 xxx 后缀访问 Servlet
/* 通配 通过任意 URL 访问 Servlet
/ 通配 通过任意 URL 访问 Servlet(除 JSP

3.2.2、@WebServlet

基于注解

Servlet 3.0 后支持,推荐使用

常用属性

  • name:Serlvet 名称(可省略)
  • value / urlPatterns:配置 URL,可以配置多个
  • loadOnStartup

示例

  1. 字面量方式:相当于省略 value=

    @WebServlet("/my")
    public class MyServlet1 extends HttpServlet {
    }
    
  2. name、多个 URL、启动时加载

    @WebServlet(name = "my", value = {"/my1", "my2"}, loadOnStartup = 0)
    public class MyAnnoServlet extends HttpServlet {
    }
    

3.3、请求 & 响应(❗)

HTTP 基于 请求/响应 的交换达成通信。

  • request 对象:处理客户端请求。
  • response 对象:向客户端返回请求资源。

3.3.1、request

常用方法

  • getParameter(String name):根据表单组件名称,获取相应数据。
  • setCharacterEncoding(String charset):设置请求报文的字符编码格式。

GET & POST

GET POST
数据 追加在 URL 之后 报文主体
特点 数据量小,明文传输,不安全 数据量大,密文传输,安全
效率 高,浏览默认请求方式 比 GET 低

中文乱码问题

原因:客户端和服务器端使用的字符编码方式不同。

  • GET 请求
    • Tomcat 7 及以下:客户端采用 UTF-8,而服务器端 request 对象采用 IS08859-1。
    • Tomcat 8 及以上:服务器端根据请求报文的编码格式,自动转换编码。
  • POST 请求:服务器端不会自动转换编码。

解决使用 setCharacterEncoding() 设置统一编码

3.3.2、response

常用方法

  • setCharacterEncoding(String):设置响应报文的字符编码格式。
  • setHeader(name,value):设置响应报文首部
  • setContentType(String):设置响应报文主体的媒体类型(实体首部)
  • getWriter():获取打印流(属于 I/O 的字符输出流)

中文乱码问题

原因:服务器端默认采用 IS08859-1 编码。

两种解决方案

  1. 设置响应报文的字符编码格式和报文首部

    response.setCharacterEncoding("utf-8");
    response.setHeader( "Content-type", " text/html;charset=UTF-8" );
    
  2. 设置响应报文主体的媒体类型推荐

    response.setContentType("text/html;charset=UTF-8");
    

3.3.3、示例

案例:模拟登录

  • 客户端表单通过 POST 方法向 Servlet 发起请求
  • request 对象接收客户端请求,response 对象通过打印流向客户端输出信息

页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>
<form action="login" method="post">
    用户名:<input type="text" name="username"><br>
    密码:<input type="password" name="psw"><br>
    <input type="submit"> <input type="reset">
</form>

</body>
</html>

LoginServlet

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 请求
        req.setCharacterEncoding("utf-8");

        String username = req.getParameter("username");
        String psw = req.getParameter("psw");
        System.out.println(username + ":" + psw);

        // 响应
        resp.setContentType("text/html;charset=utf-8");

        PrintWriter writer = resp.getWriter();
        writer.println("登录成功");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}

3.4、请求转发 & 重定向(❗)

3.4.1、request 转发

服务器端内部将请求发送到服务器上的另一个资源。

  • 说明:通常是一个 Servlet 用于接收并初步处理请求,转发给另一个资源生成响应。

  • 核心方法forward()

    request.getRequestDispatcher("/目标URL").forward (request, response);
    

特点

forward 将请求发送到服务器上的另一个资源(Servlet、JSP 或 HTML)

  1. 属于服务器行为:内部跳转
    • 客户端只发送一次请求。
    • 服务器内部对请求进行转发。
    • 浏览器地址栏不变。
  2. 转发范围:同一个 Web 应用中的资源。
  3. request 作用域:一次请求(多次转发可以共享同一作用域)
  4. 数据存取(键值对)
    • 存数据setAttribute(String key, Object value),可以存储任意类型数据
    • 取数据
      • 作用域中的数据getAttribute(String key)
      • 请求中的数据getParameter(String name)
    • 删数据req.removeAttribute(String name)

3.4.2、response 重定向

客户端发送请求给服务器,服务器响应给客户端一个新的请求地址;

客户端重新发送请求。

  • 说明:服务器向客户端发送一个临时重定向响应,设置状态码为 302,并将 相对URL 转换为 绝对URL

  • 核心方法sendRedirect()

    response.sendRedirect("目标URI");
    

特点

  1. 属于客户端行为:外部跳转
    • 客户端至少发送两次请求:对 Servlet 的请求、对重定向地址的请求。
    • 服务器发送临时重定向响应,客户端对新地址重新发出请求。
    • 浏览器地址栏是重定向后的地址。
  2. 重定向范围:任意位置资源。
  3. response 无作用域:重定向使客户端发送了不同的 request 请求,无法共享 request 数据。
  4. 数据传递:无法存储数据到作用域
    • 传数据:以 URI 拼接形式传递数据(类似 GET),只能是字符串类型
    • 取数据req.getParameter(String)

说明:当需要传递数据时,建议使用 forward() 请求转发,而不是 sendRedirect() 重定向。

3.4.3、示例

案例:使用 3.3.3 中的表单(修改 action 属性),对请求转发和重定向进行测试。

请求转发

  1. 客户端表单向 ServletA 发起请求

  2. ServletA 接收并初步处理请求(将属性保存作用域中)转发给 ServletB。

  3. ServletB 处理请求(获取作用域中的数据,向控制台输出)

  4. 客户端的地址栏不会发生改变。

    (伪代码如下,仅展示关键逻辑)

    @WebServlet("/fa")
    public class ForwardServletA extends HttpServlet {
        doGet(req, resp) {
            String username = req.getParameter("username");
            req.setAttribute("username", username);
            req.getRequestDispatcher("/fb").forward(req, resp);
        }
    	// doPost()
    }
    @WebServlet("/fb")
    public class ForwardServletB extends HttpServlet {
        @Override
        doGet(req, resp) {
            String username = (String) req.getAttribute("username");
            System.out.println(username);
        }
    	// doPost()
    }
    

重定向

  1. 客户端表单向 ServletA 发起请求

  2. ServletA 重定向给 ServletB,通过 URI 附带参数。

  3. ServletB 处理请求(获取 URI 参数数据,向控制台输出)

  4. 客户端的地址栏发生改变。

    (伪代码如下)

    @WebServlet("/ra")
    public class RedirectServletA extends HttpServlet {
        doGet(req, resp) {
            String username = req.getParameter("username");
            resp.sendRedirect("rb?username=" + username);
        }
    	// doPost()
    }
    @WebServlet("/rb")
    public class RedirectServletB extends HttpServlet {
        doGet(req, resp) {
            String username = req.getParameter("username");
            System.out.println(username);
        }
    	// doPost()
    }
    

3.4.4、URI 路径说明

URI 常涉及的问题

  1. 路径前面加不加 / ,区别是什么。
  2. 什么情况是基于当前 Web 应用访问。
  3. 什么情况是基于当前 Web 服务器访问。

根据转发和重定向的特点,就可以分辨区别。

请求转发

  • 转发范围:只能访问同一个 Web 应用中的资源。
  • 路径:无论是否有前缀 /,都是访问当前 Web 应用的资源。

重定向

  • 转发范围:任意位置资源。

    • 当前 Web 应用
    • 当前 Web 服务器的其它应用
    • 其它 Web 服务器的应用
  • 路径

    • 无前缀:访问当前 Web 应用中的资源。
    • 有前缀 /:访问当前 Web 服务器端口下的资源。

3.5、生命周期(❗)

Servlet 生命周期 4 阶段

service() 通常会执行多次,其它阶段只执行一次。

  1. 实例化:两种情况

    • 用户首次访问 Servlet 时,容器调用 Servlet 构造器进行实例化。
    • loadOnStart:容器启动时加载 Servlet 实例。
  2. 初始化:容器调用 init(),方法参数为 ServletConfig。

  3. 服务:当服务器端接收到客户端请求,容器调用 service() 将请求调度给 Servlet。

    • 方法参数:ServletRequest 与 ServletResponse 对象
    • Servlet 通常被访问多次,因此 service() 相应被执行多次。
  4. 销毁:当容器停止或重启,调用 destroy() 方法销毁 Servlet 对象。

    image-20220320102941667

3.6、线程安全

Servlet 被访问之后会产生一个实例

多线程并发访问同一个 Servlet 实例,会有线程安全问题。

情景

  1. 线程 A 访问 Servlet,执行代码 1 判断用户不合法(isValid == false)

  2. 线程 A 执行代码 2 之前,线程 B 访问 Servlet

  3. 线程 B 执行代码 1 判断用户合法(isValid == true)

  4. 此时线程 A 继续执行,执行代码 2 时 isValid 已经被修改为 true 了。

    public class SynServlet extends HttpServlet {
        private boolean isValid;
        doGet(req, resp) {
            String username = req.getParameter("username");
            String psw = req.getParameter("psw");
            // 1、判断用户合法性,若合法则赋值true
            // 2、isValid成立则输出
            if (isValid) {
                resp.getWriter().print("OK");
            }
        }
        // doPost()
    }
    

保证线程安全的方式

  1. synchronized 同步代码块
  2. SingleThreadModel 接口
  3. 改用局部变量

3.6.1、synchronized

  • 说明:将存在线程安全问题的代码放到同步代码块中。

  • 阻塞其它线程,不推荐

    synchronized (isValid) {
        // 1、判断用户合法性,若合法则赋值true
        // 2、isValid成立则输出
        if (isValid) {
            resp.getWriter().print("OK");
        }
    }
    

3.6.2、SingleThreadModel

  • 说明:每个线程访问 Servlet 时都会创建 Servlet 实例。

  • 浪费内存资源,效率低,不推荐

    public class SynServlet extends HttpServlet implements SingleThreadModel {
        // ...
    }
    

3.6.3、局部变量

  • 说明:尽量不要使用成员变量,改用局部变量。

  • 每个线程都会在方法内声明一个临时变量,推荐

    public class SynServlet extends HttpServlet {
        doGet(req, resp) {
            boolean isValid;   
    		//...
        }
    }
    

小结

Servlet 开发

  1. 开发方式
    • 原生
    • IDE 集成开发:Maven、集成 Tomcat
  2. 开发步骤(❗)
    1. 导入依赖
    2. 编写 Servlet:重点是 service() 方法
    3. 部署 Servlet
  3. 打包方式:jar、war、ear
    • Tomcat 打 war 包
    • Maven 打 jar 包

HTTP(❗)

  • 很重要,回顾 第 2 节
  • 有时间的话建议系统学习计算机网络(附 HTTP协议

Servlet 小结

  1. API
    • Servlet 接口:5 个方法
    • GenericServlet 抽象类:service() 抽象方法
    • HttpServlet 类:doXxx(),模板方法模式
  2. HttpServlet 的 2 个 service() 方法
    • 重写,用于接收请求
    • 重载,用于处理请求
  3. Servlet 配置方式
    • 配置:web.xml
    • 注解:@WebServlet
  4. 请求、响应(❗)
  5. 请求转发、重定向(❗)
  6. 生命周期(❗):实例化、初始化、服务(执行多次)、销毁
  7. 线程安全

Servlet vs 框架

  1. Serlert:性能最高,但需要手动实现很多功能。
  2. Spring MVC:基于 Servlet 的扩展,如同 MyBatis 封装 JDBC。
    • 封装共性代码,专注于业务代码。
    • 实现了 Servlet 的功能,只需在配置文件中注册。
  3. Spring Boot:自动装配,不用注册。
posted @ 2021-05-28 15:33  Jaywee  阅读(422)  评论(0编辑  收藏  举报

👇