1 Servlet介绍
1.1 简介
Servlet 是一种运行于服务器端的Java应用程序,具有独立于平台和协议的特性,可以生成动态的Web 页面,它担当客户请求与服务器响应的中间层 ,有特殊的技术规范:必须继承某个特定父类;必须配置之后才能执行;有特定的生命周期 Servlet 是在Java 代码中嵌入页面代码,JSP 是在页面代码中嵌入Java 代码 JSP 不能执行,必须转译成Servlet 并编译成class 后才能执行,Servlet 是学好JSP的基础,能了解JSP的底层运作方式
1.2 Servlet生命周期
1.2.1 生命周期
每个servlet 实例的生命周期中只有三种类型的事情,分别对应于由servlet 容器所调用的三个方法:
init() 初始化时期: 当servlet 第一次被装载的时候由servlet 容器调用init() ,且只调用一次,默认情况下调用超类的init() 方法。service() 运行时期: 接受客户请求并决定调用何种doXXX 方法,并将处理结果返回到客户端。destroy() 结束时期: 为可选方法,释放不用的servlet 实例所占内存和资源。
Servlet 的生命周期可以归纳为以下几步:
Servlet 创建一个Servlet 实例;Servlet 调用Servlet 的init() 方法;- 客户发送请求到
Servlet ; Servlet 创建一个请求对象和一个响应对象Servlet 调用service() 方法,传递请求和响应对象作为参数;service() 方法获得请求对象的参数,处理请求,访问其他资源。service() 方法通过响应对象将结果传递给Server ,最终到达客户端。- 对于更多的相同客户端请求,
Servlet 将创建新的请求和响应对象,仍然激活此Servlet 的service 方法,传递新的参数对象,不需在初始化一次。 - 当
Servlet 不再需要Servlet 时(关闭时)调用destory() 方法。
1.2.2 servlet配置
<servlet>
<servlet-name>Servlet的名称</servlet-name>
<servlet-class>该servlet类的路径</servlet-class>
<init-param>
<param-name>参数名</param-name>
<param-value>参数值</param-value>
</init-param>
<jsp-file>/路径/XXX.JSP </jsp-file>
<load-on-startup>表示web站台被启动时,自动加载该servlet的优先级别,越小越优先加载</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>对应的servlet-name名称</servlet-name>
<url-pattern>在项目运行时用以访问的URL </url-pattern>
</servlet-mapping>
url-pattern 取值:1、可以与具体的某个Servlet对应。2、也可以用/* 来指定所有的页面,更多关于servlet路径配置可以点此连接查看
1.2.3 servlet中load-on-startup作用
像上面的servlet配置中的标签<load-on-startup> ,注意到它里面包含了这段配置:<load-on-startup>1</load-on-startup> ,那么这个配置有什么作用呢?
load-on-startup 元素标记容器是否在启动的时候就加载这个servlet (实例化并调用其init() 方法)。- 它的值必须是一个整数,表示
servlet 应该被载入的顺序 - 当值为0或者大于0时,表示容器在应用启动时就加载并初始化这个
servlet ; - 当值小于0或者没有指定时,则表示容器在该
servlet 被选择时才会去加载。 - 正数的值越小,该
servlet 的优先级越高 ,应用启动时就越先加载。 - 当值相同时,容器就会自己选择顺序来加载。
所以,<load-on-startup>x</load-on-startup> ,中x的取值1,2,3,4,5代表的是优先级 ,而非启动延迟时间
1.3 Servlet接口
1.3.1 HttpServlet
HttpServlet 类,httpServlet 中各种接受请求处理的方法
方法名 | 描述 |
---|
doGet() | 处理http的get请求 | doPost() | 处理http的post请求,主要用于发送HTML文本中FORM的内容 | doHead() | 用于处理HEADER请求 | doPut() | 处理http的put请求,模仿 ftp发送 | doDelete() | 处理http的delete请求 | doOptions() | 该操作自动决定支持什么HTTP方法 | doTrace() | 处理HTTP的trace请求 |
发送请求的三种基本方式 地址栏直接键入地址(默认get方式) 例如:在地址栏输入:http://127.0.0.1/jsp2/index.jsp?a=2 使用表单提交(默认get方式)只有使用表单的method=post才是post提交 点击超链接(默认get方式)
1.3.1.1 service,doGet,doPost区别
HttpServlet 里的三个方法service(HttpServletRequest req, HttpServletResponse resp) ,doGet(HttpServletRequest req, HttpServletResponse resp) , doPost(HttpServletRequest req, HttpServletResponse res) 的区别和联系
在servlet 中默认情况下,无论是get 还是post 提交过来都会经过service() 方法来处理,然后转向到doGet 或是doPost 方法,可以查看HttpServlet 类的service 方法
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
String method = req.getMethod();
if(method.equals("GET"))
{
long lastModified = getLastModified(req);
if(lastModified == -1L)
{
doGet(req, resp);
} else
{
long ifModifiedSince = req.getDateHeader("If-Modified-Since");
if(ifModifiedSince < (lastModified / 1000L) * 1000L)
{
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else
{
resp.setStatus(304);
}
}
} else
if(method.equals("HEAD"))
{
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else
if(method.equals("POST"))
doPost(req, resp);
else
if(method.equals("PUT"))
doPut(req, resp);
else
if(method.equals("DELETE"))
doDelete(req, resp);
else
if(method.equals("OPTIONS"))
doOptions(req, resp);
else
if(method.equals("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(501, errMsg);
}
}
从上面可以看出 这里的service 是用来转向的,但是如果在自己的servlet 类中覆盖了service 方法,比如说你的service 是下面这样的,那么这时service 就不是用来转向的,而是用来处理业务的,现在不论客户端是用pos 还是get 来请求此servlet ,都会执行service 方法也只能执行servlet 方法,不会去执行doPost 或是doGet 方法
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
res.getOutputStream().print(
"image is <img src='images/downcoin.gif'></img><br>");
}
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
doPost(request,response);
}
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
ServletOutputStream out=response.getOutputStream();
String[] args=(String[])request.getParameterValues("fruit");
for(int i=0;i<args.length;i++){
out.print(args[i]+"<br>");
}
}
所以,我们在写servlet 的时候,一般都是重写doGet 或doPost 方法,不会管service 方法
1.3.2 表单提交中get和post方式区别
1.3.2.1 表单中区别
表单提交中get和post 方式的区别有5点
get 是从服务器上获取数据,post 是向服务器传送数据。get 是把参数数据队列加到提交表单的ACTION 属性所指的URL 中,值和表单内各个字段一 一对应,在URL 中可以看到。post 是通过HTTPpost 机制,将表单内各个字段与其内容放置在HTML HEADER 内一起传送到ACTION 属性所指的URL 地址。用户看不到这个过程。- 对于
get 方式,服务器端用Request.QueryString 获取变量的值,对于post 方式,服务器端用Request.Form 获取提交的数据。 get 传送的数据量较小,不能大于2KB 。post 传送的数据量较大,一般被默认为不受限制get 安全性非常低,post 安全性较高。
1.3.2.2 http中区别
HTTP 请求:get与post 方法的区别: HTTP 定义了与服务器交互的不同方法,最基本的方法是 get 和 post 。事实上 get 适用于多数请求,而保留 post 仅用于更新站点。根据 HTTP 规范,get 用于信息获取,而且应该是安全的和幂等的。所谓安全的意味着该操作用于获取信息而非修改信息。换句话说,get 请求一般不应产生副作用。幂等的意味着对同一URL 的多个请求应该返回同样的结果。完整的定义并不像看起来那样严格。从根本上讲,其目标是当用户打开一个链接时,可以确信从自身的角度来看没有改变资源。比如,新闻站点的头版不断更新。虽然第二次请求会返回不同的一批新闻,该操作仍然被认为是安全的和幂等的,因为它总是返回当前的新闻。反之亦然。但 post 请求就不那么轻松了。post 表示可能改变服务器上的资源的请求。
在FORM 提交的时候,如果不指定Method ,则默认为get 请求,Form 中提交的数据将会附加在url 之后,以? 分开与url 分开。字母数字字符原样发送,但空格转换为+ 号,其它符号转换为%XX , 其中XX 为该符号以16进制 表示的ASCII (或ISOLatin-1 )值。get 请求请提交的数据放置在HTTP 请求协议头中,而post 提交的数据则 放在实体数据中; get 方式提交的数据最多只能有1024 字节,而post 则没有此限制 在表单里使用post 和get 有什么区别 在Form 里面,可以使用post 也可以使用get 。它们都是method 的合法取值。但是,post 和get 方法在使用上至少有两点不同:
get 方法通过URL 请求来传递用户的输入。post 方法通过另外的形式。get 方式的提交需要用Request.QueryString 来取得变量的值,而post 方式提交时,必须通过Request.Form 来访问提交的内容。
2 Servlet原理
2.1 工作原理
2.1.1 问题引入
Servlet 是如何工作的?Servlet 如何实例化、共享变量、并进行多线程处理? 假设有一个运行了大量 Servlet 的 web 服务器。通过 Servlet 之间传输信息得到 Servlet 上下文,并设置 session 变量。现在,如果有两名或更多使用者向这个服务发送请求,接下来 session 变量会发生什么变化?究竟是所有用户都是用共同的变量?还是不同的用户使用的变量都不一样?如果是后者,服务器如何区分不同用户? 如果有n名用户访问一个特定的 Servlet,那么该 Servlet 是仅在第一个用户首次访问的时候实例化,还是分别为每个用户实例化?
2.1.2 Servlet容器启动
当 Servlet 容器(比如 Apache Tomcat )启动后,会部署和加载所有 web 应用。当web 应用被加载,Servlet 容器会创建一次ServletContext ,然后将其保存在服务器的内存中。 web 应用的web.xml 被解析,找到其中所有 servlet 、filter 和 Listener 或 @WebServlet 、@WebFilter 和@WebListener 注解的内容,创建一次并保存到服务器的内存中。对于所有过滤器会立即调用init() 。当 Servlet 容器停止,将卸载所有 web 应用,调用所有初始化的 Servlet 和过滤器的destroy() 方法,最后回收 ServletContext 和所有 Servlet 、Filter 与 Listener 实例。 当Servlet 配置的load-on-startup 或者 @WebServlet(loadOnStartup) 设置了一个大于 0 的值,则同样会在启动的时候立即调用 init() 方法。 load-on-startup 中的值表示那些 Servlet 会以相同顺序初始化。如果配置的值相同,会遵循 web.xml 中指定的顺序或 @WebServlet 类加载的顺序。另外,如果不设置 load-on-startup 值,init() 方法只在第一次HTTP 请求命中问题中的Servlet 时才被调用
2.1.3 HttpServletRequest 与 HttpServletResponse
Servlet 容器附加在一个 web 服务上,这个 web 服务会在某个端口号上监听 HTTP 请求,在开发环境中这个端口通常为 8080 ,生产环境中通常为 80 。当客户端(web 浏览器)发送了一个 HTTP 请求,Servlet 容器会创建新的 HttpServletRequest 和 HttpServletResponse 对象,传递给已创建好并且请求的 URL 匹配 url-pattern 的 Filter 和 Servlet 实例中的方法,所有工作都在同一个线程中处理。 request 对象可以访问所有该 HTTP 请求中的信息,例如 request header 和 request body 。response 对象提供需要的控制和发送 HTTP 响应方法,例如设置 header 和 body (通常会带有 JSP 文件中的 HTML 内容)。提交并完成HTTP 响应后,将回收 request 和 response 对象。
2.1.4 HttpSession
当用户第一次访问该 web 应用时,会通过 request.getSession() 第一次获得 HttpSession 。之后 Servlet 容器将会创建 HttpSession ,生成一个唯一的 ID (可以通过 session.getId() 获取)并储存在服务器内存中。然后 Servlet 容器在该次 HTTP 响应的 Set-Cookie 头部设置一个 Cookie ,以JSESSIONID 作为 Cookie 名字,那个唯一的 session ID 作为 Cookie 的值。 按照 HTTP cookie 规则(正常 web 浏览器和 web 服务端必须遵循的标准),当 cookie 有效时,要求客户端(浏览器)在后续请求的 Cookie 头中返回这个 cookie 。 使用浏览器内置的HTTP 流量监控器,Servlet 容器将会确定每个进入的 HTTP 请求的 Cookie 头中是否存在名为JSESSIONID 的 cookie ,然后用它的值(session ID )从服务端内存中找到关联的 HttpSession 。 可以在 web.xml 中设置 session-timeout ,默认值为 30 分钟 。超时到达之前 HttpSession 会一直存活。所以当客户端不再访问该 web 应用超过 30 分钟后,Servlet 容器就会回收这个 session 。后续每个请求,即使指定 cookie 名称也不能再访问到相同的 session 。Servlet 容器会创建一个新的 Cookie 。 另一方面,客户端上的 session cookie 有一个默认存活时间,该事件和该浏览器实例运行时间一样长。所以,当客户端关闭该浏览器实例(所有标签和窗口)后,这个 session 就会被客户端回收。新浏览器实例不再发送与该 session 关联的 cookie 。一个新的 request.getSession() 将会返回新的HttpSession 并设置一个拥有新 session ID 的 cookie
2.1.5 ServletContext
ServletContext 与 web 应用存活时间一样长。它被所有 session 中的所有请求共享。 只要客户端一直与相同浏览器实例的web 应用交互并且没有超时HttpSession 就会存在。 HttpServletRequest 和 HttpServletResponse 的存活时间为客户端发送完成到完整的响应(web页面 )到达的这段时间。不会被其他地方共享。 所有 Servlet 、Filter 和 Listener 对象在web 应用运行时都是活跃的。它们被所有 session 中的请求共享。 设置在 HttpServletRequest 、HttpServletResponse 和 HttpSession 中的所有属性在问题中的对象存活时都会一直保持存活。
2.1.6 线程安全
即便如此,最关心的可能是线程安全。现在应该学习到 Servlet 和 filter 被所有请求共享。那是 Java 的一个优点,使得多个不同线程(读取 HTTP 请求)可以使用同一个实例。否则为每个请求重新创建线程的开销实在过于昂贵。 但应该也意识到永远不要将任何 request 或 session 域中的数据赋值给 servlet 或 filter 的实例变量。它将会被所有其他 session 中的所有请求共享。那是非线程安全的 下面的示例对这种情况进行了展示:
public class ExampleServlet extends HttpServlet {
private Object thisIsNOTThreadSafe;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Object thisIsThreadSafe;
thisIsNOTThreadSafe = request.getParameter("foo");
thisIsThreadSafe = request.getParameter("foo");
}
}
由于当客户端第一次请求某一个JSP 文件时,服务端把该JSP 编译成一个CLASS 文件,并创建一个该类的实例,然后创建一个线程处理CLIENT 端的请求。如果有多个客户端同时请求该JSP 文件,则服务端会创建多个线程。每个客户端请求对应一个线程。以多线程方式执行可大大降低对系统的资源需求,提高系统的并发量及响应时间.
对JSP 中可能用的的变量说明如下:
实例变量 : 实例变量是在堆中分配的,并被属于该实例的所有线程共享,所以不是线程安全的. JSP 系统提供的8个类变量:JSP 中用到的OUT,REQUEST,RESPONSE,CONFIG,PAGE,PAGECONXT 是线程安全的(因为每个线程对应的request,respone 对象都是不一样的,不存在共享问题), SESSION,APPLICATION 在整个系统内被使用,所以不是线程安全的 ServletContext :(线程是不安全的) ServletContext 是可以多线程同时读/写属性的,线程是不安全的。要对属性的读写进行同步处理或者进行深度Clone() 。所以在Servlet 上下文中尽可能少量保存会被修改(写)的数据,可以采取其他方式在多个Servlet 中共享,比方我们可以使用单例模式来处理共享数据。 HttpSession :(线程是不安全的) HttpSession 对象在用户会话期间存在,只能在处理属于同一个Session 的请求的线程中被访问,因此Session 对象的属性访问理论上是线程安全的。 当用户打开多个同属于一个进程的浏览器窗口,在这些窗口的访问属于同一个Session ,会出现多次请求,需要多个工作线程来处理请求,可能造成同时多线程读写属性。这时我们需要对属性的读写进行同步处理:使用同步块Synchronized 和使用读/写器来解决。 ServletRequest :(线程是安全的) 对于每一个请求,由一个工作线程来执行,都会创建有一个新的ServletRequest 对象,所以ServletRequest 对象只能在一个线程中被访问。ServletRequest 是线程安全的。 注意:ServletRequest 对象在service 方法的范围内是有效的,不要试图在service 方法结束后仍然保存请求对象的引用局部变量 : 局部变量在堆栈中分配,因为每个线程都有它自己的堆栈空间,所以是线程安全的.静态类 : 静态类不用被实例化,就可直接使用,也不是线程安全的.外部资源 : 在程序中可能会有多个线程或进程同时操作同一个资源(如:多个线程或进程同时对一个文件进行写操作).此时也要注意同步问题. 使它以单线程方式执行,这时,仍然只有一个实例,所有客户端的请求以串行方式执行。这样会降低系统的性能
2.2 Servlet单例多线程
2.2.1 Servlet如何处理多个请求访问
Servlet 容器默认是采用单实例多线程 的方式处理多个请求的:
- 当
web 服务器启动的时候(或客户端发送请求到服务器时),Servlet 就被加载并实例化(只存在一个Servlet 实例); - 容器初始化
Servlet 主要就是读取配置文件(例如tomcat ,可以通过servlet.xml 的<Connector> 设置线程池中线程数目,初始化线程池通过web.xml ,初始化每个参数值等等。 - 当请求到达时,
Servlet 容器通过调度线程(Dispatchaer Thread ) 调度它管理下线程池中等待执行的线程(Worker Thread )给请求者; - 线程执行
Servlet 的service 方法; - 请求结束,放回线程池,等待被调用; (注意:避免使用实例变量(成员变量),因为如果存在成员变量,可能发生多线程同时访问该资源时,都来操作它,照成数据的不一致,因此产生线程安全问题)
从上面可以看出:
Servlet 单实例,减少了产生servlet 的开销;- 通过线程池来响应多个请求,提高了请求的响应时间;
Servlet 容器并不关心到达的Servlet 请求访问的是否是同一个Servlet 还是另一个Servlet ,直接分配给它一个新的线程;如果是同一个Servlet 的多个请求,那么Servlet 的service 方法将在多线程中并发的执行;- 每一个请求由
ServletRequest 对象来接受请求,由ServletResponse 对象来响应该请求; Servlet/JSP 技术和ASP、PHP 等相比,由于其多线程运行而具有很高的执行效率。由于Servlet/JSP 默认是以多线程模式执行的,所以,在编写代码时需要非常细致地考虑多线程的安全性问题。
2.2.2 Servlet容器如何采用单实例多线程的方式来处理请求
Java 的内存模型JMM(Java Memory Model) JMM主要是为了规定了线程和内存之间的一些关系。根据JMM的设计,系统存在一个主内存(Main Memory),Java中所有实例变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存由缓存和堆栈两部分组成,缓存中保存的是主存中变量的拷贝,缓存可能并不总和主存同步,也就是缓存中变量的修改可能没有立刻写到主存中;堆栈中保存的是线程的局部变量,线程之间无法相互直接访问堆栈中的变量。根据JMM,我们可以将论文中所讨论的Servlet实例的内存模型抽象为图所示的模型。
工作者线程Work Thread :执行代码的一组线程。 调度线程Dispatcher Thread :每个线程都具有分配给它的线程优先级,线程是根据优先级调度执行的。 Servlet 采用多线程来处理多个请求同时访问。servlet 依赖于一个线程池来服务请求。线程池实际上是一系列的工作者线程集合。Servlet 使用一个调度线程来管理工作者线程。 当容器收到一个Servlet 请求,调度线程从线程池中选出一个工作者线程,将请求传递给该工作者线程,然后由该线程来执行Servlet 的service 方法。当这个线程正在执行的时候,容器收到另外一个请求,调度线程同样从线程池中选出另一个工作者线程来服务新的请求,容器并不关心这个请求是否访问的是同一个Servlet .当容器同时收到对同一个Servlet 的多个请求的时候,那么这个Servlet 的service() 方法将在多线程中并发执行。 Servlet 容器默认采用单实例多线程的方式来处理请求,这样减少产生Servlet 实例的开销,提升了对请求的响应时间,对于Tomcat 可以在server.xml 中通过<Connector> 元素设置线程池中线程的数目。 就实现来说,调度者线程类所担负的责任如其名字,该类的责任是调度线程,只需要利用自己的属性完成自己的责任。所以该类是承担了责任的,并且该类的责任又集中到唯一的单体对象中。而其他对象又依赖于该特定对象所承担的责任,我们就需要得到该特定对象。那该类就是一个单例模式的实现了
2.2.3 Servlet多实例
服务器可以使用多个实例来处理请求,代替单个实例的请求排队带来的效益问题。服务器创建一个Servlet类 的多个Servlet 实例组成的实例池,对于每个请求分配Servlet 实例进行响应处理,之后放回到实例池中等待下此请求。这样就造成并发访问的问题。此时,局部变量(字段)也是安全的,但对于全局变量和共享数据是不安全的,需要进行同步处理。而对于这样多实例的情况SingleThreadModel 接口并不能解决并发访问问题。 SingleThreadModel 接口在servlet 规范中已经被废弃了。
Servlet 并非只是单例的. 当container 开始启动,或是客户端发出请求服务时,Container 会按照容器的配置负责加载和实例化一个Servlet (也可以配置为多个,不过一般不这么干).不过一般来说一个servlet 只会有一个实例。
Struts2 的Action 是原型,非单实例的;会对每一个请求,产生一个Action 的实例来处理。Struts1 的Action,Spring 的Ioc 容器管理的bean 默认是单实例的. Struts1 Action 是单实例的,spring mvc 的controller 也是如此。因此开发时要求必须是线程安全的,因为仅有Action 的一个实例来处理所有的请求。单例策略限制了Struts1 Action 能作的事,并且要在开发时特别小心。Action 资源必须是线程安全的或同步的。 Spring的Ioc 容器管理的bean 默认是单实例的。 Struts2 Action 对象为每一个请求产生一个实例,因此没有线程安全问题。(实际上,servlet 容器给每个请求产生许多可丢弃的对象,并且不会导致性能和垃圾回收问题)。 当Spring 管理Struts2 的Action 时,bean 默认是单实例的,可以通过配置参数将其设置为原型。(scope=prototype )
2.2.4 容器概念理解
Servlet容器,Web容器,应用服务器 Servlet 容器的主要任务就是管理Servlet 的生命周期; Web 容器也称之为web 服务器,主要任务就是管理和部署web 应用的; 应用服务器的功能非常强大,不仅可以管理和部署web 应用,也可以部署EJB 应用,实现容器管理的事务等等。。。 Web 服务器就是跟基于HTTP 的请求打交道,而EJB 容器更多是跟数据库,事务管理等服务接口交互,所以应用服务器的功能是很多的。 常见的web 服务器就是Tomcat ,但Tomcat 同样也是Servlet 服务器; 常见的应用服务器有WebLogic,WebSphere ,但都是收费的; 没有Servlet 容器,可以用Web 容器直接访问静态Html 页面,比如安装了apache 等;如果需要显示Jsp/Servlet ,就需要安装一个Servlet 容器;但是光有servlet 容器也是不够的,它需要被解析为html 显示,所以仍需要一个web 容器;所以,我们常把web 容器和Servlet 容器视为一体,因为他们两个容器都有对方的功能实现了,都没有独立的存在了,比如tomcat)
|