Servlet
作者:互联网
Servlet 介绍
Servlet 是 SUN 公司提供的一套规范,名称就叫 Servlet 规范,它也是 JavaEE 规范之一。我们可以通过访问官方 API 学习和查阅里面的内容。
打开官方 API 网址,在左上部分找到 javax.servlet 包,在左下部分找到 Servlet,如下图显示:
通过阅读 API,我们可以得到如下信息:
- Servlet 是一个运行在 Web 服务端的 Java 小程序。
- 它可以用于接收和响应客户端的请求。
- 要想实现 Servlet 功能,可以实现 Servlet 接口、继承 GenericServlet 或者 HttpServlet。
- 每次请求都会执行 service 方法。
- Servlet 还支持配置。
Servlet 基础使用
Servlet 编写步骤
1)编码
-
前期准备-创建 Java Web 工程;
-
编写一个普通类继承 GenericServlet 并重写 service 方法;
-
在 web.xml 配置 Servlet;
2)测试
-
在 Tomcat 中部署项目;
-
在浏览器访问 Servlet。
Servlet 执行过程
过程如下图所示:
- 我们通过浏览器发送请求,请求首先到达 Tomcat 服务器
- 由服务器解析请求 URL,然后在部署的应用列表中找到我们的应用。
- 在我们的应用中找到 web.xml 配置文件,在 web.xml 中找到 Servlet 的配置。
- 找到后执行 service 方法。
- 最后由 Servlet 响应客户浏览器。
Servlet 类视图
- 在 Servlet 的 API 介绍中,除了继承 GenericServlet 外还可以继承 HttpServlet。
- 通过查阅 servlet 的类视图,我们看到 GenericServlet 还有一个子类 HttpServlet。
- 同时,在 service 方法中还有参数 ServletRequest 和 ServletResponse。
它们的关系如下图所示:
Servlet 编写方式
我们在实现 Servlet 功能时,可以选择以下三种方式:
第一种:实现 Servlet 接口,接口中的方法必须全部实现。
- 使用此种方式,表示接口中的所有方法在需求方面都有重写的必要。此种方式支持最大程度的自定义。
第二种:继承 GenericServlet,service 方法必须重写,其他方可根据需求,选择性重写。
- 使用此种方式,表示只在接收和响应客户端请求这方面有重写的需求,而其他方法可根据实际需求选择性重写,使我们的开发 Servlet 变得简单。但是,此种方式是和 HTTP 协议无关的。
第三种:继承 HttpServlet。
- 它是 javax.servlet.http 包下的一个抽象类,是 GenericServlet 的子类。
如果我们选择继承 HttpServlet 时,只需要重写 doGet 和 doPost 方法,不需要覆盖 service 方法。
- 使用此种方式,表示我们的请求和响应需要和 HTTP 协议相关。也就是说,我们是通过 HTTP 协议来访问的。那么每次请求和响应都符合 HTTP 协议的规范。请求的方式就是 HTTP 协议所支持的方式(HTTP 协议支持 7 种请求方式:GET、POST、PUT、DELETE、TRACE、OPTIONS、HEAD)。
- 为了实现代码的可重用性,通常我们只需要在 doGet 或者 doPost 方法任意一个中提供具体功能即可,而另外的那个方法只需要调用提供了功能的方法。
Servlet 生命周期
对象的生命周期,就是对象从生到死的过程,即:出生——活着——死亡。用更偏向于开发的官方说法,就是对象从被创建到销毁的过程。
-
出生:请求第一次到达 Servlet 时,Servlet 对象就会被创建出来,并且初始化成功。只出生一次,放到内存中。
-
活着:服务器提供服务的整个过程中,该 Servlet 对象一直存在,每次只是执行 service 方法。
-
死亡:当服务停止时,或者服务器宕机时,Servlet 对象消亡。
通过分析 Servlet 的生命周期可以发现,它的实例化和初始化只会在请求第一次到达 Servlet 时执行,而销毁只会在 Tomcat 服务器停止时执行。
由此我们得出一个结论,Servlet 对象只会创建一次,销毁一次。所以,每一个 Servlet 只有一个实例对象
。如果一个对象实例在应用中是唯一的存在,那么我们就说它是单实例的,即运用了单例模式。
Servlet 线程安全
由于 Servlet 运用了单例模式,即在整个应用中,每一个 Servlet 类只有一个实例对象,所以我们需要分析这个唯一的实例中的类成员是否线程安全。
接下来,我们来看下面的的示例:
public class ServletDemo extends HttpServlet {
//1.定义用户名成员变量
//private String username = null;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = null;
//synchronized (this) {
//2.获取用户名
username = req.getParameter("username");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//3.获取输出流对象
PrintWriter pw = resp.getWriter();
//4.响应给客户端浏览器
pw.print("welcome:" + username);
//5.关流
pw.close();
//}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
启动两个浏览器,输入不同的参数,访问之后发现输出的结果都是一样,所以出现线程安全问题:
通过上面的测试我们发现,在 Servlet 中定义了类成员后,多个浏览器都会共享类成员的数据。每一个浏览器端就代表是一个线程,那么多个浏览器就是多个线程,所以测试的结果说明了多个线程会共享 Servlet 类成员中的数据。那么,其中任何一个线程修改了数据,都会影响其他线程。因此,我们可以认为 Servlet 不是线程安全的。
分析产生这个问题的根本原因,其实就是因为 Servlet 是单例,单例对象的类成员只会随类实例化时初始化一次,之后的操作都是改变,而不会重新初始化。
要解决这个线程安全问题,需要在 Servlet 中定义类成员时慎重。
- 如果类成员是共用的,并且只会在初始化时赋值,其余时间都是获取的话,那么是没问题的。
- 但如果类成员并非共用,或者每次使用都有可能对其赋值(如上图示例),那么就要考虑线程安全问题了,解决方案是把它定义到 doGet 或者 doPost 方法中。
Servlet 映射配置
Servlet 支持三种映射方式,以达到灵活配置的目的。
首先编写一个Servlet,代码如下:
public class ServletDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("ServletDemo5接收到了请求");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
方式一:精确映射
此种方式,只有和映射配置一模一样时,Servlet 才会接收和响应来自客户端的请求。
方式二:/开头+通配符
此种方式,只要符合目录结构即可,不用考虑结尾是什么。
例如:映射为:/servlet/*
-
访问 http://localhost:8585/servlet/itheima 或 http://localhost:8585/servlet/itcast.do 这两个 URL 都可以。
-
因为用的
*
,表示 /servlet/ 后面的内容是什么都可以。
方式三:通配符+固定格式结尾
此种方式,只要符合固定结尾格式即可,其前面的访问URI无须关心(注意协议,主机和端口必须正确)
例如:映射为:*.do
- 访问 URL:http://localhost:8585/servlet/itcast.do 或 http://localhost:8585/itheima.do 这两个 URL 都可以。
- 因为都是以 .do 作为结尾,而前面用 * 号通配符配置的映射。
优先级
通过测试我们发现,Servlet 支持多种配置方式,但是由此也引出了一个问题,当有两个及以上的 Servlet 映射都符合请求 URL 时,由谁来响应呢?
注意:HTTP 协议的特征是一请求一响应的规则。那么有一个请求,必然有且只有一个响应。所以,映射规则的优先级如下:
- 精确匹配
- /开头+通配符
- 通配符+固定格式结尾
多路径映射 Servlet
这其实是给一个 Servlet 配置多个访问映射,从而可以根据不同请求 URL 实现不同的功能。
示例 Servlet:
public class ServletDemo extends HttpServlet {
/**
* 根据不同的请求URL,做不同的处理规则
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1. 获取当前请求的 URI
String uri = req.getRequestURI();
uri = uri.substring(uri.lastIndexOf("/"), uri.length());
//2. 判断是1号请求还是2号请求
if("/servletDemo7".equals(uri)){
System.out.println("ServletDemo7执行1号请求的业务逻辑:商品单价7折显示");
}else if("/demo7".equals(uri)){
System.out.println("ServletDemo7执行2号请求的业务逻辑:商品单价8折显示");
}else {
System.out.println("ServletDemo7执行基本业务逻辑:商品单价原价显示");
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
web.xml 配置 Servlet:
<servlet>
<servlet-name>servletDemo7</servlet-name>
<servlet-class>com.itheima.web.servlet.ServletDemo7</servlet-class>
</servlet>
<!--映射路径1-->
<servlet-mapping>
<servlet-name>servletDemo7</servlet-name>
<url-pattern>/demo7</url-pattern>
</servlet-mapping>
<!--映射路径2-->
<servlet-mapping>
<servlet-name>servletDemo7</servlet-name>
<url-pattern>/servletDemo7</url-pattern>
</servlet-mapping>
<!--映射路径3-->
<servlet-mapping>
<servlet-name>servletDemo7</servlet-name>
<url-pattern>/servlet/*</url-pattern>
</servlet-mapping>
启动服务,测试运行结果:
启动时即创建 Servlet
Servlet 的创建默认情况下是请求第一次到达 Servlet 时创建的。但是我们知道,Servlet 是单例的,也就是说在应用中只有唯一的一个实例,所以在 Tomcat 启动加载应用的时候就创建也是一个很好的选择。那么两者有什么区别呢?
-
第一种:应用加载时创建 Servlet。
- 它的优势是在服务器启动时,就把需要的对象都创建完成了,从而在使用的时候减少了创建对象的时间,提高了首次执行的效率。
- 它的弊端也同样明显,因为在应用加载时就创建了 Servlet 对象,因此,有可能导致内存中充斥着大量用不上的 Servlet 对象,造成了内存的浪费。
-
第二种:请求第一次访问是创建 Servlet。
- 它的优势就是减少了对服务器内存的浪费,因为那些一直没有被访问过的 Servlet 对象就不会被创建,同时也提高了服务器的启动时间。
- 而它的弊端就是,如果有一些要在应用加载时就做的初始化操作,那么它就没法完成,从而要考虑其他技术实现。
通过上面的分析可得出,当需要在应用加载就要完成一些工作时,就需要选择第一种方式;当有很多 Servlet 且其使用时机并不确定时,就选择第二种方式
。
在 web.xml 中是支持对 Servlet 的创建时机进行配置的,配置的方式如下:
<servlet>
<servlet-name>servletDemo3</servlet-name>
<servlet-class>com.itheima.web.servlet.ServletDemo3</servlet-class>
<!-- 配置Servlet的创建顺序,当配置此标签时,Servlet就会改为应用加载时创建
配置项的取值只能是正整数(包括0),数值越小,表明创建的优先级越高。
-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>servletDemo3</servlet-name>
<url-pattern>/servletDemo3</url-pattern>
</servlet-mapping>
默认 Servlet
默认 Servlet 是由 Web 服务器提供的一个 Servlet,它配置在 Tomcat 的 conf 目录下的 web.xml 中。如下图所示:
它的映射路径是<url-pattern>/<url-pattern>
。在我们发送请求时,首先会在我们应用中的 web.xml 中查找映射配置,找到就执行。当找不到对应的 Servlet 路径时,就会去找默认的 Servlet,由默认 Servlet 处理。所以,一切都是 Servlet。
Servlet 关系总图
ServletConfig
ServletConfig 简介
概念
- ServletConfig 是 Servlet 的配置参数对象。
- 在 Servlet 规范中,允许为每个 Servlet 都提供一些初始化配置。所以,每个 Servlet 都一个自己的 ServletConfig。
- 它的作用是在 Servlet 初始化期间,把一些配置信息传递给 Servlet。
生命周期
- 由于 ServletConfig 是在初始化阶段读取了 web.xml 中为 Servlet 准备的初始化配置,并把配置信息传递给 Servlet,所以生命周期与 Servlet 相同。
- 这里需要注意的是,如果 Servlet 配置了
<load-on-startup>1</load-on-startup>
,那么 ServletConfig 也会在应用加载时创建。
ServletConfig 使用
获取
ServletConfig 可以为每个 Servlet 都提供初始化参数,所以肯定可以在每个 Servlet 中都配置。
public class ServletDemo8 extends HttpServlet {
// 定义 Servlet 配置对象 ServletConfig
private ServletConfig servletConfig;
/**
* 在初始化时为 ServletConfig 赋值
* @param config
* @throws ServletException
*/
@Override
public void init(ServletConfig config) throws ServletException {
this.servletConfig = config;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 输出ServletConfig
System.out.println(servletConfig);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
web.xml:
<servlet>
<servlet-name>servletDemo8</servlet-name>
<servlet-class>com.itheima.web.servlet.ServletDemo8</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servletDemo8</servlet-name>
<url-pattern>/servletDemo8</url-pattern>
</servlet-mapping>
配置
上面我们已经准备好了 Servlet,同时也获取到了它的 ServletConfig 对象,而如何配置初始化参数,则需要使用<servlet>
标签中的<init-param>
标签来配置。
即 Servlet 的初始化参数都是配置在 Servlet 的声明部分的,并且每个 Servlet 都支持有多个初始化参数,并且初始化参数都是以键值对的形式存在的。
配置示例:
<servlet>
<servlet-name>servletDemo8</servlet-name>
<servlet-class>com.itheima.web.servlet.ServletDemo8</servlet-class>
<!--配置初始化参数-->
<init-param>
<!--用于获取初始化参数的key-->
<param-name>encoding</param-name>
<!--初始化参数的值-->
<param-value>UTF-8</param-value>
</init-param>
<!--每个初始化参数都需要用到init-param标签-->
<init-param>
<param-name>servletInfo</param-name>
<param-value>This is Demo8</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>servletDemo8</servlet-name>
<url-pattern>/servletDemo8</url-pattern>
</servlet-mapping>
常用方法
示例:
/**
* 演示Servlet的初始化参数对象
* @author 黑马程序员
* @Company http://www.itheima.com
*/
public class ServletDemo8 extends HttpServlet {
// 定义 Servlet 配置对象 ServletConfig
private ServletConfig servletConfig;
/**
* 在初始化时为 ServletConfig 赋值
* @param config
* @throws ServletException
*/
@Override
public void init(ServletConfig config) throws ServletException {
this.servletConfig = config;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 输出ServletConfig
System.out.println(servletConfig);
// 2. 获取Servlet的名称
String servletName= servletConfig.getServletName();
System.out.println(servletName);
// 3. 获取字符集编码
String encoding = servletConfig.getInitParameter("encoding");
System.out.println(encoding);
// 4. 获取所有初始化参数名称的枚举
Enumeration<String> names = servletConfig.getInitParameterNames();
//遍历names
while(names.hasMoreElements()){
//取出每个name(key)
String name = names.nextElement();
//根据key获取value
String value = servletConfig.getInitParameter(name);
System.out.println("name:"+name+",value:"+value);
}
// 5. 获取ServletContext对象
ServletContext servletContext = servletConfig.getServletContext();
System.out.println(servletContext);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
ServletContext
ServletContext概述
ServletContext 对象是应用上下文对象
。
每一个应用有且只有一个 ServletContext 对象,它可以实现让应用中所有 Servlet 间的数据共享。
生命周期
-
出生: 应用一加载,该对象就被创建出来了。一个应用只有一个实例对象(Servlet 和 ServletContext 都是单例的)。
-
活着:只要应用一直提供服务,该对象就一直存在。
-
死亡:应用停止(或者服务器挂了),该对象消亡。
域对象概念
-
域对象指的是
对象有作用域,即有作用范围
。 -
域对象的作用,域对象可以实现数据共享。不同作用范围的域对象,共享数据的能力不一样。
-
在 Servlet 规范中,一共有 4 个域对象,ServletContext 就是其中一个。
-
ServletContext 是 web 应用中最大的作用域,叫
application 域
。每个应用只有一个 application 域,它可以实现整个应用间的数据共享功能。
ServletContext 使用
获取
只需要调用 ServletConfig 对象的getServletContext()
方法就可以了:
public class ServletDemo9 extends HttpServlet {
// 定义 Servlet 配置对象 ServletConfig
private ServletConfig servletConfig;
/**
* 在初始化时为 ServletConfig 赋值
* @param config
* @throws ServletException
*/
@Override
public void init(ServletConfig config) throws ServletException {
this.servletConfig = config;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取 ServletContext 对象
ServletContext servletContext = servletConfig.getServletContext();
System.out.println(servletContext);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
web.xml:
<servlet>
<servlet-name>servletDemo9</servlet-name>
<servlet-class>com.itheima.web.servlet.ServletDemo9</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servletDemo9</servlet-name>
<url-pattern>/servletDemo9</url-pattern>
</servlet-mapping>
更简洁的获取方法
:
在实际开发中,如果每个 Servlet 对 ServletContext 都使用频繁的话,那么每个 Servlet 里定义 ServletConfig,再获取 ServletContext 的代码将非常多,造成大量的重复代码。
而 Servlet 规范的定义中也为我们想到了这一点,所以它在 GenericServlet 中,已经为我们声明好了 ServletContext 获取的方法,如下图所示:
示例 Servlet 都是继承自 HttpServlet,而 HttpServlet 又是 GenericServlet 的子类,所以我们在获取 ServletContext 时,如果当前 Servlet 没有用到它自己的初始化参数时,就可以不用再定义初始化参数了,而是直接改成下图所示的代码即可:
配置
ServletContext 既然被称之为应用上下文对象,那么它的配置就是针对整个应用的配置,而非某个特定 Servlet 的配置。它的配置被称为应用的初始化参数配置。
配置的方式,需要在<web-app>
标签中使用<context-param>
来配置初始化参数。具体代码如下:
<!--配置应用初始化参数-->
<context-param>
<!--用于获取初始化参数的 key-->
<param-name>servletContextInfo</param-name>
<!--初始化参数的值-->
<param-value>This is application scope</param-value>
</context-param>
<!--每个应用初始化参数都需要用到 context-param 标签-->
<context-param>
<param-name>globalEncoding</param-name>
<param-value>UTF-8</param-value>
</context-param>
Servlet 注解开发
Servlet 3.0 规范
在大概十多年前,那会还是 Servlet 2.5 的版本的天下,它最明显的特征就是 Servlet 的配置要求配在 web.xml 中。
从 2007 年开始到 2009 年底的这个时间段中,软件开发开始逐步的演变,基于注解的配置理念开始逐渐出现,大量注解配置思想开始用于各种框架的设计中,例如:Spring 3.0 版本的 Java Based Configuration、JPA 规范、Apache 旗下的 struts2 和 mybatis 的注解配置开发等等。
JavaEE6 规范也是在这个期间设计并推出的,与之对应就是它里面包含了新的 Servlet 规范:Servlet 3.0 版本。
使用实例
配置步骤
步骤一:创建 Java Web 工程,并移除 web.xml
步骤二:编写 Servlet
public class ServletDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("Servlet Demo1 Annotation");
}
}
步骤三:使用注解配置 Servlet
步骤四:测试
注解源码分析
/**
* WebServlet注解
* @since Servlet 3.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebServlet {
/**
* 指定Servlet的名称。
* 相当于xml配置中<servlet>标签下的<servlet-name>
*/
String name() default "";
/**
* 用于映射Servlet访问的url映射
* 相当于xml配置时的<url-pattern>
*/
String[] value() default {};
/**
* 相当于xml配置时的<url-pattern>
*/
String[] urlPatterns() default {};
/**
* 用于配置Servlet的启动时机
* 相当于xml配置的<load-on-startup>
*/
int loadOnStartup() default -1;
/**
* 用于配置Servlet的初始化参数
* 相当于xml配置的<init-param>
*/
WebInitParam[] initParams() default {};
/**
* 用于配置Servlet是否支持异步
* 相当于xml配置的<async-supported>
*/
boolean asyncSupported() default false;
/**
* 用于指定Servlet的小图标
*/
String smallIcon() default "";
/**
* 用于指定Servlet的大图标
*/
String largeIcon() default "";
/**
* 用于指定Servlet的描述信息
*/
String description() default "";
/**
* 用于指定Servlet的显示名称
*/
String displayName() default "";
}
标签:req,Servlet,resp,配置,ServletConfig,throws 来源: https://www.cnblogs.com/juno3550/p/15361136.html