niedziela, 22 maja 2011

SOAP za pomocą Apache CXF oraz springa.

Witam po długiej przerwie. W ostatnich miesiącach zmieniłem pracę i w zabieganiu nie miałem czasu na blogowanie. Dziś zajmę się zaprezentowaniem jak postawić soap-a korzystając z cxf-a i oczywiście spring-a. Projekt ze źrodłami można pobrać stąd.
Do poczytania czym jest SOAP odsyłam do wiki oraz dokumentacji JAX-WS.

Zacznijmy od części serwerowej. Tworzenie web serwisu można zacząć albo od napisania WSDL-a (contract first) albo od napisania interfejsu serwisu i wygenerowaniu WSDL-a (contract last). Skorzystam z contract last. Mój testowy interfejs wygląda następująco:
public interface HelloSOAP {
    String sayHello(String name);
}

 Zacznijmy zabawę z SOAP-em :-). W pierwszym kroku oznaczmy adnotacją nasz interfejs:
@WebService(targetNamespace = "spring.turo.pl")
public interface HelloSOAP
Wszystkie atrybuty adnotacji @WebService są opcjonalne. Służą do oznaczenia endpoint-a web serwisu. Cóż przez to rozumiem ?
Oznaczamy tą adnotacją interfejs (bądź klasę) która deklaruje metody, które klient web serwisu będzie mógł wywołać. Innymi słowy - metody z tego interfejsu/klasy będą dostępne do wywołania. Moglibyśmy nic więcej w naszym interfejsie nie konfirugować - wtedy każda metoda byłaby zmapowana na operację serwisową o nazwie zgodnej z nazwą metody i parametrami arg0, arg1, ..., argN w WSDL-u. Nie chcę takiej sytuacji - wolę to przekonfigurować, spójrzmy jak:
@WebService(targetNamespace = "spring.turo.pl")
public interface HelloSOAP {
    @WebMethod(operationName = "przywitajSie")
    String sayHello(@WebParam(name = "firstName") String name);
}
Adnotacja @WebMethod pozawala na określenie metody jako metody web-serwisowej (możliwej do wywołania przez klienta) i pozwala m.in. na zmianę nazwy operacji w WSDL-u. Z powyższej konfiguracji wynika że operacja będzie dostępna pod nazwą 'przywitajSie' (a nie pod nazwą metody 'sayHello').
Dodatkowo użyłem adnotacji @WebParam która pozwala na opisanie jak w WSDL-u będzie nazywał się parametr operacji (uniknę dzięki temu arg0, ...).
OK. Mamy interfejs - czas na jakąś implementację:
@WebService(endpointInterface = "pl.turo.spring.HelloSOAP")
public class HelloSOAPImpl implements HelloSOAP {

    public HelloSOAPImpl() {
    }

    @Override
    public String sayHello(@WebParam(name = "firstName") String name) {
        return format("hello {0} :-)", name);
    }
i czas na springa:
<bean class="pl.turo.spring.HelloSOAPImpl" id="helloSOAP" />
Zauważmy 2 rzeczy:
  1. Klasę też oznaczyłem adnotacją @WebService i podałem w niej interfejs po którym wystawiona będzie usługa.
  2. Serwis jest 'czystym' beanem springa.
Mamy interfejs, implementację jak zatem wyczarowąć web serwis ? Korzystając z cxf-a wystarczy :
  1. Mieć na classpath odpowiednie jar'y (odsyłam do dokumentacji albo do mojego przykładowego projektu).
  2. Przeedytować konfigurację springa:
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:jaxws="http://cxf.apache.org/jaxws"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
                           http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">

    <import resource="classpath:META-INF/cxf/cxf.xml"/>
    <import resource="classpath:META-INF/cxf/cxf-servlet.xml"/>

    <jaxws:endpoint 
              id="endpoint"                     
              implementor="#helloSOAP"          
              address="/helloSOAP" />        

    <bean class="pl.turo.spring.HelloSOAPImpl" id="helloSOAP" init-method="initPersons"/>

</beans>
Dodaliśmy import dwóch konfirugacji springa z cxf-a oraz skonfigurowaliśmy endpoint. Tutaj zauważmy, że:
  1. Atrybut id nie ma żadnego znaczenia.
  2. Implementor to nasz bean. Wskazujemy go z '#' ponieważ cxf wymaga rzeczywistej referencji do obiektu pl.turo.spring.HelloSOAPImpl a nie proxy (spring może tworzyć proxy na beanie aplikując AOP, security, transakcje itd). Na obiekcie proxy nie ma adnotacji ktróre są potrzebne aby uruchomić usługę (adnotacja @WebService). Zatem nie możemy wstrzykiwać proxy, dzięki '#' mamy pewność, że nawet jeśli spring takie proxy na beanie helloSOAP będzie tworzył to jako implementacja naszego endpoint-u będziemy mieli rzeczywisty obiekt.
  3. Podaliśmy konfigurację pod jaką ścieżką (address="/helloSOAP") chcemy wystawić naszą usługę.
Pozostaje nam jeszcze wygenerować wsdl-a z naszego interfejsu. Skorzystam z plugin-u mavena :
<plugin>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-java2ws-plugin</artifactId>
    <version>${cxf.version}</version>
    <dependencies>
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-rt-frontend-jaxws</artifactId>
            <version>${cxf.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-rt-frontend-simple</artifactId>
            <version>${cxf.version}</version>
        </dependency>
    </dependencies>
    <executions>
        <execution>
            <id>process-classes</id>
            <phase>process-classes</phase>
            <configuration>
                <className>pl.turo.spring.HelloSOAP</className>
                <genWsdl>true</genWsdl>
                <verbose>true</verbose>
                <attachWsdl>true</attachWsdl>
            </configuration>
            <goals>
                <goal>java2ws</goal>
            </goals>
        </execution>
    </executions>
</plugin>
Przy takiej konfiguracji mój wsdl pojawi się w katalogu target/generated/wsdl. Pozostaje jeszcze tylko skonfigurować web.xml'a:
    
        org.springframework.web.context.ContextLoaderListener
    

    
        contextConfigLocation        /WEB-INF/spring/root-context.xml    

    
        CXFServlet
        org.apache.cxf.transport.servlet.CXFServlet
        1
    

    
        CXFServlet
        /*
    
Mamy już skonfigurowaną usługę, czas na klienta.
Aby móc wywołać metody serwisowe potrzebuję wygenerować sobie stub-y z wdsl-a. W tym celu ponownie posłużę się pluginem mavena dostarczonym przez cxf-a:
<plugin>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-codegen-plugin</artifactId>
    <version>${cxf.version}</version>
    <executions>
        <execution>
            <id>generate-sources</id>
            <phase>generate-sources</phase>
            <configuration>
                <sourceRoot>${project.build.directory}/generated-sources/java</sourceRoot>
                <wsdlOptions>
                    <wsdlOption>
                        <wsdl>${project.basedir}/src/main/resources/HelloSOAP.wsdl</wsdl>
                        <serviceName>HelloSOAPService</serviceName>
                    </wsdlOption>
                </wsdlOptions>
            </configuration>
            <goals>
                <goal>wsdl2java</goal>
            </goals>
        </execution>
    </executions>
</plugin>
Ta konfiguracja mówi, że chcę wygenerować stub-y z podanego pliku wsdl (to jest ten, który chwilę wcześniej wygenerowałem po stronie serwerowej) dla usługi nazwanej HelloSOAPService (taką nazwę na usługa w wsdl-u). Wystarczy przebudować projekt (bądź tylko wygenerować źródła poleceniem mvn generate-sources) i plugin wygeneruje potrzebne nam klasy.

Następnie należy dołożyć kawałek konfiguracji springa:
<jaxws:client id="helloClient"
              serviceClass="pl.turo.spring.HelloSOAP"
              address="http://localhost:8080/helloSOAP"/>
  1. Konfigurujemy beana będącego klientem naszego serwisu.
  2. Identyfikator beana służy jedynie do wyciągnięcia go z kontekstu springa.
  3. serviceClass składa się z 2 części : pl.turo.spring (odnosi się do wartości jaką podaliśmy w adnotacji @WebService(targetNamespace = "spring.turo.pl")) oraz HelloSOAP - nazwa naszego interfejsu serwisu.
  4. address - adres sieciowy pod jakim usługa będzie dostępna. Jak widać jest to adres który wcześniej skonfigurowaliśmy po stronie serwerowej (address="/helloSOAP") .

Mamy już część serwerową oraz część kliencką : czas na test. Najpierw uruchamiam usługę na tomcat-cie z poziomu mojego IDE:
2011-05-22 09:17:35 org.apache.catalina.core.AprLifecycleListener init
INFO: The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: /usr/lib/jvm/jdk1.6.0_25/jre/lib/i386/server:/usr/lib/jvm/jdk1.6.0_25/jre/lib/i386:/usr/lib/jvm/jdk1.6.0_25/jre/../lib/i386:.::/usr/java/packages/lib/i386:/lib:/usr/lib
2011-05-22 09:17:35 org.apache.coyote.http11.Http11Protocol init
INFO: Initializing Coyote HTTP/1.1 on http-8080
2011-05-22 09:17:35 org.apache.catalina.startup.Catalina load
INFO: Initialization processed in 398 ms
2011-05-22 09:17:35 org.apache.catalina.core.StandardService start
INFO: Starting service Catalina
2011-05-22 09:17:35 org.apache.catalina.core.StandardEngine start
INFO: Starting Servlet Engine: Apache Tomcat/6.0.32
2011-05-22 09:17:35 org.apache.catalina.startup.HostConfig deployDescriptor
INFO: Deploying configuration descriptor ROOT.xml
INFO : org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization started
INFO : org.springframework.web.context.support.XmlWebApplicationContext - Refreshing Root WebApplicationContext: startup date [Sun May 22 09:17:36 CEST 2011]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from ServletContext resource [/WEB-INF/spring/root-context.xml]
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [META-INF/cxf/cxf.xml]
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [META-INF/cxf/cxf-servlet.xml]
INFO : org.springframework.beans.factory.support.DefaultListableBeanFactory - Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@9fe84e: defining beans [cxf,org.apache.cxf.bus.spring.BusWiringBeanFactoryPostProcessor,org.apache.cxf.bus.spring.Jsr250BeanPostProcessor,org.apache.cxf.bus.spring.BusExtensionPostProcessor,helloSOAP,endpoint]; root of factory hierarchy
INFO : org.springframework.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 1079 ms
2011-05-22 09:17:37 org.apache.catalina.startup.HostConfig deployDirectory
INFO: Deploying web application directory docs
2011-05-22 09:17:37 org.apache.catalina.startup.HostConfig deployDirectory
INFO: Deploying web application directory examples
2011-05-22 09:17:37 org.apache.catalina.startup.HostConfig deployDirectory
INFO: Deploying web application directory manager
2011-05-22 09:17:37 org.apache.catalina.startup.HostConfig deployDirectory
INFO: Deploying web application directory host-manager
2011-05-22 09:17:37 org.apache.coyote.http11.Http11Protocol start
INFO: Starting Coyote HTTP/1.1 on http-8080
2011-05-22 09:17:37 org.apache.jk.common.ChannelSocket init
INFO: JK: ajp13 listening on /0.0.0.0:8009
2011-05-22 09:17:37 org.apache.jk.server.JkMain start
INFO: Jk running ID=0 time=0/13  config=null
2011-05-22 09:17:37 org.apache.catalina.startup.Catalina start
INFO: Server startup in 1724 ms
Connected to server

Z logów widać, że wstała aplikacja springowa ładując 3 konfiguracje /WEB-INF/spring/root-context.xml, META-INF/cxf/cxf.xml oraz META-INF/cxf/cxf-servlet.xml.

Przetestujmy naszego klienta:
public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("/spring-config.xml");
    HelloWS soap = (HelloWS) context.getBean("helloClient");

    // przesyłamy stringa
    System.out.println(soap.przywitajSie("Sławek"));
}
Tworzę fabrykę springa, pobieram skonfigurowanego wyżej beana i na nim mogę uruchamiać metody serwisowe.
Zauważmy, że w interfejsie HelloSOAP (po stronie serwera) mamy deklarację metody:
@WebMethod(operationName = "przywitajSie")
String sayHello(@WebParam(name = "firstName") String name);
Interfejs na kliencie posiada metodę 'przywitajSie' a nie 'sayHello'.
Na mojej konsoli widzę wynik uruchomienie metody main:
INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@89cf1e: startup date [Sun May 22 09:34:53 CEST 2011]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [spring-config.xml]
INFO : org.springframework.beans.factory.support.DefaultListableBeanFactory - Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@113beb5: defining beans [helloClient.proxyFactory,helloClient]; root of factory hierarchy
hello Sławek :-)

Serwis poprawnie odpowiedzieł.
Przesłaliśmy sobie stringa, a co z bardziej skomplikowanymi obiektami ?

Dołóżmy do interfejsu metody :
@WebService(targetNamespace = "spring.turo.pl")
public interface HelloSOAP {

    /**
     * Metoda testowa.
     * @param name
     * @return hello string :-)
     */
    @WebMethod(operationName = "przywitajSie")
    String sayHello(@WebParam(name = "firstName") String name);

    /**
     * Znajduje osobę po imieniu.
     * @param firstName imię
     * @return znaleziona osoba
     */
    @WebMethod(operationName = "locatePerson")
    Child getPersonFromName (@WebParam(name = "firstName") String firstName);

    /**
     * Znajduje ojca dla dziecka.
     * @param child dziecko
     * @return ojciec
     */
    @WebMethod(operationName = "findParent")
    Parent findParent(@WebParam(name = "child") Child child);
}
Do tego klasy modelu :
public class Child {
    private String firstName;
    private String lastName;
    private Long age;

    // konstuktory, akcesory
}

public class Parent {
    private String firstName;
    private String lastName;
    private Long age;
    private Collection<Child> children;

    // konstuktory, akcesory
}
Oraz naszą mokową implementację:
@WebService(endpointInterface = "pl.turo.spring.HelloSOAP")
public class HelloSOAPImpl implements HelloSOAP {
    private Set<Child> persons = new HashSet<Child>();
    private Set<Parent> parents = new HashSet<Parent>();

    public HelloSOAPImpl() {
    }

    public void initPersons () {
        persons.add(new Child("ala", "mikołajowicz", 32L));
        persons.add(new Child("janko", "muzykant", 15L));

        parents.add(new Parent("michał", "michołajowicz", 50L,
                    new ArrayList<Child>(Arrays.asList(
                            new Child("jacek", "michołajowicz", 15L),
                            new Child("zygmunt", "michołajowicz", 12L)))
                ));

        parents.add(new Parent("zyta", "grochowska", 43L,
                    new ArrayList<Child>(Arrays.asList(
                            new Child("benjamin", "grochowski", 5L),
                            new Child("maciej", "grochowski", 7L)
                            ))));
    }

    @Override
    public String sayHello(@WebParam(name = "firstName") String name) {
        return format("hello {0} :-)", name);
    }

    @Override
    public Child getPersonFromName(String firstName) {
        if (firstName == null) {
            throw new UnsupportedOperationException("imię musi zostać podane!!!");
        }
        Child retPerson = null;
        for (Iterator<Child> iterator = persons.iterator(); iterator.hasNext();) {
            Child next = iterator.next();
            if (firstName.equalsIgnoreCase(next.getFirstName())) {
                retPerson = next;
                break;
            }
        }
        return retPerson;
    }

    @Override
    public Parent findParent(Child child) {
        if (child == null) {
            throw new UnsupportedOperationException("dziecko musi zostać podane!!!");
        }
        Parent retParent = null;
        for (Iterator<Parent> iterator = parents.iterator(); iterator.hasNext();) {
            Parent next = iterator.next();
            if (next.getChildren().contains(child)) {
                retParent = next;
                break;
            }
        }
        return retParent;
    }
}

Poprawmy konfigurację springową :
<bean class="pl.turo.spring.HelloSOAPImpl" id="helloSOAP" init-method="initPersons"/>

Mój mokowy bean w metodzie initPersons (oznaczonej jako init-method) wypełnia sobie dane testowe.

Jeszcze raz należy przebudować projekt. Następnie ponownie skopiować wygenerowany wsdl do źródeł projektu klienckiego.
Prześlijmy obiekty :
public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext("/spring-config.xml");
    HelloSOAP soap = (HelloSOAP) context.getBean("helloClient");

    // przesyłamy obiekty
    Child child = new Child();
    child.setAge(5L);
    child.setFirstName("benjamin");
    child.setLastName("grochowski");

    Parent parent = soap.findParent(child);
    System.out.println(format("parent = {0} {1} lat {2}",
            parent.getFirstName(), parent.getLastName(), parent.getAge()));

    Child ala = soap.locatePerson("ala");
    System.out.println(format("person = {0}", ala.getFirstName()));

}
I wynika na konsoli:
INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@10a2d64: startup date [Sun May 22 09:50:09 CEST 2011]; root of context hierarchy
INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [spring-config.xml]
INFO : org.springframework.beans.factory.support.DefaultListableBeanFactory - Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@1d95da8: defining beans [helloClient.proxyFactory,helloClient]; root of factory hierarchy
parent = zyta grochowska lat 43
person = ala

OK. Widać, że nieco bardziej złożone obiekty udało mi się przesłać. Po drodze zostały one zserializowane na xml-a (korzystając z jaxb) i na kliencie zdeserializowane.
Jak widać nie musiałem robić nic aby skorzystać z tej serializacji.

Na koniec link do dokumentacji z listą typów wspieranych przez JAX-WS:

http://download.oracle.com/javaee/6/tutorial/doc/bnazc.html

Brak komentarzy:

Prześlij komentarz