美文网首页
Servlet规范和Servlet容器

Servlet规范和Servlet容器

作者: rock_fish | 来源:发表于2019-11-11 10:49 被阅读0次
    Servlet 规范 的目的

    按照面向接口编程的思想,HTTP服务端的业务逻辑类统一实现Servlet接口,使用Servlet容器用来加载和管理业务类(Servlet实现类),容器将httpServer与业务处理类之间做了解耦隔离.
    业务开发只需要实现一个 Servlet接口完成业务功能,并把它注册到 Servlet 容器中,剩下的事情就由 容器来处理.开发者不需要关心 Socket 网络通信、不需要关心 HTTP 协议,也不需要关心业务类是如何被实例化和调用的

    image.png

    Servlet 接口和 Servlet 容器这一整套规范叫作 Servlet 规范,Tomcat 和 Jetty 都按照 Servlet 规范的要求实现了 Servlet 容器,同时它们也具有 HTTP 服务器的功能.

    Servlet 接口

    Servlet 接口定义了下面五个方法:

    public interface Servlet {
        void init(ServletConfig config) throws ServletException;
        
        ServletConfig getServletConfig();
        
        void service(ServletRequest req, ServletResponse res)throws ServletException, IOException;
        
        String getServletInfo();
        
        void destroy();
    }
    
    
    • service 方法:
      具体业务类在这个方法里实现处理逻辑,参数ServletRequest 用来封装请求信息,ServletResponse 用来封装响应信息,这两个类是对通信协议的封装.
    1. HTTP 协议中的请求是HttpServletRequest类。通过 HttpServletRequest 来获取所有请求相关的信息,包括请求路径、Cookie、HTTP 头、请求参数等;创建和获取 Session。
    2. HTTP 协议中的响应 HttpServletResponse 是用来封装 HTTP 响应的,
    • init 和 destroy
      跟生命周期有关的方法
      init : Servlet 容器在加载 Servlet 类的时候会调用 init 方法,可以在 init 方法里初始化一些资源,比如 Spring MVC 中的 DispatcherServlet,就是在 init 方法里创建了自己的 Spring 容器。
      destroy : 在卸载的时候会调用 destroy 方法,可以在 destroy 方法里释放init方法里所创建的资源.

    • ServletConfig
      封装 Servlet 的初始化参数。在 web.xml 中给 Servlet 配置的参数,在程序里通过 getServletConfig 方法拿到这些参数。

    • HttpServlet
      大多数的 Servlet 实现都是在 HTTP 环境中处理的, Servet 规范提供了 HttpServlet 继承 了GenericServlet,并加入了 HTTP 特性。对于处理http情况的情况通过继承 HttpServlet 类来实现自己的 Servlet,只需要重写两个方法:doGet 和 doPost。

    Servlet容器
    • 工作流程


      image.png
    1. 客户端请求HTTP服务器.
    2. 封装客户请求.
      2.1 HTTP 服务器会用一个 ServletRequest 对象把客户的请求信息封装起来,
      2.2 调用 Servlet 容器的 service 方法.
    3. Servlet容器检索/创建Servlet类.
      3.1 Servlet 容器拿到请求后,根据请求的 URL 和 Servlet 的映射关系,找到相应的 Servlet.
      3.2 如果 Servlet 还没有被加载,就用反射机制创建这个 Servlet.
    4. 用Servlet类做业务处理并返回结果
      4.1 调用 Servlet 的 init 方法来完成初始化.
      4.2 调用 Servlet 的 service 方法来处理请求.
      4.3 结果封装到ServletResponse 对象返回给 HTTP 服务器.
    5. HTTP 服务器会把响应发送给客户端
    • Web 应用程序的方式来部署 Servlet
      一般来说,我们是以 Web 应用程序的方式来部署 Servlet 的,即将Servlet信息按照特定的目录格式存放,部署到特定的目录下, Servlet 容器就能找到并加载 Servlet.
      根据 Servlet 规范,Web 应用程序目录结构如下:
    | -  MyWebApp
          | -  WEB-INF/web.xml        -- 配置文件,用来配置 Servlet 等
          | -  WEB-INF/lib/           -- 存放 Web 应用所需各种 JAR 包
          | -  WEB-INF/classes/       -- 存放你的应用类,比如 Servlet 类
          | -  META-INF/              -- 目录存放工程的一些信息
    

    目录下分别放置了 Servlet 的类文件、配置文件以及静态资源,Servlet 容器通过读取配置文件,就能找到并加载 Servlet

    • Web应用 与 Servlet容器
      • ServletContext vs 应用 : 1 vs 1
      • 应用 vs Servlet : 1 vs n
        Servlet 规范里定义了ServletContext这个接口来对应一个 Web 应用。Web 应用部署好后,Servlet 容器在启动时会加载 Web 应用,并为每个 Web 应用创建唯一的 ServletContext 对象。你可以把 ServletContext 看成是一个全局对象,一个 Web 应用可能有多个 Servlet,这些 Servlet 可以通过全局的 ServletContext 来共享数据,这些数据包括 Web 应用的初始化参数、Web 应用目录下的文件资源等。由于 ServletContext 持有所有 Servlet 实例,你还可以通过它来实现 Servlet 请求的转发。
    扩展机制
    • Filter过滤器
      是个接口,允许对请求和响应做一些统一的定制化处理。工作原理是这样的:Web 应用部署完成后,Servlet 容器需要实例化 Filter 并把 Filter 链接成一个 FilterChain。当请求进来时,获取第一个 Filter 并调用 doFilter 方法,doFilter 方法负责调用这个 FilterChain 中的下一个 Filter。

    可以根据请求的频率来限制访问,或者根据国家地区的不同来修改响应内容。过滤器的

    • Listener 监听器
      当 Web 应用在 Servlet 容器中运行时,Servlet 容器内部会不断的发生各种事件,如 Web 应用的启动和停止、用户请求到达等。 Servlet 容器提供了一些默认的监听器来监听这些事件,当事件发生时,Servlet 容器会负责调用监听器的方法。
      可以定义自己的监听器去监听感兴趣的事件,将监听器配置在 web.xml 中。 Spring 就实现了自己的监听器,来监听 ServletContext 的启动事件,目的是当 Servlet 容器启动时,创建并初始化全局的 Spring 容器。
    总结

    Servlet 本质上是一个接口,实现了 Servlet 接口的业务类也叫 Servlet。Servlet 接口其实是 Servlet 容器跟具体 Servlet 业务类之间的接口。Servlet 接口跟 Servlet 容器这一整套规范叫作 Servlet 规范,而 Servlet 规范使得程序员可以专注业务逻辑的开发,同时 Servlet 规范也给开发者提供了扩展的机制 Filter 和 Listener。

    Filter 和 Listener 的本质区别:
    Filter 是干预过程的,它是过程的一部分,是基于过程行为的。
    Listener 是基于状态的,任何行为改变同一个状态,触发的事件是一致的。

    Tomcat的Wrapper组件-Filter-DispatcherServlet-Controller

    Tomcat&Jetty在启动时给每个Web应用创建一个全局的上下文环境,这个上下文就是ServletContext,其为后面的Spring容器提供宿主环境。
    Tomcat的web.xml配置中,配置了servlet容器的监听器:Spring的ContextLoaderListener,
    Tomcat&Jetty在启动过程中触发容器初始化事件,Spring的ContextLoaderListener会监听到这个事件,它的contextInitialized方法会被调用,在这个方法中,Spring会初始化全局的Spring根容器,这个就是Spring的IoC容器,IoC容器初始化完毕后,Spring将其存储到ServletContext中,便于以后来获取。

    Tomcat&Jetty在启动过程中还会扫描Servlet,
    会扫描到SpringMVC中的DispatcherServlet,这个Servlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个Servlet请求。

    Servlet一般会延迟加载,当第一个请求达到时,Tomcat&Jetty发现DispatcherServlet还没有被实例化,就调用DispatcherServlet的init方法,DispatcherServlet在初始化的时候会建立自己的容器,叫做SpringMVC 容器,用来持有Spring MVC相关的Bean。同时,Spring MVC还会通过ServletContext拿到Spring根容器,并将Spring根容器设为SpringMVC容器的父容器,请注意,Spring MVC容器可以访问父容器中的Bean,但是父容器不能访问子容器的Bean, 也就是说Spring根容器不能访问SpringMVC容器里的Bean。说的通俗点就是,在Controller里可以访问Service对象,但是在Service里不可以访问Controller对象。

    image.png
    https://blog.csdn.net/zhanglf02/article/details/89791797
    <web-app>
      <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
      </listener>
      <!--前端控制器-->
      <servlet>
        <servlet-name>DispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
          <param-name>contextConfigLocation</param-name>
          <param-value>classpath:springMVC.xml</param-value>
        </init-param>
      </servlet>
      <servlet-mapping>
        <servlet-name>DispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
      </servlet-mapping>
    </web-app>
    
    
    public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
        public ContextLoaderListener() {
        }
    
        public ContextLoaderListener(WebApplicationContext context) {
            super(context);
        }
    
        public void contextInitialized(ServletContextEvent event) {
            //初始化spring容器
        this.initWebApplicationContext(event.getServletContext());
        }
    
        public void contextDestroyed(ServletContextEvent event) {
            this.closeWebApplicationContext(event.getServletContext());
            ContextCleanupListener.cleanupAttributes(event.getServletContext());
        }
    }
    

    1.tomcat中配置一个spring的监听器
    2.tomcat启动时为应用创建了ServletContext, contextInitialized事件中,spring容器创建,并存入ServletContext中.
    3.tomcat启动时,扫描到DispatcherServlet这个servlet
    4.有请求来的时候,DispatcherServlet会被创建,并调用init方法,init中创建spring mvc的容器,通过ServletcContext拿到spring容器,两个容器建立关联关系, spring容器作为 spring mvc容器的父容器.spring mvc 能访问spring容器,但是spring容器访问不了spring mvc容器为啥要单向可见?


    spring的线程上下文类加载器。

    1. 如果Spring的jar包放到 common 或 shared 目录下,那么Spring框架的类就是由CommonClassLoader /SharedClassLoader来加载的;也即 Spring容器的当前类加载器是 Common/Shared ClassLoader 。
    2. Spring 要管理每个web应用程序的bean,getBean时要能访问到WebAppClassLoader 加载路径下的应用程序的类,那么在 Spring容器的当前类加载器Common/SharedClassLoader 加载不了并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class;
    3. WebAppClassLoader 能够加载 web应用目录下的类,也能使用 common 或 shared 目录下的类(因为common/shared是WebAppClassLoader的父加载器);所以这种情况下,spring容器只能通过 WebAppClassLoader 来加载用户程序中的类;怎么拿到 webAppClassLoader,并让他来加载类呢?
    4. 通过线程上下文类加载器来获取。tomcat在调用ContextLoaderListener 的contextInitialized方法之前(spring容器初始化之前) 创建web应用WebAppClassLoader 并将其放置到线程上下文类加载器中。

    看初始化spring容器的代码 initWebApplicationContext
    如何用org.springframework.web.context.ContextLoader类 来获取和使用线程上下文类加载器来装载bean

    public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
        try {
            // 创建WebApplicationContext
            if (this.context == null) {
                this.context = createWebApplicationContext(servletContext);
            }
            // 将其保存到该webapp的servletContext中     
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
            // 获取线程上下文类加载器,默认为WebAppClassLoader
            ClassLoader ccl = Thread.currentThread().getContextClassLoader();
            // 如果spring的jar包放在每个webapp自己的目录中
            // 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoader
            if (ccl == ContextLoader.class.getClassLoader()) {
                currentContext = this.context;
            }
            else if (ccl != null) {
                // 如果不同,也就是上面说的那个问题的情况,那么用一个map把刚才创建的WebApplicationContext及对应的WebAppClassLoader存下来
                // 一个webapp对应一个记录,后续调用时直接根据WebAppClassLoader来取出
                currentContextPerThread.put(ccl, this.context);
            }
            
            return this.context;
        }
        catch (RuntimeException ex) {
            logger.error("Context initialization failed", ex);
            throw ex;
        }
        catch (Error err) {
            logger.error("Context initialization failed", err);
            throw err;
        }
    }
    

    创建应用上下文类加载器
    createWebApplicationContext(servletContext);

    protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
       //beanFactory此时来指定ClassLoader,后续获取Bean的时候就用它
       beanFactory.setBeanClassLoader(this.getClassLoader());
    }
    

    this.getClassLoader() 的值是什么 ,代码在org.springframework.core.io.DefaultResourceLoader

        public ClassLoader getClassLoader() {
            return this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader();
        }
    

    继续看 ClassUtils.getDefaultClassLoader()的实现 ,可以发现会返回线程上下文类加载器.

        public static ClassLoader getDefaultClassLoader() {
            ClassLoader cl = null;
          
            try {
                //如果线程上下文类加载不是null 就返回线程上下文类加载器
                cl = Thread.currentThread().getContextClassLoader();
            } catch (Throwable var3) {
            }
    
            if (cl == null) {
                //如果线程上下文类加载器是null 就用 当前类加载器
                cl = ClassUtils.class.getClassLoader();
                if (cl == null) {
                    try {
                        //兜底用 系统类加载器.
                        cl = ClassLoader.getSystemClassLoader();
                    } catch (Throwable var2) {
                    }
                }
            }
    
            return cl;
        }
    

    Spring的getBean方法

    1. org.springframework.beans.factory.support.AbstractBeanFactory#doResolveBeanClass 中 获取类加载器,就是线程上下文类加载器 tccl
    2. org.springframework.beans.factory.support.AbstractBeanDefinition#resolveBeanClass
      Class<?> beanClass = Class.forName(类名,false,线程上下文类加载器);

    3.拿到类型进行实例化.

    当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
    当使用本类 托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。如Spring.

    ————————————————
    真正理解线程上下文类加载器(多案例分析)
    https://blog.csdn.net/yangcheng33/article/details/52631940

    相关文章

      网友评论

          本文标题:Servlet规范和Servlet容器

          本文链接:https://www.haomeiwen.com/subject/wwbcbctx.html