JavaWeb 学习笔记 3:Servlet

JavaWeb 学习笔记 3:Servlet

1.简介

Servlet 是 JavaEE 定义的一套 Web 应用开发标准(接口),实现了该技术的 Web 服务器软件(如 Tomcat)上可以运行一个 Servlet 容器,只要我们使用 Servlet 技术开发 Web 应用,就可以打成 war 包后放在 Web 服务器上,Web 服务器软件可以自动解包,并执行其中 Servlet 相关的 API 实现类,以对外提供服务。

整个过程可以表示为:

1627234763207

2.快速开始

先按照上篇文章说的,创建一个 Maven Web 项目。

这里我使用 Maven 模板的方式创建项目:

mvn archetype:generate ^
-DgroupId=cn.icexmoon ^
-DartifactId=web-demo ^
-DarchetypeArtifactId=maven-archetype-webapp ^
-Dversion=0.0.1-snapshot ^
-DinteractiveMode=false

这里是 CMD 中执行的多行命令,因为分行符号的不同,不同的终端工具写法有所不同。

会自动生成一个 JUnit 依赖,不过版本太老(3.x),这里替换为 4.x:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>

添加 Tomcat 启动插件:

<plugins>
    <!--Tomcat插件 -->
    <plugin>
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat7-maven-plugin</artifactId>
        <version>2.2</version>
    </plugin>
</plugins>

添加 Servlet API 的依赖:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>

创建一个类,并实现Servlet接口:

public class HelloServlet implements Servlet {
    public void init(ServletConfig servletConfig) throws ServletException {

    }

    public ServletConfig getServletConfig() {
        return null;
    }

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

    public String getServletInfo() {
        return null;
    }

    public void destroy() {

    }
}

其中负责处理 HTTP 请求的是service方法,这里简单的在控制台打印一句话。

新版本的 Servlet 可以使用注解的方式指定请求路径:

@WebServlet("/hello")
public class HelloServlet implements Servlet {
	// ...
}

访问 http://localhost:8080/web-demo/hello,在服务端控制台可以看到输出。

3.Servlet 生命周期

Servlet 对象由 Web 服务器创建,其中实现的Servlet接口相关方法也由 Web 服务器在适当的时候执行。这些执行时机是和 Servlet 对象的生命周期相关的。

Servlet 对象有以下生命周期:

  • 实例创建:由 Web 服务器创建 Servlet 实例,默认为延迟创建(有相关的 HTTP 请求时才创建)。
  • 初始化:Servlet 对象创建后,Web 服务器会调用 Servlet.init()方法执行初始化工作,一般用于准备 Servlet 处理 HTTP 请求需要的资源等。
  • 处理请求:有 Servlet 相关的 HTTP 请求产生后,Web 服务器会调用Servlet.service()方法处理请求并返回处理结果(HTTP 响应报文)。
  • 销毁:当 Web 服务器(正常)关闭,或者 JVM 执行内存清理时,Web 服务器会调用Servlet.destroy()方法执行清理工作,主要包含对 Servlet 拥有资源的清理和关闭等。

3.1.init

默认情况下 init() 方法只会在第一次 HTTP 请求产生后被调用:

@WebServlet("/hello")
public class HelloServlet implements Servlet {
    public void init(ServletConfig servletConfig) throws ServletException {
        System.out.println("Servlet init...");
    }
    // ...
}

这是因为我们上边说过的,Servlet 实例创建的方式默认为延迟创建,而初始化是在创建之后执行。

可以将 Servlet 对象的创建方式修改为“急切创建”:

@WebServlet(value = "/hello", loadOnStartup = 1)
public class HelloServlet implements Servlet {
	// ...
}

属性loadOnStartup决定 Servlet 对象创建的时机:

  • 负数,延迟创建。在第一次相关的 HTTP 请求产生后创建 Servlet 对象。
  • 0 或正整数,急切创建。Web 服务器启动后立即创建 Servlet 对象,数字越小优先级越高。

3.2.service

在有相关的 HTTP 请求产生时被调用。负责处理 HTTP 请求,并生成响应报文。

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

3.3.destroy

Web 服务器(正常)关闭,或者 JVM 执行内存清理时被调用。

public class HelloServlet implements Servlet {
    public void destroy() {
        System.out.println("Servlet destroy...");
    }
    // ...
}

要注意的是,像直接终止 Tomcat 进程这种操作属于非正常关闭,Tomcat 是来不及执行正常的关闭流程的,也就不会执行 Servlet 的清理工作。

因此,要触发destroy方法,需要让 Tomcat 正常退出。其中的一个方法是在命令行下用 Maven 插件启动 Tomcat,并使用热键Ctrl+C退出程序:

D:\workspace\learn-javaweb\ch3\web-demo>mvn tomcat7:run
[INFO] Scanning for projects...
[INFO]
...
九月 08, 2023 11:33:53 上午 org.apache.coyote.AbstractProtocol start
信息: Starting ProtocolHandler ["http-bio-8080"]
Servlet destroy...
终止批处理操作吗(Y/N)? y

4.Servlet 抽象层级

4.1.请求分发

默认情况下,写在 Servlet.service()方法中的内容可以用于处理任意 Request Method 类型的 HTTP 请求。这通常是不合适的,通常我们需要对特定 Reuqest Method 的请求使用特定的处理。

比如,如果要对 http://localhost:8080/web-demo/hello 请求时的 POST 和 GET 方法执行不同的处理逻辑:

public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    switch (request.getMethod()){
        case "GET":
            System.out.println("From get request.");
            break;
        case "POST":
            System.out.println("From post request.");
            break;
        default:
    }
    System.out.println("Hello World!");
}

测试 POST 请求可以使用 HTTP 调试工具(比如 APIPost)。

4.2.模板模式

如果每个 Servlet 都要这样写,就很麻烦。可以使用模板模式的思想进行重构,抽离出一个 Servlet 的公共基类:

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

    }

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

    @Override
    public final void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        if (!(servletRequest instanceof HttpServletRequest)
                || !(servletResponse instanceof HttpServletResponse)) {
            throw new RuntimeException("Servlet 请求和响应对象不是 HTTP 相关");
        }
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        switch (request.getMethod()){
            case "GET":
                this.doGet(request, response);
                break;
            case "POST":
                this.doPost(request, response);
                break;
            default:
        }
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) {

    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) {

    }

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

    @Override
    public void destroy() {

    }
}

这里有一些细节:

  • 模板类并不需要生成实例,所以被定义为抽象类(abstract)。
  • service方法负责按照 Request Method 类别进行分发,被分发后的方法(比如 doGet)需要被子类继承和重写,所以设置为protected。考虑到灵活性,这里没有将其设置为抽象方法,否则即使不用处理POST请求,也必须重写doPost方法。
  • 分发请求的service()方法不能被子类重写,所以设置为final

现在只需要很少的代码就可以实现之前的要求:

@WebServlet("/hello2")
public class Hello2Servlet extends HttpServletTemplate{
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) {
        System.out.println("From post request.");
        System.out.println("Hello World2!");
    }

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) {
        System.out.println("From get request.");
        System.out.println("Hello World2!");
    }
}

可以按照需要覆盖doXXX方法。

4.3.HttpServlet

实际上这种抽象和重构的工作并不需要我们自己完成,Servlet 本身就提供类似的抽象:

image-20230908122518757

因此我们只需要继承HttpServlet就可以了:

@WebServlet("/hello3")
public class Hello3Servlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
        System.out.println("From get request.");
        System.out.println("Hello World3!");
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
        System.out.println("From post request.");
        System.out.println("Hello World3!");
    }
}

5.urlPattern

Servlet 依赖于 @WebServlet注解的urlPatterns属性确定是否与请求路径(url)匹配。

@WebServletvalue属性是urlPatterns属性的别名。

5.1.精确匹配

常见的 urlPattern 都是采用精确匹配:

@WebServlet("/user/list")
public class UserServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
        System.out.println("UserServlet...");
    }
}

此时只有请求路径是/user/list的 HTTP 请求才会被这个 Servlet 处理。

urlPattern 可以配置多个值,此时这些规则都可以用于匹配:

@WebServlet({"/user/list","/user/1"})

现在无论是/user/list这样的请求还是/user/1这样的请求都会被这个 Servlet 处理。

5.2.路径匹配

可以使用通配符(*)匹配某个路径下的请求,比如:

@WebServlet("/user/*")
public class User2Servlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
        System.out.println("User2Servlet...");
    }
}

示例中的 Servlet 可以处理任意以/user/开头的路径。

需要注意的是,是可以一个路径同时匹配多个 Servlet 的 urlPattern 的,比如目前这个示例中,/user/list这样的请求同时可以被UserServletUser2Servlet的规则匹配。此时精确匹配的优先级高于路径匹配,所以是UserServlet处理请求。

5.3.扩展名匹配

在 url 中同样可以使用扩展名作为请求结尾,比如:

http://localhost:8080/web-demo/book/list.do

可以用下面的 Servlet 匹配和处理:

@WebServlet("*.do")
public class User3Servlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
        System.out.println("User3Servlet...");
    }
}

需要注意的是,扩展名匹配前不能加路径分隔符/,比如:

@WebServlet("/*.do")

此时应用无法启动,会报错:

java.lang.IllegalArgumentException: Invalid <url-pattern> /*.do in servlet mapping...

5.4.任意匹配

可以用//*匹配任意路径的请求。

@WebServlet("/")
public class User4Servlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
        System.out.println("User4Servlet...");
    }
}

上面的示例可以匹配任意没有被其它 Servlet 匹配的请求。

可以同时配置//*匹配,但后者的优先级更高。此外/配置后会替换掉 Tomcat 默认的DefaultServlet,该 Servlet 负责处理对静态资源的请求。换言之,使用//*匹配路径可能导致无法访问静态资源。所以不推荐在项目中使用//*匹配路径。

最后,几种匹配模式的优先级是 精确匹配 > 目录匹配> 扩展名匹配 > /* > / 。

6.用 XML 配置 Servlet

Servlet 从 3.0 版本之后支持以注解的方式进行配置,在 3.0 版本之前需要在web.xml文件中配置。

添加一个不用注解配置的 Servlet:

public class User5Servlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doGet(req, resp);
        System.out.println("User5Servlet...");
    }
}

修改 web.xml

<web-app>
  <display-name>Archetype Created Web Application</display-name>
  <servlet>
    <servlet-name>user4servlet</servlet-name>
    <servlet-class>cn.icexmoon.webdemo.urlpattern.User4Servlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>user4servlet</servlet-name>
    <url-pattern>/user/4</url-pattern>
  </servlet-mapping>
</web-app>

和用注解配置的方式效果是相同的。

The End,谢谢阅读。

本文的完整示例可以从这里获取。

7.参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值