【问题标题】:How do servlets work? Instantiation, sessions, shared variables and multithreadingservlet 是如何工作的?实例化、会话、共享变量和多线程
【发布时间】:2023-02-14 20:24:03
【问题描述】:

假设,我有一个包含大量 servlet 的网络服务器。对于在这些 servlet 之间传递的信息,我正在设置会话和实例变量。

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

还有一个类似的问题,如果有 n 用户访问一个特定的 servlet,那么这个 servlet 只在第一个用户第一次访问它时被实例化,还是为所有用户单独实例化?
换句话说,实例变量会发生什么?

【问题讨论】:

    标签: java multithreading servlets session-variables instance-variables


    【解决方案1】:

    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() 方法也会在启动期间使用新的 ServletConfig 参数调用,该参数又包含相关的 ServletContext。这些 servlet 以该值指定的相同顺序初始化(1 是第一个,2 是第二个,等等)。如果为多个 servlet 指定了相同的值,那么每个 servlet 的加载顺序与它们在 web.xmlweb-fragment.xml@WebServlet 类加载中出现的顺序相同。如果“启动时加载”值不存在,init() 方法将在 HTTP request 第一次访问该 servlet 时被调用。

    当 servlet 容器完成上述所有初始化步骤后,将使用 ServletContextEvent 参数调用 ServletContextListener#contextInitialized(),该参数又包含相关的 ServletContext。这将使开发人员有机会以编程方式注册另一个ServletFilterListener

    当 servlet 容器关闭时,它会卸载所有 Web 应用程序,调用所有已初始化的 servlet 和过滤器的 destroy() 方法,并且所有通过 ServletContext 注册的 ServletFilterListener 实例都将被丢弃。最后 ServletContextListener#contextDestroyed() 将被调用并且 ServletContext 本身将被丢弃。

    HttpServletRequestHttpServletResponse

    servlet 容器附加到 Web 服务器,该服务器在特定端口号(开发期间通常使用端口 8080,生产中通常使用端口 80)上侦听 HTTP 请求。当客户端(例如使用 Web 浏览器的用户或programmatically using URLConnection)发送 HTTP 请求时,servlet 容器会创建新的HttpServletRequestHttpServletResponse 对象,并通过链中任何已定义的Filter 传递它们,最终, Servlet 实例。

    filters 的情况下,将调用doFilter() 方法。当 servlet 容器的代码调用 chain.doFilter(request, response) 时,请求和响应继续到下一个过滤器,或者如果没有剩余的过滤器则命中 servlet。

    servlets 的情况下,将调用service() 方法。默认情况下,此方法确定根据 request.getMethod() 调用 doXxx() 方法中的哪一个。如果 servlet 中不存在确定的方法,则在响应中返回 HTTP 405 错误。

    请求对象提供对有关 HTTP 请求的所有信息的访问,例如它的 URLheadersquery string 和正文。响应对象提供了以您想要的方式控制和发送 HTTP 响应的能力,例如,允许您设置标头和正文(通常使用从 JSP 文件生成的 HTML 内容)。当提交并完成 HTTP 响应时,请求和响应对象都将被回收并可供重用。

    HttpSession

    当客户端第一次访问 webapp 和/或第一次通过request.getSession()获取HttpSession时,servlet容器创建一个新的HttpSession对象,生成一个长而唯一的ID(你可以得到session.getId()),并将其存储在服务器的内存中。 servlet 容器还在 HTTP 响应的 Set-Cookie 标头中设置 Cookie,并将 JSESSIONID 作为其名称和唯一会话 ID 作为其值。

    根据HTTP cookie specification(任何体面的网络浏览器和网络服务器都必须遵守的合同),客户端(网络浏览器)需要在Cookie标头中的后续请求中发送此cookie,只要cookie 有效(即唯一 ID 必须引用未过期的会话并且域和路径正确)。使用浏览器内置的 HTTP 流量监视器,您可以验证 cookie 是否有效(在 Chrome / Firefox 23+ / IE9+ 中按 F12,并检查网络/网络标签)。 servlet 容器将检查每个传入 HTTP 请求的 Cookie 标头是否存在名称为 JSESSIONID 的 cookie,并使用其值(会话 ID)从服务器内存中获取关联的 HttpSession

    HttpSession 一直保持活动状态,直到它空闲(即未在请求中使用)超过 <session-timeout> 中指定的超时值(web.xml 中的设置)。超时值默认为 30 分钟。因此,当客户端在超过指定时间的情况下未访问 Web 应用程序时,servlet 容器会将 session 丢弃。每个后续请求,即使指定了 cookie,也将无法再访问同一会话; servlet 容器将创建一个新会话。

    在客户端,只要浏览器实例在运行,会话 cookie 就会保持活动状态。因此,如果客户端关闭浏览器实例(所有选项卡/窗口),那么会话将在客户端被丢弃。在新的浏览器实例中,与会话关联的 cookie 将不存在,因此将不再发送它。这会导致创建一个全新的HttpSession,并使用一个全新的会话 cookie。

    简而言之

    • ServletContext 与网络应用程序一样存在。它是共享的全部要求在全部会议。
    • HttpSession 只要客户端使用相同的浏览器实例与 Web 应用交互,并且服务器端的会话没有超时,就会一直存在。它是共享的全部中的请求相同的会议。
    • HttpServletRequestHttpServletResponse 从 servlet 收到来自客户端的 HTTP 请求开始,直到完整的响应(网页)到达。这是不是在别处分享。
    • 所有ServletFilterListener 实例只要 Web 应用存在就存在。它们之间共享全部要求在全部会议。
    • ServletContextHttpServletRequestHttpSession 中定义的任何attribute 将在所讨论的对象存在时存在。对象本身代表 Bean 管理框架(例如 JSF、CDI、Spring 等)中的“作用域”。这些框架将它们的作用域 bean 存储为其最接近的匹配作用域的 attribute

    线程安全

    也就是说,您主要关心的可能是thread safety。您现在应该知道 servlet 和过滤器在所有请求之间共享。这是 Java 的优点,它是多线程的,不同的线程(阅读:HTTP 请求)可以使用同一个实例。否则,为每个请求重新创建 init()destroy() 它们会太昂贵。

    你也应该意识到你应该绝不将任何请求或会话范围内的数据分配为实例servlet 或过滤器的变量。它将在其他会话中的所有其他请求之间共享。那是不是线程安全!下面的例子说明了这一点:

    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.
        } 
    }
    

    也可以看看:

    【讨论】:

    • 因此,当我以某种方式找到发送给客户端的 JSessionId 时,我可以窃取他的会话吗?
    • @Toskan:没错。它被称为session fixation hack。请注意,这并非特定于 JSP/Servlet。所有其他通过 cookie 维护会话的服务器端语言也是敏感的,例如带有 PHPSESSID cookie 的 PHP、带有 ASP.NET_SessionID cookie 的 ASP.NET 等等。这也是为什么像某些 JSP/Servlet MVC 框架自动执行的那样使用 ;jsessionid=xxx 重写 URL 是不受欢迎的。只需确保会话 ID 永远不会在 URL 中或以其他方式在网页中公开,这样不知情的最终用户就不会受到攻击。
    • @Toskan:另外,请确保您的网络应用程序对 XSS 攻击不敏感。 IE。不要以未转义的形式重新显示任何用户控制的输入。 XSS 为收集所有最终用户的会话 ID 的方法打开了大门。另见What is the general concept behind XSS?
    • @BalusC,对不起我的愚蠢。这意味着所有用户都访问同一个 thisIsNOTThreadSafe 实例,对吗?
    • 当整个 servlet 本身不存在时,将返回 @TwoThumbSticks 404。当存在 servlet 但未实现所需的 doXxx() 方法时返回 405。
    【解决方案2】:

    会话

    简而言之:网络服务器发布一个唯一标识符每个访客在他的第一的访问。访客必须带回该 ID,以便下次被识别。该标识符还允许服务器正确地将一个会话拥有的对象与另一个会话拥有的对象分开。

    Servlet实例化

    如果启动时加载错误的:

    如果启动时加载真的:

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

    为什么每个客户端一个实例不是一个好主意?想一想:您会为每一个订单雇用一个比萨店员吗?那样做,你很快就会倒闭。

    虽然它有一个小风险。记住:这个人把所有的订单信息都装在口袋里:所以如果你对thread safety on servlets不谨慎,他可能最终会向某个客户下错订单。

    【讨论】:

    • 你的图片对我的理解非常好。我有一个问题,当比萨订单太多时,这家比萨餐厅会怎么做,是等一个比萨店员还是雇更多的比萨店员?谢谢 。
    • 他会回复to many requests at this moment. try again later的消息
    • Servlet 与比萨送货员不同,它可以同时进行不止一次送货。他们只需要特别注意在哪里写下客户的地址、披萨的味道……
    【解决方案3】:

    Java servlet 中的会话与其他语言(如 PHP)中的会话相同。它对用户来说是独一无二的。服务器可以通过不同的方式跟踪它,例如 cookie、url 重写等。这篇Java doc 文章在 Java servlet 的上下文中对其进行了解释,并指出究竟如何维护会话是留给服务器设计者的实现细节.该规范仅规定它必须在与服务器的多个连接中对用户保持唯一性。查看this article from Oracle,了解有关您的两个问题的更多信息。

    编辑有一个很棒的教程here,介绍如何在 servlet 中使用会话。 here 是 Sun 关于 Java Servlet 的一章,它们是什么以及如何使用它们。在这两篇文章之间,您应该能够回答所有问题。

    【讨论】:

    • 这给我带来了另一个问题,因为整个应用程序只有一个 servlet 上下文,我们通过这个 servletcontext 访问会话变量,那么会话变量如何对每个用户都是唯一的?谢谢..
    • 您如何从 servletContext 访问会话?你不是指 servletContext.setAttribute(),是吗?
    • @KuJon 每个网络应用程序都有一个ServletContext 对象。该对象具有零个、一个或多个会话对象——会话对象的集合。每个会话都由某种标识符字符串标识,如其他答案中的卡通所示。该标识符通过 cookie 或 URL 重写在客户端进行跟踪。每个会话对象都有自己的变量。
    【解决方案4】:

    当 servlet 容器(如 Apache Tomcat)启动时,如果出现任何问题或在容器端控制台显示错误,它将从 web.xml 文件(每个应用程序仅一个)读取,否则,它将部署和加载所有 web使用 web.xml(因此将其命名为部署描述符)的应用程序。

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

    Servlet引擎创建servletConfig接口对象将上面缺失的信息封装进去 servlet 引擎通过提供 servletConfig 对象引用作为参数来调用 servlet 的 init()。一旦 init() 完全执行,servlet 就可以为客户端请求提供服务。

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

    A)只有一次(为每个客户请求创建一个新线程) 只有一个 servlet 实例服务于任意数量的客户端请求,即在服务一个客户端请求之后,服务器不会死掉。它等待其他客户端请求,即 servlet 克服了 CGI(为每个客户端请求创建一个新进程)的限制(内部 servlet 引擎创建线程)。

    问)会话概念如何运作?

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

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

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

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

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

    笔记:

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

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

    简写形式:

    当应用程序启动时(它部署在 servlet 容器上)或首次访问时(取决于启动时加载设置),将创建一个 servlet 当servlet被实例化时,调用servlet的init()方法 然后 servlet(它的唯一实例)处理所有请求(它的 service() 方法被多个线程调用)。这就是为什么不建议在其中进行任何同步的原因,并且您应该避免使用 servlet 的实例变量 当取消部署应用程序时(servlet 容器停止),调用 destroy() 方法。

    【讨论】:

      【解决方案5】:

      会话- 克里斯汤普森说的。

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

      【讨论】:

      • 正确的。额外的想法:每个请求都有一个新的(或回收的)线程在那个单一的 Servlet 实例上运行。每个 Servlet 都有一个实例,并且可能有多个线程(如果有多个并发请求)。
      【解决方案6】:

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

      一个 servlet 容器可以通过服务发送并发请求 servlet 的方法。为了处理请求,Servlet 开发人员 必须为多个并发处理做出足够的规定 服务方法中的线程。

      虽然不推荐,但开发人员的替代方法是 实现需要容器的 SingleThreadModel 接口 保证在同一时刻只有一个请求线程 服务方式。 servlet 容器可以通过以下方式满足此要求 在 servlet 上序列化请求,或者通过维护 servlet 池 实例。如果 servlet 是 Web 应用程序的一部分 标记为可分发,容器可以维护一个 servlet 池 应用程序分布在每个 JVM 中的实例。

      对于未实现 SingleThreadModel 接口的 servlet,如果 服务方法(或诸如 doGet 或 doPost 之类的方法 调度到 HttpServlet 抽象类的服务方法) 已经用synchronized关键字定义,servlet容器 不能使用实例池方法,但必须序列化请求 通过它。强烈建议开发者不要同步 这些中的服务方法(或分派给它的方法) 由于对性能产生不利影响的情况

      【讨论】:

      【解决方案7】:

      不。Servlet 是不是线程安全的

      这允许一次访问多个线程

      如果你想让 Servlet 成为线程安全的,你可以去

      Implement SingleThreadInterface(i) 这是一个空白界面,没有

      方法

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

      我们可以使用 synchronized 使整个服务方法同步

      方法前的关键字

      例子::

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

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

      例子::

      Synchronized(Object)
      
      {
      
      ----Instructions-----
      
      }
      

      我觉得 Synchronized 块比制作整个方法要好

      同步的

      【讨论】:

        【解决方案8】:

        从上面的解释中可以清楚地看出,通过实施单线程模型,servlet 容器可以保证 servlet 的线程安全。容器实现可以通过两种方式做到这一点:

        1) 将请求序列化(排队)到单个实例——这类似于不实现 SingleThreadModel 但同步 service/doXXX 方法的 servlet;或者

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

        【讨论】:

          猜你喜欢
          • 2015-08-27
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-08-16
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多