Java Servlet 理解

本贴最后更新于 1284 天前,其中的信息可能已经渤澥桑田

1. Servlet 的 3 中使用方法

1.1 xml 用法

使用 xml 主要是将写好的 servlet 在 web.xml 文件中配置映射路径 servlet 代码如下

public class XmlServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("xmlServlet");
    }
}

web.xml 文件中如下

    <servlet>
        <servlet-name>xmlServlet</servlet-name>
        <servlet-class>com.linn.slarn.servlet.XmlServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>xmlServlet</servlet-name>
        <url-pattern>/xmlServlet</url-pattern>
    </servlet-mapping>

1.2 注解用法

注解的用法更直接了然,省去了在 web.xml 的配置

@WebServlet("/annotationServlet")
public class AnnotationServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("annotationServlet");
    }
}

1.3 SPI 机制用法

SPI 即 Service Provider Interface ,是一种将服务接口和服务实现分离达到解耦,灵活扩展的技术。

1.3.1 SPI 简单用法

举个例子,假如说 我现在要解析文档,文档有多种类型,比如 Excel,Word 等,大致大致代码如下:

public interface ParseDoc {
    /**
     * 解析文档
     **/
    void parse();
}
public class WordParse implements ParseDoc {
    @Override
    public void parse() {
        System.out.println("解析Word");
    }
}
public class ExcelParse implements ParseDoc {
    @Override
    public void parse() {
        System.out.println("解析Excel");
    }
}

可以看到 ParseDoc 接口中定义了 parse() 方法用于解析 文档,同时 WordParse  和 ExcelParse 为两个实现类,那么要解析不同的文档我们可以这样做

    public static void main(String[] args) {
        WordParse wordParse = new WordParse();
        wordParse.parse();

        ExcelParse excelParse = new ExcelParse();
        excelParse.parse();
    }

可以看到需要解析什么文档,只需要 new  出具体的 对象 在调用 parse()  方法即可。但是也可以知道,要解析不同的文档,每次都要修改源代码,这是极其不方便的,那么有没有一种方式可以直接配置就能解决呢,来看看 spi 的方式
使用 spi 需要 如下

  1. 需要定义一个接口
  2. 须在 jar 包的 META-INF/services 目录下新建一个全限定接口名的文件,在文件中将具体的实现类 全限定类名写入
  3. 调用方 通过 ServiceLoader.load(Interface.class) 来加载 对应接口的实现类

基于上,修改代码,代码结构图如下image.png测试 使用 ServiceLoader

public class Test {

    public static void main(String[] args) {
        ServiceLoader<ParseDoc> parseDocs = ServiceLoader.load(ParseDoc.class);
        for (ParseDoc parseDoc : parseDocs) {
            parseDoc.parse();
        }
    }
}

这样 如果想要 修改,则直接在 相应的文件中添加 实现类 全类名 就可以了

1.3.2 使用 SPI 添加 Servlet

首先 编写一个 Servlet 类

public class SpiServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("spiServlet");
    }
}

根据 servlet3.1 规范可知,实现了 javax.servlet.ServletContainerInitializer 接口的实现类 ,可以重写 这个接口的 onStartup 方法,在这个方法中可以 获取的 ServletContext 对象,可以通过 ServletContext 给 应用添加 servletimage.png

public class SpiServletInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> set, ServletContext servletContext) throws ServletException {
        ServletRegistration.Dynamic spiServlet = servletContext.addServlet("spiServlet", SpiServlet.class);
        spiServlet.addMapping("/spiServlet");
        spiServlet.setLoadOnStartup(1);
    }
}

这样 的话 通过,启动 tomcat , 访问 http://ip:port/xxx/spiServlet 也可以放访问到 servlet。

启动 tomcat 的时候为什么会调用到这里 ?

其实也是一样的。 在 tomcat 中 也有一个 类似 ServiceLoader.load(Interface.class) 的方式 获取到 javax.servlet.ServletContainerInitializer 的所有实现类 并执行 他的 onStartup 方法 。具体代码在如下位置:tomcat 的源码 org.apache.catalina.startup.ContextConfig#processServletContainerInitializers 的这个方法里面image.png主要 就是 找到 "META-INF/services/" 目录下 这个接口 对应的所有 实现类 然后通过反射实例化 ,最终 会用一个 initializerClassMap.put(sci, new HashSet<Class<?>>()); 存起来然后 会遍历 这个 initializerClassMap 并调用 context.addServletContainerInitializer  方法 image.png这个 context 其实是 tomcat Context 的标准实现 org.apache.catalina.core.StandardContext#addServletContainerInitializerimage.png可以看到 将其 放入到了

private Map<ServletContainerInitializer,Set<Class<?>>> initializers = new LinkedHashMap<>(); 

这个 map 中,再之后就是 tomcat 启动时 会根据生命周期 调用到 org.apache.catalina.core.StandardContext#startInternal  方法,在这个方法中 有以下代码image.png可以看到 遍历了 initializers  并执行了 onStartup()  方法, 这个方法 也即是 javax.servlet.ServletContainerInitializer#onStartup 方法

2. Spring5.x 与 Servlet 的整合

2.1 一个扩展点

从上面 servlet spi 机制 我们知道 如果某个类 实现了    javax.servlet.ServletContainerInitializer 接口后,tomcat 会回调 这个接口所有实现类的 onStartup 方法,这个方法有两个参数,代码如下image.png第一参数 是一个 Set 的集合 ,第二个参数 是 ServletContext 即 代表这个 web 应用。这里我们需要知道 第一个参数是怎么来的,这里是一个扩展点。从 servlet3.x 规范 可以知道 在我们继承 javax.servlet.ServletContainerInitializer 本身的这个类上面 可以添加 @HandlesTypes 这个注解,这个注解上可以标注多个 class, 那么 tomcat 在 执行 到 onStartup 方法时 会将 @HandlesTypes 中的类的所有实现类 传给 这个方法的第一个参数 也即 Set 的集合,这样我们可以 这些 Class 来进行 我们 想要做的事情。

2.2 spring 与 servlet 如何整合?

首先我们 找到 spring-web  这个 spring framework 的子项目image.png是不是很熟悉? 这里就是一个切入口。我想你已经明白了 ,当 tomcat 启动的时候 会去 加载 javax.servlet.ServletContainerInitializer 这个接口的实现类,在图中可以看到 这个实现类是 org.springframework.web.SpringServletContainerInitializer 也就是说 只要我们把这个类的作用搞清楚了 就基本理解 spring(web) 是怎么与 servlet 整合的了。
打开这个类image.png可以看到 这个类主要的作用 就是 拿到 WebApplicationInitializer 接口的所有实现类 (由上面的判断知过滤了 接口和抽象类) 然后 放入到 initializers 这个 list 中,最后遍历这个 initializers 调用 org.springframework.web.WebApplicationInitializer#onStartup 方法。 注意这里是 WebApplicationInitializer  的 onStartup  方法。也就是说 我们只要弄懂了 这个 onStrartup 方法也就弄懂了 怎么整合的了。image.png到这里了 我们应该怎么继续看呢。答案是 找 这个 onStartup 的 的实现类,并且是 最底层的实现类 ,因为会调用到最后的那个实现类啊,先看一眼这个 接口的实现类 混个眼熟image.png我们找到 onStartup 的最后的那个实现类 AbstractDispatcherServletInitializer image.png直接在这里打一个断点(因为我们知道肯顶会调用到这来的)在这里可以看到 总共调用了 2 个方法 一个是父类的 onStartup  方法,一个当前类 的 registerDispatcherServlet .需要着重看这两个方法了。

2.2.1 super.onStartup(servletContext)

这个方法代码如下 image.png

主要就是干了 3 件事

  • 创建 RootApplicationContext()

createRootApplicationContext();image.png这里就是创建一个 AnnotationConfigWebApplicationContext 。注意 getRootConfigClasses() 是一个空方法 ,需要我们提供一个 配置类( @Configuration 标注的类)。如果 我们没有提供配置类 则这里直接返回 null ,也就是没有创建 rootApplicationContext,不会走后面的逻辑

  • 创建 ContextLoaderListener

ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);这里注意 如果 配置 rootApplicationContext != null 时才会创建    image.pngContextLoaderListener 继承了 ContextLoader 实现了 ServletContextListener 并且可以知道 new ContextLoaderListener(rootAppContext) 是调用了父类(ContextLoader )的 有参构造方法 ,主要是在 ContextLoader 中保存了一份 rootApplicationContextimage.png    

  • 向 servletContext 中添加 listener

servletContext.addListener(listener);只有执行了这一句 tomcat 在启动的时候 才会执行 contextInitialized(ServletContextEvent sce) 方法
再来看下 contextInitialized(ServletContextEvent sce) 方法,最中主要是调用到 父类 ContextLoader 的 initWebApplicationContext 方法image.png主要可以看到 会执行 上面框中的两行代码 其中 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); 这里是给 servletContext 设置了一个属性,key 为image.pngvalue 为 那个 rootApplicationContext
configureAndRefreshWebApplicationContext 为如下image.png

2.2.2 this.registerDispatcherServlet(servletContext);

image.png大致看一眼,你就会发现 这和上面的 那个 SpiServlet 有点类似有没有

FrameworkServlet dispatcherServlet = this.createDispatcherServlet(servletAppContext);
Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
registration.setLoadOnStartup(1);
registration.addMapping(this.getServletMappings());

springmvc 也就是向 tomcat 中 添加了一个 servlet,并且设置了 loadOnStartup=1(tomcat 启动时 会执行 init 方法) 并且添加了映射(访问这个设置的路径后会被 这个 Servlet 处理)
这里主要看下面两个方法

  • this.createServletApplicationContext();

image.png这里直接就创建了 AnnotationConfigWebApplicationContext 然后 如果有配置类 添加 后返回,和上面创建 RootApplicationContext (有配置类才创建)时不同

  • this.createDispatcherServlet(servletAppContext);

这个里面主要是 创建了一个 DispatcherServlet。
创建 DispatcherServlet(webApplicationContext) 有必要先看下 这个类的继承图 image.png

首先 new DispatcherServlet(webApplicationContext) 是调用的有参构造函数,这里直接是直接将 创建的 AnnotationConfigWebApplicationContext 赋值给 org.springframework.web.servlet.FrameworkServlet#webApplicationContext 的属性
前面设置了 loadOnStartup=1 则 tomcat 容器启动时 会调用到 servlet 的 init 方法,这里可以直接找到 init 方法的最后的继承类 HttpServletBean image.png这里直接调用了 initServetBean 在 FrameworkServlet 中实现了改方法,在 initServletBean 方法中 直接执行了 initWebApplicationContext() 方法 initWebApplicationContextimage.png

image.png可以看到 是将创建的 webApplicationContext 进行初始化(refresh),设置其父容器(如果有),并设置 servletContext/servletConfig

对于 springmvc 的初始化这里并不是走到上图中下面的方法中执行的,而是使用了 spring 的事件来初始化的,具体实现如下image.pngimage.pngimage.png如果对 spring 的中的事件了解的话,这里通过实现 ApplicationListener 重写 onApplicationEvent 来调用 onFresh() 的 在 子容器(webApplicationContext)刷新的时候就完成了

2.2.3 问题

  1. ContextLoaderListener 中的 contextInitialized 是如何被调用的?

这是 事件驱动设计模式 Listener 是 ServletContextListener, 事件源是 ServletContext, 事件对象是 ServletContextEvent 代码中执行了 servletContext.addListener(listener);  是 向事件源 ServletContext 中添加了 ServletContextListener 监听器 ,容器在启动时候 调用了 org.apache.catalina.core.StandardContext#listenerStart 方法在这个方法中 获取了所有的 listeners 并执行相应的方法image.png

  1. 子容器 初始化 mvc 时 为什么不直接调用 onFresh,而要通过使用 spring 的事件来调用?
    ...

从代码中可以发现 一个 Servlet 其实和一个 ConfigurableWebApplicationContext(springweb 容器) 对应,也就是说 new 多个 DispatchServlet(context) 其实是和 这个 context 绑定着,多个 servlet/context 彼此可以隔离,并且可以有共同的 父 WebApplicationContext。父容器可以做公共的一部分功能,比如扫描 dao,service 包,整合 mybatis,redis,mq 等第三方框架

2.1 传统 xml 配置整合 spring-mvc

看一下传统整合 springmvc 的一个配置文件 web.xml

<!-- 配置监听器 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
    </context-param>
    <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:spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
        <!-- 是否使用异步处理(servlet3.0新特新),可以提高并发能力 -->
        <async-supported>true</async-supported>
    </servlet>

看一眼了,你可能知道大致是怎么弄了的吧。。。

  • Servlet
    21 引用 • 29 回帖
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3168 引用 • 8207 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...