什么是Servlet
在Java Web中Servlet、请求和响应是最基本的三个对象,在Web容器的管理下,这三者能够完成基本的HTTP请求处理。
Servlet的作用是为客户提供服务。servlet的角色是接受一个客户的请求,再返回一个响应。请求可能非常简单,例如:给我提供一个欢迎页面;也可能非常复杂,例如:为当前的购物车结账,这个请求会带一些客户端传来的参数,servlet需要知道自己如何使用请求中的参数,还需要知道该返回什么样的响应。
Java Web服务器处理用户请求的基本过程:用户在客户端点击一个链接,浏览器会向Web应用服务器发送一个URL请求,该URL会指向一个servlet;Web容器看出这个请求指向某个servlet A,就会创建两个对象(HttpServletRequest和HttpServletResponse),并分配或创建一个线程,调用servlet A对应的service方法(上述请求和响应对象作为参数);service根据HTTP请求区分出客户端发来的是GET还是POST请求,并调用对应的doGet()或doPost()方法;在doGet()或doPost()方法中进行业务逻辑的处理,处理完成后的结果通过响应对象返回写回给客户端。
Servlet体系结构
Servlet
Servlet的框架是由两个Java包组成的:javax.servlet与javax.servlet.http。在javax.servlet包中定义了所有的Servlet类都必须实现或者扩展的通用接口和类。在javax.servlet.http包中定义了采用Http协议通信的HttpServlet类。Servlet的框架的核心是javax.servlet.Servlet接口,所有的Servlet都必须实现这个接口。
在Servlet接口中定义了5个方法:
init(ServletConfig)方法:负责初始化Servlet对象,在Servlet的生命周期中,该方法执行一次;该方法执行在单线程的环境下,因此开发者不用考虑线程安全的问题;
service(ServletRequest req,ServletResponse res)方法:负责响应客户的请求;为了提高效率,Servlet规范要求一个Servlet实例必须能够同时服务于多个客户端请求,即service()方法运行在多线程的环境下,Servlet开发者必须保证该方法的线程安全性;
destroy()方法:当Servlet对象退出生命周期时,负责释放占用的资源;
getServletInfo:就是字面意思,返回Servlet的描述;
getServletConfig:这个方法返回由Servlet容器传给init方法的ServletConfig。
写一个简单的servlet来验证一下这些方法的执行:
①创建一个web项目
②编写一个MyServlet实现servlet接口,重写方法
package com.yj.servlet;
import java.io.IOException;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class MyServlet implements Servlet {
@Override
public void destroy() {
System.out.println("destroy");
}
@Override
public ServletConfig getServletConfig() {
System.out.println("getServletConfig");
return null;
}
@Override
public String getServletInfo() {
System.out.println("getServletInfo");
return null;
}
@Override
public void init(ServletConfig arg0) throws ServletException {
System.out.println("init");
}
@Override
public void service(ServletRequest arg0, ServletResponse arg1) throws ServletException, IOException {
System.out.println("service");
}
public MyServlet() {
super();
System.out.println("MyServlet 构造器");
}
}
③在 web.xml 文件中配置和映射这个servlet
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
<display-name>Servlet</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<!-- 配置和映射 Servlet -->
<servlet>
<!-- Servlet 注册的名字 -->
<servlet-name>MyServlet</servlet-name>
<!-- Servlet 的全类名 -->
<servlet-class>com.yj.servlet.MyServlet</servlet-class>
<!-- 配置启动时加载 -->
<!-- <load-on-startup>1</load-on-startup> -->
</servlet>
<servlet-mapping>
<!-- 需要和某一个 servlet 节点的 serlvet-name 子节点的文本节点一致 -->
<servlet-name>MyServlet</servlet-name>
<!-- 映射具体的访问路径: / 代表当前 WEB 应用的根目录 -->
<url-pattern>/hello</url-pattern>
</servlet-mapping>
</web-app>
浏览器发起请求后,如何找到对应的servlet来执行呢?
url-pattern-》servlet-name-》servlet-name-》servlet-class-》执行service方法
为了解耦,http服务器不直接调用servlet,而是把请求交给servlet容器(比如tomcat)来处理:https://blog.csdn.net/qq_24313635/article/details/105714724
ServletRequest & ServletResponse
对于每一个HTTP请求,servlet容器会创建一个封装了HTTP请求的ServletRequest实例传递给servlet的service方法,ServletResponse则表示一个Servlet响应,其隐藏了将响应发给浏览器的复杂性。通过ServletRequest的方法你可以获取一些请求相关的参数,而ServletResponse则可以将设置一些返回参数信息,并且设置返回内容。
ServletConfig
ServletConfig封装可以通过@WebServlet或者web.xml传给一个Servlet的配置信息,以这种方式传递的每一条信息都称做初始化信息,初始化信息就是一个个K-V键值对。为了从一个Servlet内部获取某个初始参数的值,init方法中调用ServletConfig的getinitParameter方法或getinitParameterNames方法获取,除此之外,还可以通过getServletConfig获取ServletConfig对象。
<servlet>
<servlet-name>MyServlet</servlet-name>
<servlet-class>com.yj.servlet.MyServlet</servlet-class>
<!-- 配置servlet的初始化参数,且节点必须要load-on-startup节点前面 -->
<init-param>
<param-name>user</param-name>
<param-value>root</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
ServletContext
servlet引擎为每个web应用程序都创建一个对应的servletContext对象,servletContext对象被包含在servletConfig对象中,调用servletConfig.getServletContext方法可以返回servletContext对象的引用。由于一个web应用程序中所有的servlet都共享同一个servletContext对象,所以,servletContext对象被称为application对象(web应用程序对象)。
<!-- 配置当前web应用的初始化参数,可以被所有的servlet获取-->
<context-param>
<param-name>driver</param-name>
<param-value>com.mysql.jdbc.Drvice</param-value>
</context-param>
ServletContext方法:
- 获取初始化的参数
getInitParameter(),这个和ServletConfig的getInitParameter方法类似
- 获取当前web应用的根路径下的某一个文件在服务器上面的绝对路径(是发布到tomcat之后的路径):
servletContext.getRealPath(String path)
- 获取当前web应用的根路径:
servletContext.getContextPath()
- 获取当前web应用的某一个文件对应的输入流
servletContext.getResourceAsStream(String path)
GenericServlet
前面编写的Servlet应用中通过实现Servlet接口来编写Servlet,但是我们每次都必须为Servlet中的所有方法都提供实现,还需要将ServletConfig对象保存到一个类级别的变量中,GenericServlet抽象类就是为了为我们省略一些模板代码,实现了Servlet和ServletConfig,完成了以下几个工作:
将init方法中的ServletConfig赋给一个类级变量,使的可以通过getServletConfig来获取。
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
同时为避免覆盖init方法后在子类中必须调用super.init(servletConfig),GenericServlet还提供了一个不带参数的init方法,当ServletConfig赋值完成就会被带参数的init方法调用。这样就可以通过覆盖不带参数的init方法编写初始化代码,而ServletConfig实例依然得以保存为Servlet接口中的所有方法提供默认实现。
HttpServlet
HttpServlet是基于Http协议实现的Servlet的基类,写Servlet时直接继承HttpServlet即可,不需要再重头实现Servlet接口,SpringMvc中的dispatcherServlet就是HttpServlet的子类。 HttpServlet是与Http协议相关的,HttpServlet处理请求主要是通过重写父类的service方法来完成具体的请求处理的。在service方法中首先是将ServletRequest和ServletResponse转换成HttpServletRequest和HttpServletResponse,然后根据请求的不同路由到不同的处理过程中去【处理方法就是我们常见的doXXX的方法。最常见的就是doGet和doPost】
Servlet的注册和运行
- servlet程序必须通过servlet容器来启动运行,并且储存目录有特殊要求,通需要存储在<WEB应用程序目录>\WEB-INF\classes目录中。
- servlet程序必须在WEB应用程序的web.xml文件中进行注册和映射其访问路径,才可以被servlet引擎加载和被外界访问。
- 一个<servlet>元素用于注册一个 servlet,它包含有两个主要的子元素<servlet-name>和<servlet-class>,分别用于设置 servlet的注册名称和 Servlet的完整类名。
- 一个<servlet-mapping>元素用于映射一个已注册的servlet的一个对外访问路径,它包含有两个子元素:<servlet-name>和<url-pattern>,分别用于指定servlet的注册名称和 servlet的对外访问路径。
关于servlet-mapping:
- 同一个servlet可以被映射到多个url上,即多个<servlet-mapping>元素的<servlet-name>子元素的设置值可以是同一个servlet的注册名
- 在servlet映射到的url中可以使用*通配符,但是只能有两种固定的格式,一种格式是“*.拓展名”,另一种是以正斜杆/开头,并以/*结尾
既带/又带拓展名的不合法:
<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<url-pattern>/*.html</url-pattern>
</servlet-mapping>
Servlet工作原理
当Web服务器接收到一个HTTP请求时,它会先判断请求内容——如果是静态网页数据,Web服务器将会自行处理,然后产生响应信息;如果牵涉到动态数据,Web服务器会将请求转交给Servlet容器。此时Servlet容器会找到对应的处理该请求的Servlet实例来处理,结果会送回Web服务器,再由Web服务器传回用户端。
针对同一个Servlet,Servlet容器会在第一次收到http请求时建立一个Servlet实例,然后启动一个线程。第二次收到http请求时,Servlet容器无须建立相同的Servlet实例,而是启动第二个线程来服务客户端请求。所以多线程方式不但可以提高Web应用程序的执行效率,也可以降低Web服务器的系统负担。
接着我们描述一下Servlet 容器Tomcat与Servlet是如何工作的,工作原理时序:
-
Web Client 向Servlet容器(Tomcat)发出Http请求;
-
Servlet容器接收Web Client的请求;
-
Servlet容器创建一个HttpServletRequest对象,将Web Client请求的信息封装到这个对象中,然后调用 Servlet 容器的 service 方法;
-
Servlet容器创建一个HttpServletResponse对象;
-
Servlet 容器拿到请求后,根据请求的 URL 和 Servlet 的映射关系,找到相应的 Servlet,如果 Servlet 还没有被加载,就用反射机制创建这个 Servlet,并调用 Servlet 的 init 方法来完成初始化,接着调用HttpServlet对象的service方法,把HttpServletRequest对象与HttpServletResponse对象作为参数传给 HttpServlet对象;
-
HttpServlet调用HttpServletRequest对象的有关方法,获取Http请求信息;
-
HttpServlet调用HttpServletResponse对象的有关方法,生成响应数据;
-
Servlet容器把HttpServlet的响应结果传给Web Client;
HttpServlet的service方法,根据请求的类型,分别调用不同的do方法执行:
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) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < lastModified) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
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);
}
}
Servlet生命周期
以前我们写的java类,都是由我们自己手动来进行调用。而servlet的生命周期则是由servlet容器控制,servlet容器负责创建servlet,并调用servlet的相关生命周期的方法。这里的说的servlet容器指的是运行servlet、jsp,filter等的软件环境(tomcat)。
Servlet生命周期管理主要有几个方法:
- 构造方法
- init()初始化阶段
- service()处理客户端请求阶段
- destroy()终止阶段
构造器方法:
只被调用一次(单例,共享的全局变量的话,会有线程安全的问题),只有第一次请求servlet时,创建servlet对象。
在容器启动时,XXXServlet在JVM的管理下被实例化(调用构造方法)为一个对象,这时候它还不是servlet。需要在容器的管理下调用init()方法进行初始化,获得ServletConfig和ServletContext对象的引用后,才称为一个真正的servlet。
init初始化阶段:
Servlet容器加载Servlet,加载完成后,Servlet容器会创建一个Servlet实例并调用init()方法,init()方法只会调用一次,
Servlet容器会在以下几种情况装载Servlet:
- Servlet容器启动时自动装载某些servlet,实现这个需要在web.xml文件中添加<load-on-startup>1</load-on-startup>
- 在Servlet容器启动后,客户首次向Servlet发送请求
- Servlet类文件被更新后,重新装载
service处理客户端请求阶段:
每次收到请求,调用一次,每收到一个客户端请求,服务器就会产生一个新的线程去处理。
对于用户的Servlet请求,Servlet容器会创建一个特定于请求的ServletRequest和ServletResponse。
对于tomcat来说,它会将传递来的参数放入一个HashTable中,这是一个String-->String[]的键值映射
终止阶段:
只被调用一次,当web应用被终止,或者Servlet容器终止运行,或者Servlet重新装载Servlet新实例时,Servlet容器会调用Servlet的destroy()方法,用于释放当前servlet锁占用的资源。
load-on-startup参数可以指定servlet被创建的时机(也就是调用构造方法和init方法):
- 若为负数,则在第一次请求时被创建
- 若为0或者正数,则在当前web应用被servlet容器加载时创建实例,且数字越小,越先被创建。
Servlet中的Listener
Listener 使用的非常广泛,它是基于观察者模式设计的,Listener 的设计对开发 Servlet 应用程序提供了一种快捷的手段,能够方便的从另一个纵向维度控制程序和数据。目前 Servlet 中提供了 5 种两类事件的观察者接口,它们分别是:4 个 EventListeners 类型的,ServletContextAttributeListener、ServletRequestAttributeListener、ServletRequestListener、HttpSessionAttributeListener 和 2 个 LifecycleListeners 类型的,ServletContextListener、HttpSessionListener。如下图所示:
它们基本上涵盖了整个 Servlet 生命周期中,你感兴趣的每种事件。这些 Listener 的实现类可以配置在 web.xml 中的 <listener> 标签中。当然也可以在应用程序中动态添加 Listener,需要注意的是 ServletContextListener 在容器启动之后就不能再添加新的,因为它所监听的事件已经不会再出现。掌握这些 Listener 的使用,能够让我们的程序设计的更加灵活。