最近优化项目架构针对Logback日志框架需要结合Spring的profiles配置文件达到不同运行环境自动配置参数.
最开始想法是使用ServletContextListener监听器实现在项目初始化时读取Spring的profiles配置文件手动放入当前环境变量中但在测试过程中发现问题!
环境变量还没有开始加载, Logback框架已经开始被加载!
跟踪日志信息发现一个类LogbackServletContainerInitializer, Logback是通过该类进行Web环境初始化的!该类实现了javax.servlet.ServletContainerInitializer接口
第一次接触ServletContainerInitializer查找了一下相关规范, 于是自己也写了一个ServletContainerInitializer接口的类LogInitializer在但发现放置WebContent/META-INF/services/javax.servlet.ServletContainerInitializer文件无法被读取, 网上说只有放置在WebContent/WEB-INF/lib下的Jar包中才能被读取!
单独建立一个新的项目通过Maven引入到原有项目中, 忽然发现LogbackServletContainerInitializer和LogInitializer存在先后加载顺序问题, 经过几次测试发现跟Jar包名称的先后顺序有关, 于是开始跟踪Tomcat运行时的Debug调试看看Tomcat是怎么加载ServletContainerInitializer实例的!
开发工具: IDEA 2017.2.6版
Servlet容器: Tomcat 8.5.20
在LogInitializer#onStartup()方法第一行打断点等待
进入断点后观察调用堆栈, 发现是org.apache.catalina.core.StandardContext#startInternal() 调用了LogInitializer#onStartup()
进入org.apache.catalina.core.StandardContext#startInternal()方法, 发现一个成员变量迭代调用
跟踪成员属性initializers发现是一个链表哈希映射
继续根据initializers被调用情况, 发现org.apache.catalina.core.StandardContext#addServletContainerInitializer()向成员属性中放入实例
查找org.apache.catalina.core.StandardContext#addServletContainerInitializer()方法被调用情况, 发现在org.apache.catalina.startup.ContextConfig#webConfig()方法中一个成员属性迭代调用
跟踪initializerClassMap成员属性发现类似于刚才的initializers
跟踪initializerClassMap被放入对象, 发现org.apache.catalina.startup.ContextConfig#processServletContainerInitializers()方法
进入org.apache.catalina.startup.WebappServiceLoader#load()方法观察ServletContainerInitializer实例被加载情况
观察了一下javax.servlet.ServletContext.ORDERED_LIBS静态常量发现与web.xml中的<absolute-ordering>元素相关, 因为没有配置所以先不考虑, 这样判断orderedLibs!=null不会被执行类加载器loader的引用就是当前项目Servlet容器的类加载器去加载资源
查找到五个资源, Tomcat容器自身2个, LogInitializer, LogbackServletContainerInitializer, SpringServletContainerInitializer(Spring的) 这个时候已经明了ServletContainerInitializer一般情况下是基于类加载器加载资源的枚举迭代顺序, 因为委托加载机制会从最顶级类加载向下一直加载到当前项目Web容器类加载器下的资源, 到此基本已经弄清楚ServletContainerInitializer加载流程!
因为看见了ORDERED_LIBS常量所以想继续研究一下<absolute-ordering>元素, 大概看了一下相关规范文档定义, 该元素是Servlet3.0出现的为第三方插件Jar包自动配置一些Servlet, Filter时所有第三方插件按照指定绝对顺序加载Jar包资源
1. 在Jar包的META-INF目录下建立web-fragment.xml
2, 配置web.xml
在org.apache.catalina.startup.WebappServiceLoader#load()方法打断点, 发现找到一个jar包
将当前项目Web容器类加载器替换为Tomca容器类加载器是因为委托加载机制从最顶级类加载器加载到当前类加载器所以会导致当前项目Web容器不会被加载到, 就会跳过当前项目下WEB-INF/lib中的Jar包查找所以只有log-initializer-0.1.jar中的ServletContainerInitializer被加载, LogbackServletContainerInitializer, SpringServletContainerInitializer的配置丢失
不符合预期结果, 继续看<absolute-ordering>文档, 发现一个<others />元素, 在<absolute-ordering>中追加
重新开始在org.apache.catalina.startup.WebappServiceLoader#load()方法断点Debug
这次就有所有Jar包了, 但很好奇去项目部署的Tomcat下WEB-INF/lib 看了一下Jar包数量 44个 而非37个逐一匹配发现缺少这些Jar包
非常好奇为什么会丢失Jar包, 如果只是丢失apache相关的也可能Tomcat排除自身lib下相关的, 但是slf4j, aspecj居然也没有, 本着求知的好奇心继续调试, 找出缺少Jar包的原因
因为是通过ORDERED_LIBS常量获取的容器属性值, 所以直接在org.apache.catalina.core.ApplicationContext#setAttribute()使用条件断点
查看堆栈信息, 发现被org.apache.tomcat.util.descriptor.web.WebXml#orderWebFragments()方法调用
跟踪局部变量orderedFragments
发现是局部变量fragments映射迭代放入, 而fragments是由外部传递
继续向上跟踪堆栈
找到org.apache.tomcat.JarScanner实现类org.apache.tomcat.util.scan.StandardJarScanner#scan()方法
找到org.apache.tomcat.JarScanFilter实现类org.apache.tomcat.util.scan.StandardJarScanFilter#check()方法
至此基本上算是了解ServletContainerInitializer加载机制!