SpringBoot打包为JAR时ServletContextEvent.getServletContext返回空的解决方法
哥们,你这个问题我之前在项目里也碰到过!核心原因是Spring Boot以JAR包方式启动时,嵌入式Servlet容器(比如Tomcat)的初始化逻辑和IDE中直接启动的场景存在时序差异,再加上你的类同时实现了CommandLineRunner和ServletContextListener,两种生命周期接口的执行时机冲突,导致JAR启动时sce.getServletContext()返回null,而IDE启动时刚好时序凑对了所以没问题。
接下来给你两种靠谱的解决办法,都是我亲测有效的:
方案一:改用ServletContextAware接口获取ServletContext
Spring专门提供了ServletContextAware接口,用来让Bean自动获取ServletContext,这种方式完全贴合Spring的生命周期管理,能完美规避时序问题:
修改后的代码如下:
@Order(10) @Component("startupRunner") public class StartupRunner implements CommandLineRunner, ServletContextAware { private ServletContext application = null; @Resource private FilmService filmService; @Resource private WebSiteInfoService webSiteInfoService; @Resource private LinkService linkService; @Autowired private WebSiteService webSiteService; @Override public void run(String... args) throws Exception { this.loadData(); } public void loadData() { System.out.println("vincent.applicaiton" + application); // 加个空判断更稳妥,避免意外情况 if (application == null) { throw new IllegalStateException("ServletContext还没初始化完成呢!"); } application.setAttribute("newestInfoList", webSiteInfoService.list(null, 0, 10)); Film film = new Film(); film.setHot(1); application.setAttribute("newestHotFilmList", filmService.list(film, 0, 10)); application.setAttribute("newestIndexHotFilmList", filmService.list(film, 0, 32)); application.setAttribute("newestWebSiteList", webSiteService.newestList(0, 10)); application.setAttribute("newestFilmList", filmService.list(null, 0, 10)); application.setAttribute("linkList", linkService.listAll()); } @Override public void setServletContext(ServletContext servletContext) { this.application = servletContext; } }
这个改动很简单:去掉ServletContextListener的实现,换成ServletContextAware,实现setServletContext方法让Spring自动注入ServletContext。不管是IDE启动还是JAR包启动,Spring都会在Bean初始化时把有效的ServletContext传给你。
方案二:监听ServletContextInitializedEvent事件
如果你更喜欢用事件驱动的方式,也可以实现ApplicationListener<ServletContextInitializedEvent>接口,这样只有当ServletContext完全初始化完成后,才会触发数据加载操作,从根源上避免空指针:
@Order(10) @Component("startupRunner") public class StartupRunner implements ApplicationListener<ServletContextInitializedEvent> { @Resource private FilmService filmService; @Resource private WebSiteInfoService webSiteInfoService; @Resource private LinkService linkService; @Autowired private WebSiteService webSiteService; @Override public void onApplicationEvent(ServletContextInitializedEvent event) { ServletContext application = event.getServletContext(); System.out.println("vincent.applicaiton" + application); application.setAttribute("newestInfoList", webSiteInfoService.list(null, 0, 10)); Film film = new Film(); film.setHot(1); application.setAttribute("newestHotFilmList", filmService.list(film, 0, 10)); application.setAttribute("newestIndexHotFilmList", filmService.list(film, 0, 32)); application.setAttribute("newestWebSiteList", webSiteService.newestList(0, 10)); application.setAttribute("newestFilmList", filmService.list(null, 0, 10)); application.setAttribute("linkList", linkService.listAll()); } }
这种方式的好处是逻辑更清晰:明确绑定到ServletContext的初始化完成事件,不用担心时序问题,而且不需要额外维护成员变量存储ServletContext,直接从事件中获取即可。
再啰嗦两句为什么原代码会失效
在Spring Boot JAR启动模式下,嵌入式Servlet容器的启动是由Spring Boot的SpringApplication统筹的,和传统WAR包部署到外部容器的流程不一样。当你的类同时实现CommandLineRunner和ServletContextListener时,CommandLineRunner.run()可能在ServletContextListener.contextInitialized()之前就执行了,或者contextInitialized()执行时,ServletContext还没被容器完全初始化好,所以返回null。而IDE中直接启动时,容器的初始化时序刚好让两个方法的执行顺序符合你的预期,所以没出问题。
内容的提问来源于stack exchange,提问作者Vincent Chan

