Standalone-applikationer i Java

Rena kommandoradsapplikationer i Java kanske inte hör till vanligheterna nu mera, men det finns tillämpningar. Jag håller för tillfället på med ett sådant program. Det är en slags (av prestandaskäl) multitrådad övervakningsapplikation. Varje tråd ligger och pollar en databas efter något att arbeta med i ganska täta (några sekunders) intervall. När tråden hittar något att processa, så pratar den bl.a. JDBC med en stordatorapplikation.

Denna typ av beteende skulle vara ganska svårt att få till inom ramen för en appserver. Att starta trådar är ju inte aktuellt. Att använda Quartz eller ha ett cron-jobb som anropar en EJB via wget eller liknande skulle vara möjligt men verkar lite krystat, speciellt då den pollar så pass ofta.

En vanlig Java-applikation fick det bli, således. Vad bör man tänka på när man skriver en sådan applikation? Den är ju nästan som en slags server, som skall köra kontinuerligt. Den får inte gå ner så lättvindigt och den kommer sannolikt köra på en Linuxburk i ett serverrum någonstans utan något egentligt användargränssnitt. Här kommer således några tips för den standalone-inriktade.

Lägg till en Shutdown hook

En applikation som inte har någon som helst användargränssnitt går inte att stänga ner på annat sätt än med t.ex. kill på Linux. Om man inte aktar sig så kommer en sådan applikation aldrig att stänga ner ”rent”, d.v.s. stänga eventuella JDBC-kopplingar, RMI-kopplingar, råa sockets och liknande. Sannolikheten är väl kanske stor att OS:et eller Java gör det åt oss, men det är inte så elegant. Då kan man ta till anropet addShutdownHook().

Runtime.getRuntime().addShutdownHook(new Thread() {
   public void run() {
      logger.info("Exiting cleanly");
      myConnection.close();
   }
});

Koden man skriver i run() kommer köras just innan JVM:en stänger ner, åtminstone i många fall. Vid en vanlig kill på Linux körs den, men inte en kill -9. Gör man Ctrl+C i en linuxterminal så körs den också. Gör man Ctrl+C på Windows verkar den dessvärre inte köras – men det är i alla fall bättre än ingenting.

Fånga alla exceptions

I en sådan här applikation är det inte särskilt ändamålsenligt att kasta vidare Exception, RuntimeException eller Error från main(). Eftersom ingen lär titta på vad som kommer på konsollen (om man inte pipar den någonstans eller så), så är det bättre att logga alla sådana exceptions direkt till fil i stället. Kanske finns det till och med något övervakningssystem som är intresserat när sådant händer? Eller skall applikationen rent av startas om automatiskt (se nedan)?

Throwable är superklass till alla exceptions, så det är mycket enkelt.

private static final Logger logger = Logger.getLogger(App.class);
public static void main(String[] args) {
   try {
      // App logic goes here
   }
   catch (Throwable e) {
      logger.error(e);
      System.err.println("A serious error occurred, exiting");
   }
}

För att detta skall fungera fullt ut, så måste man dock lägga till kodsnutten på toppnivån i alla Runnable-implementationer (alla trådar alltså) man skriver. Detta kan man komma undan genom att använda uncaughtExceptionHandler i stället:

Thread.setDefaultUncaughtExceptionHandler(
   new Thread.UncaughtExceptionHandler() {
      public void uncaughtException(Thread t, Throwable e) {
         // Handle error here
      }
});

Detta slår igenom för alla applikationens trådar, så det räcker att göra det en gång.

Kolla om applikationen redan kör

För applikationer som lyssar på TCP-portar, och många andra, är det problematiskt om man råkar starta mer än en instans av applikationen. För att undvika det kan man lägga till kod som kollar om det redan kör en instans. Den vanligaste åtgärden när en sådan situation uppstår är väl att skriva ut ett felmeddelande och sedan avsluta. Motsvarigheten i GUI-världen vore att ge den befintliga instansen fokus och sedan avsluta – så fungerar många Windowsprogram.

Hur man gör detta i Java är inte alldeles självklart och det finns lite olika varianter – dessutom finns en massa dåliga idéer om detta på webben, visar det sig. Det bästa tycker jag är att öppna en fil för exklusiv läsning vid uppstart. Om den redan är låst så kör applikationen redan. Detta, till skillnad från att bara skriva en fil som man tar bort vid avslut, bör fungera även om JVM:en krashar t.ex. Så här kan man skriva:

final String fileName =
   System.getProperty("java.io.tmpdir") + "myApp.lock";
final FileChannel channel =
   new RandomAccessFile(fileName, "rw").getChannel();
if (channel.tryLock() == null) {
   System.out.println("Already running");
}
else {
   System.out.println("Not running");
}

Skicka mail vid problem

Om man inte har något fullödigt övervakningssystem som kan hålla koll på att applikationen är uppe, så kan man sätta upp en enkel mailskickarfunktion i stället. Om man t.ex. får ett toppnivåsexception så kan man skicka ett mail som talar om det, och eventuellt skicka med en loggfil. På Linux är det enkelt att skriva ett skript som gör det och exekvera detta från javakoden. Annars kan man, plattformsoberoende, låta Java skicka mailet själv.

JavaMail är ett mailpaket från Sun som ingår i Java EE, och kan laddas ner för Java SE. Det finns också en uppsjö enklare SMTP-klienter i Java på webben som man kan använda.

Starta om sig själv

En annan, kanske lite mera udda, åtgärd skulle kunna vara att lägga till kod som automatiskt startar om applikationen om något går ordentligt fel. Jag har tidigare använt det i applikationer med GUI:n som kör på publika maskiner (s.k. kiosker). Enklast är att använda ett skript som startar om applikationen och exekvera det från javakoden. På så sätt kan man få till en liten paus mellan stopp och start, och undvika eventuella ”port in use”-problem. Pausen är enkel att få till på Linux, för där finns ju kommandot ”sleep”. På Windows kan man i stället använda något litet program som pausar, t.ex. sleep.exe som finns här.

Omstartskript, restart.sh:

#!/bin/bash
echo Restarting application in 10 s
sleep(10)
cd /opt/myapp
java -jar myapp.jar

Och i javaprogrammet kan man skriva så här:

void handleSeriousError() {
   Runtime.getRuntime().exec("bash /opt/myapp/restart.sh");
   System.exit(-1);
}

Skriv en ”fjärrkontroll

Om man vill ha mera kontroll på hur en sådan här applikation stoppas och hanteras i övrigt så kan man satsa på att skriva ett litet ”fjärrkontrollsprogram”. Det kan man t.ex. göra med råa sockets eller RMI. Här har jag valt att skriva lite enkel socketkod.

Själva applikationsarbetet utförs i en worker-tråd som startas i början av main(). Huvudtråden lyssnar sedan efter uppkopplingar på porten 12345. Datat som skickas av fjärrkontrollen är mycket enkelt: Ett kommando följt av ett semikolon. Det finns två kommandon: ”printTime” och ”quit”. PrintTime skriver ut vad klockan är, och ”quit” avslutar applikationen. Man kunde mycket väl låta kommandon returnera information till fjärrkontrollen också. När det kommer ett quit-kommando så sätts en flagga i worker-tråden så att den avslutas så fort som möjligt, och huvudtråden väntar på att den skall göra det m.h.a. join().

Så här ser huvudprogrammet ut. Felhanteringen är lite bristfällig för att spara plats:

public class App {
   private static final String QUIT = "quit";
   private static final String PRINT_TIME = "printTime";


   public static void main(String[] args) throws
      IOException, InterruptedException {
       // Start a worker thread that sits in a loop, doing work
       final ApplicationLogic appLogic = new ApplicationLogic();
       final Thread workTread = new Thread(appLogic);
       workTread.start();


       // Listen for remote control messages.
       // Returns when it's time to exit.
       listenForMessages();
       appLogic.exit();


       // Wait for worker thread to exit
       workTread.join(10000);
       System.out.println("Exiting cleanly");
    }


    private static class ApplicationLogic implements Runnable {
       private boolean timeToExit;
       public void run() {
          try {
             while (!timeToExit) {
                // Any application logic goes here
                System.out.print("Working, ");
                Thread.sleep(2000);
             }
          } catch (InterruptedException e) {
             System.err.println("Worker thread interrupted");
          }
       }


       public synchronized void exit() {
          System.out.println("ApplicationLogic shall exit");
          timeToExit = true;
       }
    }


    private static void listenForMessages() {
       try {
          final ServerSocket serverSocket = new ServerSocket();
          serverSocket.bind(new InetSocketAddress(12345));


          // Listen for new connections over and over
          while (true) {
             System.out.println("Listening for connections");
             final Socket socket = serverSocket.accept();
             System.out.println("Got connection");
             StringBuilder msg = new StringBuilder();
             final InputStream inputStream = socket.getInputStream();
             // Read the messages
             while (true) {
                final int c = inputStream.read();
                if (c == -1) {
                   // Client disconnected, break out of
                   // inner loop and listen again
                   System.out.println("Connection closed");
                   break;
                }
                else if (c == ';') {
                   boolean cont = handleMessage(msg.toString());
                      if (!cont) {
                         // We should exit
                         serverSocket.close();
                         return;
                      }
                   msg = new StringBuilder();
                }
                else {
                   msg.append((char) c);
                }
             }
          }
       } catch (IOException e) {
          e.printStackTrace();
       }
    }


    private static boolean handleMessage(String msg) {
       boolean cont = true;
       if (msg.equalsIgnoreCase(QUIT)) {
          cont = false;
       }
       else if (msg.equalsIgnoreCase(PRINT_TIME)) {
          System.out.println("The time is " + new Date());
       }
       else {
          System.err.println("Unknown message " + msg);
       }
       return cont;
    }
}

Här kommer fjärrkontrollsdelen som är mycket enkel – Java är riktigt coolt ibland, eller hur?

Programmet tar ett kommando som argument.

public class RemoteControl {
    public static void main(String[] args) throws IOException {
       if (args.length != 1) {
          System.err.println("Please specify a command");
          return;
       }
       final Socket socket = new Socket("localhost", 12345);
       final String command = args[0];
       System.out.print("Sending command " + command + "... ");
       socket.getOutputStream().write((command + ";").getBytes());
       System.out.println("Done");
       socket.close();
    }
}

Med allt detta på plats så kan man stänga ner applikationen på ett kontrollerat sätt genom att göra något i stil med

java test.RemoteControl quit

i stället för att göra

kill (pid)

eller vad det nu kan vara – mycket trevligare.

Notera att man också kan göra fjärrkontrollen till en del av huvudprogrammet, så att det fungerar som klient eller server beroende på någon parameter. Kanske lite elegantare?

Happy coding!