Web开发之Servlet

Servlet 简述

Servlet 是运行在实现了 Java Servlet 规范的服务器端一个小程序,是 Java Web 的三大组件之一(ServletListener 监听器、Filter 过滤器),它属于动态资源,能够动态的根据请求的信息返回不同的响应

Servlet 是我们深入学习 SpringMVC 框架的基础

Servlet 接口

javax.serlvet.Servlet 接口是 Servlet 规范中最顶级的接口。此接口定义了初始化 servlet、为请求提供服务以及从服务器中删除 servlet 的方法。这些方法称为 Servlet 的生命周期方法,它们按照一定顺序调用

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

public interface Servlet {
    
    

    /**
     * servlet 容器在实例化 servlet 后调用 init 方法一次进行初始化。只有完成该方法(不抛出异常),然后servlet才能接收任何请求
     * @param config 包含 servlet 的配置和初始化参数的 ServletConfig 对象
     */
    public void init(ServletConfig config) throws ServletException;

    /**
     * 返回一个 ServletConfig 对象,该对象包含此servlet的初始化和启动参数,返回的ServletConfig对象是传递给init方法的参数对象
     * 此接口的实现负责存储ServletConfig对象,以便此方法可以返回它,实现此接口的 GenericServlet 类已经执行了此操作
     *
     * @return 辅助初始化servlet的ServletConfig对象。
     */
    public ServletConfig getServletConfig();

    /**
     * 由 servlet容器(Web 服务器)调用,以允许servlet处理和响应请求。只有在servlet的 init()方法成功完成后,才能调用此方法
     *
     * @param req 包含客户端请求的 ServletRequest 对象
     * @param res 包含 servlet 响应的 ServletResponse 对象
     */
    public void service(ServletRequest req, ServletResponse res)
            throws ServletException, IOException;

    /**
     * 返回有关 servlet 的信息,如作者、版本和版权。
     * 此方法返回的字符串为纯文本,而不是任何类型的标记(如 HTML、XML等)语言。
     *
     * @return 返回有关 servlet 的信息字符串
     */
    public String getServletInfo();

    /**
     * 由 servlet(Web 服务器)容器调用,以指示 servlet 正在停用
     * 当服务器正常关闭时会调用此方法,使servlet有机会清理正在保留的任何资源(例如, 内存、文件句柄、线程)
     * 在 servlet 容器调用此方法后,它将不会再在此 servlet 上调用service方法。
     */
    public void destroy();
}

上面的 Servlet 实例和方法都是由 Servlet 容器(如 tomcat 服务器)调用的,因此我们的项目不需要任何 main 方法,也不需要创建任何 Servlet 实例

Servlet 的生命周期和请求处理逻辑

  • Web 服务器接收到 HTTP 请求,将请求转发给 servlet 容器,servlet 容器中根据请求路径查找对应的 Servlet
  • 如果 servlet 实例还未创建,那么就创建一个 servlet 实例,随后 servlet 容器调用 servletinit() 方法对 servlet 进行初始化(该方法只会在 servlet 第一次被载入时调用)。被创建的 servlet 实例会被保留在容器中,当后续有同样的请求到来时可以直接处理,不再需要重新创建和初始化
  • servlet 容器会解析到来的 HTTP 请求,创建一个 Request 对象,并且创建一个 Response 响应对象,调用该 servlet 实例的 service() 方法将这两个对象作为参数传递进来,以期待 service() 方法中的业务逻辑处理 HTTP 请求,因此 service() 方法就是我们要重点开发的方法
  • 我们业务逻辑会将要返回的结果封装到 Servlet 容器提供的 Response 对象中,service() 方法执行完毕之后,Web 服务器会将 Response 对象中保存的动态生成的结果返回给客户端浏览器
  • 如果 Servlet 实例被销毁,那么将会由 Servlet 容器执行 destory() 方法,通常都是在服务器关闭时 Servlet 才会销毁,destory() 方法可以进行资源的释放

使用 Servlet 开发示例

我们使用 Servlet 时,需要对 Servlet 进行实现。一般来说,实现方式有 3

  • 实现 javax.servlet.Servlet 接口
  • 继承 javax.servlet.GenericServlet 抽象类
  • 继承 javax.servlet.http.HttpServlet 抽象类

实现 Servlet 接口

@WebServlet("/first-servlet")
public class FirstServlet implements Servlet {
    
    

    @Override
    public void init(ServletConfig servletConfig) {
    
    
        System.out.println("---------------");
        System.out.println("servletConfig: " + servletConfig);
        System.out.println("init");
    }

    @Override
    public ServletConfig getServletConfig() {
    
    
        System.out.println("---------------");
        System.out.println("getServletConfig");
        return null;
    }

    /**
     * 我们主要开发的方法
     *
     * @param servletRequest  请求对象
     * @param servletResponse 响应对象
     */
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
    
    
        //获取请求的数据
        String message = servletRequest.getParameter("xx");
        //封装响应数据
        servletResponse.setContentType("text/html");

        // Hello
        PrintWriter out = servletResponse.getWriter();
        out.println("<html><body>");
        out.println("<h1>" + message + "</h1>");
        out.println("</body></html>");
    }

    @Override
    public String getServletInfo() {
    
    
        System.out.println("getServletInfo");
        return null;
    }

    @Override
    public void destroy() {
    
    
        System.out.println("destroy");
    }
}

想要成功运行 servlet 还要进行 servlet 的路径配置。这里,我们使用 servlet 3.0 开始提供的 @WebServlet 注解来配置 servlet 的路径。当然也可以使用 web.xml 来配置,但是这比较麻烦。这里的路径必须使用 “/” 开头,并且相对的是当前应用根目录

如果是配置 web.xml,那么可以这样配置

<servlet>
    <!--3、 servlet的内部名称-->
    <servlet-name>firstServlt</servlet-name>
    <!--4、 servlet的类全路径名-->
    <servlet-class>com.example.servlet_01.FirstServlet</servlet-class>
</servlet>
<!-- 映射 -->
<servlet-mapping>
    <!--2、 servlet 的映射内部名称,通过他可以找到 servlet 的内部名称,不区分大小写-->
    <servlet-name>firstServlt</servlet-name>
    <!--1、 请求 servlet 的映射路径-->
    <url-pattern>/first-servlet</url-pattern>
</servlet-mapping>

继承 GenericServlet 抽象类

java.servlet.GenericServlet 类使编写 servlet 变得更容易,它实现了 ServletServletConfig 接口的方法(除了 service() 方法)。GenericServlet 类也实现了 log() 方法,这是一个在 ServletContext 接口中定义的方法,同时具有自己的 init()log() 方法

Generic 意为 “通用的”,自己编写的 servlet 直接扩展 GenericServlet 类比实现 Servlet 接口更加方便

@WebServlet("/generic-servlet")
public class MyGenericServlet extends GenericServlet {
    
    

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
    
    
        // 获取请求的数据
        String message = req.getParameter("xx");
        // 封装响应数据
        res.setContentType("text/html");

        // Hello
        PrintWriter out = res.getWriter();
        out.println("<html><body>");
        out.println("<h1>" + message + "</h1>");
        out.println("</body></html>");
    }
}

继承 HttpServlet 抽象类

实际上 GenericServlet 作为一个通用的 Servlet 实现,只提供了基本的功能,在某些情况下仍然不方便,比如我们的 Web 应用基本上都是基于 HTTP 协议的请求,因此非常有必要创造一个专门针对 HTTP 协议的 Servlet,比如处理各种请求,获取请求头信息等等

Java 已经为我们提供了这样的一个 Servlet 实现,那就是javax.servlet.http.HttpServletHTTP 的请求方式包括 DELETE、GET、OPTIONS、POST、PUT、TRACE,在 HttpServlet 类中分别提供了相应的服务方法,它们是 doDelete()、doGet()、doOptions()、doPost()、doPut()、doTrace()

当通过继承 HttpServlet 来创建一个 Servlet 时,我们只需要根据要处理的请求的类型,来重写不同的方法就可以了,处理 get 请求,则重写 doGet(),处理 post 请求,则重写 doPost(),无需实现其他方法,开发 Servlet 时更加简单

@WebServlet("/http-servlet")
public class MyHttpServlet extends HttpServlet {
    
    

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    
    
        // 获取请求的数据
        String message = req.getParameter("xx");
        // 封装响应数据
        resp.setContentType("text/html");
        // Hello
        PrintWriter out = resp.getWriter();
        out.println("<html><body>");
        out.println("<h1>" + message + "</h1>");
        out.println("</body></html>");
    }
}

HttpServlet 抽象类的设计

HttpServlet 类中提供了 service(HttpServletRequest,HttpServletResponse) 方法,这个方法是 HttpServlet 自己的方法,不是从 Servlet 继承来的

HttpServlet 重写自父类的 service(ServletRequest,ServletResponse) 方法中会把ServletReques t和 ServletResponse 强转成 HttpServletRequestHttpServletResponse,然后调用 service(HttpServletRequest,HttpServletResponse) 方法,这就不用自己去强转请求和响应对象了

@Override
public void service(ServletRequest req, ServletResponse res)
	throws ServletException, IOException {
    
    

	HttpServletRequest  request;
	HttpServletResponse response;

	try {
    
    
		request = (HttpServletRequest) req;
        response = (HttpServletResponse) res;
	} catch (ClassCastException e) {
    
    
        throw new ServletException("non-HTTP request or response");
	}
	service(request, response);
}
protected 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;
            try {
    
    
            	ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
            } catch (IllegalArgumentException iae) {
    
    
                ifModifiedSince = -1;
            }
            if (ifModifiedSince < (lastModified / 1000 * 1000)) {
    
                       
           		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 {
    
    
    	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);
    }
}

通过这些细可以发现,HttpServlet 的目的是让我们在进行基于 HTTP 请求的 Web 开发时更加简单,让开发者把更多的精力放在业务逻辑上去,而不是放在网络请求的解析上

Servlet 细节

Servlet 与单例

Servlet 是单例的,在 web 容器当中,在服务器关闭之前,自始至终就是同一个 Servlet。即同时并发的请求也是由同一个 servlet 处理的。这样的好处是能够减少对象的创建,提升性能,但是我们在开发 Servlet 的时候需要注意线程安全问题,尽量不要定义有状态的全局变量

多个映射路径

一个 Servlet,可以绑定一个或多个映射路径,通过这些映射路径发送的请求都会到达该Servlet

对于 web.xml 可以如下配置,配置多个 servlet-mapping,它们的 servlet-name 一致,但是 url-pattern 不一致

<servlet>
    <!--3、 servlet的内部名称-->
    <servlet-name>firstServlt</servlet-name>
    <!--4、 servlet的类全路径名-->
    <servlet-class>com.example.servlet_01.FirstServlet</servlet-class>
</servlet>

<servlet-mapping>
    <!--2、 servlet的映射内部名称,通过他可以找到servlet的内部名称,不区分大小写-->
    <servlet-name>firstServlt</servlet-name>
    <!--1、 请求servlet的映射路径-->
    <url-pattern>/first-servlet1</url-pattern>
</servlet-mapping>

<servlet-mapping>
    <!--2、 servlet的映射内部名称,通过他可以找到servlet的内部名称,不区分大小写-->
    <servlet-name>firstServlt</servlet-name>
    <!--1、 请求servlet的映射路径-->
    <url-pattern>/first-servlet2</url-pattern>
</servlet-mapping>

或者在一个 servlet-mapping 中配置多个 url-pattern

<servlet-mapping>
    <!--2、 servlet的映射内部名称,通过他可以找到servlet的内部名称,不区分大小写-->
    <servlet-name>firstServlt</servlet-name>
    <!--1、 请求servlet的映射路径-->
    <url-pattern>/first-servlet3</url-pattern>
    <url-pattern>/first-servlet4</url-pattern>
    <url-pattern>/first-servlet5</url-pattern>
</servlet-mapping>

对于采用 @WebServlet 注解的方式,那就更简单了,该注解的 value 是一个 String 数组,因此我们直接传递多个路径地址的数组就行了

@WebServlet({
    
    "/generic-servlet","/generic-servlet2","/generic-servlet3"})

映射路径通配符

Servlet 映射路径可以使⽤通配符 * ,通配符可以匹配任何的字符串

  • 如果路径是 /* 结尾,则表示匹配具有指定前缀路径的所有的路径(优先级最高)。它会拦截所有的请求,包括访问直接静态资源、jsp 文件的请求
  • 如果路径是 *. 扩展名,则表示匹配后缀结尾的任意路径
  • 这两种格式不能混用

Servlet 映射路径优先级

  • 对于某个具体的访问路径,如果存在该路径的 Servlet 映射,那么该 Servlet 优先级最高
  • 如果不存在,那么具有 /* 结尾的通配符路径的 Servlet 优先级最高
  • 如果不存在,那么具有 *.后缀名 结尾的通配符路径的 Servlet 优先级最高
  • 如果不存在,那么最后是具有 / 默认路径通配符的 Servlet,它也被称为默认 Servlet

如果使用了 /* 通配符 Servlet,那么当该 Servlet 中存在请求 include、forward、redirect 到其他没有显示指定路径的资源时(比如转发到某个 jsp、静态资源等),那么这个请求将会一直在这个 /* 通配符 Servlet 中循环直到抛出异常,因为转发的路径并没有在第一步匹配到具体的 Servlet 的映射,进而又匹配到了这个通配符 Servlet 本身,而这个通配符 Servlet 中又再一次进行了转发

JspServlet

对于以 *.jsp 结尾的访问路径,默认是被一个在 tomcat 容器的 conf/web.xml 配置文件中配置的 JspServlet 处理的,名为 jsp

<servlet>
	<servlet-name>jsp</servlet-name>
    <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
    <init-param>
    	<param-name>fork</param-name>
        <param-value>false</param-value>
    </init-param>
    
    <init-param>
    	<param-name>xpoweredBy</param-name>
        <param-value>false</param-value>
    </init-param>
    <load-on-startup>3</load-on-startup>
</servlet>

它的映射如下

<servlet-mapping>
	<servlet-name>jsp</servlet-name>
    <url-pattern>*.jsp</url-pattern>
    <url-pattern>*.jspx</url-pattern>
</servlet-mapping>

默认的 Servlet

Java Web 应用的一切请求都是在请求对应的 Servlet,动态的资源就不必说了,有自己的路径,对于静态资源,比如 html、css、js、图片等,虽然在请求 URL 中看起来是访问的静态资源的地址,但是实际上仍然是通过一个默认 Servlet 查找资源的,没找到就返回 404

默认 servlet 也是在 tomcat 容器在 conf/web.xml 配置文件中帮我们配置的,名为 default

<servlet>
	<servlet-name>default</servlet-name>
	<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
	
	<init-param>
		<param-name>debug</param-name>
        <param-value>0</param-value>
	</init-param>
	
	<init-param>
		<param-name>listings</param-name>
		<param-value>false</param-value>
	</init-param>
	<load-on-startup>1</load-on-startup>
</servlet>

它的映射如下

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

所以说,静态资源的请求,会被这个由 tomcat 为我们配置的 Default 拦截并处理,在该 Servlet 中,会尝试查找请求 URL 路径对应的静态资源,找到了就返回该资源,找不到就返回 404
  
如果在项目中自定义了映射路径为 /Servlet,那么会覆盖容器为我们配置的Default,转而走我们自己配置的 Servlet 的逻辑

Servlet 初始化时机

Servlet 在默认情况下,只有当对应的请求第一次到来时才会初始化,也可以配置在启动服务器时立即初始化某些 Servlet。这样的好处是,在第一次请求的时候不会再创建 Servlet 对象了,提升处理速度,缺点就是启动服务器时会更多的对象,可能会延长服务器启动时间

对于 web.xml 可以如下配置,在 servlet 标签中添加一个 load-on-startup 子标签,该标签用于指示容器是否应该在 web 应用程序启动的时候就加载这个 Servlet(实例化并调用其 init() 方法)

<servlet>
    <servlet-name>firstServlt</servlet-name>
    <servlet-class>com.example.servlet_01.FirstServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
</servlet>

标签中填写的是启动的优先级

  • 它的值必须是一个整数,当值为 0 或者大于 0 时,表示容器在应用启动时就加载并初始化这个 servlet。值越小,该 servlet 的优先级越高,应用启动时就越先加载
  • 如果该元素的值为负数或者没有设置,则容器会当 Servlet 被请求时再加载
  • 当值相同时,容器就会自己选择顺序来加载
  • 对于@WebServlet 注解,可以在 loadOnStartup 属性中配置该特性

猜你喜欢

转载自blog.csdn.net/weixin_38192427/article/details/115030300