前言
Servlet
(即Server applet
服务端小程序)是一个会在服务端被调用的Java
程序,来处理请求。前面在分析Tomcat
的时候我们知道,Tomcat
本身包含了一个Servlet
容器,用来存放Servlet
实例,当有请求到来时就会调用对应的Servlet
实例来处理。Servlet
其实就是一个Java
类,但如果它仅仅只是一个普通的Java
类,Tomcat
又如何知道该调用它的哪些方法呢?所以,我们设计了一个协议,或者说约定。当一个普通的Java
类实现了某些约定的方法,就可以被看作是一个Servlet
实例。因此Servlet
更像是一座联系服务器和服务端程序的桥梁。
Servlet 接口
Servlet
接口里就给我们定义了一个合格Servlet
类要实现哪些接口。
Servlet Interface
package javax.servlet;
import java.io.IOException;
public interface Servlet {
void init(ServletConfig var1) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
String getServletInfo();
void destroy();
}
以下给出了几个主要方法的介绍
方法名 | 介绍 |
---|---|
init() | 当一个Servlet 类实例化的时候会被调用的逻辑 |
service() | 对一个到来请求的具体处理逻辑 |
destory() | 一个Servlet 类实例被销毁时(通常是在tomcat 关闭时)调用的逻辑 |
我们可以看到service()
方法,Tomcat
会把一个请求(注意这里请求并不一定是http
请求,Servlet
可以处理多种请求)包装成ServletRequest
类的实例传入,service()
处理完,我们再把处理结果包装成ServletResponse
类的实例返回出去。理论上只要实现了这些接口,我们就可以构造出一个Servlet
了,但我们却很少这么做,因为这个过程过于繁琐,JavaEE
已经帮我们包装好了一些实现了Servlet
接口的类,我们使用的时候只要简单的重写这些类中的某个方法就可以了。继承这些类然后重写一些方法就好了,比如对于处理http
请求的Servlet
,就提供了HttpServlet
类供我们继承,我们来看看这些类的继承关系
上图我们可以看到,
HttpServlet
并没有直接实现Servlet
接口,而是继承了GenericServlet
类并在它的基础上针对http
进行了定制化。我们看看GenericServlet
做了什么。
-
GenericeServlet
类
public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {
private static final long serialVersionUID = 1L;
private transient ServletConfig config;
public GenericServlet() {
}
public void destroy() {
}
public String getInitParameter(String name) {
return this.getServletConfig().getInitParameter(name);
}
public Enumeration<String> getInitParameterNames() {
return this.getServletConfig().getInitParameterNames();
}
public ServletConfig getServletConfig() {
return this.config;
}
public ServletContext getServletContext() {
return this.getServletConfig().getServletContext();
}
public String getServletInfo() {
return "";
}
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {
}
public void log(String message) {
this.getServletContext().log(this.getServletName() + ": " + message);
}
public void log(String message, Throwable t) {
this.getServletContext().log(this.getServletName() + ": " + message, t);
}
public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
public String getServletName() {
return this.config.getServletName();
}
}
我们看到GenericServlet
类只是简单的实现了3
个借口中的方法,并没有什么实质性的代码,那么GenericServlet
这个抽象类的意义是什么?其实它更多的是提供一个模版,供子类去修改,而不是让每个子类都去实现这些接口中的所有方法,避免重复劳动。下面让我们具体分析继承了GenericServlet
的HttpServlet
做了什么。
-
HttpServlet
类实现
HttpServlet
HttpServlet
依然是一个抽象类,提供了一个模版。我们可以看到其内部重载定义了2
个Service
方法,我们先看最基础的以ServletRequest
为参数的service()
方法。
public void service(ServletRequest req, ServletResponse res)
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest)req;
response = (HttpServletResponse)res;
} catch (ClassCastException var6) {
throw new ServletException(lStrings.getString("http.non_http"));
}
this.service(request, response);
}
逻辑很简单,就是把请求和响应都强制类型转换成HttpServletRequest
和HttpServletResponse
类型,再调用protected void service(HttpServletRequest req, HttpServletResponse resp)
方法。若强制类型转换失败则说明该请求不是一个http
请求,直接抛出异常即可。
protected void service(HttpServletRequest req, HttpServletResponse resp)
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
long lastModified;
if (method.equals("GET")) {
lastModified = this.getLastModified(req);
if (lastModified == -1L) {
this.doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader("If-Modified-Since");
} catch (IllegalArgumentException var9) {
ifModifiedSince = -1L;
}
if (ifModifiedSince < lastModified / 1000L * 1000L) {
this.maybeSetLastModified(resp, lastModified);
this.doGet(req, resp);
} else {
resp.setStatus(304);
}
}
} else if (method.equals("HEAD")) {
lastModified = this.getLastModified(req);
this.maybeSetLastModified(resp, lastModified);
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[]{method};
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
这个service()
方法很长,但是逻辑很简单,就是根据http
请求中的方法去调用对应该方法的处理逻辑,比如http
请求是GET
方法就调用doGet()
,若是POST
就调用doPost()
。再让我们看看假设http
请求是GET
方法,在doGet()
中会发生什么。
-protected void doGet(HttpServletRequest req, HttpServletResponse resp)
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
它直接告诉我们这个方法还未实现,然后返回错误参数。所以doGet()
、doPost()
...等方法才是我们真正要实现的方法,来完成对应请求的处理逻辑。(init()
和destory()
也需要们实现)
配置和使用Servlet
下面利用利用实现一个简单的web
项目,来实践Servlet
,首先用IDEA
创建一个默认的web
项目
-
项目结构
项目结构 -
在
src
下编写一个Servlet
实例HelloServlet
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("HelloServlet中doGet()方法被调用了!");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
- 在
index.jsp
里设置一个超链接来指向这个Servlet
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<a href="HelloServlet">点击调用HelloServlet</a><br/>
</body>
</html>
让我们来运行这个项目, 然后点击这个超链接,但却给我们报了一个404
错误。我们之前说了tomcat
内部有一个Servlet
容器。就像配置Spring IOC
容器一样,我们需要通过配置的方式告诉tomcat
我们这个web
应用里有哪些Servlet
,还要告诉tomcat
当访问哪些路径时,调用这个Servlet
实例。配置以上信息的方式有2种,web.xml
和注解的方式,我们先尝试使用xml
的方式。
- 修改
web.xml
配置servlet
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/HelloServlet</url-pattern>
</servlet-mapping>
这里有2
部分组成, <servlet></servlet>
表示注册一个Servlet
实例到tomcat
的 servlet
容器中,类的名字叫HelloServlet
,并给他取了一个别名叫hello
。第二部分 <servlet-mapping></servlet-mapping>
则说明了一个url和对应servlet
实例的映射关系。表示当访问localhost:8080/webstudy/HelloServlet
路径时,会去调用一个别名为hello
的Servlet
实例对象的相应方法。配置好后让我们实验一下。
Connected to server
[2020-04-16 09:47:09,326] Artifact webstudy:war exploded: Artifact is being deployed, please wait...
[2020-04-16 09:47:09,601] Artifact webstudy:war exploded: Artifact is deployed successfully
[2020-04-16 09:47:09,601] Artifact webstudy:war exploded: Deploy took 275 milliseconds
HelloServlet中doGet()方法被调用了!
因为超链接是一个GET
方法,所以这里HelloServlet
的deGet()
方法被调用了,配置生效。
- 利用注解的方式配置
Servlet
Servlet3.0
之后开始支持注解的的方式配置Servlet
,编写一个WelcomeServlet
类,并用注解的方式把它配置到tomcat
中去。
@WebServlet("/WelcomeServlet")
public class WelcomeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("WelcomeServlet的doGet()方法被调用了");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
@WebServlet("/WelcomeServlet")
表示当访问localhost:8080/webstudy/WelcomeServlet
时,会调用这个对应的Servlet
中的对应方法。让我们添加一个链接到index.jsp
中去来测试该配置。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<a href="HelloServlet">点击调用HelloServlet</a><br/>
<a href="WelcomeServlet">点击调用WelcomeServlet</a><br/>
</body>
</html>
运行,并点击点击调用WelcomeServlet
这个链接,后台打印信息如下。
WelcomeServlet的doGet()方法被调用了
说明该配置方法也是生效的。
Servlet
生命周期
我们之前提到过Servlet
提供了3
个主要方法,HttpServlet
重点重写了service()
方法,而init()
和destroy()
方法并没有重写,GenericHttp
中也只是对这2
个方法给出了空实现。我们知道,service()
方法会在每个请求到来的时候被调用,来处理请求。那么这init()
和destroy()
个方法会在什么时候调用呢,让我们来实验一下。
- 修改
HelloServlet
类
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("HelloServlet的init()方法被调用了");
}
@Override
public void destroy() {
System.out.println("HelloServlet的destory()方法被调用了");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("HelloServlet中doGet()方法被调用了!");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
部署运行该项目, 观察控制台的输出
Connected to server
[2020-04-16 11:22:40,748] Artifact webstudy:war exploded: Artifact is being deployed, please wait...
[2020-04-16 11:22:40,973] Artifact webstudy:war exploded: Artifact is deployed successfully
[2020-04-16 11:22:40,973] Artifact webstudy:war exploded: Deploy took 225 milliseconds
16-Apr-2020 23:22:50.448 信息 [Catalina-utility-1] org.apache.catalina.startup.HostConfig.deployDirectory 把web 应用程序部署到目录 [/Users/LENN/tomcat/apache-tomcat-9.0.34/webapps/manager]
16-Apr-2020 23:22:50.478 信息 [Catalina-utility-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/Users/LENN/tomcat/apache-tomcat-9.0.34/webapps/manager] has finished in [30] ms
HelloServlet的init()方法被调用了
HelloServlet中doGet()方法被调用了!
我们发现当tomcat
启动的时候,init()
方法并没有被调用,也就说HelloServlet
并没有在tomcat
的servlet
容器中生成一个实例对象,而是直到第一次调用HelloServlet
中的doGet()
方法时,这个Servlet
才被实例化并加入tomcat
容器中去,此时init()
方法才被调用。是一种懒加载的思想。那么有没有什么办法,让tomcat
能够在启动的时候就把这些Servlet
实例化然后加载到容器中去呢?我们可以在web.xml
添加如下标签。
- 修改
web.xml
让HelloServlet
在tomcat
启动时就加载到容器中去
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>HelloServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/HelloServlet</url-pattern>
</servlet-mapping>
<load-on-startup>1</load-on-startup>
表示在tomcat
启动时就实例化并加载到tomcat
的servlet
容器里,中间的数字可以指明多个Servlet
的加载顺序,现在让我们重新启动tomcat
。
- 启动时
init()
就被调用
Connected to server
[2020-04-16 11:38:31,671] Artifact webstudy:war exploded: Artifact is being deployed, please wait...
HelloServlet的init()方法被调用了
[2020-04-16 11:38:31,913] Artifact webstudy:war exploded: Artifact is deployed successfully
[2020-04-16 11:38:31,914] Artifact webstudy:war exploded: Deploy took 243 milliseconds
ServletConfig类
我们一直重写的init()
方法都是无参数的,和Servlet
接口中定义的同名方法并不一样,我们来看看Servlet
接口中是如何定义的。
-
Servlet
接口中定义的init()
方法
void init(ServletConfig var1) throws ServletException;
我们可以看到这里有一个ServletConfig
类型的形参。我们一直重写的无参init()
方法实际上由GenericServlet
提供。
-
GenericServlet
中的init()
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {
}
GenericServlet
自动的帮我们把传入的ServletConfig
保存进一个类变量里,然后再调用我们重写的无参init()
的方法。显然这个ServletConfig
类型的变量是由tomcat
生成并传入进来的,如同它的名字一样,代表了一个Servlet
的配置。那么如何使用这个类呢。再看看GenericServlet
中的其他方法。
GenericServlet
public ServletConfig getServletConfig() {
return this.config;
}
public String getInitParameter(String name) {
return this.getServletConfig().getInitParameter(name);
}
发现我们可以利用getInitParameter(String name)
来获得一个Servlet
的初始参数配置,而这些配置则是以键值对的形式存在的,我们可以在web.xml
定义这些和Servlet
配置有关的键值对。
- 再
web.xml
定义InitParameter
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>HelloServlet</servlet-class>
<init-param>
<param-name>servletParam</param-name>
<param-value>servletValue</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
在些init-param
的作用范围是一个Servlet
内部,调用这个Servlet
的所有请求都可以拿到这些init-param
,让我们做个实验。
- 修改
HelloServlet
获取init-param
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("HelloServlet的init()方法被调用了");
System.out.println(super.getInitParameter("servletParam"));
}
}
运行tomcat
可以在控制台看到如下信息
Connected to server
[2020-04-16 11:46:10,610] Artifact webstudy:war exploded: Artifact is being deployed, please wait...
HelloServlet的init()方法被调用了
servletValue
我们成功获得了键servletParam
对应的值servletValue
。
除了在web.xml
配置一个Servlet
的init-param
,我们还可以用注解的方式定义。
- 利用注解定义
init-param
@WebServlet(value = "/WelcomeServlet", initParams = {@WebInitParam(name = "servletParam", value = "servletValue")})
public class WelcomeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("WelcomeServlet的doGet()方法被调用了");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
ServletContext
ServletConfig
代表了一个Servlet
的配置信息,其中主要包含了很多初始化参数。ServletContext
与ServletConfig
相似,只不过它的作用范围是整个tomcat
的Servlet
容器,也就是说所有的Servlet
实例都可以访问到ServletConfig
中的信息。GenericServlet
提供了一个方法可以让我们拿到这个类的实例
public ServletContext getServletContext() {
return this.getServletConfig().getServletContext();
}
可以看到ServletContext
由tomcat
包装进每一个Servlet
的ServletConfig
中,再在初始化的时候传入,从而可以共享这些信息。我们可以在web.xml
中定义这些信息。
- 定义
context-param
<context-param>
<param-name>globalParam</param-name>
<param-value>globalValue</param-value>
</context-param>
- 获取这些
context-param
public class HelloServlet extends HttpServlet {
@Override
public void init() throws ServletException {
System.out.println("HelloServlet的init()方法被调用了");
System.out.println(super.getInitParameter("servletParam"));
ServletContext servletContext = super.getServletContext();
System.out.println(servletContext.getInitParameter("globalParam"));
}
}
启动tomcat
,观察控制台
Connected to server
[2020-04-17 02:21:40,825] Artifact webstudy:war exploded: Artifact is being deployed, please wait...
HelloServlet的init()方法被调用了
servletValue
globalValue
成功获得context-param
网友评论