Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

setUncaughtExceptionHandler not working in Maven project

I'm trying to handle uncaught exceptions on the main thread in my application so I can log them to a file (my app is a command line app which runs on an overnight job, so if something goes wrong I want admins to be able to easily see exceptions). I've reduced this to as simple a test case as I can.

Maven app generated with (as per the getting started docs):

   mvn -B archetype:generate \
      -DarchetypeGroupId=org.apache.maven.archetypes \
      -DgroupId=com.mycompany.app \
      -DartifactId=my-app

App.java:

package com.mycompany.app;

public class App {

    public static void main(String[] args) throws Exception {
        Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println("Handled exception - let's log it!");
                // Logging code here
            }
        });

        System.out.println("Exception testing");
        throw new Exception("Catch and log me!");
    }

}

Running with mvn exec:java produces:

[INFO] Error stacktraces are turned on.
[INFO] Scanning for projects...
[WARNING] 
[WARNING] Some problems were encountered while building the effective model for com.mycompany.app:my-app:jar:1.0-SNAPSHOT
[WARNING] 'build.plugins.plugin.version' for org.codehaus.mojo:exec-maven-plugin is missing. @ line 20, column 15
[WARNING] 
[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.
[WARNING] 
[WARNING] For this reason, future Maven versions might no longer support building such malformed projects.
[WARNING] 
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building my-app 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- exec-maven-plugin:1.4.0:java (default-cli) @ my-app ---
Exception testing
[WARNING] 
java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:293)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.Exception: Catch and log me!
    at com.mycompany.app.App.main(App.java:14)
    ... 6 more
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.493 s
[INFO] Finished at: 2015-10-26T10:57:00+00:00
[INFO] Final Memory: 8M/240M
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.codehaus.mojo:exec-maven-plugin:1.4.0:java (default-cli) on project my-app: An exception occured while executing the Java class. null: InvocationTargetException: Catch and log me! -> [Help 1]
org.apache.maven.lifecycle.LifecycleExecutionException: Failed to execute goal org.codehaus.mojo:exec-maven-plugin:1.4.0:java (default-cli) on project my-app: An exception occured while executing the Java class. null
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:216)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:153)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:145)
    at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject(LifecycleModuleBuilder.java:116)
    at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject(LifecycleModuleBuilder.java:80)
    at org.apache.maven.lifecycle.internal.builder.singlethreaded.SingleThreadedBuilder.build(SingleThreadedBuilder.java:51)
    at org.apache.maven.lifecycle.internal.LifecycleStarter.execute(LifecycleStarter.java:128)
    at org.apache.maven.DefaultMaven.doExecute(DefaultMaven.java:307)
    at org.apache.maven.DefaultMaven.doExecute(DefaultMaven.java:193)
    at org.apache.maven.DefaultMaven.execute(DefaultMaven.java:106)
    at org.apache.maven.cli.MavenCli.execute(MavenCli.java:862)
    at org.apache.maven.cli.MavenCli.doMain(MavenCli.java:286)
    at org.apache.maven.cli.MavenCli.main(MavenCli.java:197)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced(Launcher.java:289)
    at org.codehaus.plexus.classworlds.launcher.Launcher.launch(Launcher.java:229)
    at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode(Launcher.java:415)
    at org.codehaus.plexus.classworlds.launcher.Launcher.main(Launcher.java:356)
Caused by: org.apache.maven.plugin.MojoExecutionException: An exception occured while executing the Java class. null
    at org.codehaus.mojo.exec.ExecJavaMojo.execute(ExecJavaMojo.java:345)
    at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo(DefaultBuildPluginManager.java:134)
    at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:208)
    ... 20 more
Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at org.codehaus.mojo.exec.ExecJavaMojo$1.run(ExecJavaMojo.java:293)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.Exception: Catch and log me!
    at com.mycompany.app.App.main(App.java:14)
    ... 6 more

The same code run as a simple Java app works fine. App.java:

public class App {

    public static void main(String[] args) throws Exception {
        Thread.currentThread().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println("Handled exception - let's log it!");
                // Logging code here
            }
        });

        System.out.println("Exception testing");
        throw new Exception("Catch and log me!");
    }

}

Running javac App.java && java App produces:

Exception testing
Handled exception - let's log it!

I suspect the advice to be given is "you shouldn't have any uncaught exceptions", but I'm not sure I guarantee that and am curious in any case as to the difference between the Maven and non-Maven results.

like image 559
Tom Jardine-McNamara Avatar asked Oct 18 '22 23:10

Tom Jardine-McNamara


1 Answers

The JavaExecMojo doesn't fork a new process. Instead it invokes the main method in the same thread as the mojo. So they must catch any exceptions thrown by the main method and emulate the jvm's behavior.

I think this is a bug in the maven exec plugin. Take a look at the source code of ExecJavaMojo.

The JavaExecMojo only uses

Thread.currentThread().getThreadGroup().uncaughtException( Thread.currentThread(), e );

to emulate the uncaught exception behavior. But this is not correct since the uncaughtException method of ThreadGroup is only invoked by the JVM if no uncaught exception handler is registered for the current thread. See the javadoc of ThreadGroup.uncaughtException(Thread t, Throwable e).

Called by the Java Virtual Machine when a thread in this thread group stops because of an uncaught exception, and the thread does not have a specific Thread.UncaughtExceptionHandler installed.

I guess what the developers wanted to do is

try {
   .... 
   // invoke main method
} catch (Exception e) {
  Thread currentThread = Thread.currentThread();
  Thread.UncaughtExceptionHandler ueh = currentThread.getUncaughtExceptionHandler();
  if (ueh == null) {
    currentThread.getThreadGroup().uncaughtException(currentThread, e);
  } else {
    ueh.uncaughtException(currentThread, e);
  }
}

This would emulate the JVM's behavior.

You can use the defaultUncaughtExceptionHandler as a quick fix.

Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
  public void uncaughtException(Thread t, Throwable e) {
    System.out.println("Handled exception - let's log it!");
    // Logging code here
  }
});

EDIT

However, using defaultUncaughtExceptionHandler produces the same java.lang.reflect.InvocationTargetException.

You are right. They did another trick. They use an own ThreadGroup implementation. See IsolatedThreadGroup

class IsolatedThreadGroup extends ThreadGroup {
     private Throwable uncaughtException; // synchronize access to this

     public IsolatedThreadGroup( String name ){
        super( name );
     }

    public void uncaughtException( Thread thread, Throwable throwable ) {
        if ( throwable instanceof ThreadDeath ) {
           return; // harmless
        }
        synchronized ( this ) {
            if ( uncaughtException == null ) {
                uncaughtException = throwable; // will be reported eventually
           }
        }
        getLog().warn( throwable );
    }
 }

The javadoc of uncaughtException enforces that

The uncaughtException method of ThreadGroup does the following:

  • If this thread group has a parent thread group, the uncaughtException method of that parent is called with the same two arguments.
  • Otherwise, this method checks to see if there is a default uncaught exception handler installed, and if so, its uncaughtException method is called with the same two arguments.
  • Otherwise, this method determines if the Throwable argument is an instance of ThreadDeath. If so, nothing special is done. Otherwise, a message containing the thread's name, as returned from the thread's getName method, and a stack backtrace, using the Throwable's printStackTrace method, is printed to the standard error stream.

So their implementation is not api compliant. That's why the defaultUncaughtExceptionHandler doesn't work.

Conclusion

Don't use the JavaExecMojo. Use the ExecMojo instead. E.g.

 mvn exec:exec -Dexec.executable="java" -Dexec.workingdir="someDir" -Dexec.args="-cp target/classes"

Specify the plugin properties in the pom and you can use placeholders. E.g.

<workingdir>${basedir}</workingdir>
<args>-cp ${project.build.outputDirectory}</args>
like image 99
René Link Avatar answered Nov 03 '22 21:11

René Link