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 需要 如下
- 需要定义一个接口
- 须在 jar 包的
META-INF/services
目录下新建一个全限定接口名的文件,在文件中将具体的实现类 全限定类名写入 - 调用方 通过
ServiceLoader.load(Interface.class)
来加载 对应接口的实现类
基于上,修改代码,代码结构图如下测试 使用 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
给 应用添加 servlet
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
的这个方法里面主要 就是 找到 "META-INF/services/" 目录下 这个接口 对应的所有 实现类 然后通过反射实例化 ,最终 会用一个 initializerClassMap.put(sci, new HashSet<Class<?>>());
存起来然后 会遍历 这个 initializerClassMap 并调用 context.addServletContainerInitializer
方法 这个 context 其实是 tomcat Context 的标准实现 org.apache.catalina.core.StandardContext#addServletContainerInitializer可以看到 将其 放入到了
private Map<ServletContainerInitializer,Set<Class<?>>> initializers = new LinkedHashMap<>();
这个 map 中,再之后就是 tomcat 启动时 会根据生命周期 调用到 org.apache.catalina.core.StandardContext#startInternal
方法,在这个方法中 有以下代码可以看到 遍历了 initializers
并执行了 onStartup()
方法, 这个方法 也即是 javax.servlet.ServletContainerInitializer#onStartup
方法
2. Spring5.x 与 Servlet 的整合
2.1 一个扩展点
从上面 servlet spi 机制 我们知道 如果某个类 实现了 javax.servlet.ServletContainerInitializer
接口后,tomcat 会回调 这个接口所有实现类的 onStartup
方法,这个方法有两个参数,代码如下第一参数 是一个 Set 的集合 ,第二个参数 是 ServletContext
即 代表这个 web 应用。这里我们需要知道 第一个参数是怎么来的,这里是一个扩展点。从 servlet3.x 规范 可以知道 在我们继承 javax.servlet.ServletContainerInitializer
本身的这个类上面 可以添加 @HandlesTypes 这个注解,这个注解上可以标注多个 class, 那么 tomcat 在 执行 到 onStartup
方法时 会将 @HandlesTypes 中的类的所有实现类 传给 这个方法的第一个参数 也即 Set 的集合,这样我们可以 这些 Class 来进行 我们 想要做的事情。
2.2 spring 与 servlet 如何整合?
首先我们 找到 spring-web
这个 spring framework
的子项目是不是很熟悉? 这里就是一个切入口。我想你已经明白了 ,当 tomcat 启动的时候 会去 加载 javax.servlet.ServletContainerInitializer
这个接口的实现类,在图中可以看到 这个实现类是 org.springframework.web.SpringServletContainerInitializer
也就是说 只要我们把这个类的作用搞清楚了 就基本理解 spring(web) 是怎么与 servlet 整合的了。
打开这个类可以看到 这个类主要的作用 就是 拿到 WebApplicationInitializer
接口的所有实现类 (由上面的判断知过滤了 接口和抽象类) 然后 放入到 initializers
这个 list 中,最后遍历这个 initializers
调用 org.springframework.web.WebApplicationInitializer#onStartup
方法。 注意这里是 WebApplicationInitializer
的 onStartup
方法。也就是说 我们只要弄懂了 这个 onStrartup 方法也就弄懂了 怎么整合的了。到这里了 我们应该怎么继续看呢。答案是 找 这个 onStartup 的 的实现类,并且是 最底层的实现类 ,因为会调用到最后的那个实现类啊,先看一眼这个 接口的实现类 混个眼熟我们找到 onStartup 的最后的那个实现类 AbstractDispatcherServletInitializer
直接在这里打一个断点(因为我们知道肯顶会调用到这来的)在这里可以看到 总共调用了 2 个方法 一个是父类的 onStartup
方法,一个当前类 的 registerDispatcherServlet
.需要着重看这两个方法了。
2.2.1 super.onStartup(servletContext)
这个方法代码如下
主要就是干了 3 件事
- 创建 RootApplicationContext()
createRootApplicationContext();这里就是创建一个 AnnotationConfigWebApplicationContext 。注意 getRootConfigClasses() 是一个空方法 ,需要我们提供一个 配置类( @Configuration 标注的类)。如果 我们没有提供配置类 则这里直接返回 null ,也就是没有创建 rootApplicationContext,不会走后面的逻辑
- 创建 ContextLoaderListener
ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);这里注意 如果 配置 rootApplicationContext != null 时才会创建 ContextLoaderListener 继承了 ContextLoader 实现了 ServletContextListener 并且可以知道 new ContextLoaderListener(rootAppContext) 是调用了父类(ContextLoader )的 有参构造方法 ,主要是在 ContextLoader 中保存了一份 rootApplicationContext
- 向 servletContext 中添加 listener
servletContext.addListener(listener);只有执行了这一句 tomcat 在启动的时候 才会执行 contextInitialized(ServletContextEvent sce) 方法
再来看下 contextInitialized(ServletContextEvent sce) 方法,最中主要是调用到 父类 ContextLoader 的 initWebApplicationContext 方法主要可以看到 会执行 上面框中的两行代码 其中 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); 这里是给 servletContext 设置了一个属性,key 为value 为 那个 rootApplicationContext
configureAndRefreshWebApplicationContext 为如下
2.2.2 this.registerDispatcherServlet(servletContext);
大致看一眼,你就会发现 这和上面的 那个 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();
这里直接就创建了 AnnotationConfigWebApplicationContext 然后 如果有配置类 添加 后返回,和上面创建 RootApplicationContext (有配置类才创建)时不同
- this.createDispatcherServlet(servletAppContext);
这个里面主要是 创建了一个 DispatcherServlet。
创建 DispatcherServlet(webApplicationContext) 有必要先看下 这个类的继承图
首先 new DispatcherServlet(webApplicationContext) 是调用的有参构造函数,这里直接是直接将 创建的 AnnotationConfigWebApplicationContext 赋值给 org.springframework.web.servlet.FrameworkServlet#webApplicationContext 的属性
前面设置了 loadOnStartup=1 则 tomcat 容器启动时 会调用到 servlet 的 init 方法,这里可以直接找到 init 方法的最后的继承类 HttpServletBean 这里直接调用了 initServetBean 在 FrameworkServlet 中实现了改方法,在 initServletBean 方法中 直接执行了 initWebApplicationContext() 方法 initWebApplicationContext
可以看到 是将创建的 webApplicationContext 进行初始化(refresh),设置其父容器(如果有),并设置 servletContext/servletConfig
对于 springmvc 的初始化这里并不是走到上图中下面的方法中执行的,而是使用了 spring 的事件来初始化的,具体实现如下如果对 spring 的中的事件了解的话,这里通过实现 ApplicationListener 重写 onApplicationEvent 来调用 onFresh() 的 在 子容器(webApplicationContext)刷新的时候就完成了
2.2.3 问题
- ContextLoaderListener 中的 contextInitialized 是如何被调用的?
这是 事件驱动设计模式 Listener 是 ServletContextListener, 事件源是 ServletContext, 事件对象是 ServletContextEvent 代码中执行了 servletContext.addListener(listener);
是 向事件源 ServletContext 中添加了 ServletContextListener 监听器 ,容器在启动时候 调用了 org.apache.catalina.core.StandardContext#listenerStart 方法在这个方法中 获取了所有的 listeners 并执行相应的方法
- 子容器 初始化 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>
看一眼了,你可能知道大致是怎么弄了的吧。。。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于