简述
这篇笔记主要记录一些比较重要,但是上一篇又没有提到的内容。其中,Spring MVC对Servlet 3.0的实现是重中之重。
JAVA SPI
动态替换服务实现的机制,目前Dubbo就是基于SPI提供扩展机制。
Demo
写一个小demo感受一下:
- bad-printer:某家厂商
- good-printer:另一家厂商
- interface:规范制定者
- invoker:main方法入口
interface
一个只包含一个Java类文件的项目,制定了一个接口:
1 | package com.joy.api; |
bad-printer
pom.xml:
1 | <dependencies> |
实现interface里的接口:
1 | public class BadPrinter implements Printer { |
同时,在resources/
下创建META-INF/services
文件夹,将interface里接口的完整包名+类名的路径作为文件名com.joy.api.Printer
,文件内容是实现类完整包名+类名:
1 | com.joy.service.BadPrinter |
good-printer
操作同上。
invoker
pom.xml:
1 | <dependency> |
main:
1 | public class MainApp { |
结果
依赖为bad-printer时,控制台输出:
1 | I am a bad man |
不改变代码,将依赖替换为good-printer,执行main方法:
1 | I am a good man |
这也是设计模式总纲-开闭原则的一种实现。
总结
在经典的日志框架jcl-over-slf4j-xxxx.jar
中,就有SPI的运用:
1 | META-INF\services\org.apache.commons.logging.LogFactory: |
来到最新的DriverManager
,有这么一个静态代码块:
1 | /** |
这是一个根据jdbc.drivers
这个系统参数来加载驱动的方法,接着往下走,重头戏来了:
1 | ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); |
注释就说了:如果有Driver类通过SPI的方式实现,就加载它。
再结合类的生命周期:加载--->连接(验证-准备-解析)--->初始化--->使用--->卸载
,我们不难猜到,下一步就是要准备初始化了:
1 | for (String aDriver : driversList) { |
这就变回了我们当初刚开始学Java+数据库时,手动使用Class.forName("com.mysql.jdbc.Driver")
的方式加载驱动并初始化。
再到一个jdbc实现类中看一眼,以mysql的为例,最新的mysql驱动为:com.mysql.cj.jdbc.Driver
,有一个静态代码块:
1 | static { |
可以看到,它会向java的驱动管理类注册自身,同时也保证了旧的 Class.forName()
的正常使用。
顺带提一句,即使配置了原来的类名com.mysql.jdbc.Driver
,也是可以使用的,打开类看一下:
1 | public class Driver extends com.mysql.cj.jdbc.Driver { |
但是会提示这个类被废弃了。
Servlet 3.0
3.0以前的Filter、Listener、Servlet:在web.xml
中一一配置
3.0以后:提供了@WebServlet
、@WebListener
等注解
同时提供了:
ServletRegistration.Dynamic addServlet(String servletName,Class<? extends Servlet> servletClass)
ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)
ServletRegistration.Dynamic addServlet(String servletName, String className)
T createServlet(Class clazz)
ServletRegistration getServletRegistration(String servletName)
Map<String,? extends ServletRegistration> getServletRegistrations()
:增加动态映射
等方法,这些方法,在ServletContextListener
的#contextInitialized()
方法中调用,或者ServletContainerInitializer
的#onStartup
方法中调用,ServletcontainerInitializer
也是3.0新增的一个类,容器启动时会通过这个方法处理WEB/INF/lib
下的jar包,使用注解@HandlersTypes
可以过滤需要处理的类型。
最后,在我们自己实现的ServletcontainerInitializer
的项目路径下创建META-INF/services/javax.servlet.ServletContainerInitializer
,即可实现彻底摆脱web.xml而创建servlet了。
Spring中的Servlet 3.0
1 | org.springframework.web.SpringServletContainerInitializer |
其子类继承结构图如下:
AbstractContextLoaderInitializer
:创建Root wac,添加contextLoadListenerAbstractDispatcherServletInitializer
:创建Servlet wac,注册DispactcherServlet
1 | public void onStartup(ServletContext servletContext) throws ServletException { |
而在spring-web的META-INF/services/javax.servlet.ServletContainerInitializer
中,就是指定了SPI的实现类。
Spring Boot 中的Servlet 3.0
Spring Boot依旧支持上一节中提到的几个注解,但是需要使用@ServletComponentScan
来支持。也可以通过声明bean的方式来指定:
1 |
|
打开FilterRegistrationBean
看一眼继承结构(spring boot 2.1.0.RELEASE):
这几个类都是RegistrationBean
的子类,其核心就是对ServletContextInitializer
的实现了。
我们都知道,spring boot既可以使用Jar包来运行,也可以打成war包使用外部容器来运行,而ServletContainerInitializer是在当使用war包在外部运行时的策略,在Jar包中,这种算法策略会出现问题。所以,spring boot使用了自己的一个org.springframework.boot.web.embedded.tomcat.TomcatStarter
来初始化,虽然,它继承了ServletContainerInitializer
,但是,并没有在META-INF/services
里创建文件。
看一眼Spring Boot使用内置Tomcat启动的一个调用栈(部分):
在图一,可以很明显看到,它创建了一个TomcatServletWebFactory
,来看看是怎么创建的:
1 | protected ServletWebServerFactory getWebServerFactory() { |
可以看到这里获取了BeanFactory
,并且通过类型获得了beanname数组,如果有多个,只会按照数组的第一个返回实际的Bean,而ServletWebServerFactory
的实现类,可以通过类图看到:
获得TomcatServletWebFactory
以后,又调用了#getWebServer()
方法,进入一看,方法比较简单,new了Tomcat、Connector,配置connector、engine、host等,然后开始#prepareContext(),这个方法还没细看,应该都是设置一些server.xml
中,<Host>
标签所涉及到的一些配置,继续往下,重点来了,#configureContext(context, initializersToUse)
这个方法中,有如下代码:
1 | ...... |
手动new了org.springframework.boot.web.embedded.tomcat.TomcatStarter
类,然后放到了上下文中。
回到#getWebServer()
方法,这个方法还传入了一个参数,开起来是一个lambda表达式的极度简写,返回了一个ServletContextInitializer
的匿名实现类,该实现类采用了方法引用的形式,引用的方法就是下面的方法:
1 | private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() { |
这种异步回调的形式,会产生什么结果呢?
在该方法最后一行是getTomcatWebServer(tomcat);
,这个方法会传入上面new的Tomcat对象,同时,如果端口号>0,就会自动启动。
剩下的步骤在上图debug2中就简单明了了,进入到Tomcat的启动周期了:
tomcat.start()---> LifecycleBase #start()--->StandardContext #startInternal()
这里的StandardContext
实际上就是#prepareContext()
方法中new Tomcat以后创建的上下文。
在StandardContext
的startInternal()
方法中,有如下代码:
1 | ...... |
没错,注释都告诉了我们调用ServletContainerInitializers
的onStartup()
方法,而上文也说了,这个ServletContainerInitializers
就是TomcatStarter
,最终执行的就是下面这个代码,从而也就就执行了匿名类中的#selfInitialize()
:
1 | try { |
这个方法,实际上就是执行spring boot中那些自定义的RegistrationBean
的#onStartup()
方法,利用上文提到的Servlet 3.0动态添加Servlet的特性,动态创建Servlet,彻底摆脱了web.xml
。
1 |
|
spring boot没有完全遵守Servlet 3.0,没有使用SPI,但是在灵活性和可扩展性上更胜一筹,这就是spring boot的终极魅力所在啊!
Spring Aware
这本来应该是Spring中的内容,但是由于Spring MVC略有涉及,因此小记一下。
What?Spring中的Aware本质是一个标记interface,也是容器的核心接口之一,实现了该接口的bean具有被Spring容器通知的能力;
How?采用回调的方式通知,该接口是一个空接口,实际方法签名由子接口确定,并且该接口通常只有一个接受但参数的set方法,方法命名为set+去掉Aware后缀的接口名;
When?
在AbstractAutowireCapableBeanFactory.java
中,可以略见一二:
1 | private void invokeAwareMethods(final String beanName, final Object bean) { |
Spring提供了大量Aware接口,但是先来个小例子感受一下:
普通maven项目,首先在resources目录下创建一个空的spring.xml
文件(idea 专业版可以自动生成),然后代码如下:
1 | public class MyApplicationAware implements BeanNameAware, BeanFactoryAware, BeanClassLoaderAware, ApplicationContextAware { |
如果main换成以下:
1 | public static void main(String[] args) { |
原因:通过BeanFactory.getBean()
获取bean时,只检测了BeanNameAware, BeanFactoryAware, BeanClassLoaderAware
三个接口的实现(参见上文)。但是Root wac会检测所有实现的子Aware。
部分常用的Aware如下:
LoadTimeWeaverAware
:加载Spring Bean时织入第三方模块,如AspectJBeanClassLoaderAware
:加载Spring Bean的类加载器BootstrapContextAware
:资源适配器BootstrapContext,如JCA,CCIResourceLoaderAware
:底层访问资源的加载器BeanFactoryAware
:声明BeanFactoryPortletConfigAware
:PortletConfigPortletContextAware
:PortletContext- ServletConfigAware`:ServletConfig
ServletContextAware
:ServletContextMessageSourceAware
:国际化ApplicationEventPublisherAware
:应用事件NotificationPublisherAware
:JMX通知BeanNameAware
:声明Spring Bean的名字。
总结
这一篇主要是对基础知识的补足,下一节开始学习HandlerMapping组件。
参考
芋艿的源码解析,以及徐妈的博客:
http://svip.iocoder.cn/Spring-MVC/context-init-integration-with-Servlet-3.0/
http://svip.iocoder.cn/Spring-MVC/context-init-integration-with-SpringBoot/