2018-10-01 · Develop

Tomcat 启动时重复加载项目

Tomcat 启动时项目重复加载问题:

起因是因为在开启多数据源的编程式事务时,出现 SpringBean 初始化失败,
而之前没有出错是项目里没有使用编程式事务的方式,所以一致没有暴露出来。
经过排查,找出是 Spring 和 Atomikos 构建 jta 分布式事务出现如下错误信息:

Error creating bean with name 'userTransactionService' defined in file [/opt/tomcat/webapps/xxx/WEB-INF/classes/spring-dao.xml]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Can't overwrite cause with java.lang.RuntimeException: Log already in use?
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:334)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1214)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:543)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
        at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:772)
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:839)
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:538)
        at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:444)
        at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:326)
        at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:107)
        at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4887)
        at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5381)
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
        at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:901)
        at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:877)
        at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:633)
        at org.apache.catalina.startup.HostConfig.deployWAR(HostConfig.java:977)
        at org.apache.catalina.startup.HostConfig$DeployWar.run(HostConfig.java:1655)
        at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)
        at java.util.concurrent.FutureTask.run(FutureTask.java:262)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
        at java.lang.Thread.run(Thread.java:745)

Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field: private org.springframework.transaction.jta.JtaTransactionManager xxx.xxx.PrefConfigStatusUptJob.jtaTransactionManager; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userTransactionService' defined in file [/opt/tomcat7/webapps/zhph_operation/WEB-INF/classes/spring-dao.xml]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Can't overwrite cause with java.lang.RuntimeException: Log already in use?
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:573)
        at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88)
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:331)
        ... 26 more

在 google 中查找各种问题发现配置、使用方式、Spring 和 Atomikos 的集成都没有问题,
为什么会出现方法执行两次和启动时的 Log already in use? 异常。

最后排查出是 Tomcat 启动时项目重复加载问题:


在解决这个问题前,我们想回顾下 Tomcat 部署项目的方式:

Tomcat 占用的端口含义

Tomcat 启动后会默认占用 8080,8009 和 8005 三个端口,占用的这三个端口的意义如下:

在 Tomcat 5.5之前

Context 体现在 /conf/server.xml 中的 Host 里的元素,它由 Context 接口定义。每个元素代表了运行在虚拟主机上的单个 Web 应用。

<Context path="/demo" docBase="demo" debug="0" reloadbale="true"/>

一个 Host 元素中嵌套任意多的 Context 元素。每个 Context 的路径必须是惟一的,由 path 属性定义。另外,你必须定义一个 path="" 的 context ,这个 Context 称为该虚拟主机的缺省 web 应用,用来处理那些不能匹配任何 Context 的 Context 路径的请求。

在tomcat 5.5之后

不推荐在 server.xml 中进行配置,而是在 /conf/context.xml 中进行独立的配置。因为 server.xml 是不可动态重加载的资源,服务器一旦启动了以后,要修改这个文件,就得重启服务器才能重新加载。而 context.xml 文件则不然, tomcat 服务器会定时去扫描这个文件。一旦发现文件被修改(时间戳改变了),就会自动重新加载这个文件,而不需要重启服务器。

<Context path="/demo" docBase="demo" debug="0" reloadbale="true" privileged="true">
<WatchedResource>WEB-INF/web.xml</WatchedResource>
<WatchedResource>WEB-INF/demo.xml</WatchedResource> 监控资源文件,如果 web.xml || demo.xml 改变了,则自动重新加载改应用。

<Resource name="jdbc/testSiteds"
    auth="Container"
    type="javax.sql.DataSource"
    maxActive="100"
    maxIdle="30"
    maxWait="10000"
    username="root"
    password="root"
    driverClassName="com.mysql.jdbc.Driver"
    url="jdbc:mysql://localhost:3306/demo" />
</Context>
<!-- name: 表示指定的jndi名称 -->
<!-- auth: 表示认证方式,一般为Container-->
<!-- maxActive: 连接池支持的最大连接数-->
<!-- maxIdle: 连接池中最多可空闲maxIdle个连接-->
<!-- maxWait: 连接池中连接用完时,新的请求等待时间,毫秒-->
<!-- username: 表示数据库用户名-->
<!-- password: 表示数据库用户的密码-->
<!-- driverClassName: 表示JDBC DRIVER-->
<!-- url: 表示数据库URL地址-->

context.xml 的三个作用范围:

解决问题:

通过查询部署脚本发现 Dockerfile 里面讲 war 包复制到 tomcat 的 webapps 目录下解压,然后在 server.xml 的 Context 中配置项目地址:

<Host name="localhost"  appBase="webapps" unpackWARs="true" autoDeploy="true">
	<Context path="" docBase="demo/" debug="0" reloadable="true" />
</Host>

因为默认情况下主机名是 localhost ,而且通过 Tomcat 的网络服务器传递给机器的请求都将默认主机作为(默认)路由,所以在默认 Host 中增加 Context 能工作。

unpackWARs 如果此项设置为 true ,表示把 WEB 应用的 WAR 文件先展开为开放目录结构后再运行.如果设为 false 将直接运行为 WAR 文件。

autoDeploy 如果此项设为 true,表示 Tomcat 服务处于运行状态时,能够监测 appBase 下的文件,如果有新有 web 应用加入进来,会自运发布这个 WEB 应用。

结论:

如果将 autoDeploy 设置为 true ,就会发生再次部署的现象,第一次因 server.xml 中的 Context 配置而被部署(因为 deployOnstartup="true" ),而第二次因 autoDeploy 被设置为 true 而发生自动部署(默认情况下,在没有显式 Context 的这些属性时,它们每个的默认值都是 true )。

显式设置 autoDeploy 为 false 。避免了在 server.xml 中增加 Context 配置时两次部署相同的 Web 应用程序。

再解释下 appBase 和 docBase 的区别:


参考文档:

解决Tomcat启动时项目重复加载问题
Tomcat项目部署方式
tomcat配置详解
Tomcat 部署WAR文件之server.xml Context部署
详解 Tomcat 配置文件 server.xml