第 04 章 Servlet
该部分的代码主要集中在servlet-jspdemo04中。
引入Servlet
JSP的初中就是了更加方便的进行视图展示。但是在作为页面又嵌入了大量的Java代码。
那么为了改善JSP的运行和开发模式:JSP只负责进行数据的展示,而Servlet用于负责进行业务的传递。那么就形成了下面的开发模式:
- JSP,JSP本身就是一种负责显示视图的技术。
- 接受请求,调用JavaBean去处理请求。
- 显示处理后的数据
- JavaBean
- 封装数据
- 执行业务逻辑
MVC
- 模型层Model:其实就是之前所提到的
Dao
以及Service
。其代表的框架就有:jdbc、Mybatis、Hirbernat等等。- Dao层,主要负责数据持久化,调用数据库
- Service层,主要负责业务数据的处理
- 视图层View:JSP(严格意义称为后台技术)以及VUE。进行数据的展示。负责视图的展示。
- 交互层Controller:也叫数据交互层,负责接收前端视图层传递的数据,交由数据模型层(交给Service层,再由Service层交递Dao层)来进行业务处理。
Servlet作用
- 本身不做任何业务处理。由Service进行数据处理。
- 只是接收请求并决定调用哪个JavaBean去处理请求。
- 确定用哪个页面来显示处理返回的数据。
JavaWebApp生命周期[1]
JavaWeb 应用的生命周期是由 Servlet
容器来控制的。归纳起来,JavaWeb 应用的生命周期包括 3 个阶段。
- 启动阶段:加载Web应用的有关数据,创建`ServletContext对象 ,对Filter和一些Servlet进行初始化。
- 运行时阶段:为客户端服务。
- 终止阶段:释放Web应用所占用的各种资源。
Servlet生命周期以及web.xml
配置
下面将通过编写实现
Servlet
的方式讲解Servlet
在各个生命周期中的变化。要实现Servlet的相关功能也就不得不讲解web.xml
(demo04/src/main/webapp/WEB-INF/web.xml
)的作用以及如何配置。
关于Servlet在web.xml
中配置
与配置Servlet相关的标签有两个:
配置Servlet时,需要注意下面几点:
-
与
两个标签是成对出现的。 - 每一对
与
中的
必须一致! - 关于Url-pattern的配置方式
- 使用绝对路径
- 指定前缀,比如
/test*
,那么任何访问URL携带/testaaaaa
还是/testbbbbb
都将匹配到指定的Servlet类上。 - 使用通配符指定后缀,如
*.do
比如,下面的xml配置简单实现了一个Servlet访问。
ServletCreateMode1
com.ermao.servlet.controller.ServletCreateMode1
ServletCreateMode1
/servlet
这里已经配置好了一个servlet。下面将通过访问servlet一窥生命周期。
Servlet生命周期概述
Servlet生命周期分为四个阶段:
- 加载和实例化;
- 初始化;
- 处理请求;
- 销毁;
下面将通过实现Servlet
接口,讲解各个生命周期:
public class ServletCreateMode1 implements Servlet {
@Override
public void init (ServletConfig servletConfig) throws ServletException {
System.out.println("Servlet初始化");
}
@Override
public ServletConfig getServletConfig () {
System.out.println("获取Servlet配置");
return null;
}
@Override
public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println("业务被调用");
}
@Override
public String getServletInfo () {
System.out.println("获取Service信息");
return null;
}
@Override
public void destroy () {
System.out.println("Servlet被销毁");
}
}
从上面的代码中,我们可以看到实现Servlet
接口时,必须实现的5个方法,不过先关注下面三个方法。
-
init (ServletConfig servletConfig)
,Servlet被初始化和加载 -
service (ServletRequest servletRequest, ServletResponse servletResponse)
业务被调用 -
destroy ()
,Servlet被销毁。
加载和实例化
在请求抵达后,由容器加载完成。实例化过程既调用所编写的Servlet类的空参构造函数。
这里我们提供两个Servlet的代码;
public class ServletCreateMode1 implements Servlet {
public ServletCreateMode1 () {
System.out.println(this.getClass().getSimpleName()+"被实例化!");
}
@Override
public void init (ServletConfig servletConfig) throws ServletException {
System.out.println("【"+this.getClass().getSimpleName()+"】执行init()进行初始化!");
}
@Override
public ServletConfig getServletConfig () {
System.out.println("获取Servlet配置");
return null;
}
@Override
public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println(this.getClass().getSimpleName()+"执行业务代码service()");
}
@Override
public String getServletInfo () {
System.out.println("获取Service信息");
return null;
}
@Override
public void destroy () {
System.out.println(this.getClass().getSimpleName()+"Servlet被销毁!");
}
}
第二个Servlet代码如下:
public class ServletCreateMode2 extends GenericServlet {
public ServletCreateMode2 () {
System.out.println(this.getClass().getSimpleName() + "被实例化");
}
@Override
public void init () throws ServletException {
System.out.println("【"+this.getClass().getSimpleName() + "】执行init()进行初始化!");
}
@Override
public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println(this.getClass().getSimpleName() + "执行业务代码service");
}
@Override
public void destroy () {
super.destroy();
System.out.println(this.getClass().getSimpleName()+"Servlet被销毁!");
}
}
下面则为web.xml
的配置文件:
ServletCreateMode1
com.ermao.servlet.controller.ServletCreateMode1
ServletCreateMode1
/servlet
ServletCreateMode2
com.ermao.servlet.controller.ServletCreateMode2
ServletCreateMode2
/genericservlet
在控制台中开启运行后,先将tomcat的启动的日志清除掉后,再观察控制台输出(分别访问相关Servlet的地址,这里使用谷歌和火狐浏览器分别访问对应地址)。得到下面的输出日志:
# 在谷歌浏览器中访问/servlet(第一次访问)
ServletCreateMode1被实例化!
【ServletCreateMode1】执行init()进行初始化!
ServletCreateMode1执行业务代码service()
# 在谷歌浏览器中访问/genericservlet(第一次访问)
ServletCreateMode2被实例化
【ServletCreateMode2】执行init()进行初始化!
ServletCreateMode2执行业务代码service
# 下面两条日志则是在火狐浏览器中的访问结果(第2次以及第3次访问)
ServletCreateMode1执行业务代码service()
ServletCreateMode2执行业务代码service
ServletCreateMode1执行业务代码service()
ServletCreateMode2执行业务代码service
从上面的结果中可以看出,满足我们之前的描述。当容器接受请求后,将加载和实例化对应Servlet。如果此时Servlet没有进行过初始化,那么对应的Servlet将执行各自的初始化方法init()
。
初始化
可结合上一节的内容(《加载和初始化》)理解整个生命周期。
在Servlet的生命周期中,仅执行一次init()
方法,它是在服务器装入Servlet时执行的,可以配置服务器,以在启动服务器或客户机首次访问Servlet时装入Servlet。无论有多少客户机访问Servlet,都不会重复执行init()
;
只会初始化一次,实例被加载时。
当用户调用一个 Servlet 时,就会创建一个 Servlet 实例,每一个用户请求都会产生一个新的线程,适当的时候移交给 doGet 或 doPost 方法。init() 方法简单地创建或加载一些数据,这些数据将被用于 Servlet 的整个生命周期。
处理请求
每次有请求抵达,都会触发业务处理。service()
方法是执行实际任务的主要方法。Servlet 容器(即 Web 服务器)调用 service()
方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。
每次服务器接收到一个 Servlet 请求时,服务器会产生一个新的线程并调用服务。service() 方法检查 HTTP 请求类型(GET
、POST
、PUT
、DELETE
等),并在适当的时候调用 doGet
、doPost
、doPut
,doDelete
等方法。
下面是该方法的特征:
销毁
容器关闭,或者servlet被回收时,该方法将会被调用。这里将结合《加载和实例化》中的代码,进行演示。将tomcat关闭后。控制台输出结果如下所示。
# 在谷歌浏览器中访问/servlet(第一次访问)
ServletCreateMode1被实例化!
【ServletCreateMode1】执行init()进行初始化!
ServletCreateMode1执行业务代码service()
# 在谷歌浏览器中访问/genericservlet(第一次访问)
ServletCreateMode2被实例化
【ServletCreateMode2】执行init()进行初始化!
ServletCreateMode2执行业务代码service
# 下面两条日志则是在火狐浏览器中的访问结果(第2次以及第3次访问)
ServletCreateMode1执行业务代码service()
ServletCreateMode2执行业务代码service
ServletCreateMode1执行业务代码service()
ServletCreateMode2执行业务代码service
/Users/futianyu/Development.localized/Code.localized/WebServer/apache-tomcat-9.0.36/bin/catalina.sh stop
Disconnected from the target VM, address: '127.0.0.1:51324', transport: 'socket'
20-Jun-2020 20:14:56.527 信息 [main] org.apache.catalina.core.StandardServer.await 通过关闭端口接收到有效的关闭命令。正在停止服务器实例。
20-Jun-2020 20:14:56.527 信息 [main] org.apache.coyote.AbstractProtocol.pause 暂停ProtocolHandler["http-nio-8084"]
20-Jun-2020 20:14:56.538 信息 [main] org.apache.catalina.core.StandardService.stopInternal 正在停止服务[Catalina]
ServletCreateMode1Servlet被销毁!
ServletCreateMode2Servlet被销毁!
20-Jun-2020 20:14:56.549 信息 [main] org.apache.coyote.AbstractProtocol.stop 正在停止ProtocolHandler ["http-nio-8084"]
20-Jun-2020 20:14:56.552 信息 [main] org.apache.coyote.AbstractProtocol.destroy 正在摧毁协议处理器 ["http-nio-8084"]
Disconnected from server
从控制台的结构可以看出,当容器关闭后,各个Servlet将逐一执行destroy()
方法,销毁各自的Servlet。
Servlet中web.xml
配置
下面将讲解servlet中常用的一些配置信息。当然还有其他的配置,
web.xml配置总览
名称 | 描述 | 常用 |
---|---|---|
display-name | 定义了WEB应用的名字 | T |
description | 声明WEB应用的描述信息 | T |
distributable | 元素为空标签,它的存在与否可以指定站台是否可分布式处理.如果web.xml中出现这个元素,则代表站台在开发时已经 被设计为能在多个JSP Container 之间分散执行. | F |
context-param | 声明应用范围内的初始化参数。在应用内共享. | T |
filter | 过滤器元素将一个名字与一个实现javax.servlet.Filter接口的类相关联。 | T |
filter-mapping | 一旦命名了一个过滤器,就要利用filter-mapping元素把它与一个或多个servlet或JSP页面相关联。 | T |
listener | servlet API的版本2.3增加了对事件监听程序的支持,事件监听程序在建立、修改和删除会话或servlet环境时得到通知。Listener元素指出事件监听程序类。 | T |
servlet | 在向servlet或JSP页面制定初始化参数或定制URL时,必须首先命名servlet或JSP页面。Servlet元素就是用来完成此项任务的。 | T |
servlet-mapping | 定义了servlet与url之间的映射关系,其name与 |
T |
session-config | 如果某个会话在一定时间内未被访问,服务器可以抛弃它以节省内存。可通过使用HttpSession的setMaxInactiveInterval方法明确设置单个会话对象的超时值,或者可利用session-config元素制定缺省超时值。 | T |
mime-mapping | 如果Web应用具有想到特殊的文件,希望能保证给他们分配特定的MIME类型,则mime-mapping元素提供这种保证。 | F |
welcome-file-list | 欢迎页面(html,htm或jsp等) | T |
error-page | 异常被抛出时,指定将要显示的页面。 | T |
jsp-config | 用于为Web应用程序中的JSP文件提供全局配置信息。 它有两个子元素,taglib和jsp-property-group。 | F |
security-constraint | 用于将安全约束与一个或多个Web资源集合相关联 | F |
login-config | 指定服务器应该怎样给试图访问受保护页面的用户授权。它与sercurity-constraint元素联合使用。 | T |
security-role | 定义安全角色.该定义包括对安全角色的可选描述以及安全角色名称。 | F |
resource-env-refType | 声明与资源相关的一个管理对象。 | F |
resource-ref | 声明一个资源工厂使用的外部资源。 | F |
下面的介绍,并不会全部介绍,其中只介绍了部分常用的web.xml
配置。其他的可以在《参考资料》中提供的链接可以找到相关使用。
Servlet初始化参数配置
ServletCreateMode1
com.ermao.servlet.controller.ServletCreateMode1
charSetContent
UTF-8
ServletCreateMode1
/servlet
通过设置
设置对应的Servlet初始化的参数,其可设置的一般有下面两个标签:
-
设置参数名称,英文; -
设置参数指,也是英文。UTF-8
设置完成后,可以通过init(ServletConfig servletConfig)
中的参数servletConfig
获取在web.xml
中设置的参数信息,比如下面的代码:
public void init(ServletConfig config) throws ServletException {
String initParam=config.getInitParameter("charSetContent");
System.out.println(initParam);
}
其次,设置的初始化参数只对所设置的servlet有效,对未设置的servlet则无效,其他的servlet无法获取到。比如:ServletA设置了编码格式,那么ServletB则无法获取到ServletA的初始化参数值,但是ServletA的子类可以获取到。
通过配置实现Servlet的加载顺序
在
下面还有一个参数
,是用于控制,Servlet的加载顺序的。如果没有设置,那么容器将按照自定义的顺序进行加载(当请求抵达容器后,加载初始化对应的Servlet类)。
myServlet
com.ermao.demo.MyServlet
1
中的数字指明servlet加载顺序,数字小的先加载。如果值为负或未指定,web容器可以在任何时候加载servlet。
下面通过一个例子来看下其加载的顺序变化:
ServletCreateMode1
com.ermao.servlet.controller.ServletCreateMode1
charSetContent
UTF-8
1
ServletCreateMode1
/servlet
ServletCreateMode2
com.ermao.servlet.controller.ServletCreateMode2
2
ServletCreateMode2
/genericservlet
ServletCreateMode3
com.ermao.servlet.controller.ServletCreateMode3
3
ServletCreateMode3
/httpservlet
当容器启动后,控制台输出日志如下:
# 容器启动后,随机分别加载了三个Servlet并执行了初始化。
ServletCreateMode1被实例化!
【ServletCreateMode1】执行init()进行初始化!
UTF-8
ServletCreateMode2被实例化
【ServletCreateMode2】执行init()进行初始化!
ServletCreateMode3被实例化!
【ServletCreateMode3】执行init()进行初始化!
[2020-06-21 02:41:44,476] Artifact demo04:war exploded: Artifact is deployed successfully
[2020-06-21 02:41:44,476] Artifact demo04:war exploded: Deploy took 618 milliseconds
ServletContext配置
ServletContext
也就是我们JSP中说的Application
作用域,ServletContext
中配置的参数,不属于任何一个Servlet
,而是属于整个webapp
的。由于ServletContext属于webapp,所以在层次上与
标签属于同一层。
访问初始化值
visitTimes
0
登录初始化值
loginTimes
0
仍然需要注意的一点是:
必须唯一。如何获取整个Servlet的配置信息?
@Override
protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Enumeration names = getServletConfig().getServletContext().getInitParameterNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
System.out.println(name+":"+getServletConfig().getServletContext().getInitParameter(name));
}
}
通过获取ServletContext
上下文,getServletConfig().getServletContext().getInitParameterNames()
来获取整个webapp的初始化参数值。
Session会话超时配置
会话超时配置的时间单位为:分钟!
120
配置错误页面
配置错误页面有两种方式,一种是通过定义
来定义错误错误页面;另一种是通过定义通过异常的类型
来定义错误错误页面。
第一种,当系统发生404错误时,跳转到错误处理页面NotFound.jsp。定义方式:
404
/NotFound.jsp
第二种,通过异常的类型配置error-page
,下面面配置了当系统发生java.lang.NullException
(即空指针异常)时,跳转到错误处理页面error.jsp
java.lang.NullException
/error.jsp
其他配置
用于定义项目名称。
用于定义项目的描述信息。
下面有两个子标签
-
标签内值为/路径/image.gif
。small-icon元素应指向web站台中某个小图标的路径,大小为16 X 16 pixel,但是图象文件必须为GIF或JPEG格式,扩展名必须为:.gif
或.jpg
。
mime-mapping
是web.xml
中的一个节点,用来指定对应的格式的浏览器处理方式。如下所示:
html
text/html;charset=UTF-8
设置的是欢迎页面的列表。
index.jsp
index.html
index.htm
访问一个网站时,默认看到的第一个页面就叫欢迎页,一般情况下是由首页来充当欢迎页的。一般情况下,我们会在web.xml
中指定欢迎页
如果指定了多个欢迎页面,显示时按顺序从第一个找起,如果第一个存在,就显示第一个,后面的不起作用。如果第一个不存在,就找第二个,以此类推。
访问一个网站时,默认看到的第一个页面就叫欢迎页,一般情况下是由首页来充当欢迎页的。一般情况下,我们会在web.xml
中指定欢迎页。但web.xml
并不是一个Web的必要文件,没有web.xml,网站仍然是可以正常工作的。只不过网站的功能复杂起来后,web.xml
的确有非常大用处,所以,默认创建的动态web工程在WEB-INF文件夹下面都有一个web.xml
文件。默认去项目路径下寻找.jsp
或者.html
文件
Servlet作用域
在讲解JSP时,提到了有4个作用域,他们分别是:HttpServletRequest
、HttpSession
、application
(其本质是ServletContext
)以及PageContext
,这四个作用域。我们可以通过Tomcat所编译的JSP文件的代码可以看出在Servlet中如何获取相关作用域。因为Servlet属于非视图层,所以这里无法查看PageContext
。故我们主要看下Servlet中如何获取其他三个作用域:HttpServletRequest
、HttpSession
、application
(其本质是ServletContext
)。
下面给出的是JSP编译生成的java文件。我们主要看_jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
方法。
public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
throws java.io.IOException, javax.servlet.ServletException {
...
// 定义所使用到的变量:request,session,application,pageContext;
final javax.servlet.jsp.PageContext pageContext;
javax.servlet.http.HttpSession session = null;
final javax.servlet.ServletContext application;
final javax.servlet.ServletConfig config;
javax.servlet.jsp.JspWriter out = null;
final java.lang.Object page = this;
javax.servlet.jsp.JspWriter _jspx_out = null;
javax.servlet.jsp.PageContext _jspx_page_context = null;
try {
response.setContentType("text/html;charset=UTF-8");
pageContext = _jspxFactory.getPageContext(this, request, response,null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
_jspx_out = out;
....
} catch (java.lang.Throwable t) {
....
} finally {
_jspxFactory.releasePageContext(_jspx_page_context);
}
}
从上面的代码中,我们可以看出:
-
request
就是HttpServletRequest
; -
session
就是HttpSession
; -
application
就是ServletContext
;
那么在JSP中应该如何获取上面的作用域?从下面的方法service()
中,我们可以看出一些端倪:
public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
}
servletRequest
与servletResponse
与JSP中的HttpServletRequest
与HttpServletResponse
很像。但是,ServletRequest
与ServletResponse
是无法满足我们的开发需求,我们查看下其继承关系如下:
-
HttpServletRequest
是继承自ServletRequest
; -
HttpServletResponse
是继承自ServletResponse
;
那么可以通过下面的代码,直接进行强转HttpServletRequest
与HttpServletResponse
。从而获取到对应的其他三个作用域,以及请求的其他信息:
@Override
public void init (ServletConfig servletConfig) throws ServletException {
System.out.println("【"+this.getClass().getSimpleName()+"】执行init()进行初始化!");
// 获取编码设置
charSetContent = servletConfig.getInitParameter("charSetContent");
// 将配置信息存储到servletConfig中。
this.servletConfig = servletConfig;
System.out.println(charSetContent);
}
@Override
public ServletConfig getServletConfig () {
System.out.println("获取Servlet配置");
return servletConfig;
}
@Override
public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
System.out.println(this.getClass().getSimpleName()+"执行业务代码service()");
// 作用域获取:Servlet中只能获取到三个作用域,他不像JSP可以获取到4个作用域
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
HttpSession session = httpServletRequest.getSession();
ServletContext application = getServletConfig().getServletContext();
// 获取Response相应输出流对象
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
// 获取Http请求基本信息
System.out.println("=================== 获取Http请求基本信息 ===================");
// 获取请求方法
System.out.println(httpServletRequest.getMethod());
// 获取请求虚拟路径
System.out.println(httpServletRequest.getRequestURI());
// 获取请求全资源路径URL
System.out.println(httpServletRequest.getRequestURL());
// 获取客户端的cookie
System.out.println("=================== 获取请求的参数:COOKIE ===================");
Cookie[] cookies = httpServletRequest.getCookies();
// 遍历cookie
for (Cookie cookie:cookies) {
System.out.println("获取cookie的生命周期:" + cookie.getMaxAge());
System.out.println("获取cookie名字:" + cookie.getName());
System.out.println("获取cookie指:" + cookie.getValue());
System.out.println(System.lineSeparator());
}
}
Servlet的三种创建方式
通过实现
Servlet
接口的了解,可以更好的了解GenericServlet
以及HttpServlet
作用
实现Servlet
接口
时所有Java Servlet
的基础接口类,规定了必须由Servlet具体类实现的方法集。
public class ServletCreateMode1 implements Servlet {
private ServletConfig servletConfig;
@Override
public void init (ServletConfig servletConfig) throws ServletException {
// Servlet初始化
this.servletConfig = servletConfig;
}
@Override
public ServletConfig getServletConfig () {
// 获取该Servlet的配置信息。
return servletConfig;
}
@Override
public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
// 执行业务代码。
}
@Override
public String getServletInfo () {
// 获取Servlet作者信息
return null;
}
@Override
public void destroy () {
// 销毁Servlet时,需要做什么。
}
}
其中getServletConfig()
与getServletInfo()
其实实际开发中用的相对较少。
-
getServletConfig()
可以其子类通过super.getServletConfig()
来获取其配置信息。比如我们所设置的编码格式等等。当然,需要在初始化阶段(init(ServletConfig servletConfig)
)将配置信息赋值给其属性。-
ServletConfig
包含了servlet的初始化参数信息。
-
getServletInfo()
一般是获取作者信息,如:由谁创建的Servlet,通过HttpServletResponse
输出出去。这两个方法属于可选方法。init()
:Servlet的初始化方法,仅仅会执行一次(已经多次讲到)-
service()
:处理请求和生成响应,其中ServletRequest
以及ServletResponse
其作用如下:-
ServletRequest
- 封装客户的请求信息
- 作用相当于JSP内置对象request
-
ServletResponse
- 创建响应信息,将处理结果返回给客户端
- 作用相当于JSP内置对象response
-
destroy()
:在服务器停止并且程序中的Servlet对象不再使用的时候调用,只执行一次
继承GenericServlet
类
是Servlet
的通用版本,是一种与协议无关的Servlet
。任何情况下能使用。
public class ServletCreateMode2 extends GenericServlet {
@Override
public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
}
}
继承HttpServlet
类
在GenericServlet
基础上扩展的基于Http协议的Servlet
public class ServletCreateMode3 extends HttpServlet {
@Override
protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// super.doGet(req, resp);
System.out.println("do Get");
}
@Override
protected void doPost (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// super.doPost(req, resp);
System.out.println("do Get");
}
@Override
protected void service (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// super.service(req, resp);
System.out.println("do Service");
}
}
通过上面的代码,可以观察下,其是如何生效的。
- 当这三个方法都存在时,Servlet将默认调用
service()
方法。 - 当
service()
方法没有被重写时,如果请求时GET
那么将默认调用doGet()
方法。同理,当请求方法为POST
时,那么将默认掉哟个doPost()
方法。
其实我们也可以通过HttpServlet
中的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 doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
// Invalid date header - proceed as if none was set
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// 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);
}
}
从上面的代码中,我们也可以看出,其实现的原理是什么,在HttpServlet
抽象类中service()
根据不同请求调用不同处理的方法(通过多态实现)。
在重写HttpServlet的方法过程中,则不用调用HttpServlet
中的方法。因为会直接跑错。比如,HttpServlet
中的doGet()
方法:
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(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
} else {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
}
}
protected void doHead(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
if (DispatcherType.INCLUDE.equals(req.getDispatcherType())) {
doGet(req, resp);
} else {
NoBodyResponse response = new NoBodyResponse(resp);
doGet(req, response);
response.setContentLength();
}
}
Servlet输出
前面讲到了如何解析数据,包括如何设置
web.xml
。那么关键的是,业务处理完成后,因该如何输出响应信息?
使用PrintWrite输出内容
调用getWriter()
将会返回一个PrintWriter
对象,Servlet 用它来输出字符串像是的正文数据。
@Override
public void service (ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
// 输出作用域的值web.xml中配置的Context值
httpServletResponse.setContentType("text/html;charset:utf-8");
httpServletResponse.setCharacterEncoding(StandardCharsets.UTF_8.name());
Enumeration names = this.servletConfig.getInitParameterNames();
PrintWriter writer = httpServletResponse.getWriter();
while (names.hasMoreElements()){
String name = names.nextElement();
writer.write(name + ":" + this.servletConfig.getInitParameter(name)+ System.lineSeparator());
}
writer.flush();
writer.close();
}
使用ServletOutputStream输出内容
getOutputStream()
:返回一个 ServletOutputStream
对象,Servlet
用它来输出二进制的正文数据。
@Override
protected void doGet (HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// super.doGet(req, resp);
System.out.println("do Get");
// 使用 outputStream 发送信息
resp.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
resp.setContentType("text/html;charset=UTF-8");
ServletOutputStream outputStream = resp.getOutputStream();
Enumeration names = getServletConfig().getServletContext().getInitParameterNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
outputStream.write((name+":"+getServletConfig().getServletContext().getInitParameter(name)).getBytes(StandardCharsets.UTF_8.displayName()));
}
String outStr = "你好!";
outputStream.write(outStr.getBytes(StandardCharsets.UTF_8.displayName()));
outputStream.flush();
outputStream.close();
}
关于输出对象ServletResponse
与HttpServletResponse
ServletResponse
中响应正文的默认 MIME 类型为 text/plain
,即纯文本类型。而 HttpServletResponse
中响应正文的默认 MIME 类型为 text/html
,即HTML文档类型。
注意事项
关于输出响应编码设置
关于输出响应设置编码格式由两个部分组成:
-
HttpServletResponse.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
仅仅是发送的浏览器的内容是UTF-8编码的,至于浏览器是用哪种编码方式显示不管。 所以当浏览器的显示编码方式不是UTF-8的时候,就会看到乱码,需要手动再进行一次设置。 -
HttpServletResponse.setContentType("text/html;charset=UTF-8");
不仅发送到浏览器的内容会使用UTF-8编码,而且还通知浏览器使用UTF-8编码方式进行显示。设置响应头的Content-Type
。
这两种方式都需要在response.getWriter()
或者response.getOutputStream()
调用之前执行才能生效。如果,单设置其中一个,仍然会看到乱码!
输出响应时机
为了提高输出数据的效率,ServletOutputStream
和 PrintWriter
先把数据写到缓冲区内。当缓冲区内的数据被提交给客户后,ServletResponse
的 isCommitted()
方法返回 true
。在以下几种情况下,缓冲区内的数据会被提交给客户,即数据被发送到客户端:
- 当缓冲区内的数据已满时,
ServletOutputStream
或PrintWriter
会自动把缓冲区内的数据发送给客户端,并且清空缓冲区。 -
Servlet
调用ServletResponse
对象的flushBuffer()
方法。 -
Servlet
调用ServletOutputStream
或PrintWriter
对象的flush()
方法或close()
方法。
为了确保 ServletOutputStream
或 PrintWriter
输出的所有数据都会被提交给客户,比较安全的做法是在所有数据都输出完毕后,调用 ServletOutputStream
或 PrintWriter
的 close()
方法[1]。
也就是说,如果需要提前将信息输出到客户端则可以调用PrintWriter
的 close()
或者ServletResponse
对象的 flushBuffer()
。但是,如果未确定是否将信息已经返回给客户端时,可以调用ServletResponse
的 isCommitted()
判断下。
关于输出缓冲区
在《输出响应时机》一节中提到了关于输出响应的缓冲区,下面需要引入关于缓冲区相关操作的API。
-
int size = HttpServletResponse.getBufferSize();
返回当前缓冲区的大小,单位是B(字节),默认情况下是8192,即8KB大小 -
HttpServletResponse.setBufferSize(16*1024);
设置缓冲区的大小,单位B(字节)
设置了缓冲区大小后,将在全局webapp内生效!
练习
完成一个简单的请求分发,上面的学习过程中,我们已经知道了,当需要访问一个Servlet时,我们需要在web.xml
配置Servlet。但是如果有100个Servlet,那么就需要在web.xml
配置100个Servlet
。岂不是做了很多的重复工作?
所以通过练习实现一个简单的请求分发(路由功能),将对应的URI转发到指定的Servlet中去。简单的实现将在demo05
中查看。
参考资料
- 《web.xml中的servlet配置》,作者:奇客谷教程,发布时间:2019年3月1日
- 《JavaWeb-web.xml》,作者:舒山,发布时间:2019年3月1日
- 《Servlet 3.0 新特性详解》,作者:张建平,发布时间:2010年4月23日
- 《Servlet 4.0 入门》,作者:Alex Theedom,发布时间:2018年5月29日
- 《Java Web 扫盲行动》,作者:
你在我家门口,发布时间:2019年04月26日
-
引用自《Java Web 扫盲行动》,详情查询请查看参考资料中链接。 ↩ ↩