我正在使用NetBeans, Tomcat似乎在.war应用程序中部署了两次,两次启动web应用程序。
我尝试了Tomcat 6和7,结果相同。
我有一个Spring MVC, Hibernate和Thymeleaf应用程序。META-INF下的Context.xml包含以下内容:
<?xml version="1.0" encoding="UTF-8"?>
<Context path="/website"/>
这是日志。
**First deployment starts**
[ INFO] 07:13:09 ContextLoader - Root WebApplicationContext: initialization started
[ INFO] 07:13:09 XmlWebApplicationContext - Refreshing Root WebApplicationContext: startup date [Thu May 23 07:13:09 EST 2013]; root of context hierarchy
2013-05-23 07:13:10 JRebel: Monitoring Spring bean definitions in '/Users/pack/NetBeansProjects/mysite/site/src/main/webapp/WEB-INF/applicationContext- data.xml'.
[ INFO] 07:13:10 XmlBeanDefinitionReader - Loading XML bean definitions from ServletContext resource [/WEB-INF/applicationContext-data.xml]
[ INFO] 07:13:10 ClassPathScanningCandidateComponentProvider - JSR-330 'javax.inject.Named' annotation found and supported for component scanning
***(tomcat initializes hibernate and other spring beans)***
...
May 23, 2013 7:13:17 AM org.apache.catalina.startup.Catalina start
INFO: Server startup in 15552 ms
***Tomcat started***
***Tomcat tries to shut down the context***
[ INFO] 07:13:18 XmlWebApplicationContext - Closing WebApplicationContext for namespace 'spring-mvc-servlet': startup date [Thu May 23 07:13:15 EST 2013]; parent: Root WebApplicationContext
[ INFO] 07:13:18 DefaultListableBeanFactory - Destroying singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@5bbe2de2: defining beans [blHeadProcessor,blHeadProcessorExtensionManager,navigationProcessor,blPaginationPageLinkPro cessor,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.internalPersistenceAnnotationProcessor,blRegisterCustomerValidator,blCategoryController,com.package.ui.thymeleaf.CategoryHandlerMapping#0,templateResolver,templateEngine,org.thymeleaf.spring3.view.ThymeleafViewResolver#0,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor]; parent: org.springframework.beans.factory.support.DefaultListableBeanFactory@521e7f21
[ INFO] 07:13:18 XmlWebApplicationContext - Closing Root WebApplicationContext: startup date [Thu May 23 07:13:09 EST 2013]; root of context hierarchy
[ INFO] 07:13:18 DefaultListableBeanFactory - Destroying singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@521e7f21: defining beans [org.springframework.aop.config.internalAutoProxyCreator,org.springframework.transaction.annotation.AnnotationTransactionAttributeSource#0,org.springframework.transaction.interceptor.TransactionInterceptor#0,org.springframework.transaction.config.internalTransactionAdvisor,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.internalPersistenceAnnotationProcessor,org.springframework.context.support.PropertySourcesPlaceholderConfigurer#0,blCategoryDao,blCustomerDao,blIdGenerationDao,nlpDao,jpaTemplate,webDS,entityManagerFactory,transactionManager,org.springframework.security.filterChains,org.springframework.security.filterChainProxy,org.springframework.security.web.DefaultSecurityFilterChain#0,org.springframework.security.web.DefaultSecurityFilterChain#1,org.springframework.security.web.DefaultSecurityFilterChain#2,org.springframework.security.web.DefaultSecurityFilterChain#3,org.springframework.security.web.DefaultSecurityFilterChain#4,org.springframework.security.web.DefaultSecurityFilterChain#5,org.springframework.security.web.PortMapperImpl#0,org.springframework.security.web.PortResolverImpl#0,org.springframework.security.authentication.ProviderManager#0,org.springframework.security.web.context.HttpSessionSecurityContextRepository#0,org.springframework.security.web.savedrequest.HttpSessionRequestCache#0,org.springframework.security.web.access.channel.ChannelDecisionManagerImpl#0,org.springframework.security.access.vote.AffirmativeBased#0,org.springframework.security.web.access.intercept.FilterSecurityInterceptor#0,org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator#0,org.springframework.security.authentication.AnonymousAuthenticationProvider#0,org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#0,org.springframework.security.userDetailsServiceFactory,org.springframework.security.web.DefaultSecurityFilterChain#6,org.springframework.security.authentication.dao.DaoAuthenticationProvider#0,org.springframework.security.authentication.DefaultAuthenticationEventPublisher#0,org.springframework.security.authenticationManager,blUserDetailsService,blCatalogService,blCustomerService,entityService,fbPageService,blIdGenerationService,blLoginService,nlpService,priceIncreaseValidator,searchFacetService,blEntityConfiguration,blPasswordEncoder,solrIndexService,solrEmbedded,textEncryptor,org.springframework.context.annotation.ConfigurationClassPostProcessor.importAwareProcessor]; root of factory hierarchy
[ INFO] 07:13:18 LocalContainerEntityManagerFactoryBean - Closing JPA EntityManagerFactory for persistence unit 'blPU'
[ INFO] 07:13:18 SessionFactoryImpl - closing
May 23, 2013 7:13:18 AM org.apache.catalina.loader.WebappClassLoader clearReferencesJdbc
SEVERE: The web application [/website] registered the JDBC driver [com.mysql.jdbc.Driver] but failed to unregister it when the web application was stopped. To prevent a memory leak, the JDBC Driver has been forcibly unregistered.
May 23, 2013 7:13:19 AM org.apache.catalina.startup.HostConfig deleteRedeployResources
INFO: Undeploying context [/website]
May 23, 2013 7:13:19 AM org.apache.catalina.startup.HostConfig deployDescriptor
INFO: Deploying configuration descriptor /Users/pack/Servers/apache-tomcat- 7.0.34/conf/Catalina/localhost/website.xml
2013-05-23 07:13:23 JRebel: Monitoring Log4j configuration in 'file:/Users/pack/NetBeansProjects/mysite/site/target/mycompany/WEB-INF/classes/log4j.xml'.
***Tomcat tries to restart again***
[ INFO] 07:13:23 ContextLoader - Root WebApplicationContext: initialization started
[ INFO] 07:13:23 XmlWebApplicationContext - Refreshing Root WebApplicationContext: startup date [Thu May 23 07:13:23 EST 2013]; root of context hierarchy
2013-05-23 07:13:24 JRebel: Monitoring Spring bean definitions in '/Users/pack/NetBeansProjects/mysite/site/src/main/webapp/WEB-INF/applicationContext- data.xml'.
[ INFO] 07:13:24 XmlBeanDefinitionReader - Loading XML bean definitions from ServletContext resource [/WEB-INF/applicationContext-data.xml]
并成功启动。
我不明白的是为什么Tomcat要部署应用程序两次。第一次在新的tomcat实例上部署应用程序时不会出现这种情况,因为site.xml文件还没有在tomcat/conf/catalina/localhost文件夹中。但是当我停止并从netbeans再次运行tomcat时,website.xml文件仍然在/conf/catalina/localhost文件夹中,但在第二次部署即将发生之前被删除并重新部署。
我尝试将Tomcat的server.xml文件中的autoDeploy
设置为false,但没有帮助。
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="false">
可能Tomcat应该在每次Tomcat停止时删除/conf/catalina/localhost文件夹下的website.xml文件。
localhost文件夹下的website.xml文件的样子
<?xml version="1.0" encoding="UTF-8"?>
<Context
docBase="/Users/pack/NetBeansProjects/mysite/site/target/mycompany"
path="/website"
/>
我发现删除文件conf/localhost/myappname.xml可以防止应用程序初始化两次。基本上,Tomcat正在重新启动,并重新启动您的应用程序的旧版本。然后,当Netbeans部署它时,它再次启动。作为解决方案,我在我的ContextListener contextDestroyed()事件中添加了几行代码:
public void contextDestroyed(ServletContextEvent sce) {
...
String delme = sce.getServletContext().getInitParameter("eraseOnExit");
if (delme != null && delme.length() > 0) {
File del = new File(delme);
if (del.exists()) {
System.out.println("Deleting file " + delme);
del.delete();
}
}
在web.xml中在开发环境中添加以下内容:
<context-param>
<description>Workaround for Tomcat starting webapp twice</description>
<param-name>eraseOnExit</param-name>
<param-value>/Users/xxx/apache-tomcat-7.0.42/conf/Catalina/localhost/myappname.xml</param-value>
</context-param>
那么下次部署应用程序时,它不会在部署之前再次启动,因此不会启动两次。在部署之前或关闭时删除文件的任何其他想法,将不胜感激。
感谢epoch和Steven Neiner的回答。
这是我的代码版本。我的差异:
- 标记方法为
synchronized
。- 理论上不需要,但考虑到我们在这里处理奇怪的多次启动问题,安全总比后悔好。
- 替换调用第三方工具(Spring?)。
- 通过在
catalina.base
路径中寻找某些措辞来检测是否在开发中运行。 - 删除
static
修饰符。使用作为enum实现的Singleton - 作为2016-05,重写代码更容易阅读和理解(至少对我来说)。只进行了简短的测试,所以在使用之前一定要检查源代码(必须完全由您自己承担风险)。
他们解决这个bug的核心是删除以你的web应用程序的名字命名的文件(你的"servlet上下文"),并附加.xml
。
例如,如果您的web应用程序名为AcmeApp
,找到并删除名为AcmeApp.xml
的文件。该文件嵌套存储在"Catalina base"文件夹中。
将此删除作为web应用程序运行的最后一步。因此,当web应用程序再次启动时,该文件将不存在,并将被重新创建。记住,这只是在开发模式。如果在生产环境中单独使用Tomcat,则不会出现该错误。
那么我们如何运行这个变通代码作为我们的web应用程序的最后一个行为的执行?读下去。
如何使用
作为Servlet规范2.3及以后版本的一个标准部分,每个Servlet容器都有钩子,可以在web启动和web应用程序关闭时调用代码。这不是Tomcat特有的;Jetty、GlassFish、WildFly/JBoss等等,都按照Servlet规范的要求包含了这个特性。
要使用上面显示的代码,请向您的项目添加一个新类。将新类命名为"MyServletContextListener.java"。将该类声明为实现ServletContextListener接口。
实现这个接口需要的两个方法。一个方法是由你的Servlet容器(Tomcat)在你的web应用程序启动时调用,保证在第一个用户点击你的应用程序之前运行。另一个方法是在你的web应用程序被Servlet容器(Tomcat)关闭时调用。
在contextDestroyed
方法中,调用上面所示的方法。这样的:@Override
public void contextInitialized ( ServletContextEvent sce )
{
// Web app launching.
// This method runs *before* any execution of this web app’s servlets and filters.
// Do nothing. No code needed here.
}
@Override
public void contextDestroyed ( ServletContextEvent sce )
{
// Web app shutting down.
// This method runs *after* the last execution of this web app’s servlets and filters.
// Workaround for NetBeans problem with launching Tomcat twice.
this.workaroundTomcatNetbeansRedeployBug( sce );
}
配置简单。只需将该类与servlet类一起包含在WAR文件/文件夹中即可。@WebListener注释使Servlet容器"注意到"这个侦听器类,加载并实例化它,并在适当的时候执行它的每个方法。如果需要,您可以使用其他配置模式来代替注释,但注释是最简单的途径。
下面是一个完整的AppListener类作为完整的例子。为了更容易阅读和理解,我重写了之前发布的代码版本。
package com.basilbourque;
import java.io.File;
import java.io.FilenameFilter;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
/**
* Hooks into the web app launching and quitting, as a workaround for Tomcat
* running from NetBeans causing the web app to rapidly deploy, undeploy and
* redeploy.
*
* © 2016 Basil Bourque. This source code may be used freely, and entirely at
* your own risk, according to terms of the ISC License at:
* https://opensource.org/licenses/ISC
*
* @author Basil Bourque
*/
@WebListener
public class AppListener implements ServletContextListener {
@Override
public void contextInitialized ( final ServletContextEvent servletContextEventArg ) {
System.out.println ( "Basil launch" );
}
@Override
public void contextDestroyed ( final ServletContextEvent servletContextEventArg ) {
System.out.println ( "Basil exit" );
this.workaroundTomcatNetbeansRedeployBug ( servletContextEventArg );
}
synchronized private void workaroundTomcatNetbeansRedeployBug ( final ServletContextEvent servletContextEventArg ) {
// When running Tomcat 8 from NetBeans 8, as we do in development, a bug causes the web app to rapidly deploy, undeploy, and redeploy.
// This bug causes multiple bad side-effects.
//
// Workaround: When running in development mode with NetBeans & Tomcat, delete the XML file with name of web app, found in {catalina-base}/conf/Catalina/localhost/YourWebAppNameHere.xml.
// Example of file name to delete: If your app is named “AcmeApp”, then in a Vaadin multi-module Maven archetype app, the target file might be named “AcmeApp-ui.xml”.
// In a simpler project, the target file might be named “AcmeApp.xml”.
// So we need to determine the name of the web app in a soft-coded fashino.
// We extract from a context path. For example, '/AcmeApp-ui'. We need to remove that slash (SOLIDUS) at the front.
// Then we append a “.xml” to create our target file name.
// We look for that file in the folder nested in the Cataline base folder (see line above for path).
// If file is found, add it to the list of files to be deleted. That list will have only one element.
// Lastly, delete the file.
if ( AppUtility.INSTANCE.isInDevelopmentMode () ) { // Find a strategy to determine if you are in development mode.
final String catalinaBase = System.getProperty ( "catalina.base" );// Path to the folder the working folder of this web app.
final String contextPath = servletContextEventArg.getServletContext ().getContextPath ();
final String contextName = contextPath.substring ( 1 ); // Strip the SOLIDUS (slash) from first character position. Example: '/AcmeApp-ui' becomes 'AcmeApp-ui'.
final String fileNameToDelete = contextName + ".xml";
final File catalinaBaseContext = new File ( catalinaBase , "conf/Catalina/localhost" ); // While in development, running Tomcat from NetBeans, the web app’s name is 'localhost'.
if ( catalinaBaseContext.exists () && catalinaBaseContext.canRead () ) { // Confirm that we found the expected configuration folder nested in Catalina’s 'base' folder.
// Make an array of File objects that match our criterion of having one of our expected file names.
// Populate this array by defining a filter of filenames via a functional interface, to be applied against each file found in folder.
final File[] filesToDelete = catalinaBaseContext.listFiles ( new FilenameFilter () {
@Override
public boolean accept ( File dir , String name ) {
boolean accepting = ( name.equals ( fileNameToDelete ) );
return accepting;
}
} );
if ( filesToDelete.length == 0 ) { // If list of files is empty…
// FIXME Handle error. Should always find one file to delete.
System.out.println ( "ERROR - Found no file to delete as workaround for NetBeans+Tomcat double-launch bug. Expected file name: " + fileNameToDelete + " | Message # 42ec5857-9c1b-431a-b5c1-2588669a0ee2." );
return;
}
if ( filesToDelete.length > 1 ) { // If list of files has more than one file…
// FIXME Handle error. Should never find more than one file to delete.
System.out.println ( "ERROR - Found more than one file to delete as workaround for NetBeans+Tomcat double-launch bug." + " | Message # 0afbd6ca-3722-4739-81dc-b2916e9dbba4." );
return;
}
for ( File file : filesToDelete ) {
file.delete (); // Delete first file found in our filtered array.
// FIXME You may want to log this deletion.
System.out.println ( "TRACE - Deleting file as workaround for NetBeans+Tomcat double-launch bug: " + file + " | Message # 5a78416c-6653-40dc-a98c-6d9b64766d96." );
break; // Should be exactly one element in this list. But out of abundant caution, we bail-out of the FOR loop.
}
}
}
}
}
下面是确定是否在开发模式下运行的helper类。阅读评论以获得更多讨论。其结果是,在开发过程中似乎没有一种简单明了的方法来检测,也没有一种方法可以检测何时从NetBeans运行Tomcat而不是自己运行Tomcat。我已经问过了,但没有收到任何更好的解决办法。
警告:您必须更改此isInDevelopmentMode
方法以匹配您特定的开发环境。
package com.basilbourque;
import java.util.ArrayList;
import java.util.List;
/**
* Detects if this web app is running in the Apache Tomcat web container from
* within NetBeans during development time.
*
* © 2016 Basil Bourque. This source code may be used freely, and entirely at
* your own risk, according to terms of the ISC License at:
* https://opensource.org/licenses/ISC
*
* @author Basil Bourque.
*/
public enum AppUtility {
INSTANCE;
transient private Boolean isDevMode;
synchronized public Boolean isInDevelopmentMode () {
// There is no simple direct way to detect if running in development.
// As a workaround, I use some facts specific to my running Tomcat from NetBeans while developing.
//
// The “Catalina base” is the folder used by Tomcat’s Catalina module to do the work of your servlets.
// The names of the folders in the path to that folder can be a clue about running in development.
//
// By default, the Catalina base folder is nested within Tomcat’s own folder.
//
// If you run NetBeans with a bundled Tomcat installation that path may contain the word “NetBeans”.
// At least this is the case on Mac OS X where that bundled Tomcat is stored within the NetBeans app (an app is actually a folder in Mac OS X).
//
// You mant to create your own folder to hold Tomcat’s “base” folder.
// I do this on my development machine. I create a folder named something like "apache-tomcat-base-dev" in my home folder.
// Nested inside that folder are additional folders for each version of Tomcat I may be using, such as 'base-8.0.33'.
// Since I do not use such a name on my production environment, I can example the path for that phrasing to indicate development mode.
//
if ( null == this.isDevMode ) { // Lazy-loading.
// Retrieve the folder path to the current Catalina base folder.
String catalinaBaseFolderPath = System.getProperty ( "catalina.base" );
this.isDevMode = Boolean.FALSE;
// Examine that path for certain wording I expect to occur only in development and never in production.
List<String> list = new ArrayList<> ();
list.add ( "Application Support" );// Specific to Mac OS X only.
list.add ( "NetBeans" );
list.add ( "apache-tomcat-base-dev" ); // My own name for an external folder to keep Catalina base separate, outside of NetBeans and Tomcat.
for ( String s : list ) {
if ( catalinaBaseFolderPath.contains ( s ) ) {
this.isDevMode = Boolean.TRUE;
break; // Bail-out of the FOR loop after first hit.
}
}
}
return this.isDevMode;
}
}
标题>
首先,谢谢Steven!下面是一个更易于移植的修复版本:
/**
* tomcat workaround bug, in development mode, if tomcat is stopped and application is not un-deployed,
* the old application will start up again on startup, and then the new code will be deployed, leading
* to a the app starting two times and introducing subtle bugs, when this app is stopped and in dev mode
* remove the deployment descriptor from catalina base
*/
private static void preventTomcatNetbeansRedeployBug(final ServletContextEvent sce) {
final String contextPath = sce.getServletContext().getContextPath();
final String catalinaBase = System.getProperty("catalina.base");
if (StringUtil.checkValidity(contextPath, catalinaBase)
&& FrameworkContext.getInstance().isDevEnvironment()) {
final File catalinaBaseContext = new File(catalinaBase, "conf/Catalina/localhost");
if (catalinaBaseContext.exists() && catalinaBaseContext.canRead()) {
final File[] contexts = catalinaBaseContext.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.equals(contextPath.substring(1) + ".xml");
}
});
if (contexts != null && contexts.length > 0) {
LOG.info("Deleting core context[" + contexts[0].getAbsolutePath() + "] since we are in dev");
contexts[0].delete();
}
}
}
}
PS:用未知引用代替你自己的版本:)
从ServletContextListener
实现的contextDestroyed
方法调用这个自定义方法。
在我的情况下,它是(在NetBeans中点击'run'后):
- 在Tomcat中部署app(不必要)
- 在Tomcat中取消部署应用程序(不必要)
- 构建应用
- 在Tomcat中部署app
我修复了这个问题,只是通过在运行之前清理项目- 'clean' -> 'run':)