I min introduktions-artikel om Terracotta lovade jag återkomma med körbar exempelkod! Sagt och gjort…

Jag har gjort en minimalistisk chat-applikation. ”Arkitekturen” (om man kan använda så stora ord för så små program) består av två trådar som håller koll på en text-sträng. Text-strängen innehåller den senaste chat-raden som någon lagt ut. Den ena tråden läser från tangentbordet och sätter text-strängen till det som skrivs in (och lägger till ditt namn längst fram i strängen), medan den andra tråden skriver ut strängen på skärmen när den får en signal om att den ändrats.

Eftersom String-objektet är immutable (kan inte ändras på) så byts det ju ut hela tiden. Det gör att det inte är meningsfullt att använda själva text-strängen som trådlås. Istället har jag allokerat en sträng-array med ett element, så att jag kan låsa tråd-åtkomsten på array-objektet och byta ut text-strängen som ligger i arrayen.

Hela programmet, med Eclipse-projektfiler och en enkel Windows-bat-fil för att köra klienten, finns att ladda ner som zip-fil här, men eftersom det faktiskt är så lite kod så går jag igenom den här först. Först har vi koden som hanterar åtkomsten till text-strängen:

    // this field is terracotta-clustered
    private String[] chatLineHolder = new String[1]; 

    public String getChatline() {
        synchronized (chatLineHolder) {
            try {
                chatLineHolder.wait();
                return chatLineHolder[0];
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public void setChatline(String cl) {
        synchronized (chatLineHolder) {
            chatLineHolder[0] = nick + ": " + cl;
            chatLineHolder.notifyAll();
        }
    }

Variabeln ”nick” som används i setChatLine() är en medlemsvariabel som innehåller ditt smeknamn, och deklareras så här:

    private String nick = "Anonymous";

Vi har sedan två metoder som representerar läs- respektive skriv-tråden i programmet. (Observera att det bara är läs-tråden som skapas i en egen tråd — skrivtråden körs i programmets huvudtråd). De ser ut så här:

    private void doReadLoop() throws IOException {
        System.out.println("Go ahead and chat");
        BufferedReader in = 
            new BufferedReader(new InputStreamReader(System.in));
        String line = in.readLine();
        while (!"quit".equals(line)) {
            setChatline(line);
            line = in.readLine();
        }
    }

    private void startListener() {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                while(true) {
                    String chatline = getChatline();
                    System.out.println(chatline);
                }
            }
        });
        thread.start();
    }

Slutligen, för att göra det till ett komplett program, så syr vi ihop det hela med en main()-metod:

     public static void main(String[] args) throws IOException {
        App app = new App();
        if (args.length > 0) {
            app.nick = args[0];
        }
        app.startListener();
        app.doReadLoop();
        System.exit(0); // kills listener thread as well
    }

Som du ser i koden går det alltså utmärkt att köra programmet stand-alone, men det är ju lite tråkigt i längden att chatta med sig själv (om man inte är lagd åt det hållet).

Om man istället startar programmet med Terracotta och den bifogade tc-config.xml-filen, så klustras applikationen. Vad som faktiskt klustras är bara den array som håller i text-strängen. Varje instans av programmet som startas (mot samma Terracotta-server) innehåller var sin skrivar- och var sin lyssnar-tråd, som alla hanterar samma (klustrade) objekt. När vi sedan anropar ”notifyAll()” i setChatline()-metoden så signaleras alla trådar i alla klustrade VMar. Vi får den önskade effekten: när något program skriver en chat-rad så sprids den ut till alla klustrade chat-klienter. Prova själv!

Konfigurationsfilen tc-config.xml ser ut så här i sin helhet:

<?xml version="1.0" encoding="UTF-8"?>
<tc:tc-config xmlns:tc="http://www.terracotta.org/config">
  <application>
    <dso>
      <roots>
        <root>
          <field-name>se.cygni.tcchat.App.chatLineHolder</field-name>
        </root>
      </roots>
      <locks>
        <autolock>
          <method-expression>* se.cygni.tcchat.App.*(..)</method-expression>
          <lock-level>write</lock-level>
        </autolock>
      </locks>
      <instrumented-classes>
        <include><class-expression>se.cygni.tcchat..*</class-expression></include>
      </instrumented-classes>
    </dso>
  </application>
</tc:tc-config>

Vi pekar ut tre saker i tc-config.xml:

  1. Vilka objekt ska vara klustrade, dvs ska ha ett tillstånd som delas mellan alla klustrade VMar. Det ligger i elementet ”root”.
  2. Vilka metoders synchronized-låsningar ska hållas koll på — specificeras i elementet ”locks”.
  3. I vilka klasser ska Terracotta fläta in ”instrumenterad” övervakningskod för att kunna utföra önskad effekt. Det ligger i elementet ”instrumented-classes”, och jag har valt det enklaste: ta med alla klasser i Java-paketet som applikationen ligger i (och alla underpaket).

Ladda hem programmet och prova själv så får du se hur enkelt det är. Du måste naturligtvis också ladda hem Terracotta först, eftersom det kräver att du kör en Terracotta-server för att köra klustrat (men du kan faktiskt köra programmet oklustrat utan att ens ha Terracotta installerat — det är en av fördelarna med aspektorienterade ramverk: använder du dem inte så behöver du inte känna till dess existens).

Terracotta kan ju användas på flera sätt och med flera syften, och det här kan väl sägas vara ett enkelt exempel på ”klustring för interprocess-kommunikation”!