Tomcat deploying the same application twice in netbeans
Thanks for the answer by epoch and the answer by Steven Neiner.
Here is my version of their code. My differences:
- Marked method as
synchronized
.- Theoretically not needed, but given that we are dealing with weird multiple launching problem here it is better to be safe than sorry.
- Replaced calls to third-party utility (Spring?).
- Detect if running in development by looking for certain wording in the
catalina.base
path. - Deleted the
static
modifier. Using a Singleton implemented as an enum. - As of 2016-05, rewrote the code to be easier to read and comprehend (at least for me). Only briefly tested, so be sure to review the source code before using (and must be used entirely at your own risk).
Concept
The core of their workaround to this bug is to delete the file named with the name of your web app (your “servlet context”) and appended with .xml
.
For example, if your web app is named AcmeApp
, locate and delete the file named AcmeApp.xml
. This file is stored nested within the “Catalina base” folder.
Do this deletion as the very last step of your web app’s run. So when the web app launches again, that file will not exist, and will be recreated. Remember this is only in development mode. The bug does not occur when using Tomcat on its own in production.
So how do we run this workaround code as the last act of our web app’s execution? Read on.
How To Use
As a standard part of version 2.3 and later of the Servlet spec, every Servlet container has hooks to call your code when your web launches and again when your web app is being shut down. This is not Tomcat specific; Jetty, GlassFish, WildFly/JBoss, and so on, all include this feature as required by the Servlet spec.
To use the code shown above, add a new class to your project. Name the new class something like "MyServletContextListener.java". Declare that class as implementing the ServletContextListener interface.
Implement the two methods required by this interface. One method is called by your Servlet container (Tomcat) when your web app launches, guaranteed to run before the first user hits your app. The other method is called when your web app is being shut down by the Servlet container (Tomcat).
In the contextDestroyed
method, call the method shown above. Like this:
@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 );
}
The configuration is easy. Merely include this class alongside your servlet class it a WAR file/folder. The @WebListener annotation causes the Servlet container to “notice” this listener class, load and instantiate it, and when appropriate execute each of its methods. You may use alternate modes of configuration instead of the annotation if needed, but the annotation is the simplest route.
Here is an entire AppListener class as a full example. I have re-written the previously posted version of this code to be easier to read and comprehend.
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.
}
}
}
}
}
And here is the helper class to determine if running in development mode. Read the comments for more discussion. The upshot is that there seems to be no simple clean way to detect when in development, no way to detect when running Tomcat from NetBeans rather than running Tomcat on its own. I have asked but have not received any better solution.
CAVEAT: You must alter this isInDevelopmentMode
method to match your particular development environment.
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;
}
}
First of all, thanks Steven! Here is a more portable version of the fix:
/**
* 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: substitute unknown references for your own version of it :)
Call this custom method from the contextDestroyed
method of your ServletContextListener
implementation.
I found that deleting the file conf/localhost/myappname.xml prevents the app from initializing twice. Basically Tomcat is restarting, and restarting the old version of your app. Then it starts again when Netbeans deploys it. As a workaround, I added a few lines of code in my ContextListener contextDestroyed() event:
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();
}
}
In the web.xml add the following in a dev environment:
<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>
Then the next time the app is deployed, it won't be started again prior to the deployment, hence will not start twice. Any other ideas for deleting a file prior to deploying, or on shutdown, would be appreciated.