Servlet
简述
Servlet
是运行在实现了 Java Servlet
规范的服务器端一个小程序,是 Java Web
的三大组件之一(Servlet
、Listener
监听器、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
容器调用servlet
的init()
方法对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
变得更容易,它实现了 Servlet
和 ServletConfig
接口的方法(除了 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.HttpServlet
。HTTP
的请求方式包括 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
强转成 HttpServletRequest
和 HttpServletResponse
,然后调用 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
属性中配置该特性