Service Component Architecture (SCA) praktycznie - zestawienie środowiska z Apache Tuscany i Eclipse IAM

Z Jacek Laskowski - Wiki Projektanta Java EE

Service Component Architecture (w skrócie SCA) to kolejna specyfikacja, która może niejednemu spędzać sen z powiek i wielu zachodzi w głowę, co takiego oferuje, czego do tej pory nie było, albo nie używaliśmy. Czasami nieświadomość naszej niewiedzy może być zbawienna, bo kiedy już dowiemy się o niej, zwykle nie pozostaje nam nic innego jak po prostu się podszkolić, a to może być kolejnym zadaniem na naszej liście "Do zrobienia".

SCA, jako (jeszcze jeden) model budowania aplikacji opartych o usługi, oddzielający implementację od sposobu komunikacji ze światem zewnętrznym (czy to w roli usługodawcy - serwera, czy usługobiorcy - klienta), posłuży nam do ukrycia szczegółów obsługi różnych protokołów, co powinno skutkować znacznym uprozczeniem tworzenia rozwiązań rozproszonych. Mając tę wiadomość nie ma mowy o (z)ignorowaniu SCA. Nie po tym, czego się właśnie dowiedziałeś/-aś.

W tym artykule przedstawię uruchomienie usługi udostępnianej protokołem Atom, bez wchodzenia w szczegóły samego protokołu (kto by tam miał głowę do poznawania ich wszystkich, nie wspominając o ich zakamarkach). Z dokładnością do pewnych założeń implementacyjnych, użycie protokołu nie wpływa na tworzony komponent SCA i właśnie o to w tym wszystkich chodzi. My, programiści javowi, wiemy, że kontrakt opiera się na interfejsie, a sama implementacja to detal (który niestety, ale często wpływa na nasze samopoczucie). Będziemy mogli przekonać się praktycznie, bez wchodzenia w teoretyczne rozważania, dlaczego tworzenie komponentów SCA, bez koniecznośći rozpoznawania protokołów komunikacyjnych, jest tak praktyczne w tworzeniu rozproszonych aplikacji usługowych zgodnych z założeniami Service-Oriented Architecture (SOA). "Praktycznie" może być synonimem "prościej" i z takim nastawieniem podejdziemy do tematu poznawania SCA i obsługi komunikacji za pomocą protokołu Atom.

Artykuł opiera się na dokumentacji Ready, Set, Go - Getting started with Tuscany, w której znalazłem ciekawy sposób na uruchomienie SCAleń z wbudowanym kontenerem servletów (w naszym przypadku będzie to Jetty, ale możliwe jest również uruchomienie z Tomcatem) oraz Eclipse. Wystarczyła mi sama obserwacja, że org.apache.tuscany.sca.host.embedded.SCADomain uruchamia usługi, których potrzebuje do poprawnego uruchomienia SCAlenia, np. serwer HTTP, kiedy binding.atom jest w użyciu i nie mogłem się po prostu oprzeć, aby nie popróbować się z tematem. Doskonałe uproszczenie nauki SCA. Pozostało jedynie spróbować.

Do pełni szczęścia, zakłada się wcześniejszą instalację następującego oprogramowania (obok wykorzystywana wersja):

Lektura wcześniejszych moich artykułów nt. SCA może znacznie pomóc w jego rozpoznaniu:

Kody źródłowe projektu znajdują się w repozytorium Mercuriala na Google Code jako sca-praktycznie.

Spis treści

Konfiguracja projektu sca-praktycznie

Podzielimy nasze praktyczne podejście do poznawania SCA na 4 typowe etapy projektowe: konfiguracja, oprogramowanie, uruchomienie i (manualne) testowanie projektu.

Stworzenie struktury projektowej z Apache Maven - mvn archetype:generate

Jeszcze tym razem zaczniemy z poziomu linii poleceń (przyzwyczajenie drugą naturą człowieka, jak powiadają) od mvn archetype:generate.

mvn archetype:generate -B \
   -DarchetypeArtifactId=maven-archetype-quickstart \
   -DgroupId=pl.jaceklaskowski.sca \
   -DartifactId=sca-praktycznie \
   -Dversion=1.0

W ten sposób stworzyłem strukturę projektu sca-praktycznie.

Import projektu do IDE - Eclipse IAM

Zakładając zainstalowaną obsługę projektów mavenowych w Eclipse przez wtyczkę Eclipse IAM, wystarczy wybrać menu Import > Maven 2 > Maven 2 Project

Plik:eclipse-import-maven2-project.png

i wskazać na przed chwilą stworzony projekt sca-praktycznie - wcisnąć przycisk Next >, a później Browse... przy polu Maven 2 Project Location.

Plik:eclipse-maven2-project-import-selected.png

Wciskamy przycisk Finish.

Nie jest to specjalnie konieczne, ale dobrze jest zaprowadzić ład i porządek w nowoutworzonym projekcie, i skasować klasy src/main/java/pl/jaceklaskowski/sca/App.java oraz src/test/java/pl/jaceklaskowski/sca/AppTest.java, z których nie będziemy korzystać.

Dodanie zależności projektowych - Apache Tuscany 1.6 i JUnit 4.7

Z uruchomionym projektem w Eclipse IDE możemy rozpocząć konfigurację zależności projektowych. Eclipse IAM dostarcza specjalizowany edytor plików pom.xml - Maven 2 Pom Editor, więc sprawa sprowadza się do wyboru odpowiednich zakładek (Dependencies) i przycisków (New ..., bądź Edit).

Plik:eclipse-iam-pom-editor.png

Można również skorzystać z menu kontekstowego Maven 2 > Manage Dependencies.

Plik:eclipse-iam-manage-deps.png

Dodajemy następujące zależności projektowe:

  • junit:junit:4.7 - najnowsze wydanie JUnit dostępne w repozytorium mavenowym, które umożliwia wykorzystanie adnotacji do pisania testów
  • org.apache.tuscany.sca:tuscany-implementation-java-runtime:1.6 - podstawowa zależność do uruchomienia implementation.java z Tuscany. Przez swoje zależności przechodnie pobiera wymagane biblioteki do podstawowej (=implementacje komponentów w Javie) pracy z Apache Tuscany
  • org.apache.tuscany.sca:tuscany-binding-atom:1.6 - możliwość użycia binding.atom
  • org.apache.tuscany.sca:tuscany-binding-atom-abdera:1.6 - możliwość uruchomienia binding.atom
  • org.apache.tuscany.sca:tuscany-host-embedded:1.6 - podstawa do uruchomienia wbudowanego serwera HTTP dla wiązań (ang. binding), które korzystają z tego protokołu, np. Atom
  • org.apache.tuscany.sca:tuscany-host-jetty:1.6 - kontener servletów Jetty, do uruchomienia serwera HTTP. Jeden z dwóch, poza Tomcatem.

Ostatecznie plik pom.xml powinien przedstawiać się jak poniżej (zakładka Source w edytorze Maven 2 Pom Editor).

<project xmlns="http://maven.apache.org/POM/4.0.0"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>pl.jaceklaskowski.sca</groupId>
  <artifactId>sca-praktycznie</artifactId>
  <packaging>jar</packaging>
  <version>1.0</version>
  <name>sca-praktycznie</name>
  <url>http://www.jaceklaskowski.pl/wiki/Service Component Architecture (SCA) praktycznie - zestawienie środowiska z Apache Tuscany i Eclipse IAM</url>
  <build>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.7</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.tuscany.sca</groupId>
      <artifactId>tuscany-implementation-java-runtime</artifactId>
      <version>1.6</version>
    </dependency>
    <dependency>
      <groupId>org.apache.tuscany.sca</groupId>
      <artifactId>tuscany-binding-atom</artifactId>
      <version>1.6</version>
    </dependency>
    <dependency>
      <groupId>org.apache.tuscany.sca</groupId>
      <artifactId>tuscany-binding-atom-abdera</artifactId>
      <version>1.6</version>
    </dependency>
    <dependency>
      <groupId>org.apache.tuscany.sca</groupId>
      <artifactId>tuscany-host-embedded</artifactId>
      <version>1.6</version>
    </dependency>
    <dependency>
      <groupId>org.apache.tuscany.sca</groupId>
      <artifactId>tuscany-host-jetty</artifactId>
      <version>1.6</version>
    </dependency>
  </dependencies>
</project>

Przy każdorazowej zmianie w pom.xml, w widoku Events pojawią się komunikaty o przebudowaniu projektu. Warto na niego spojrzeć, kiedy przez dłuższy czas Eclipse przestanie odpowiadać - pobieranie zależności może trochę potrwać i zamrozić Eclipse na dłuższy czas.

Oprogramowanie projektu sca-praktycznie

Interfejs usługowy - pl.jaceklaskowski.sca.atom.Slownik

W skrócie, założeniem protokołu Atom jest udostępnianie kolekcji danych. Na tym kończy się moja wiedza o protokole Atom, ale mimo to wiem, że bez tego subskrypcja wiadomości w Sieci (blogi, serwisy, itp.) nie byłaby tak prosta i właśnie w tym artykule mam niepowtarzalną okazję poznać go w działaniu.

Tworzymy interfejs zdalny usługi - pl.jaceklaskowski.sca.atom.Slownik. W Tuscany udostępnienie kolekcji via Atom jest skojarzone z realizacją interfejsu org.apache.tuscany.sca.data.collection.ItemCollection.

Warto również zwrócić uwagę na użycie adnotacji org.osoa.sca.annotations.Remotable, która deklaratywnie wskazuje interfejs jako udostępniony zewnętrznym klientom.

package pl.jaceklaskowski.sca.atom;
 
import org.apache.tuscany.sca.data.collection.ItemCollection;
import org.osoa.sca.annotations.Remotable;
 
@Remotable
public interface Slownik extends ItemCollection {
}

Implementacja usługi - pl.jaceklaskowski.sca.atom.impl.SlownikPolskoNiemiecki

Implementacja usługi Slownik to po prostu realizacja interfejsu pl.jaceklaskowski.sca.atom.Slownik. Nic nadzwyczajnego wiedząc, że konieczne jest dostarczenie treści do metod operujących na pewnej strukturze danych, w tym przypadku java.util.Map.

Istotnym elementem tej klasy jest wykorzystanie adnotacji org.osoa.sca.annotations.Scope, której wartość COMPOSITE oznacza (patrz rozdział "1.2.4.3. Composite scope" w specyfikacji SCA Service Component Architecture Java Common Annotations and APIs str. 3):

All service requests are dispatched to the same implementation instance for the lifetime of the containing composite.

Innymi słowy, tworzymy strukturę dostępną tak długo, jak długo istnieje SCAlenie, czyli wystarczająco długo, aby zmiany w niej były widoczne między kolejnymi żądaniami klienckimi. Domyślna wartość STATELESS powoduje, że struktura staje się bezstanowa bez przechowywania zmian między żądaniami.

package pl.jaceklaskowski.sca.atom.impl;
 
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
import org.apache.tuscany.sca.data.collection.Entry;
import org.apache.tuscany.sca.data.collection.Item;
import org.apache.tuscany.sca.data.collection.NotFoundException;
import org.osoa.sca.annotations.Scope;
 
import pl.jaceklaskowski.sca.atom.Slownik;
 
@Scope("COMPOSITE")
public class SlownikPolskoNiemiecki implements Slownik {
 
    private Map<String, Item> slowka = new HashMap<String, Item>();
 
    @Override
    public void delete(String slowoPl) throws NotFoundException {
        System.out.println("Kasujemy " + slowoPl);
        slowka.remove(slowoPl);
    }
 
    @Override
    public Item get(String slowoPl) throws NotFoundException {
        System.out.println("Pobranie opisu dla słowa: " + slowoPl);
        return slowka.get(slowoPl);
    }
 
    @SuppressWarnings("unchecked")
    @Override
    public Entry<String, Item>[] getAll() {
        int liczbaSlowek = slowka.size();
        System.out.println("Pobieranie wszystkich słów (razem: " + liczbaSlowek + ")");
        Entry<String, Item>[] entries = new Entry[liczbaSlowek];
        int i = 0;
        for (Map.Entry<String, Item> e : slowka.entrySet()) {
            entries[i++] = new Entry<String, Item>(e.getKey(), e.getValue());
        }
        return entries;
    }
 
    @Override
    public String post(String slowkoPl, Item item) {
        System.out.println("Wpisanie słowa do słownika: " + slowkoPl);
        slowka.put(slowkoPl, item);
        return slowkoPl;
    }
 
    @Override
    public void put(String slowkoPl, Item item) throws NotFoundException {
        System.out.println("Edycja słowa " + slowkoPl);
        slowka.put(slowkoPl, item);
    }
 
    @SuppressWarnings("unchecked")
    @Override
    public Entry<String, Item>[] query(String slowkoPl) {
        System.out.println("Zapytanie o słowo: " + slowkoPl);
        List<Entry<String, Item>> slowkaList = new ArrayList<Entry<String, Item>>();
 
        Item slowkoDe = slowka.get(slowkoPl);
        if (slowkoDe != null) {
            slowkaList.add(new Entry<String, Item>(slowkoPl, slowkoDe));
        }
        return slowkaList.toArray(new Entry[slowkaList.size()]);
    }
 
}

Deskryptor SCAlenia - slownik.composite

Tworzę katalog src/main/resources/META-INF, a w nim deskryptor slownik.composite.

<?xml version="1.0" encoding="UTF-8"?>
<composite xmlns="http://www.osoa.org/xmlns/sca/1.0" xmlns:tuscany="http://tuscany.apache.org/xmlns/sca/1.0"
  targetNamespace="http://slownik" name="SlownikComposite">
  <component name="SlownikPolskoNiemiecki">
    <implementation.java class="pl.jaceklaskowski.sca.atom.impl.SlownikPolskoNiemiecki" />
    <service name="Slownik">
      <tuscany:binding.atom uri="http://localhost:8080/slownik/pl-de" title="Tytuł kanału" />
    </service>
  </component>
</composite>

Uruchomienie projektu sca-praktycznie

Pomysł uruchomienia projektu z poziomu Eclipse został zaczerpnięty ze wspomnianego artykułu Ready, Set, Go - Getting started with Tuscany. Było to tak niezwykle odkrywcze dla mnie, że z pewnością był to jeden z powodów, dla których postanowiłem napisać ten artykuł.

Cała idea tej sztuczki polega na wykorzystaniu klasy org.apache.tuscany.sca.host.embedded.SCADomain, która wczyta konfigurację SCAlenia i uruchomieniu domeny SCA z poziomu Eclipse. Dla wielu nic odkrywczego, ale mnie wystarczy niewiele, abym pisał peany nt. takich błahostek :)

Stworzenie klasy uruchomieniowej dla Eclipse - pl.jaceklaskowski.sca.eclipse.Launcher

Tworzymy klasę pl.jaceklaskowski.sca.eclipse.Launcher w katalogu src/main/java.

package pl.jaceklaskowski.sca.eclipse;
 
import org.apache.tuscany.sca.host.embedded.SCADomain;
 
public class Launcher {
 
    public static void main(String[] args) throws Exception {
        System.out.println("Uruchomienie domeny SCA...");
        SCADomain scaDomain = SCADomain.newInstance("META-INF/slownik.composite");
        System.out.println("Słownik gotowy do działania (wciśnij ENTER, aby zakończyć)");
        System.in.read();
        System.out.println("Zatrzymanie...");
        scaDomain.close();
        System.out.println();
    }
 
}

Definicja uruchomienia w Eclipse - Run > Run Configurations... > Java Application

W Eclipse korzystamy z menu Run > Run Configurations... > Java Application do zdefiniowania naszej klasy uruchomieniowej pl.jaceklaskowski.sca.eclipse.Launcher.

Plik:eclipse-run-configuration.png

Odpalenie uruchomienia

Po zdefiniowaniu uruchomienia wybieramy Run (przycisk w postaci zielonej strzałki w menu z ikonkami na górze Eclipse)

Plik:eclipse-run-launcher.png

W widoku Console zobaczymy wynik uruchomienia:

Plik:eclipse-run-launcher-output.png

Zatrzymanie działania klasy uruchomieniowej to wciśnięcie klawisza Enter lub czerwonego kwadratu w widoku Console.

Testowanie projektu z curl

Narzędzie curl to doskonałe narzędzie do testowania komunikacji przez HTTP (jeden z wielu protokołów wspieranych przez to narzędzie). Wywołanie akcji w SCAleniu z binding.atom sprowadza się do podejścia RESTful, gdzie komunikaty Atom'owe sprowadzają się do odpowiednich typów żądań HTTP.

Rozpocznijmy od rozpoznania dostępnych kolekcji - blogów, słowników, itp.

devmac:~ jacek$ curl -s -X GET http://devmac.local:8080/slownik/pl-de/atomsvc
<?xml version='1.0' encoding='UTF-8'?>
<service xmlns="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom">
  <workspace xml:base="http://devmac.local:8080/">
    <atom:title type="text">Tytuł kanału</atom:title>
    <collection href="http://devmac.local:8080/slownik/pl-de">
      <atom:title type="text">Tytuł kanału</atom:title>
      <accept>application/json; type=feed</accept>
      <accept>application/atom+xml; type=entry</accept>
      <categories />
    </collection>
  </workspace>
</service>

Sprawdźmy początkową zawartość kolekcji spod wskazanego adresu w elemencie collection - http://devmac.local:8080/Slownik/pl-de (nie zapomnij zmienić devmac.local na localhost).

devmac:~ jacek$ curl -s -X GET http://devmac.local:8080/slownik/pl-de/
<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title type="text">Tytuł kanału</title>
  <id>Feed2046265711</id>
</feed>

Nie ma nic w kolekcji. Zwrócona kolekcja jest pusta.

Stwórzmy pierwszy element - wpis do słownika odpowiadający słowu ranek (po niemiecku morgen). Wysyłamy żądanie typu POST o treści

<?xml version="1.0" encoding="UTF-8"?>
<entry xmlns="http://www.w3.org/2005/Atom">
  <author><name>Jacek Laskowski</name></author>
  <content type="HTML">morgen</content>
  <contributor>Jacek Laskowski</contributor>
  <id>ranek</id>
  <title>ranek</title>
  <updated>2010-02-20T18:30:00Z</updated>
</entry>

Niektóre z elementów są obowiązkowe, niektóre nie. Prawdę powiedziawszy, nie jest to na tyle interesujące, aby odciągać siebie od SCA i binding.atom.

Treść komunikatu zapisujemy w pliku ranek.xml, który podamy jako wartość argumentu --data poprzedzoną małpką @ w poleceniu curl.

W odpowiedzi na tak sformułowane żądanie POST otrzymujemy:

devmac:~ jacek$ curl -s -X POST -H "Content-type: application/atom+xml; type=entry" --data @ranek.xml http://devmac.local:8080/slownik/pl-de/
<entry xmlns="http://www.w3.org/2005/Atom">
  <id>ranek</id>
  <title type="text">ranek</title>
  <content type="html">morgen</content>
  <link href="ranek" />
  <updated>2010-02-20T18:30:00.000Z</updated>
</entry>

A teraz sprawdzenie słownika dla słowa ranek:

devmac:~ jacek$ curl -s -X GET http://devmac.local:8080/slownik/pl-de/ranek
<entry xmlns="http://www.w3.org/2005/Atom">
  <id>ranek</id>
  <title type="text">ranek</title>
  <content type="html">morgen</content>
  <link href="ranek" />
  <updated>2010-02-20T18:30:00.000Z</updated>
</entry>

Działa. Możnaby jeszcze popróbować się ze zmianami w opisie słowa w słowniku (komunikat PUT) oraz skasowaniem słowa (komunikat DELETE). Mi jednak już wystarczy tej ATOMistyki i w żaden sposób nie czuję się RESTful :)

Osobiste