springboot应用在内置tomcat和在独立tomcat里Listener加载顺序不同的问题

6 篇文章 0 订阅
5 篇文章 0 订阅

我们的一个语言国际化的实现思路是:

通过Listener在应用被加载的时候读取properties 资源文件,然后把对象放入 ServletContext 中,I18NUtils 工具类通过注入 ServletContext,实例化时从上下文获取对象,简化 API(读文件的时间放到应用启动而不是业务初次调用时)。

@WebListener
public class VinciContextLoaderListener extends ContextLoaderListener {
	private Logger logger = LoggerFactory.getLogger(VinciContextLoaderListener.class);
	
	@Override
	public void contextInitialized(ServletContextEvent event) {
		ServletContext servletContext = event.getServletContext();				
		Map<String, String> resourceMap = doMessagesInit();
		servletContext.setAttribute(VinciConstants.I18N, resourceMap);
	}
	
	private Map<String, String> doMessagesInit(){
		logger.info("进入初始化国际化资源文件信息的方法 doMessagesInit()");
		...
	}
}
// I18NUtils 工具类:
@Component
public class InternationalizationUtils {
	private Logger logger = LoggerFactory.getLogger(InternationalizationUtils.class);
	static Map<String, String> i18nMap = null;
	
	@Autowired
    private ServletContext ctx;
	
	@PostConstruct
	public void init(){
		logger.info("-------------开始初始化国际化工具类---------------");
		i18nMap = (Map<String, String>) ctx.getAttribute(VinciConstants.I18N);
		logger.info("(i18nMap == null) ------------- " + (i18nMap == null));
	}

	public static String getString(String key){
		return i18nMap.get(key);
	}
}

直接在 Eclipse 通过引导类启动,控制台的日志为:

...
11:02:14 INFO  [o.a.c.c.C.[Tomcat].[localhost].[/vinci-web]] - Initializing Spring embedded WebApplicationContext
11:02:14 INFO  [org.springframework.web.context.ContextLoader] - Root WebApplicationContext: initialization completed in 9346 ms
11:02:14 INFO  [c.h.vinci.interceptor.VinciContextLoaderListener] - 进入初始化国际化资源文件信息的方法 doMessagesInit()
11:02:14 INFO  [com.hebta.vinci.interceptor.SessionFilter] - Start session manager。。。
11:02:14 INFO  [c.h.vinci.common.util.InternationalizationUtils] - -------------开始初始化国际化工具类---------------
11:02:18 INFO  [c.h.vinci.common.util.InternationalizationUtils] - (i18nMap == null) ------------- false
log4j:WARN No appenders could be found for logger (com.alibaba.druid.pool.DruidDataSource).
...

可以看到,初始化国际化资源和将其暴露给应用都是期望的执行顺序,但是如果应用打包成 war 并放到外部的 tomcat 里:

10:56:18 INFO  [org.springframework.web.context.ContextLoader] - Root WebApplicationContext: initialization completed in 2397 ms
10:56:18 INFO  [o.s.boot.web.servlet.RegistrationBean] - Filter errorPageFilter was not registered (possibly already registered?)
10:56:18 INFO  [c.h.vinci.common.util.InternationalizationUtils] - -------------开始初始化国际化工具类---------------
10:56:18 INFO  [c.h.vinci.common.util.InternationalizationUtils] - (i18nMap == null) ------------- true
Load model sucess
...
10:56:24 INFO  [com.hebta.vinci.VinciApplication] - Started VinciApplication in 7.983 seconds (JVM running for 49.806)
10:56:24 INFO  [c.h.vinci.interceptor.VinciContextLoaderListener] - 添加一个全局的Map变量,用来防止一个用户账号多处登录
10:56:24 INFO  [c.h.vinci.interceptor.VinciContextLoaderListener] - 进入初始化国际化资源文件信息的方法 doMessagesInit()
...

顺序反了,导致应用无法获取任一资源消息。通过 debug Spring 的启动方法,就可以知道原因了,springboot 的 run 方法会调用 refresh 方法,它是 Spring 的核心方法,里面有个方法就是 createWebServer(), 从实现可知,如果没有已经运行的 web 容器,那么 webServer 变量为 null, 它将使用默认的 TomcatWebServer 新建一个 web 容器:

 

默认的 TomcatWebServer 启动后会按序实例化应用注册的 listener (当然包括实现了 ContextLoaderListener 的类), filter, servlet。对于我这个应用,它读取了资源文件,并把对象放到了 servletContext 中备用。

最后,Spring 进行 IoC 容器的初始化,这里就是 finishBeanFactoryInitialization() 方法:

可以看到我的应用的国际化工具类 @PostContruct 注解的方法(也就是 BeanPostProcessor 在完成实例化后的 post 操作)可以取到 servletContext 里的消息集合属性。

所以,springboot 中关键的方法 createWebServer() 会视情况创建 web 容器。

如果 Springboot 应用打成 war 包,放到独立的 tomcat,那么 tomcat 启动后,就会并解压 springboot 应用,创建 ServletContext,(根据 Servlet 3.0 规范)webappclassloader 会查找实现了 ServletContextInitializer 接口的类,作为加载应用的入口。我们知道 springboot 应用要打包成 war 则必须继承 SpringBootServletInitializer,我这里直接让启动类继承:

@SpringBootApplication(scanBasePackages = {"com.hebta.data.processor", "com.hebta.vinci"})
@ServletComponentScan
@MapperScan("com.hebta.vinci.dao")
@EnableTransactionManagement
public class VinciApplication extends SpringBootServletInitializer {
	@Override
	protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {		
		return application.sources(VinciApplication.class);
	}
	public static void main(String[] args) {
	
		SpringApplication.run(VinciApplication.class, args);
	}
}

继续看 SpringBootServletInitializer.onStartup() 就可以看到 createRootApplicationContext() 方法的最后和 springboot 的 main 方法一样调用 run() 方法,到了 createWebServer() 这里,此时独立的 tomcat 完成了 servletContext 的实例化,springboot 会继续 bean 的实例化和 IoC容器的创建,完了后将控制权交给 tomcat,tomcat 再按序实例化应用里的 listeners (包括实现了 ContextLoaderListener 的类), filters。由于 I18NUtils bean 的初始化先于  VinciContextLoaderListener 运行,所以无法从 servletContext 里拿到消息集合属性。

问题根源找到了,我的问题也就好解决了,考虑该工具类只会在业务代码调用的时候才会用到,可以使用 static 式的单例实现:

public class InternationalizationUtils {	
	static Map<String, String> i18nMap = null;
	
	static {
		i18nMap = (Map<String, String>)SessionUtil.getRequest().getServletContext().getAttribute(VinciConstants.I18N);
	}

	public static String getString(String key){
		return i18nMap.get(key);
	}
}

其实,我们这个应用是从 springmvc 改造到 springboot 的,原来的 VinciContextLoaderListener.java :

原来的应用是 tomcat 通过 web.xml 找到此 listener, 先读取资源文件,然后创建 spring 容器。而 springboot 则是遵从了 Servlet 3.0 消除 web.xml 的规范(红框里的代码不可出现在 springboot 的 contextInitialized() 方法里),入口变了,是先构建 IoC 容器,然后执行其他 listener 逻辑。

总结:
# 普通的 spring 应用放到 tomcat 里:
1. tomcat 启动后,使用 webapplicationclassloader 加载应用
2. 加载后,将创建一个 servletConext 作为该 web 应用的全局上下文,相当于一个 HashMap,对 web 应用下的各容器可见
3. spring 应用通过继承 ContextLoaderListener 并注册在 web.xml,该监听器监听到 servletConext 初始化时,调用 contextInitialized() 方法, spring 应用通过 initWebApplicationContext 方法初始化 spring 应用上下文
5. 结束后,如果还有其他的 listener, filter 等,会一并加载并初始化,servlet 会延迟初始化

# springboot 应用打成 war 包放到 tomcat 里:
1, 2 步同上
3. springboot 应用通过遵守 servlet 3.0+ 规范,以编程的方式实现 WebApplicationInitializer.onStartup,容器启动时会调用实现了该接口的类作为容器启动入口
4. SpringBootServletInitializer 实现将先实例化 webapplicationcontext, 然后 refresh context 完成 spring IoC 容器初始化
5. webapplicationclassloader 加载其他的 listener, filter

# springboot 应用以内置 web 容器启动:
1. springboot 直接走 SpringApplication.run()
2. 先实例化 webapplicationcontext, 然后刷新 context, 刷新 context 过程中,使用内置 web 容器创建 webServer,关联 webapplicationcontext 和全局上下文 servletContext,最后实例化应用里的 listener, filter 等
3. 完成 IoC 容器初始化,启动 webServer 对外提供服务

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值