Java

Własne usługi w JSE

Spis treści

     1 Wstęp
     2 Przygotowanie środowiska
     3 Interfejs serwisu
          3.1 Testowanie usług
          3.2 Ładowanie implementacji
     4 Implementacja usługi
          4.1 Klasa usługi
          4.2 Test usługi
          4.3 Implementacja interfejsu biznesowego
          4.4 Zadanie dla czytelnika
     5 Aplikacja testowa


Wstęp


Jednym z założeń dobrego programu obiektowego powinno być luźne połączenie pomiędzy poszczególnymi obiektami. Idealne API powinno wykorzystywać niewielką ilość klas i przede wszystkim operować na interfejsach. Pozwala to na zmianę implementacji poszczególnych funkcjonalności bez wpływu na kod klienta biblioteki. Wraz z Javą w wersji 1.6 dołączono do oficjalnej specyfikacji języka mechanizm Services. Pozwala on na wyszukiwanie implementacji wskazanego interfejsu na podstawie odpowiedniej konfiguracji zawartej w plikach jar. Umożliwia to programistom projektowanie aplikacji opartych o mechanizmy wtyczek.
Polem testowym dla tej technologii była platforma NetBeans wraz ze swoim systemem modułów. Bardzo podobne rozwiązanie zastosowano w Eclipse.
W poniższym artykule przybliżę czytelnikom zasady tworzenia aplikacji opartej o mechanizm Services. Ponad to przedstawię jedną z bardziej elastycznych metod tworzenia serwisów oraz zarządzania nimi. Jest ona oparta na moich doświadczeniach związanych z tworzeniem tego typu aplikacji.

Przygotowanie środowiska


Pomimo, że tak jak każdą aplikację java można tworzyć za pomocą najprostszych narzędzi zademonstruję cały proces tworzenia aplikacji za pomocą Mavena. Wynika to tylko z chęci pracowania w środowisku elastycznym i wydajnym. Maven ze swoim wsparciem zarządzania zależnościami świetnie nadaje się do zademonstrowania technologi Services.
Pierwszym krokiem będzie utworzenie projektu Services. Będzie on zawierał inne projekty, które będą zawierały odpowiednio interfejs, implementację i aplikację testową. Będzie też przechowywał wspólną dla wszystkich projektów część konfiguracji mavena.
Pom.xml projektu Services. Zwróć uwagę na to, że packaging jest ustawiony na pom.

<?xml version="1.0" encoding="UTF-8"?>
<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.koziolekweb.programmers.services</groupId>
        <artifactId>Services</artifactId>
        <packaging>pom</packaging>
        <version>1.0-SNAPSHOT</version>
        <name>Services</name>
        <url>http://maven.apache.org</url>
        <build>
                <plugins>
                        <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-compiler-plugin</artifactId>
                                <configuration>
                                        <source>1.6</source>
                                        <target>1.6</target>
                                </configuration>
                                <inherited>true</inherited>
                        </plugin>
                        <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-eclipse-plugin</artifactId>
                                <version>2.6</version>
                                <configuration>
                                        <downloadJavadocs>true</downloadJavadocs>
                                        <downloadSources>true</downloadSources>
                                </configuration>
                                <executions>
                                        <execution>
                                                <id>clean</id>
                                                <phase>clean</phase>
                                                <goals>
                                                        <goal>clean</goal>
                                                </goals>
                                        </execution>
                                        <execution>
                                                <id>eclispe</id>
                                                <phase>clean</phase>
                                                <goals>
                                                        <goal>eclipse</goal>
                                                </goals>
                                        </execution>
                                </executions>
                        </plugin>
                </plugins>
        </build>
        <dependencies>
                <dependency>
                        <groupId>junit</groupId>
                        <artifactId>junit</artifactId>
                        <version>4.7</version>
                        <scope>test</scope>
                </dependency>
        </dependencies>
        <modules>
                <module>ServicesInterface</module>
                <module>ServicesImpl</module>
                <module>ServicesTestApp</module>
        </modules>
</project>


Podprojekty będziemy dodawać sukcesywnie w miarę postępu prac.

Interfejs serwisu


Utwórzmy w katalogu projektu Services podprojekt ServicesInterface. Jego plik pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
        xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <modelVersion>4.0.0</modelVersion>
        <parent>
                <artifactId>Services</artifactId>
                <groupId>pl.koziolekweb.programmers.services</groupId>
                <version>1.0-SNAPSHOT</version>
        </parent>
        <groupId>pl.koziolekweb.programmers.services</groupId>
        <artifactId>ServicesInterface</artifactId>
        <version>1.0-SNAPSHOT</version>
        <name>ServicesInterface</name>
        <url>http://maven.apache.org</url>
        <build>
                <plugins>
                        <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-jar-plugin</artifactId>
                                <executions>
                                        <execution>
                                                <id>test</id>
                                                <phase>package</phase>
                                                <goals>
                                                        <goal>jar</goal>
                                                        <goal>test-jar</goal>
                                                </goals>
                                        </execution>
                                </executions>
                        </plugin>
                </plugins>
        </build>
</project>


Pierwszą decyzją jaką zazwyczaj musimy podjąć gdy tworzymy aplikację opartą o usługi jest opisanie interfejsu. Istnieje kilka "szkół" jedna z nich zakłada, że usługa dostarcza rzeczywisty interfejs biznesowy. Jest ona słuszna i najczęściej stosowana. Zazwyczaj nie ma potrzeby obudowywania serwisu dodatkowymi interfejsami. Ja przedstawię jednak troszkę inne podejście. Dostarczany będzie nie interfejs biznesowy, ale interfejs fabryki. Będzie on posiadał dwie metody. Pierwsza będzie dostarczała obiekty implementujące interfejs biznesowy. Druga będzie dostarczała specjalny obiekt, który będzie zawierał dodatkowe informacje o dostawcy usługi. W naszym przypadku będzie to zwykły String z nazwą dostawcy. W praktyce nie potrzeba nic więcej, ale czasami tworzone są specjalne obiekty z opisem dostawcy. Gwarantują one unikalność dostawcy w ramach większej aplikacji i nie konfliktowanie go z innymi dostawcami.
Nasz interfejs będzie zatem wyglądał tak:

package pl.koziolekweb.programmers.services;
 
/**
 * Ten interfejs będzie implementowany przez dostawcę serwisu jako publiczny
 * punkt dostępowy. Jest to interfejs fabrykujący, którego implementacja
 * dostarcza implementacji interfejsu biznesowego. Dzięki takiemu podejściu
 * można łatwo rozdzielić funkcjonalności biznesowe od zagadnień związanych z
 * dostarczaniem usługi.
 * 
 * @author koziolek
 * 
 */
public interface MyService {
 
        /**
         * Metoda pozwoli zlokalizować serwis na podstawie nazwy. Specyfikacja nie
         * wymaga jej, ale jest bardzo przydatna.
         * 
         * @return nazwa serwisu.
         */
        public String getServiceName();
 
        /**
         * Metoda fabrykująca dostarcza implementacji interfejsu biznesowego usługi.
         * 
         * @return implementacji interfejsu biznesowego.
         */
        public BussinesInterface getBussinesInterface();
 
}


Sam interfejs biznesowy wygląda następująco:

package pl.koziolekweb.programmers.services;
 
/**
 * Interfejs biznesowy związany z usługą. Klient korzysta z tego interfejsu, a
 * zadaniem serwisu jest dostarczenie jego implementacji.
 * 
 * @author koziolek
 * 
 */
public interface BussinesInterface {
        /**
         * * Metoda biznesowa. Wykonuje dodawanie parametrów i zwraca wynik jako
         * [email protected] String}.
         * 
         * @param x
         *            czynnik
         * @param y
         *            kolejne czynniki
         * @return wynik dodawania parametrów.
         * @throws NullPointerException
         *             w przypadku gdy pierwszy argument jest <code&gt;null</code&gt;.
         * @throws IllegalArgumentException
         *             w przypadku gdy wynik przekracza zasięg dla [email protected] Integer}.
         */
        public String bussinesMethod(int x, int... y) throws NullPointerException, IllegalArgumentException;
 
}


Testowanie usług


Mamy już interfejs, możemy w praktyce spakować go w plik JAR i upublicznić. Tyle tylko, że jeżeli napiszemy program - kontener, w którym będziemy korzystać z implementacji różnych dostawców to szybko okaże się, że nasz program ma same błędy. Większość z nich będzie leżała w komponentach niezależnych dostawców, ale to nam klienci będą zgłaszać problemy.
Mała dygresja na temat klientów i zgłaszania błędów. Jedną z przyczyn zapaści na rynku sklepów internetowych na początku XXI wieku, tzw. bańki dotcomów, było błędne założenie, że klienci będą szukać wsparcia w rozwiązaniu problemów i zgłaszać reklamacje do producenta, a nie do sprzedawcy. Spowodowało to, że większość sklepów tnąc koszty pozbywała się działów wsparcia ograniczając je tylko do pomocy w zakresie problemów z dostawą/płatnościami. Założenie to okazało się na tyle błędne, że klika dużych sklepów musiało pożegnać się z rynkiem.
Skoro zatem to my będziemy stanowić pierwszą linię wsparcia w razie problemów z usługami warto zadbać o odpowiednie nastawienie dostawców oraz poziom dostarczanego softu. Można to zrobić poprzez dostarczenie razem z interfejsem zestawu testów, które implementacja powinna przejść by można uznać ją za zgodną z naszymi założeniami. Jest to bardzo częsta praktyka w branży IT. Moduły Linuxa zanim trafią do repozytorium przechodzą testy zgodności. Podobnie ma się rzecz z serwerami Java EE. Serwer może nazywać się serwerem EE jeżeli "zda" testy przygotowane przez Suna.
Zaimplementujmy więc prostą listę testów, które musi przejść implementacja interfejsu by można było mówić, że jest w pełni zgodna z naszymi założeniami. Będą one sprawdzały podstawowe zachowania interfejsu. Interesuje nas tylko czy dla przekazanych danych wynik jest prawidłowy. Nie wnikamy w jaki sposób zaimplementowano interfejs.
Oczywiście takie podejście może spowodować, że implementacja będzie zachowywała się inaczej niż oczekujemy w pewnych specyficznych warunkach np. w przypadku przerwania łączności sieciowej. Jest to jednak szczegół związany z implementacją i nas nie interesuje. W gestii dostawczy leży prawidłowe przetestowanie kodu pod kątem jego specyficznych elementów na przykład obsługi ruchu sieciowego czy awarii w klastrze.

package pl.koziolekweb.programmers.services;
 
import static org.junit.Assert.assertEquals;
 
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
 
/**
 * Test interfejsu biznesowego. Rozszerz tą klasę i zaimplementuj metodę
 * [email protected] #getInterface()} w celu przetestowania swojej implementacji
 * interfejsu. Jeżeli twoja implementacja przejdzie pomyślnie ten test możesz
 * wystąpić o <i>Kolejny Nic Niewarty, Nikomu Do Szczęścia Nie Potrzebny
 * Certyfikat Zgodności</i>.
 * 
 * @author koziolek
 * 
 */
public abstract class AbstractBussinesInterfaceTest {
 
        private BussinesInterface bussinesInterface;
 
        /**
         * Zaimplementują tą metodę w celu przygotowania [email protected] BussinesInterface
         * interfejsu biznesowego} do testów.
         * 
         * @return implemrntacja [email protected] BussinesInterface interfejsu biznesowego}.
         */
        public abstract BussinesInterface getInterface();
 
        /**
         * Tworzy instancję interfejsu biznesowego na potrzeby pojedynczego testu.
         */
        @Before
        public void setup() {
                bussinesInterface = getInterface();
        }
 
        /**
         * Usuwa instancję interfejsu biznesowego po zakończonym teście.
         */
        @After
        public void tearDown() {
                bussinesInterface = null;
        }
 
        /**
         * Test sprawdza czy implementacja dobrze działa z przykładowymi, poprawnymi
         * danymi.
         */
        @Test
        public void dwaPlusDwaTest() {
                assertEquals("4", bussinesInterface.bussinesMethod(2, 2));
        }
 
        /**
         * W tym teście sprawdzamy czy dodatkowe parametry przekazane do metody dają
         * prawidłowe rezultaty.
         */
        @Test
        public void dwaPlusDwaPlusDwaTest() {
                assertEquals("6", bussinesInterface.bussinesMethod(2, 2, 2));
        }
 
        /**
         * W tym teście sprawdzane jest zachowanie implementacji w przypadku
         * przekazania tylko jednego parametru. Implementacja powinna zwrócić wynika
         * tak jakby inne parametry były równe 0.
         */
        @Test
        public void zeroTest() {
                assertEquals("0", bussinesInterface.bussinesMethod(0));
        }
 
        /**
         * W tym przypadku sprawdzane jest czy implementacja prawidłowo obsługuje
         * pierwszy parametr równy NULL. Powinna zwrócić wyjątek.
         */
        @Test(expected = NullPointerException.class)
        public void nullTest() {
                assertEquals("0", bussinesInterface.bussinesMethod((Integer) null));
        }
 
        /**
         * Ten test sprawdza zachowanie się implementacji w przypadku przekazania
         * jednego lub więcej parametrów mniejszego od 0.
         */
        @Test
        public void dwaMinusDwaTest() {
                assertEquals("0", bussinesInterface.bussinesMethod(2, -2));
                assertEquals("0", bussinesInterface.bussinesMethod(-2, 2));
                assertEquals("-4", bussinesInterface.bussinesMethod(-2, -2));
        }
 
        /**
         * Test sprawdza czy interfejs prawidłowo zachowa się w przypadku
         * przekroczenia zasięgu dla wyniku.
         */
        @Test(expected = IllegalArgumentException.class)
        public void overMaxTest() {
                assertEquals("0", bussinesInterface.bussinesMethod(Integer.MAX_VALUE, 1));
        }
 
        /**
         * Test sprawdza czy interfejs prawidłowo zachowa się w przypadku
         * osiągnięcia zasięgu dla wyniku.
         */
        @Test
        public void maxTest() {
                assertEquals(Integer.MAX_VALUE + "", bussinesInterface.bussinesMethod(Integer.MAX_VALUE, 0));
                assertEquals((Integer.MAX_VALUE - 1) + "", bussinesInterface.bussinesMethod(Integer.MAX_VALUE, -1));
        }
 
        /**
         * Test sprawdza czy interfejs prawidłowo zachowa się w przypadku
         * przekroczenia zasięgu dla wyniku.
         */
        @Test(expected = IllegalArgumentException.class)
        public void overMinTest() {
                assertEquals("0", bussinesInterface.bussinesMethod(Integer.MIN_VALUE, -1));
        }
 
        /**
         * Test sprawdza czy interfejs prawidłowo zachowa się w przypadku
         * osiągnięcia zasięgu dla wyniku.
         */
        @Test
        public void minTest() {
                assertEquals(Integer.MIN_VALUE + "", bussinesInterface.bussinesMethod(Integer.MIN_VALUE, 0));
                assertEquals((Integer.MIN_VALUE + 1) + "", bussinesInterface.bussinesMethod(Integer.MIN_VALUE, 1));
        }
}


Na szczególną uwagę w tym kodzie zasługuje sposób w jaki dostawca powinien przeprowadzać testy. Klasa testów jest abstrakcyjna i posiada abstrakcyjną metodę getInterface(), która jest implementowana przez dostawcę. Dostawca może w dowolny sposób ją zaimplementować dodając swoje elementy do testu np. sprawdzenie połączenia sieciowego.

Ładowanie implementacji


Razem z biblioteką interfejsu warto dostarczyć prosty mechanizm wyszukujący wszystkie jego implementacje. Tu przechodzimy do pierwszego ważnego elementu prezentowanej technologii.
Java udostępnia klasę ServiceLoader<?>, która ma za zadanie wyszukanie wszystkich implementacji dla interfejsu. Przechowuje ona informacje o nich w postaci kolekcji i pozwala na swobodny dostęp do obiektów. Tak. W trakcie ładowania serwisów tworzone są konkretne instancje dla każdej implementacji. Dlatego tak ważne jest odpowiednie dobranie sensu interfejsu serwisu. Jeżeli obiekt biznesowy może mieć więcej niż jedną instancję powinien on być klonowalny, implementować interfejs Cloneable, albo dostarczać metody fabrykującej. Dlatego też warto w takim przypadku utworzyć interfejs fabrykujący i to jego dostarczać jako serwis. Inną możliwością jest dość specyficzna implementacja wzorca singleton oparta o dodatkowe pole przechowujące instancję. Konstruktor w takim przypadku powinien zwracać wyjątek IllegalStateException w przypadku tworzenia kolejnej kopi obiektu. Sama klasa powinna posiadać metodę dostępową, oraz metodę usuwającą. Jest to dość skomplikowane podejście. Zazwyczaj programiści i projektanci wychodzą z założenia, że nie będzie sytuacji w której będzie tworzone więcej obiektów usługi. Jest to optymistyczne podejście, które ma rację bytu jeżeli kontrolujemy kod wyszukujący dostawców.
W naszym przypadku wybrałem interfejs fabrykujący jako interfejs usługi. Dodatkowo razem z biblioteką będziemy dostarczać klasę MyServiceLoader, która zajmie się wyszukaniem dostawców usługi i ich załadowaniem. Jeżeli klient będzie chciał uzyskać dostęp do usługi za pomocą interfejsu serwisu to zamiast samodzielnie tworzyć obiekt reprezentujący implementację serwisu powinien skorzystać z poniższej implementacji dostawcy:

package pl.koziolekweb.programmers.services;
 
import java.util.HashMap;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
 
/**
 * Klasa narzędziowa zarządzająca serwisami. Pozwala na ich ładowanie i
 * wyszukiwanie. Stanowi punkt dostępowy do serwisów dla aplikacji klienckiej.
 * Ładowanie jest procesem zachłannym i następuje przy uruchomieniu aplikacji.
 * 
 * @author koziolek
 * 
 */
public class MyServiceLoader {
 
        private static final ServiceLoader<MyService> SERVICE_LOADER = ServiceLoader.load(MyService.class);
 
        private static Map<String, MyService> myServices = new HashMap<String, MyService>();
 
        static {
                loadServices();
        }
 
        /**
         * Wyszukuje serwis na podstawie nazwy. Zwraca
         * [email protected] IllegalArgumentException} w przypadku gdy serwis o podanej nazwie
         * nie istnieje.
         * 
         * @param serviceName
         *            nazwa serwisu.
         * @return poszukiwany serwis.
         * @throws IllegalArgumentException
         *             wyrzucany jeżeli serwis o podanej nazwie nie istnieje.
         */
        public static MyService getServiceByName(String serviceName) throws IllegalArgumentException {
                if (myServices.containsKey(serviceName)) {
                        return myServices.get(serviceName);
                }
                throw new IllegalArgumentException("Brak serwisu o nazwie: " + serviceName);
        }
 
        /**
         * Pozwala sprawdzić czy serwis o podanej nazwie istnieje.
         * 
         * @param serviceName
         *            nazwa poszukiwanego serwisu.
         * @return <code&gt;true</code&gt; jeżeli serwis istnieje.
         */
        public Boolean serviceExist(String serviceName) {
                return myServices.containsKey(serviceName);
        }
 
        /**
         * Zwraca listę nazw zarejestrowanych serwisów.
         * 
         * @return lista nazw serwisów.
         */
        public static Set<String> getServicesNames() {
                return myServices.keySet();
        }
 
        /**
         * Zwraca mapę serwisów.
         * 
         * @return
         */
        public static Map&lt;string, myservice="MyService"&gt; getAllServices() {
                return myServices;
        }
 
        /**
         * Ładuje serwisy. Wywoływana przy ładowaniu klasy.
         */
        private static void loadServices() {
                for (MyService myService : SERVICE_LOADER) {
                        myServices.put(myService.getServiceName(), myService);
                }
        }
 
}
&lt;/code&gt;
 
Oczywiście kod ten jest nic niewarty bez odpowiedniego testu. Jako, że test wymaga dostarczenia w skuteczny sposób implementacji, przynajmniej testowej, zatem poniżej zaprezentuję tylko kod testu, a sam proces tworzenia implementacji omówię w kolejnej części.
 
<code=java>
package pl.koziolekweb.programmers.services;
 
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
 
import java.util.Map;
import java.util.Set;
 
import org.junit.Test;
 
/**
 * Testy usługi ładującej implementacje [email protected] MyService}.
 * 
 * @author koziolek
 * 
 */
public class MyServiceLoaderTest {
 
        /**
         * Nazwa usługi testowej.
         */
        private static final String TEST_SERVICE_NAME = "MyServiceTestImpl";
 
        /**
         * Ładuje usługi i sprawdza czy:
         * <ul>
         * <li>Załadowana została dokładnie jedna usługa.</li>
         * <li>Załadowana usługa ma nazwę taką jak [email protected] #TEST_SERVICE_NAME} -
         * [email protected] #TEST_SERVICE_NAME}.</li>
         * <li>Usługa ładująca prawidłowo wiąże nazwę usługi z jej klasą.</li>
         * <li>Załadowana usługa to usługa testowa.</li>
         * </ul>
         */
        @Test
        public void loadTest() {
                Map<String, MyService> allServices = MyServiceLoader.getAllServices();
                assertEquals(1, allServices.size());
 
                Set<String> servicesNames = MyServiceLoader.getServicesNames();
                assertEquals(TEST_SERVICE_NAME, servicesNames.iterator().next());
 
                MyService myService = MyServiceLoader.getServiceByName(TEST_SERVICE_NAME);
                assertNotNull(myService);
                assertTrue(myService instanceof MyServiceTestImpl);
        }
}


Na zakończenie prac z projektem należy zainstalować go w lokalnym repozytorium mavena. Umożliwi to konfigurację zależności od interfejsu w projektach dostawców usług.  

Implementacja usługi


W katalogu projektu Services utwórzmy projekt ServicesImpl. pom.xml tego projektu wygląda następująco:

<?xml version="1.0" encoding="UTF-8"?>
<project
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
        xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <modelVersion>4.0.0</modelVersion>
        <parent>
                <artifactId>Services</artifactId>
                <groupId>pl.koziolekweb.programmers.services</groupId>
                <version>1.0-SNAPSHOT</version>
        </parent>
        <groupId>pl.koziolekweb.programmers.services</groupId>
        <artifactId>ServicesImpl</artifactId>
        <version>1.0-SNAPSHOT</version>
        <name>ServicesImpl</name>
        <build>
                <plugins>
                        <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-jar-plugin</artifactId>
                                <version>2.2</version>
                                <configuration>
                                        <includes>
                                                <include>**/services/*</include>
                                                <include>**/*.class</include>
                                        </includes>
                                </configuration>
                        </plugin>
                </plugins>
        </build>
        <dependencies>
                <dependency>
                        <groupId>pl.koziolekweb.programmers.services</groupId>
                        <artifactId>ServicesInterface</artifactId>
                        <version>1.0-SNAPSHOT</version>
                </dependency>
                <dependency>
                        <groupId>pl.koziolekweb.programmers.services</groupId>
                        <artifactId>ServicesInterface</artifactId>
                        <version>1.0-SNAPSHOT</version>
                        <type>test-jar</type>
                        <scope>test</scope>
                </dependency>
        </dependencies>
</project>


Zanim napiszemy kod muszę krótko omówić zasadę działania klasy ServiceLoader<?>. W momencie wywołania metody load(Class) rozpoczyna się przeszukiwanie wszystkich katalogów i plików jar w poszukiwaniu plików opisu usługi. Plik taki znajduje się zawsze w katalogu META-INF/services nazywa się tak jak implementowany interfejs. Jego zawartość stanowi nazwa klasy implementującej ten interfejs. Jeżeli chcemy stworzyć własną implementację musimy zatem utworzyć plik pl.koziolekweb.programmers.services.MyService. Umieszczamy go w katalogu META-INF/services, a ten katalog znajduje się w naszym przypadku w katalogu src/main/resources/. Dzieję się tak dlatego, że w trakcie budowania pliku jar maven załączy zawartość tego katalogu do archiwum.
Jak widać cała "magia" rozwiązania opiera się na jednym stosunkowo prostym założeniu. Jest nim stworzenie pliku z odpowiednią zawartością i dostarczenie go do klientów wraz z biblioteką. Przystąpmy zatem do implementacji serwisu.

Klasa usługi


Pierwszym krokiem jest utworzenie klasy implementującej usługę. Jest ona bardzo prosta. Dla każdego żądania dostarcza nową instancję interfejsu biznesowego. Można zatem od razu przystąpić do kodowania. Klasa MyServieImpl wygląda tak:

package pl.koziolekweb.programmers.services;
 
/**
 * Implementacja usługi [email protected] MyService} o nazwie <code&gt;MyServiceImpl</code&gt;.
 * 
 * @author koziolek
 * 
 */
public class MyServiceImpl implements MyService {
 
        /**
         * Nazwa serwisu jest wstawiona na stałe w kod.
         */
        private static final String SERVICE_NAME = "MyServiceImpl";
 
        /*
         * (non-Javadoc)
         * 
         * @see pl.koziolekweb.programmers.services.MyService#getBussinesInterface()
         */
        @Override
        public BussinesInterface getBussinesInterface() {
                return new BussinesInterfaceImpl();
        }
 
        /*
         * (non-Javadoc)
         * 
         * @see pl.koziolekweb.programmers.services.MyService#getServiceName()
         */
        @Override
        public String getServiceName() {
                return SERVICE_NAME;
        }
 
}


Jak widać tworzone są obiekty klasy BussinesInterfaceImpl, ale zanim omówimy tą klasę należy zrobić coś innego.

Test usługi


Przy omawianiu testów interfejsu biznesowego wspomniałem, że dostawca powinien odpowiednio zaimplementować testy by móc mówić o zgodności swojej usługi ze specyfikacją. Na zgodność tą w naszym przypadku składają się dwa elementy. Pierwszy z nich to sama implementacja usługi. Nie nałożyliśmy na nią żadnych ograniczeń poza tym, że usługa ma implementować nasz interfejs. Drugim elementem jest zgodność z testami interfejsu biznesowego.
Poniżej przedstawiona jest klasa testowa, która dostarcza implementacji BussinesInterface dla klasy AbstractBussinesInterfaceTest.

package pl.koziolekweb.programmers.services;
 
/**
 * Test zgodności usługi ze specyfikacją. Nie ma dodatkowych testów.
 * 
 * @author koziolek
 * 
 */
public class BussinesInterfaceImplTest extends AbstractBussinesInterfaceTest {
 
        /**
         * Dostarcza obiekty klasy [email protected] BussinesInterfaceImpl} na potrzeby testów.
         * 
         * @return implementacja interfejsu biznesowego.
         */
        @Override
        public BussinesInterface getInterface() {
                return new BussinesInterfaceImpl();
        }
 
}


Jak widać jest to bardzo prosta klasa, która implementuje tylko jedną metodę. Wynika to z założenia, że implementacja jest po przygotowaniu poddawana testom na zewnątrz. Podejście takie jest właściwie testom integracyjnym, ale naszym przypadku obejmuje testy jednostkowe.

Implementacja interfejsu biznesowego


Nie będę zagłębiał się w dokładny opis implementacji. Poniższy kod nie jest optymalny, ale jego optymalizacja nie znajduje się w zakresie artykułu.

package pl.koziolekweb.programmers.services;
 
import java.math.BigInteger;
 
/**
 * Implementacja interfejsu biznesowego. Oparta jest o klasę [email protected] BigInteger}.
 * 
 * @author koziolek
 * 
 */
class BussinesInterfaceImpl implements BussinesInterface {
 
 
        /* (non-Javadoc)
         * @see pl.koziolekweb.programmers.services.BussinesInterface#bussinesMethod(int, int[])
         */
        @Override
        public String bussinesMethod(int x, int... y) throws NullPointerException, IllegalArgumentException {
 
                BigInteger sum = new BigInteger(x + "");
                for (int i : y) {
                        sum = sum.add(new BigInteger(i + ""));
                }
 
                if (sum.compareTo(new BigInteger(Integer.MAX_VALUE + "")) > 0) {
                        throw new IllegalArgumentException();
                }
                if (sum.compareTo(new BigInteger(Integer.MIN_VALUE + "")) < 0) {
                        throw new IllegalArgumentException();
                }
 
                return sum.toString();
        }
}


Zadanie dla czytelnika


Utwórz kod testowy potrzebny do uruchomienia MyServiceLoaderTest

Aplikacja testowa


Utwórzmy w katalogu projektu Services podprojekt ServicesTestApp. Jego plik pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project
        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
        xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <modelVersion>4.0.0</modelVersion>
        <parent>
                <artifactId>Services</artifactId>
                <groupId>pl.koziolekweb.programmers.services</groupId>
                <version>1.0-SNAPSHOT</version>
        </parent>
        <groupId>pl.koziolekweb.programmers.services</groupId>
        <artifactId>ServicesTestApp</artifactId>
        <version>1.0-SNAPSHOT</version>
        <name>ServicesTestApp</name>
        <url>http://maven.apache.org</url>
        <dependencies>
                <dependency>
                        <groupId>pl.koziolekweb.programmers.services</groupId>
                        <artifactId>ServicesInterface</artifactId>
                        <version>1.0-SNAPSHOT</version>
                </dependency>
                <dependency>
                        <groupId>pl.koziolekweb.programmers.services</groupId>
                        <artifactId>ServicesImpl</artifactId>
                        <version>1.0-SNAPSHOT</version>
                </dependency>
        </dependencies>
        <build>
                <plugins>
                        <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-jar-plugin</artifactId>
                                <version>2.2</version>
                                <configuration>
                                        <archive>
                                                <manifest>
                                                        <mainClass>pl.koziolekweb.programmers.services.Main</mainClass>
                                                        <addClasspath>true</addClasspath>
                                                        <classpathPrefix>dependency/</classpathPrefix>
                                                </manifest>
                                        </archive>
                                </configuration>
                        </plugin>
                        <plugin>
                                <groupId>org.apache.maven.plugins</groupId>
                                <artifactId>maven-dependency-plugin</artifactId>
                                <version>2.1</version>
                                <executions>
                                        <execution>
                                                <id>copy</id>
                                                <phase>package</phase>
                                                <goals>
                                                        <goal>copy-dependencies</goal>
                                                </goals>
                                        </execution>
                                </executions>
                        </plugin>
                </plugins>
        </build>
</project>


Poniżej aplikacja testowa. Dopisz własne implementacje usługi by sprawdzić jak będzie się zachowywał.

package pl.koziolekweb.programmers.services;
 
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.regex.Pattern;
 
/**
 * Program testowy.
 */
public class Main {
        public static void main(String[] args) {
                Set<String> servicesNames = MyServiceLoader.getServicesNames();
                Map<Integer, String> out = new HashMap<Integer, String>();
                int i = 1;
                for (String s : servicesNames) {
                        out.put(i, s);
                        i++;
                }
                Scanner scanner = new Scanner(System.in);
                System.out.println("Dodawanie Liczb.\nOdnaleziono " + servicesNames.size() + " dostawców");
                while (true) {
                        for (i = 1; i <= out.size(); i++) {
                                System.out.println(i + ". " + out.get(i));
                        }
                        System.out.println("W. by zakończyć program");
                        String nextLine = scanner.nextLine();
                        if ("W".equalsIgnoreCase(nextLine)) {
                                System.exit(0);
                        }
                        if (Pattern.matches("\\d*", nextLine)) {
                                Integer in = new Integer(nextLine);
                                if (!out.containsKey(in)) {
                                        System.out.println("Nieprawidłowa wartość!");
                                        continue;
                                }
                                MyService myService = MyServiceLoader.getServiceByName(out.get(in));
                                System.out.println("Wybrany dostawca: " + myService.getServiceName());
                                BussinesInterface bussinesInterface = myService.getBussinesInterface();
                                System.out.println("Wprowadź liczby do sumowania rozdzielając je spacją lub przecinkiem.\n"
                                                + "Liczba ujemna nie może być rozdzielona od znaku\nnp:\n-123 - dobrze\n- 123 - źle");
                                String[] numbers = scanner.nextLine().trim().split(" |,");
                                if (numbers.length <= 0) {
                                        System.out.println("Nieprawidłowa wartość!");
                                        continue;
                                }
                                int x = new Integer(numbers[0]);
                                int[] y = null;
                                if (numbers.length - 1 > 0) {
                                        y = new int[numbers.length - 1];
                                        for (i = 1; i < numbers.length; i++) {
                                                y[i - 1] = new Integer(numbers[i]);
                                        }
                                }
                                System.out.println(bussinesInterface.bussinesMethod(x, y));
                        }
                }
        }
}


Jest ona uproszczona i ma posłużyć czytelnikowi do testów własnych implementacji.