Launcher: add generalised executable class loading support
authorEddie <dev@fun2be.me>
Sat, 6 Jun 2015 07:15:14 +0000 (08:15 +0100)
committerEddie <dev@fun2be.me>
Sat, 6 Jun 2015 07:15:14 +0000 (08:15 +0100)
The launcher was intended as a single point of launch for all the
executable applications in the project: servers and clients.

The improvements here generalise the code to support loading of any
(executable) class using its own classloader and instantiating it.

The result is that a single Java Virtual Machine (JVM) will execute
all the project applications rather than the usual case where there
is a single executable application per JVM.

Experimentation with this single-JVM approach reveals that it is only
suitable for daemon services or console (terminal) applications but not
for any GUI application that is based on Java Abstract Window Toolkit
(AWT) and/or Java Swing.

The reason is that the AWT is written to only have a single Event
Disptacher thread per JVM, unless some extremely invasive modifcations
using custom classloaders is implemented.

For examples of the code required for that review the circa 2002 project
Echidna which unfortunately had its web-site disappear from the web in 2005.
The Internet Archive has the last snapshot of it from 2005-12-17:

https://web.archive.org/web/20051217194617/http://www.javagroup.org/echidna/

The source code is still available from its SourceForge project
pages:

http://echidna.cvs.sourceforge.net/viewvc/echidna/echidna/

src/uk/ac/ntu/n0521366/wsyd/Launcher.java

index 03b1293..e036e34 100644 (file)
@@ -25,73 +25,146 @@ package uk.ac.ntu.n0521366.wsyd;
 
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
+import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import uk.ac.ntu.n0521366.wsyd.server.ServerSocial;
+import uk.ac.ntu.n0521366.wsyd.server.ServerChat;
 import uk.ac.ntu.n0521366.wsyd.management.ServerManagement;
 import uk.ac.ntu.n0521366.wsyd.client.ClientGUI;
 
 /**
+ * Allow execution of multiple independent application classes in a single Java Virtual Machine under
+ * control of a single Launcher class.
+ *
+ * For testing and running large projects that contain more than one executable application class
+ * this launcher can be set as the Project's 'main' class to be executed when the Project us built
+ * and run.
+ *
+ * The launcher will create and start a Thread for each executable application class inside the
+ * same Java Virtual Machine that the launcher is executing in.
+ *
+ * The alternative is to manually launch each executable application class each time the project
+ * needs to be executed as a whole.
+ *
+ * This is extremely useful in development IDEs where the project properties will only accept a
+ * single ('main') class for execution when the project is run.
+ *
+ * It is also useful in packaging complete projects to make it easy for the end-user to start
+ * all the required executable classes without external operating-system specific shell scripts
+ * or other mechanisms.
  *
  * @author Eddie Berrisford-Lynch <dev@fun2be.me>
  */
 public class Launcher {
-    
+
+    enum EXECUTE { NO, YES};
+    /**
+     * Wrap an executable application class in a Runnable that can be instantiated in a separate Thread.
+     */
     static class App implements Runnable {
 
         private final Class<?> _classRef;
         private final String[] _args;
+        private final EXECUTE _shouldExecute;
 
-        public App(Class<?> classRef, String[] args) {
+        /**
+         * Build an executable application.
+         *
+         * @param classRef The application class (must contain the method: static void main(String[])
+         * @param args The command-line arguments to pass to the application's main() method
+         * @param execute Whether this application should be started
+         */
+        public App(Class<?> classRef, String[] args, EXECUTE execute) {
             _classRef = classRef;
             _args = args;
+            _shouldExecute = execute;
         }
 
+        /**
+         * Instantiate an executable class and execute it.
+         *
+         * Execute the static main() method of an executable class in its own thread
+         * and run the class's application in a Thread of execution in the current
+         * Java Virtual Machine rather than as a stand-alone operating system process.
+         */
         @Override
         public void run() {
-            try {
-                ClassLoader cl = _classRef.getClassLoader();
-                try {
-                    Object o = cl.loadClass(_classRef.getName()).newInstance();
-                    Class<?> newClass = o.getClass();
-                    Method m = newClass.getMethod("main", String[].class);
-                    m.invoke(null, (Object) this._args);
-                } catch (ClassNotFoundException ex) {
-                    Logger.getLogger(Launcher.class.getName()).log(Level.SEVERE, null, ex);
-                } catch (InstantiationException ex) {
+            if (this._shouldExecute == EXECUTE.YES) {
+                 Logger.getLogger(Launcher.class.getName()).log(Level.INFO, MessageFormat.format("Starting app {0}", _classRef.getClass().getName()));
+               try {
+                    // Use the class loader that knows how to load the classes imported by the application class
+                    ClassLoader cl = _classRef.getClassLoader();
+                    try {
+                        Class<?> appClass = cl.loadClass(_classRef.getName()).newInstance().getClass();
+                        Method main = appClass.getMethod("main", String[].class);
+                        Logger.getLogger(App.class.getName()).log(Level.INFO, MessageFormat.format("Invoking {0}.main(_args)", _classRef.getName()));
+                        // invoking a static (not instance) method so the object instance is null
+                        main.invoke(null, (Object) this._args);
+                    } catch (ClassNotFoundException | InstantiationException ex) {
+                        Logger.getLogger(Launcher.class.getName()).log(Level.SEVERE, null, ex);
+                    }
+                } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
                     Logger.getLogger(Launcher.class.getName()).log(Level.SEVERE, null, ex);
                 }
-            } catch (NoSuchMethodException ex) {
-                Logger.getLogger(Launcher.class.getName()).log(Level.SEVERE, null, ex);
-            } catch (SecurityException ex) {
-                Logger.getLogger(Launcher.class.getName()).log(Level.SEVERE, null, ex);
-            } catch (IllegalAccessException ex) {
-                Logger.getLogger(Launcher.class.getName()).log(Level.SEVERE, null, ex);
-            } catch (IllegalArgumentException ex) {
-                Logger.getLogger(Launcher.class.getName()).log(Level.SEVERE, null, ex);
-            } catch (InvocationTargetException ex) {
-                Logger.getLogger(Launcher.class.getName()).log(Level.SEVERE, null, ex);
             }
-
+            else; // do nothing and return immediately
         }
     }
+
+    /**
+     * List of executable application classes to execute.
+     */
     static ArrayList<App> apps;
+    
+    /**
+     * Quantity of ClientGUI processes to start
+     */
+    static int clientQty = 1;
 
+    /**
+     * Execute each of the application classes in its own Thread.
+     *
+     * Use "--clients X" command-line option to set the number of ClientGUI applications to start.
+     *
+     * @param args the launcher's command-line arguments into each executable application's
+     * main() method.
+     */
     public static void main(String[] args) {
-        
+
         apps = new ArrayList<>();
-        
-        apps.add(new App(    ServerSocial.class, args));
-        apps.add(new App(ServerManagement.class, args));
-        for (int qty = 1; qty <= 1; qty++) {
-            apps.add(new App(ClientGUI.class, args));
+
+        // possibly set number of clients to create from command line options
+        boolean getClients = false;
+        for (String arg: args)
+            if (getClients) {
+                clientQty = Integer.parseInt(arg);
+                break;
+            } else if (arg.equals("--clients")) {
+                getClients = true;
+            } else if (arg.equals("--help")) {
+                System.out.println(
+                    "Usage: java -cp . uk.ac.ntu.n0521366.wsyd.Launcher [options]\n" +
+                    "    --help\tthis usage hint\n" +
+                    "    --clients\tnumber of clients to create\n" +
+                    "    --server\tIP address (or host name) of Social Server\n" +
+                    "    --announce\tUse multicast group announcements for host discovery\n"
+                );
+                return;
+            }
+
+        // create Runnable objects that encapsulate each of the executable application classes
+        apps.add(new App(    ServerSocial.class, args, EXECUTE.YES));
+        apps.add(new App(    ServerChat.class, args, EXECUTE.NO));
+        apps.add(new App(ServerManagement.class, args, EXECUTE.YES));
+        for (int qty = 1; qty <= clientQty; qty++) {
+            apps.add(new App(ClientGUI.class, args, EXECUTE.YES));
         }
-        
+
         for (App app: apps) {
-            System.out.println("Starting app " + app._classRef.getTypeName());
+            // start each executable application class in its own Thread
             new Thread(app).start();
         }
     }
-    
 }