Od WSDL k webovej službe – Glassfish Metro

2008/07/14

V predošlom dieli sme si ukázali príklad popisovača webovej služby v podobe WSDL súboru. Ak máme tento súbor, môžeme použiť niektoré z existujúcich nástrojov na vygenerovanie zdrojového kódu pre serverovskú časť služby i pre klienta, ktorý k nej bude pristupovať.

Tento spôsob je k dispozícii vo všetkých významných Java frameworkoch pre webové služby - teda aj v Apache CXF, Axis 1, Axis 2 a Glassfish Metro.

Samotná Java poskytuje na prístup k webovým službám viacero API združených pod rôznymi špecifikáciami. Najnovšou je v súčasnosti JAX-WS 2.0 (JSR-224), ktorá je už súčasťou JEE 5 a pri návrhu a implementácii webových služieb používa všetky vymoženosti Javy 5. Aplikačný rámec Glassfish Metro, ktorá bude pracovať nad triedami a interfejsmi JAX-WS budeme demonštrovať práve v tejto časti.

Inštalácia

Z domovskej stránky Metra si stiahneme príslušný JAR, rozbalíme na vhodné miesto, vytvoríme systémovú premennú METRO_HOME ukazujúcu na adresár inštalácie a voliteľne si dodáme cestu k skriptom do PATHu.

Generovanie serverovskej časti

Predpokladajme, že máme k dispozícii WSDL súbor z predošlej časti seriálu. Java triedy vygenerujeme nasledovným spôsobom:

SET METRO_HOME=C:/java/metro
SET PATH=%PATH%;%METRO_HOME%/bin
wsimport -keep -s src -d bin movie.wsdl

Ak by sme vynechali všetky parametre, vygenerovali by sa priamo CLASS súbory do aktuálneho adresára. -keep ponechá po generovaní zdrojové súbory, -s určí adresár pre vygenerované zdrojové súbory a -d určí, kam pôjdu skompilované CLASSy.

Vygenerujú sa nasledovné súbory:

Balíček sk.novotnyr.movie.ws.types obsahuje triedy, ktoré zodpovedajú elementom a typom definovaným v XML schéme. Názov tohto balíčka je odvodený od cieľového menného priestoru (targetNamespace) v schéme. V schéme sme mali http://movie.novotnyr.sk/ws/], čo sa priamo namapuje na názov balíčka.

Na mapovanie medzi inštanciami XML schémy a objektami v Jave sa v Metre používa technológia JAXB 2.0. Vygenerované triedy presne zodpovedajú tomu, čo by sme získali použitím kompilátora xjc z implementácie JAXB. Príkladom triedy je:

package sk.novotnyr.movie.ws.types;

import java.util.*;
import java.util.List;
import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
    "id",
    "reservedSeats"
})
@XmlRootElement(name = "movieReservationResponse")
public class MovieReservationResponse {

    protected int id;
    @XmlElement(type = Integer.class)
    protected List<Integer> reservedSeats;

    public int getId() {
        return id;
    }

    public void setId(int value) {
        this.id = value;
    }

    public List<Integer> getReservedSeats() {
        if (reservedSeats == null) {
            reservedSeats = new ArrayList<Integer>();
        }
        return this.reservedSeats;
    }

}

Všimnite si, že je to klasické POJO, kde atribúty elementu zodpovedajú inštančným premenným a anotácie určujú prepis na XML.

V balíčku sk.novotnyr.movie.wsdl sa nachádzajú interfejsy a triedy, ktoré súvisia s elementom service, resp. portType vo WSDL. Každý service sa namapuje na samostatnú triedu a každý portType tiež. Názov balíčka sa odvodí od cieľového menného priestoru WSDL súboru.

Ako sme spomínali, abstraktný port vo WSDL predstavuje množinu operácií, ktoré je možné volať. Jeho analógiou je interfejs v Jave. wsimport vygeneruje nasledovnú triedu:

package sk.novotnyr.movie.wsdl;

import javax.jws.*;
import javax.jws.soap.´*;
import javax.xml.bind.annotation.*;
import sk.novotnyr.movie.ws.types.*;

@WebService(name = "movieReservationPortType", 
            targetNamespace = "http://movie.novotnyr.sk/wsdl")
@SOAPBinding(parameterStyle = SOAPBinding.ParameterStyle.BARE)
@XmlSeeAlso({
    ObjectFactory.class
})
public interface MovieReservationPortType {
  @WebMethod
  @WebResult(name = "movieReservationResponse", 
             targetNamespace = "http://movie.novotnyr.sk/ws/types", 
             partName = "part1")
  public MovieReservationResponse movieReservation(
        @WebParam(name = "movieReservationRequest", 
                  targetNamespace = "http://movie.novotnyr.sk/ws/types", 
                  partName = "part1")
        MovieReservationRequest part1);

}

Máme teda interfejs webovej služby, potrebujeme však aj jeho implementáciu. Vytvoríme triedu s príslušnými metódami a anotujeme tu tak, aby ukazovala na interfejs, ktorá má anotáciu @WebService.

package sk.novotnyr.movie.ws;

import java.util.Arrays;

import javax.jws.WebService;

import sk.novotnyr.movie.ws.types.MovieReservationRequest;
import sk.novotnyr.movie.ws.types.MovieReservationResponse;

@WebService(
  endpointInterface="sk.novotnyr.movie.wsdl.MovieReservationPortType",
)
public class DefaultMovieReservationService {
  public MovieReservationResponse movieReservation
    (MovieReservationRequest part1) 
  {
    MovieReservationResponse confirmation 
      = new MovieReservationResponse();
    confirmation.setId(23456674);
    confirmation.getReservedSeats().addAll(Arrays.asList(2, 3, 2));
    
    return confirmation;
  }
}

Týmto máme všetky potrebné komponenty na to, aby sme mohli spustiť a naštartovať webovú službu. V Metre je k dispozícii trieda Endpoint, ktorá dokáže naštartovať interný HTTP server a spustiť v ňom príslušnú triedu tak, že na to postačí jediný riadok:

package sk.novotnyr.movie;

import javax.xml.ws.Endpoint;

import sk.novotnyr.movie.ws.DefaultMovieReservationService;

public class Server {
  public static void main(String[] args) {
    Endpoint.publish("http://localhost:8080/movies/ws", 
                     new DefaultMovieReservationService());
   }
}

Navštívením adresy http://localhost:8080/movies/ws?wsdl môžeme vidieť WSDL súbor tejto služby. Tu si však všimneme jednu dôležitú vec - WSDL nie je identický s tým, z ktorého sme generovali službu! Metro totiž k publikovanej službe poskytne automaticky generované WSDL, ktorého parametre sa odvodia na základe anotácií portu, tried, metód a parametrov.

Výsek z WSDL vyzerá potom nasledovne:

<binding xmlns:ns1="http://movie.novotnyr.sk/wsdl" 
         name="DefaultMovieReservationServicePortBinding" 
         type="ns1:movieReservationPortType">
  <soap:binding transport="http://schemas.xmlsoap.org/soap/http" 
                style="document" /> 
  <operation name="movieReservation">
  <soap:operation soapAction="" /> 
  <input>
    <soap:body use="literal" /> 
  </input>
  <output>
    <soap:body use="literal" /> 
  </output>
  </operation>
</binding>
<service name="DefaultMovieReservationServiceService">
  <port name="DefaultMovieReservationServicePort" 
        binding="tns:DefaultMovieReservationServicePortBinding">
    <soap:address location="http://localhost:8080/movies/ws" /> 
  </port>
</service>

Všimnime si automaticky odvodené pomenovania:

To by za normálnych okolností stačilo, ale keďže by sme chceli klientom zverejniť to WSDL, ktoré sme si ručne navrhli, automatické WSDL nie je to správne.

Špecifikovať zverejnené WSDL môžeme v anotácii @WebService na implementačnej triede. Použijeme na to atribút wsdlLocation. Popri tom však musíme uviesť ďalšie veci:

@WebService(
 endpointInterface="sk.novotnyr.movie.wsdl.MovieReservationPortType",
 targetNamespace="http://movie.novotnyr.sk/wsdl",
 serviceName="movieReservationService",
 portName="movieReservationPort",
 wsdlLocation="META-INF/movie.wsdl"
)
public class DefaultMovieReservationService {
//...

Generovanie klientskej časti

Generovanie klientskej časti je jednoduché. Použitím wsimport na WSDL dostaneme nielen serverovské triedy, ale aj klientské. Klientská časť je predstavovaná triedou MovieReservationService (tá dedí od javax.xml.Service), ktorá je okorenená mnohými anotáciami.


package sk.novotnyr.movie.wsdl;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.logging.Logger;
import javax.xml.namespace.QName;
import javax.xml.ws.*;

@WebServiceClient(name = "movieReservationService", 
                  targetNamespace = "http://movie.novotnyr.sk/wsdl", 
                  wsdlLocation = "file:/D:/Projects/metro/movie.wsdl")
public class MovieReservationService extends Service
{

    // .. vygenerovaný kód vynechaný
    
    public MovieReservationService(URL wsdlLocation,
                                   QName serviceName) {
        // .. vygenerovaný kód vynechaný
    }

    public MovieReservationService() {
        // .. vygenerovaný kód vynechaný
    }

    @WebEndpoint(name = "movieReservationPort")
    public MovieReservationPortType getMovieReservationPort() {
        // .. vygenerovaný kód vynechaný
    }

    @WebEndpoint(name = "movieReservationPort")
    public MovieReservationPortType getMovieReservationPort(
                WebServiceFeature... features) {
       // .. vygenerovaný kód vynechaný
    }
}

Možno nám udrie do očí atribút wsdlLocation, ktorý uvádza cestu k WSDL v súborovom systéme. To nie je ktoviečo. Napraviť to môžeme troma spôsobmi:

Použitie klienta

Použitie klienta webovej služby je potom priamočiare. Jediným problémom je šaškovanie s dátumom. V tomto prípade wsimport nevygeneroval inštančné premenné s klasickými java.util.Date, ale s triedami zodpovedajúcimi XML typom. (Dôvodom je to, že XML typy pre prácu s dátumom a časom môžu niekedy reprezentovať väčší rozsah hodnôt než java.util.Date).

package sk.novotnyr.movie;

import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.XMLGregorianCalendar;

import sk.novotnyr.movie.ws.types.MovieReservationRequest;
import sk.novotnyr.movie.ws.types.MovieReservationResponse;
import sk.novotnyr.movie.wsdl.MovieReservationPortType;
import sk.novotnyr.movie.wsdl.MovieReservationService;

public class Client {
  public static void main(String[] args) 
    throws DatatypeConfigurationException 
  {
    MovieReservationService service = new MovieReservationService();
    MovieReservationPortType port = service.getMovieReservationPort();
    
    MovieReservationRequest request = new MovieReservationRequest();
    request.setTitle("Godzilla");
    request.setNumberOfTickets(2);
    
    XMLGregorianCalendar date 
      = DatatypeFactory
        .newInstance()
        .newXMLGregorianCalendarDate(
            2008, 12, 12, 
            DatatypeConstants.FIELD_UNDEFINED);
    request.setDate(date);
    
    MovieReservationResponse response
      = port.movieReservation(request);
    System.out.println(
      response.getId() 
      + " " 
      + response.getReservedSeats());
  }
}

Mapovanie WSDL elementov na Java elementy

wsdl:service

wsdl:port

wsdl:portType

wsdl:operation

wsdl:binding

wsdl:message

wsdl:part

wsdl:types

Prehľadová schéma:

Nasledovná schéma zobrazuje prepojenie medzi WSDL elementami a triedami grafickým spôsobom:

>> Home