Normally a command-line Java application will have an entry method that looks something like:
public static void main(String... args) { /* Code that launches the application/deals with command line params here */ } |
This is far too easy, there must be a way to NIH it ¬_¬. There are a few annoyances with the public static void main method.
- Args are passed in as an Array, rather than something like a List<String>
- We can’t choose to call the main method something other than main.
- It’s a bit obscure how the startup process works.
There is a common way to execute code on app startup without using a static void main method. You can use a static initialization block:
static { /* Code here run on class load. */ } |
So we could launch our application from within a static initialization block. However this presents a number of problems:
- We get a nasty error when the application finishes (Exception in thread “main” java.lang.NoSuchMethodError: main)
- We have no access to the command line arguments
- We can’t have multiple “Main” classes with this method (Both static initialization blocks will be run, if both classes are loaded)
- It doesn’t really provide any benefits over static void main.
These problems can, however, all be overcome.
Suppressing NoSuchMethodError
We can just call System.err.close(); at the end of our initialization method.
Access to the command line arguments
This one is probably the most difficult. I’ve not found any “good” way to do this, but there is a hack that works at present on Sun’s Java, though it may stop working at any point.
sun.misc.VMSupport.getAgentProperties().get("sun.java.command") |
This will give the command line string used to start the application. There are alternatives involving attaching to the currently running VM with the Agent API or shelling out to external processes and using platform-specific commands to work out the command used to start the application. If anyone knows a proper way to access the command line arguments using the public API then please let me know.
Multiple Main Classes
We need to get our initialization system to ignore invocations from classes other than the one used to start our application. This can be done by checking the length of the StackTrace. e.g.
new Throwable().fillInStackTrace().getStackTrace().length == 2; |
Reflection Examples
With these 3 issues solved it’s possible to do things like:
package MainlessDemo; import java.util.List; import static uk.co.benjiweber.realjava.mainless.Bootstrap.init; public class ReflectionMainless { static { init(); } public void main(final List<String> args) { System.out.println("Hello World"); System.err.println("From std err"); for (String arg : args) { System.out.println(arg); } } } /* Output: $ javac -cp .:./RealJava.jar ./MainlessDemo/*.java && java -cp .:./RealJava.jar MainlessDemo.ReflectionMainless Foo Bar Baz Hello World From std err Foo Bar Baz */ |
Here a statically imported “init” method instantiates our main class, invokes our non-static main method, and passes it the command line arguments. The class to instantiate can be determined by walking back up the stack trace again.
We can also inject constructor arguments if we wish:
package MainlessDemo; import java.util.List; import static uk.co.benjiweber.realjava.mainless.Bootstrap.init; public class ReflectionMainlessWithArgs { // This time we pass in the constructor argument static { init("Hello World"); } private final String message; // Even though we load another class that is Launchable it doesn't get launched. static final ReflectionMainless test = new ReflectionMainless(); public ReflectionMainlessWithArgs(final String message) { this.message = message; } public void main(final List<String> args) { System.out.println(message + " (Passed in through constructor)"); for (String arg : args) { System.out.println(arg); } } } /* Output: $ javac -cp .:./RealJava.jar ./MainlessDemo/*.java && java -cp .:./RealJava.jar MainlessDemo.ReflectionMainlessWithArgs Foo Bar Baz Hello World (Passed in through constructor) Foo Bar Baz */ |
Interface Examples
Unfortunately, apart from substituting an argument Array for a List, we ‘ve not really improved anything. However, now that we’re in control of the initialization process we can do more interesting things, like use an interface:
public interface Launchable { public void main(List<String> args); } |
package MainlessDemo; import java.util.List; import uk.co.benjiweber.realjava.mainless.Launchable; import static uk.co.benjiweber.realjava.mainless.Bootstrap.init; public class SaferMainless implements Launchable { static { init(new SaferMainless()); } public void main(final List<String> args) { System.out.println("Hello from a Launchable"); for (String arg : args) { System.out.println(arg); } } } /* Output: $ javac -cp .:./RealJava.jar ./MainlessDemo/*.java && java -cp .:./RealJava.jar MainlessDemo.SaferMainless Foo Bar Baz Hello from a Launchable Foo Bar Baz */ |
Now it’s clearer how the initialization process works, you can use your IDE to follow execution through from the init block to the main method. This also gives us the freedom to call our main method something different, and inject dependencies before reaching the main method.
package MainlessDemo; import java.util.List; import uk.co.benjiweber.realjava.mainless.Launchable; import static uk.co.benjiweber.realjava.mainless.Bootstrap.init; public class SaferAnonymousMainless { static { init(new Launchable() { public void main(final List<String> args) { SaferAnonymousMainless mainless = new SaferAnonymousMainless(); mainless.setMessage("I was injected manually"); mainless.someMethodNotCalledMain(args); } }); } private String message; public void setMessage(final String message) { this.message = message; } public void someMethodNotCalledMain(final List<String> args) { System.out.println(message); for (String arg : args) { System.out.println(arg); } } } /* Output: $ javac -cp .:./RealJava.jar ./MainlessDemo/*.java && java -cp .:./RealJava.jar MainlessDemo.SaferAnonymousMainless Foo Bar Baz I was injected manually Foo Bar Baz */ |
Source for the BootStrap.init method is here
Thanks to Faux for some of the ideas.
Yes I know this isn’t remotely a good idea, no need to tell me :)