一、传统Web应用
想从无到有自己搭建一个web服务器?
-- 基本不可能
这是一个造轮子的过程,而且难度还相当大。日常所说的实现Web应用服务,其实就是利用现有的成熟轮子(web容器,如熟知的tomcat等),再遵循Servlet规范,构建三大组件(Servlet, Filter, Listener)以对外提供自定义HTTP服务的过程。
1)web服务启动,main()方法在哪里?
我们在编写web应用时,是不会编写main方法的。众所周知,java的启动入口是main()方法,web服务启动时,其实是启动web容器的main方法,tomcat容器的启动入口在 org.apache.catalina.startup.Bootstrap#main
。一个web服务变得可用的过程,不称做“启动”,而叫“部署”,部署到web容器。
2)传统web服务部署
传统的web应用服务(有web.xml
),按照servlet规范,web容器在启动过程中,会在约定的目录结构中,寻找web.xml
文件,来初始化三大组件。这个约定的目录结构是:
root
|-[META-INF]
|-WEB-INF
|-classes
|-lib
|-web.xml
页面,资源等可以放在根目录下,或者根目录下的自定义目录中。
web应用的部署过程,就是把对应的资源文件,按照上述servlet规范约定的目录一一放置正确,然后web容器启动的过程。
部署约定详细说明参考 Apache Tomcat 7 Deployment
3)ServletContext: Servlet, Filter, Listener
ServletContext
定义了一个方法集合,Servlet
使用这些方法与servlet容器交互,例如获取文件的MIME类型,分发请求或者写日志。一个JVM中的web应用有且仅有一个servletConext。
典型的web.xml配置,会定义处理请求的servlet,过滤请求的过滤器Filter以及监听各种变化的监听器Listener。web容器启动时,通过分析web.xml文件,这些定义的组件都会加入到servletContext中,以提供功能完备的HTTP服务。
所以如果忽略业务细节,web应用的开发过程,简单的说就是将这几类组件加入到servletContext,配置servletContext的过程。
二、编程式Web应用
Servlet3.0以前,配置servletContext通过web.xml。Servlet3.0开始,提供一种编程式的机制,允许第三方库侦测到web容器启动阶段,并且开展一些必要servlet, filter, listener的编程式注册操作(参考 Servlet3.0规范#8.2.4 共享库 / 运行时可插拔性)。
利用这种机制,第三方组件如Spring MVC,可以以编程的方式配置servletContext。
1)Servlet3.0 ServletContainerInitializer
具体到实现,Servlet3.0提供了接口javax.servlet.ServletContainerInitializer
,如下:
ServletContainerInitializer{
onStartup(Set<Class<?>>, ServletContext):void
}
第三方组件实现这个接口,其onStartup()方法将会在容器启动阶段被容器调用。这样,第三方组件得以获取容器的ServletContext,编程式的往ServletContext中加入需要的组件,配置ServletContext。流程很简单,如下:
编程式servletContext配置2)ServletContainerInitializer 接口实现发现
那么,第三方组件的ServletContainerInitializer实现如何被发现?
JAVA SPI 服务发现
SPI全称Service Provider Interface。不经常出现在日常编码中,因为这是针对厂商或者第三方插件提供的一种服务发现机制。JDK中java.util.ServiceLoader
类对此有详细的介绍。
SPI是松耦合以及可插拔原则的实践。相同的功能,往往有不同的实现方案,如日志功能,有log4j, log4j2, logback等实现。良好的面向对象设计经验,是不依赖具体的实现,而依赖他们共同的接口,即基于接口编程。如果依赖了具体实现,那么当需要更换到另一种实现时(如log4j更换到log4j2),就不得不修改每一处依赖代码;而基于接口的编程,只要将具体实现替换即可。这是使用日志时推荐基于使用Facade模式的common-logging
或者slf4j
的原因。因此需要一种接口实现的发现机制,SPI就是这样一种接口实现发现机制,与IOC思想相似,将依赖的控制权转移出调用者。
SPI规定,使用方依赖接口;第三方组件提供接口的实现,并在jar包的/META-INF/services/目录下创建一个与接口全限定名称相同的文件,文件内容是接口实现的全限定名称。
调用方使用第三方组件时,在类路径下遍历所有的/META-INF/services/,寻找符合接口的具体实现,通过反射实例化实现,完成接口实现的发现与注入。
ServletContainerInitializer 实现发现
根据Servlet3.0规范,ServletContainerInitializer 接口的实现将被运行时的服务查找机制或语义上与它等价的容器特定机制发现。 从设计角度来看,这是一种松耦合的优秀设计思想,容器不依赖具体组件的ServletContainerInitializer 实现。
ServletContainerInitializer 的实现在 META-INF/services 目录下创建javax.servlet.ServletContainerInitializer
文件,并写入实现的全限定类名。web容器使用SPI或者等价的发现机制发现实现类。
2)javax.servlet.annotation.HandlesTypes注解
ServletContainerInitializer 的onStartup(Set<Class<?>>, ServletContext):void
方法有两个参数,其中第二个是由容器传递ServletContext,第一个Class<?>
集合是注解HandlesTypes
指定的,ServletContainerInitializer “感兴趣”的类型。
注解如下:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface HandlesTypes {
Class<?>[] value();
}
web容器必须将以下类的类对象,组织成集合,传递给onStartup()方法:
- 继承@HandlesTypes指定类的子类(包括抽象类)
- 实现@HandlesTypes指定接口的子类(含抽象类),继承@HandlesTypes指定接口的子接口
- 被@HandlesTypes指定注解修饰的类/接口
利用这个特性,ServletContainerInitializer 的实现可以自定义其servletContext流程。
三、编程式ServletContext配置实践
有了以上理论知识,是时候实践一下了。
1)ServletContainerInitializer的自定义实现
@HandlesTypes(MyHandlesTypes.class)
public class CustomizedServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
System.out.println(c);
}
}
发现
src/main/resource下的 META-INF/services/目录中新建文件javax.servlet.ServletContainerInitializer
,内容写入:demo.spring.mvc.framework.my.servlet.CustomizedServletContainerInitializer
2)HandlesTypes-感兴趣的类
这里,感兴趣的类是一个注解类型:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyHandlesTypes {
}
凡是被该注解修饰的类,都会出现在onStartup()的参数集合中
3)启动
按照约定的目录,将应用部署到tomcat容器,并以调试模式启动tomcat容器。可以看到:
自定义web启动此处可以配置servlet,filter,listener并且加入到servletConetxt即以编程的方式可完成的配置。
四、Spring MVC的编程式实现
Spring MVC也提供了编程方式配置servletContext。
1)SpringServletContainerInitializer
查看spring web包,SPI下的javax.servlet.ServletContainerInitializer
文件在约定目录下。
其中内容为:
org.springframework.web.SpringServletContainerInitializer
这个类就是javax.servlet.ServletContainerInitializer
的实现,web容器启动时将会调用其onStartup()方法。
2)WebApplicationInitializer
打开SpringServletContainerInitializer的源码,其感兴趣的类用是:
@HandlesTypes(WebApplicationInitializer.class)
SpringServletContainerInitializer的onStartup实现中:
- 遍历集合
Set<Class<?>>
,其中非接口,非抽象类,并且继承自WebApplicationInitializer
的类,使用反射实例化,并加入一个WebApplicationInitializer的集合; - 使用基于
org.springframework.core.annotation.Order
的排序工具,将上述集合排序 - 遍历上述集合,逐一调用其
void onStartup(ServletContext)
方法
由此可知,在Spring MVC中,只要实现了接口org.springframework.web.WebApplicationInitializer
,就可以以编程的方式,配置servletContext了。
因此编程式Spring MVC的入口是自定义的WebApplicationInitializer
接口实现的onStartup()方法,可以在其中完成Spring容器的初始化等基本操作。
3)实例
以下是一个基于Spring MVC,编程式配置ServletContext的实例:
public class WebInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext();
ctx.register(MyMvcConfig.class); // 基于注解的spring配置
ctx.setServletContext(servletContext);
ServletRegistration.Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(ctx));
servlet.addMapping("/");
servlet.setLoadOnStartup(1);
}
}
四、总结
得益于Servlet3.0的ServletContainerInitializer
机制,第三方组件可使用编程的方式配置ServletContext。
编程式配置步骤如下:
- 实现ServletContainerInitializer接口,在onStartup方法中配置servletContext,加入servlet, filter, listener组件;
- 基于容器ServletContainerInitializer实现发现机制,在/META-INF/services/下新建文件;
javax.servlet.ServletContainerInitializer
并写入具体实现的全限定类名; - 按照约定的部署目录,部署web应用,启动web容器即可。
使用Spring MVC方式则更简单,省略了服务发现的配置,步骤如下:
- 实现接口WebApplicationInitializer,在onStartup方法中配置servletContext,加入servlet, filter, listener组件;
- 按照约定的部署目录,部署web应用,启动web容器即可。
Talk is cheap. Show you the code
源码地址:
code_based_spring_mvc
REFER TO
- Servlet3.0研究之ServletContainerInitializer接口
- 一个基于注解配置的Web项目的启动流程分析
- JAVA SPI
- Servlet3.0 - ServletContainerInitializer注册JAVA组件
- Servlet3.0规范#8.2.4章节: 共享库 / 运行时可插拔性
网友评论