Servlets是如何工作的?实例化、会话、共享变量和多线程。

1255

假设我有一个包含多个servlet的Web服务器。为了在这些servlet之间传递信息,我设置了会话和实例变量。

现在,如果有2个或更多用户向该服务器发送请求,那么会话变量会发生什么情况呢?
它们对所有用户都是共同的还是每个用户都不同?
如果它们是不同的,那么服务器如何区分不同的用户?

再提一个类似的问题,如果有n个用户访问特定的servlet,那么这个servlet只在第一个用户访问时被实例化,还是对所有用户分别被实例化?
换句话说,实例变量会发生什么情况?

8个回答

1946

ServletContext

当Servlet容器(例如Apache Tomcat)启动时,它会部署和加载所有的Web应用程序。当一个Web应用程序被加载时,Servlet容器会创建一个ServletContext的实例,并将其保存在服务器的内存中。Web应用程序的web.xml和所有包含的web-fragment.xml文件都会被解析,每个<servlet><filter><listener>(或者每个使用@WebServlet@WebFilter@WebListener注解的类)都会被实例化一次,并且也会保存在服务器的内存中,通过ServletContext进行注册。对于每个实例化的过滤器,它的init()方法会被调用,参数是一个FilterConfig的实例,该实例包含了相关的ServletContext
当一个Servlet具有<servlet><load-on-startup>@WebServlet(loadOnStartup)值为0或更大时,它的init()方法也会在启动期间被调用。这些Servlet按照指定的值的顺序进行初始化。如果为多个Servlet指定了相同的值,则每个Servlet都按照它们在web.xmlweb-fragment.xml@WebServlet类加载中出现的顺序进行加载。如果"load-on-startup"值不存在或为负数,则init()方法将在HTTP请求第一次命中该Servlet时被调用。有两个init()方法,一个接受一个ServletConfig实例作为参数,该实例包含相关的ServletContext,另一个不接受任何参数,但可以通过继承的getServletContext()方法获得ServletContext
当servlet容器完成上述所有的初始化步骤后,将调用ServletContextListener#contextInitialized(),并传入一个ServletContextEvent参数,该参数包含了相关的ServletContext。这将为开发人员提供机会,以编程方式注册另一个ServletFilterListener
当Servlet容器关闭时,它会卸载所有的Web应用程序,调用所有已初始化的Servlet和过滤器的destroy()方法,并且通过ServletContext注册的所有Servlet、过滤器和监听器实例都会被销毁。最后,将调用ServletContextListener的contextDestroyed()方法,并且ServletContext本身也会被销毁。
HttpServletRequest和HttpServletResponse Servlet容器附加在一个Web服务器上,该服务器监听特定端口号上的HTTP请求(开发过程中通常使用端口8080,生产环境中使用端口80)。当客户端(例如使用Web浏览器的用户,或者通过编程使用URLConnection的方式)发送HTTP请求时,Servlet容器会创建HttpServletRequestHttpServletResponse的新实例,并将它们通过定义的过滤器链传递,最终到达Servlet实例。
过滤器的情况下,会调用doFilter()方法。当Servlet容器的代码调用chain.doFilter(request, response)时,请求和响应会继续传递给下一个过滤器,如果没有剩余的过滤器,则传递给Servlet。
Servlet的情况下,会调用service()方法。默认情况下,该方法根据request.getMethod()确定要调用的doXxx()方法。如果Servlet中没有找到确定的方法,则会在响应中返回HTTP 405错误。
请求对象提供了访问有关HTTP请求的所有信息的途径,例如其URLheadersquery string和正文。响应对象提供了控制和发送HTTP响应的能力,您可以通过设置标头和正文(通常是从JSP文件生成的HTML内容)来自定义响应。当HTTP响应被提交和完成后,请求和响应对象都会被回收并可供重用。

HttpSession

当客户首次访问Web应用程序和/或通过request.getSession()首次获取HttpSession时,Servlet容器会创建一个新的HttpSession对象,并生成一个长而唯一的ID(可以通过session.getId()获取),并将其存储在服务器的内存中。Servlet容器还在HTTP响应的Set-Cookie头中设置一个名为JSESSIONID的Cookie,其值为唯一的会话ID。
根据HTTP cookie specification(任何体面的网络浏览器和网络服务器都必须遵守的合同),客户端(即网络浏览器)需要在随后的请求中通过Cookie头部发送此cookie,只要cookie有效(即唯一ID必须指向未过期的会话,并且域名和路径正确)。通过浏览器内置的HTTP流量监视器,您可以验证cookie的有效性(在Chrome / Edge / Firefox 23+ / IE9+中按F12,然后检查Net/Network选项卡)。Servlet容器将检查每个传入的HTTP请求的Cookie头部,以查看是否存在名为JSESSIONID的cookie,并使用其值(会话ID)从服务器内存中获取关联的HttpSessionHttpSession会一直保持活动状态,直到它在请求中没有被使用(即空闲)的时间超过了<session-timeout>中指定的超时值,这是在web.xml中的一个设置。默认的超时值取决于Servlet容器,通常为30分钟。因此,当客户端超过指定的时间不访问Web应用程序时,Servlet容器会销毁会话。即使在指定了cookie的情况下,每个后续请求也将无法再访问相同的会话;Servlet容器将创建一个新的会话。
在客户端上,会话cookie会保持有效,只要浏览器实例在运行(通常情况下)。除非浏览器配置为恢复上次的浏览器会话,当客户端关闭浏览器实例(所有标签/窗口)时,会话在客户端端丢失。在新的浏览器实例中,与会话关联的cookie将不存在,因此不再发送。这将导致创建一个全新的HttpSession,并使用全新的会话cookie。
简而言之,
  • ServletContext的生命周期与Web应用程序的生命周期相同。它在所有会话中的所有请求之间共享。
  • HttpSession的生命周期与客户端与Web应用程序的交互以及会话在服务器端未超时的时间相同。它在同一会话中的所有请求之间共享。
  • HttpServletRequestHttpServletResponse的生命周期从Servlet接收到客户端的HTTP请求开始,直到完整的响应(网页)到达为止。它不在其他地方共享。
  • 所有的ServletFilterListener实例的生命周期与Web应用程序的生命周期相同。它们在所有会话中的所有请求之间共享。
  • ServletContextHttpServletRequestHttpSession中定义的任何attribute将与所涉及的对象的生命周期相同。对象本身代表了诸如JSF、CDI、Spring等Bean管理框架中的"scope"。这些框架将其作用域化的Bean存储为其最匹配的作用域的attribute

线程安全

话虽如此,你可能最关心的是线程安全。你现在应该知道,servlets和filters是在所有请求之间共享的。这就是Java的好处,它是多线程的,不同的线程(即:HTTP请求)可以使用同一个实例。否则,为每个请求重新创建、init()destroy()将会非常昂贵。

你还应该意识到,你绝对不应该将任何请求或会话作用域的数据分配为servlet或filter的实例变量。它将会在其他会话中的所有其他请求之间共享。这是不安全的!下面的例子说明了这一点:

public class ExampleServlet extends HttpServlet {

    private Object thisIsNOTThreadSafe;

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Object thisIsThreadSafe;

        thisIsNOTThreadSafe = request.getParameter("foo"); // BAD!! Shared among all requests!
        thisIsThreadSafe = request.getParameter("foo"); // OK, this is thread safe.
    } 
}

另请参阅:


26
当我设法找到发送给客户端的JSessionId时,我就可以窃取他的会话吗? - Toskan
56
@Toskan:没错,这被称为[会话固定攻击](http://en.wikipedia.org/wiki/Session_fixation)。请注意,这不仅适用于JSP / Servlet。所有通过cookie维护会话的其他服务器端语言也很敏感,例如具有PHPSESSID cookie的PHP,具有ASP.NET_SessionID cookie的ASP.NET等等。这也是为什么像一些JSP / Servlet MVC框架自动执行的使用;jsessionid = xxx进行URL重写被反对的原因之一。只需确保会话ID从未在网页中通过URL或其他方式公开,以使不知情的最终用户不受攻击。 - BalusC
12
@Toskan:另外,确保你的网络应用程序不易受到跨站脚本攻击(XSS)。也就是说,不要以未经转义的形式重新显示任何用户可控输入的内容。XSS会为收集所有最终用户的会话ID打开大门。另请参阅XSS背后的一般概念是什么? - BalusC
2
@BalusC,抱歉我的愚蠢。这意味着所有用户都访问thisIsNOTThreadSafe的相同实例,对吗? - overshadow
4
当整个Servlet本身缺失时,会返回404错误。当Servlet存在但是所需的doXxx()方法未被实现时,会返回405错误。 - BalusC
显示剩余20条评论

445

会话

enter image description here enter image description here

简而言之:Web服务器在每个访问者的第一次访问时为其发放一个唯一标识符。访问者必须携带该标识符以便下次被识别。该标识符还允许服务器正确地将属于一个会话的对象与另一个会话的对象隔离开来。

Servlet实例化

如果load-on-startupfalse

enter image description here enter image description here

如果load-on-startuptrue

enter image description here enter image description here

一旦他进入服务模式并进入状态,相同的 servlet 将处理来自所有其他客户端的请求。

enter image description here

为什么每个客户端都有一个实例不是一个好主意?想一想:你会为每个订单雇用一个披萨师傅吗?如果这样做,你很快就会破产。

然而,这种方法存在一定的风险。请记住:这个单一的人将所有订单信息都放在他的口袋里。因此,如果您没有在Servlet上注意线程安全性, 他可能会给某个客户错误的订单。


29
你的图片对于我的理解非常有帮助。我有一个问题,如果订单过多,这家披萨店会怎么做?是等一个披萨师傅还是雇佣更多的披萨师傅?谢谢。 - zh18
7
他会返回一条信息,内容为“此时请求过多,请稍后再试”。 - NaN
4
Servlets(网络应用程序中的Java组件)可以同时处理多个请求,而不像比萨送货员一次只能送一份披萨。它们只需小心地记录客户地址、披萨口味等信息即可实现这一点。 - bruno

42

Java Servlet中的会话与其他语言(如PHP)中的会话相同,它是唯一的用户。服务器可以通过不同的方式来跟踪它,例如使用cookies、URL重写等等。这个Java文档在Java Servlet上下文中解释了它,并指出会话的维护方式完全由服务器设计者自行决定。规范仅规定必须将其维护为对于多个连接到服务器的用户而言是唯一的。请查看此Oracle文章获取更多有关您两个问题的信息。

编辑 这里有一个非常好的教程,介绍如何在Servlet内部使用会话,以及这是Sun关于Java Servlets的一章,介绍了它们是什么以及如何使用它们。通过这两篇文章,您应该能够回答所有问题。


这给我带来了另一个问题, 既然整个应用程序只有一个servlet上下文,并且我们通过这个servlet上下文访问会话变量,那么如何使会话变量对每个用户都是唯一的? 谢谢。 - Ku Jon
1
你是如何从servletContext访问会话的?你不是在引用servletContext.setAttribute()吧? - matt b
5
每个Web应用程序都有一个ServletContext对象。该对象具有零个、一个或多个会话对象,即会话对象的集合。每个会话都由某种标识符字符串标识,如在其他答案中所见的卡通图像。该标识符可通过cookie或URL重写在客户端上跟踪。每个会话对象都有其自己的变量。 - Basil Bourque

33

当Servlet容器(例如Apache Tomcat)启动时,如果在容器控制台上出现错误或显示错误,则它将从web.xml文件中读取(每个应用程序只有一个)。否则,它将使用web.xml(因此被称为部署描述符)部署和加载所有Web应用程序。

在servlet实例化阶段,servlet实例已准备就绪,但由于缺少两个信息,它无法提供客户端请求:
1:上下文信息
2:初始配置信息

Servlet引擎创建servletConfig接口对象,将上述缺失的信息封装到其中 servlet引擎通过提供servletConfig对象引用作为参数调用servlet的init()。一旦init()完全执行,servlet就准备好提供客户端请求了。

Q)在servlet的生命周期中,实例化和初始化发生多少次?

A) 只发生一次(对于每个客户端请求,都会创建一个新线程) servlet的一个实例可以为任意数量的客户端请求提供服务,即在为一个客户端请求提供服务之后,服务器不会关闭。它会等待其他客户端请求,即利用servlet(内部servlet引擎创建线程)克服了CGI(为每个客户端请求创建一个新进程)的限制。

Q)会话概念是如何工作的?

A) 每当HttpServletRequest对象上调用getSession()时

步骤1:对传入的会话ID评估请求对象。

步骤2:如果ID不可用,则创建一个全新的HttpSession对象并生成其相应的会话ID(即哈希表的ID)。将会话ID存储到httpservlet响应对象中,并将HttpSession对象的引用返回给servlet(doGet/doPost)。

步骤3:如果ID可用,则不会创建全新的会话对象,而是从请求对象中选择会话ID,并使用会话ID作为键在会话集合中进行搜索。

一旦搜索成功,会话ID将存储到HttpServletResponse中,并将现有的会话对象引用返回给UserDefinedServlet的doGet()或doPost()。

注意:

1)当控制权从servlet代码转移到客户端时,不要忘记会话对象由servlet容器即servlet引擎持有。

2)多线程留给servlet开发人员来实现,即处理客户端的多个请求,无需担心多线程代码。

简而言之:

当应用程序启动(部署在servlet容器上)或首次访问时(取决于负载平衡设置),就会创建一个servlet。 当servlet被实例化时,调用servlet的init()方法。 然后该servlet(仅有的一个实例)处理所有请求(通过多个线程调用其service()方法)。因此,在其中不建议使用任何同步策略,并且应避免使用servlet的实例变量。 当应用程序被卸载(servlet容器停止)时,将调用destroy()方法。


21

会话- Chris Thompson所说的内容。

实例化- 当容器接收到与servlet映射的第一个请求时,servlet将被实例化(除非在web.xml中使用<load-on-startup>元素配置了启动时加载servlet)。同一实例用于处理后续请求。


3
没问题。额外的想法:每个请求都会获得一个新的(或被回收再利用的)线程来运行在同一 Servlet 实例上。每个 Servlet 只有一个实例,可能有许多线程(如果有许多同时请求)。 - Basil Bourque

13

Servlet规范JSR-315明确定义了Web容器在服务(以及doGet、doPost、doPut等方法)方法中的行为(2.3.3.1多线程问题,第9页):

Servlet容器可以通过servlet的service方法并发发送请求。为处理这些请求,Servlet开发人员必须为服务方法中的多个线程提供充分的并发处理。

虽然不建议使用,但开发人员的另一个选择是实现SingleThreadModel接口,该接口要求容器保证在服务方法中同时只有一个请求线程。Servlet容器可以通过在Servlet上序列化请求或维护Servlet实例池来满足此要求。如果Servlet属于已被标记为可分发的Web应用程序,则容器可以在应用程序跨越的每个JVM中维护Servlet实例池。

对于没有实现SingleThreadModel接口的Servlet,如果已经使用synchronized关键字定义了服务方法(或类似于doGet或doPost这样的方法,这些方法被派发到HttpServlet抽象类的服务方法),则Servlet容器无法使用实例池方法,而必须通过它进行请求序列化。强烈建议在这些情况下开发人员不要对服务方法(或派发给它的方法)进行同步,因为这会对性能产生不利影响。


2
顺便提一下,当前的Servlet规范(2015-01)是3.1版,由JSR 340定义。 - Basil Bourque
JSR 340: Java Servlet 3.1 SpecificationJSR 369: JavaTM Servlet 4.0 Specification 是与Java Servlet相关的编程规范。 - Reva
1
非常整洁的答案!@tharindu_DG - Tom Taylor

0

不是。 Servlets 不是线程安全的

这允许同时访问多个线程

如果您想将其作为线程安全的Servlet。, 您可以选择

实现SingleThreadInterface(i) 这是一个空接口,没有

方法

或者我们可以使用同步方法

我们可以通过在方法前面使用synchronized关键字来使整个服务方法同步

例子::

public Synchronized class service(ServletRequest request,ServletResponse response)throws ServletException,IOException

或者我们可以将代码块放在同步块中

示例:

Synchronized(Object)

{

----Instructions-----

}

我觉得同步块比整个方法更好

同步


0

从上面的解释可以清楚地看出,通过实现SingleThreadModel,Servlet容器可以确保Servlet的线程安全性。容器实现可以通过以下两种方式实现:

1)将请求序列化(排队)到单个实例 - 这类似于Servlet没有实现SingleThreadModel但同步服务/ doXXX方法;或者

2)创建一个实例池 - 这是更好的选择,也是Servlet启动/初始化工作/时间与托管Servlet的环境的限制参数(内存/ CPU时间)之间的权衡。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接