☆* o(≧▽≦)o *☆嗨~我是小奥🍹
📄📄📄个人博客:小奥的博客
📄📄📄CSDN:个人CSDN
📙📙📙Github:传送门
📅📅📅面经分享(牛客主页):传送门
🍹文章作者技术和水平有限,如果文中出现错误,希望大家多多指正!
📜 如果觉得内容还不错,欢迎点赞收藏关注哟! ❤️
Servlet详解
Servlet是Server + Applet的缩写,表示一个服务器应用。
其实,Servlet就是一套规范,我们按照这套规范写出来的代码就可以直接在Java服务器上运行。
Servlet3.1中的Servlet结构如下图所示:
1. Servlet接口
说到规范,那当然还是接口最重要了,我们看一下接口的定义:
package javax.servlet;
import java.io.IOException;
/**
* 定义所有servlet必须实现的方法。
* servlet是在Web服务器中运行的小Java程序。servlet通常通过HTTP(超文本传输协议)接收和响应来自Web客户机的请求。
* 要实现这个接口,您可以编写一个扩展javax.servlet.GenericServlet的通用servlet,或者编写一个扩展javax.servlet.http.HttpServlet的HTTP servlet。
* 该接口定义了初始化servlet、服务请求和从服务器中删除servlet的方法。这些被称为生命周期方法,按以下顺序调用:
* 先构造servlet,然后用init方法进行初始化。
* 从客户机到服务方法的任何调用都将被处理。
* servlet退出服务,然后使用destroy方法销毁,然后进行垃圾收集并最终完成。
* 除了生命周期方法之外,该接口还提供了getServletConfig方法和getServletInfo方法,
* servlet可以使用该方法获取任何启动信息,getServletInfo方法允许servlet返回关于自身的基本信息,例如作者、版本和版权。
*/
public interface Servlet {
/**
* 由servlet容器调用,以指示servlet正在被放入服务中。
* servlet容器在实例化servlet之后只调用init方法一次。init方法必须成功完成,servlet才能接收任何请求。
* 如果使用init方法,servlet容器不能将servlet放入服务中:
* 1.抛出ServletException
* 2.没有在Web服务器定义的时间段内返回
*/
public void init(ServletConfig config) throws ServletException;
/**
*
* 返回一个ServletConfig对象,其中包含此servlet的初始化和启动参数。
* 返回的ServletConfig对象是传递给init方法的对象。
* 这个接口的实现负责存储ServletConfig对象,以便这个方法可以返回它。实现这个接口的GenericServlet类已经做到了这一点。
*/
public ServletConfig getServletConfig();
/**
* 由servlet容器调用,以允许servlet响应请求。
* 此方法仅在servlet的init()方法成功完成后调用。
* 响应的状态码应该始终为抛出或发送错误的servlet设置。
* servlet通常运行在多线程servlet容器中,可以并发处理多个请求。
* 开发人员必须注意同步访问任何共享资源,如文件、网络连接以及servlet的类和实例变量。
*/
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
/**
* 返回有关servlet的信息,如作者、版本和版权。
* 此方法返回的字符串应该是纯文本,而不是任何类型的标记(如HTML、XML等)。
*/
public String getServletInfo();
/**
*
* 由servlet容器调用,以指示servlet正在退出服务。
* 此方法仅在servlet服务方法中的所有线程都退出或超时时间过后才调用。
* 在servlet容器调用此方法之后,它将不会在此servlet上再次调用该服务方法。
* 此方法为servlet提供了一个机会来清理任何被占用的资源(例如,内存、文件句柄、线程),
* 并确保任何持久状态都与servlet在内存中的当前状态同步。
*/
public void destroy();
}
简单介绍一下几个方法的作用:
init
:init方法在容器启动时被容器调用(当load-on-startup
设置位负数或者不设置时会在Servlet
第一次用到时才会被调用),只会调用一次;getServletConfig
:用于获取ServletConfig
,即Servlet
的配置;service
:用于具体处理一个请求;getServletInfo
:获取一些Servlet
相关的信息,如作者、版权等,默认返回空串;destroy
:用于在Servlet
销毁时释放资源,也只会调用一次;
(1) ServletConfig
ServletConfig
指的是Servlet
的配置,也就是我们在web.xml
中定义的Servlet
时通过init-param
标签配置的参数就是通过ServletConfig
来保存的。
比如定义Spring MVC
的Servlet
时指定配置文件位置的contextConfigLocation
参数就保存在ServletConfig
中,如下:
<servlet>
<servlet-name>demoDispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>demo-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
下面我们看一下ServletConfig
接口的定义:
package javax.servlet;
import java.util.Enumeration;
/**
* 一个servlet配置对象,由servlet容器使用,在初始化期间向servlet传递信息。
*/
public interface ServletConfig {
/**
* 返回此servlet实例的名称。该名称可以通过服务器管理提供,在web应用程序部署描述符中分配,
* 或者对于未注册(因此未命名)的servlet实例,它将是servlet的类名。
*/
public String getServletName();
/**
* 返回对调用者正在其中执行的ServletContext的引用。
*/
public ServletContext getServletContext();
/**
* 获取具有给定名称的初始化参数的值。
*/
public String getInitParameter(String name);
/**
* 返回servlet初始化参数的名称,作为String对象的枚举,如果servlet没有初始化参数,则返回空枚举。
*/
public Enumeration<String> getInitParameterNames();
}
简单介绍一下几个方法的作用:
getServletName
:用于获取Servlet
的名字,也就是我们在web.xml
中定义的servlet-name
;getServletContext
:获取ServletContext
,表示我们这个应用本身;getInitParameter
:获取init-param
配置的参数;getInitParameterNames
:获取配置的所有init-param
的名字集合;
(2) ServletContext
ServletContext
非常重要,代表的其实就是我们应用本身。
那么自然,ServletContext
中设置的参数可以被我们当前应用的所有Servlet
共享,也就是我们在写功能时,把一些参数保存在Application
中,其实就是保存在了ServletContext
中了。
这里只是简单了解一下ServletContext
。
(3) 使用
ServletConfig
和 ServletContext
最常见的使用之一是传递初始化参数。
<!-- 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_3_1.xsd"
version="3.1" metadata-complete="true">
<display-name>initParam Demo</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>application-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>DemoServlet</servlet-name>
<servlet-class>com.excelib.DemoServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>demo-servlet.xml</param-value>
</init-param>
</servlet>
...
</web-app>
- 通过
context-param
配置的contextConfigLocation
配置到了servletContext
中 servlet
下面的init-param
配置的contextConfigLocation
配置到了ServletConfig
中
在Servlet中可以分别通过它们的getInitParameter
方法进行获取,比如:
String contextLocation = getServletConfig().getServletContext().getInitParameter("contextConfigLocation");
String servletLocation = getServletConfig().getInitParameter("contextConfigLocation");
另外**ServletContext
中非常常用的用法就是保存Application
级别的属性**,比如:
getServletContext().setAttribute("contextConfigLocation", "new path");
需要注意的是,这里设置的同名的Attribute
并不会覆盖initParameter
中的参数值,它们是两套数据,互不干扰。
ServletConfig
不可以设置属性。
2.GenericServlet
GenericServlet
是Servlet
的默认实现,主要做了三件事情:
- 实现了
ServletConfig
接口,我们可以直接调用该接口里面的方法; - 提供了无参的
init
方法; - 提供了
log
方法;
实现了ServletConfig接口
这样我们在需要调用ServletConfig中方法的时候可以直接调用,而不需要先获取ServletConfig了。其实是在底层帮我们内部调用了,源码如下:
/**
* 返回对servlet在其中运行的ServletContext的引用。
* 提供这种方法是为了方便。它从servlet的ServletConfig对象获取上下文。
*/
public ServletContext getServletContext() {
ServletConfig sc = getServletConfig(); // 先获取ServletConfig
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getServletContext();
}
提供了无参的init方法
GenericServlet
实现了Servlet的init(ServletConfig config)
方法,在里面将config设置给了内部变量config,然后调用无参的init方法,该方法是一个模板方法,在子类中可以通过覆盖它来完成自己的初始化工作,如下:
/**
* 由servlet容器调用,以指示servlet正在被放入服务中。
* 这个实现存储了它从servlet容器接收到的ServletConfig对象,以供以后使用。
* 当重写这种形式的方法时,调用super.init(config)。
*/
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
/**
*一个方便的方法,可以被重写,这样就不需要调用super.init(config)。
* 而不是重写init(ServletConfig),只需重写这个方法,它将被GenericServlet调用。
* init (ServletConfig配置)。ServletConfig对象仍然可以通过getServletConfig来检索。
*/
public void init() throws ServletException {
}
这样做有三个好处:
- 将参数
config
设置给内部属性config
,这样就可以在ServletConfig
的接口方法中直接调用config
的相应方法来执行; - 之后写
Servlet
时只需要处理自己的初始逻辑,不需要再关注config
; - 重写
init
方法时也不需要再调用super.init(config)
;
提供了log方法
- 记录日志的log
- 记录异常的log
具体实现是通过传给ServletContext的日志实现的。
/**
* 将指定的消息写入servlet日志文件,并以servlet的名称作为前置。
*/
public void log(String msg) {
getServletContext().log(getServletName() + ": "+ msg);
}
/**
* 将给定Throwable异常的解释性消息和堆栈跟踪写入servlet日志文件,并以servlet名称为前缀。
*/
public void log(String message, Throwable t) {
getServletContext().log(getServletName() + ": " + message, t);
}
完整源码如下:
package javax.servlet;
import java.io.IOException;
import java.util.Enumeration;
import java.util.ResourceBundle;
/**
* 定义一个通用的、独立于协议的servlet。要编写用于Web的HTTP servlet,请扩展javax.servlet.http.HttpServlet。
* GenericServlet实现Servlet和ServletConfig接口。GenericServlet可以由servlet直接扩展,
* 尽管更常见的是扩展特定于协议的子类,如HttpServlet。
* GenericServlet使编写servlet更容易。它提供了生命周期方法init和destroy的简单版本,以及ServletConfig接口中的方法。
* GenericServlet还实现了在ServletContext接口中声明的日志方法。
* 要编写通用servlet,只需要重写抽象服务方法。
*/
public abstract class GenericServlet
implements Servlet, ServletConfig, java.io.Serializable
{
private static final String LSTRING_FILE = "javax.servlet.LocalStrings";
private static ResourceBundle lStrings =
ResourceBundle.getBundle(LSTRING_FILE);
private transient ServletConfig config;
/**
*
* 什么也不做。所有servlet初始化都是由其中一个init方法完成的。
*
*/
public GenericServlet() { }
/**
* 由servlet容器调用,以指示servlet正在退出服务。
*/
public void destroy() {
}
/**
* 返回一个包含命名初始化参数值的字符串,如果参数不存在则返回null。
* 提供这种方法是为了方便。它从servlet的ServletConfig对象获取指定参数的值。
*/
public String getInitParameter(String name) {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getInitParameter(name);
}
/**
* 以String对象的枚举形式返回servlet初始化参数的名称,如果servlet没有初始化参数,则返回空枚举。
* 提供这种方法是为了方便。它从servlet的ServletConfig对象获取参数名。
*/
public Enumeration<String> getInitParameterNames() {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getInitParameterNames();
}
/**
* 返回servlet的ServletConfig对象。
*/
public ServletConfig getServletConfig() {
return config;
}
/**
* 返回对servlet在其中运行的ServletContext的引用。
* 提供这种方法是为了方便。它从servlet的ServletConfig对象获取上下文。
*/
public ServletContext getServletContext() {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getServletContext();
}
/**
* 返回有关servlet的信息,如作者、版本和版权。默认情况下,此方法返回一个空字符串。重写此方法以使其返回一个有意义的值。
*/
public String getServletInfo() {
return "";
}
/**
* 由servlet容器调用,以指示servlet正在被放入服务中。
* 这个实现存储了它从servlet容器接收到的ServletConfig对象,以供以后使用。
* 当重写这种形式的方法时,调用super.init(config)。
*/
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
/**
*一个方便的方法,可以被重写,这样就不需要调用super.init(config)。
* 而不是重写init(ServletConfig),只需重写这个方法,它将被GenericServlet调用。
* init (ServletConfig配置)。ServletConfig对象仍然可以通过getServletConfig来检索。
*/
public void init() throws ServletException {
}
/**
* 将指定的消息写入servlet日志文件,并以servlet的名称作为前置。
*/
public void log(String msg) {
getServletContext().log(getServletName() + ": "+ msg);
}
/**
* 将给定Throwable异常的解释性消息和堆栈跟踪写入servlet日志文件,并以servlet名称为前缀。
*/
public void log(String message, Throwable t) {
getServletContext().log(getServletName() + ": " + message, t);
}
/**
* 由servlet容器调用,以允许servlet响应请求。
* 这个方法被声明为抽象的,所以子类,比如HttpServlet,必须覆盖它。
*/
public abstract void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
/**
* 返回此servlet实例的名称。
*/
public String getServletName() {
ServletConfig sc = getServletConfig();
if (sc == null) {
throw new IllegalStateException(
lStrings.getString("err.servlet_config_not_initialized"));
}
return sc.getServletName();
}
}
3. HttpServlet
HttpServlet
是用HTTP协议实现的Servlet
的基类,写Servlet
时直接继承它就可以了,不需要再从头实现Servlet
接口。
HttpServle
t是跟协议有关的,所以我们就关注一下如何处理请求。
HttpServlet
的重点主要在service
中,在service
方法中首先将ServletRequest
和ServletResponse
转换为HttpServletRequest
和HttpServletResponse
,然后根据Http请求的类型不同将请求路由到了不同的处理方法:
@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和response类型
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
// 调用http的处理方法
service(request, response);
}
/**
* 从公共服务方法接收标准HTTP请求,并将其分派给该类中定义的doXXX方法。
* 该方法是Servlet的http特定版本Servlet. service方法。不需要重写这个方法。
*/
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不支持if-modified-since,则无需进一步执行昂贵的逻辑
doGet(req, resp);
} else {
// 获取请求头中的if-modified-since时间
long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
if (ifModifiedSince < lastModified) {
// 如果servlet修改时间较晚,则调用doGet()
// 向下舍入最近的秒,以进行正确比较
// ifModifiedSince为-1将始终小于lastModified
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
// 返回状态码为304(未修改)
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
// 获取最后修改时间
long lastModified = getLastModified(req);
// 设置响应头中的最后修改时间
maybeSetLastModified(resp, lastModified);
// 调用doHead()
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 {
// 注意,这意味着没有servlet支持该服务器上任何地方请求的任何方法。
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
// 返回状态码为501(未实现)
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}