Od WSDL k webovej službe — generovanie servera a klienta pomocou Metro na Java 17

2022/10/30

Úvod

Navrhli sme webovú službu pre SOAP pomocou contract-first? Máme teda kontrakt reprezentovaný súborom WSDL?

Poďme vygenerovať kód pre server SOAP na kombinácii:

  • Java 17

  • špecifikáciu Jakarta XML Web Services 4.0 (nástupca JAX-WS)

  • referenčnú implementáciu Eclipse Metro.

  • plugin pre Maven z knižnice Eclipse Metro.

Príklad a štruktúra dát

Vybudujme si službu pre objednanie parkovania. V požiadavke pošleme EČV vozidla a parkovaciu zónu a v odpovedi získame identifikátor parkovacieho lístka a dátum platnosti.

Pripravíme si:

  • schému pre správy v tvare XSD (XML Schema)

  • popisný súbor WSDL

  • mavenovský projekt s pom.xml pre serverovskú časť.

XML schéma

Vezmeme schému pre správy pomocou XML Schemy.

Kompletný súbor schémy parking.xsd nájdeme na konci článku, resp. v repozitári na GitHube.

WSDL súbor

Vezmeme si hotový súbor pre WSDL.

Kompletný súbor schémy parking.wsdl nájdeme na konci článku, resp. v repozitári na GitHube.

Projekt pre serverovskú časť

Založme si nový projekt založený na buildovacom nástroji Maven.

V pom.xml definujeme:

  • verziu kódu pre kompilátor: použijeme Javu 17

  • závislosť na knižnici Eclipse Metro

  • Maven plugin pre generovanie kódu servera

Verzia kompilátora

Dodajme nasledovné projektové vlastnosti:

pom.xml
<properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
</properties>

Závislosť na knižnici Metro

Pridajme si závislosť na knižnici Metro:

pom.xml
<dependency>
    <groupId>com.sun.xml.ws</groupId>
    <artifactId>jaxws-rt</artifactId>
    <version>4.0.0</version>
</dependency>

Maven Plugin

Generovanie kostry serverovskej časti projektu, teda generovanie zdrojových kódov podľa dodaného WSDL súboru uskutočníme cez mavenovský plugin.

pom.xml
<plugin>
    <groupId>com.sun.xml.ws</groupId>
    <artifactId>jaxws-maven-plugin</artifactId>
    <version>4.0.0</version>
    <configuration>
        <wsdlDirectory>src/main/resources</wsdlDirectory>(2)
       <wsdlFiles>
           <wsdlFile>parking.wsdl</wsdlFile>(1)
       </wsdlFiles>
        <wsdlLocation>/parking.wsdl</wsdlLocation>(3)
    </configuration>
</plugin>
1 Definujme odkaz na názov WSDL, z ktorého vygenerujeme kód.
2 Uvedieme cestu k adresáru v projekte, z ktorého vytiahneme WSDL súbor.
3 Aby sa v generovanom kóde zbytočne neobjavovali celé cesty k lokálnemu súborovému systému používateľa, uveďme explicitnú adresu ku kódu.

Ak umiestnime WSDL súbor medzi prostriedky (resources), objaví sa vo výslednom JAR archíve a teda v ceste CLASSPATH (v koreni). Takto sa na tento súbor odkážeme z bežiaceho servera a vieme ho poskytnúť klientovi.

Lokácia wsdlLocation s lomkou na začiatku znamená, že WSDL súbor pri budovaní klientskeho kódu sa vytiahne z cesty CLASSPATH.

Celý pom.xml

Celý súbor pom.xml nájdeme na na GitHube.

Rozmiestnenie súborov

Oba súbory — parking.wsdl aj parking.xsd umiestnime do src/main/resources, pretože tak sme to nastavili v pom.xml v adresári wsdlDirectory.

Zostavenie projektu

Zdrojáky pre server nagenerujeme nasledovne:

mvn jaxws:wsimport package
Ak spúšťame Maven zo shellu, dajme si pozor, aby Java, v ktorej sa spúšťal Maven, bola vo verzii 17.

Zdrojáky sa nagenerujú do priečinka:

target/generated-sources/wsimport/

Pre naše WSDL sa vygeneruje nasledovná štruktúra:

 org
└── example
   └── parking
      ├── ObjectFactory.java
      ├── package-info.java
      ├── ParkingService.java
      ├── ParkingRequest.java
      ├── ParkingServices.java
      └── ParkingTicket.java
Názvy priečinkov / balíčkov sa odvodia z targetNamespace vo WSDL či schéme.

Konfigurácia projektu

Adresár target/generated-sources/wsimport/ je užitočné potrebné pridať do projektu ako miesto so zdrojovými kódmi.

Eclipse

Pravý klik na adresár v strome Package List, a z kontextového menu Build Path | Use as Source Folder.

IntelliJ

Pravý klik na adresár target/generated-sources-wsimport v projektovom strome, a z kontextového menu Mark Directory As | Generated Sources Root.

Implementácia servera

Implementácia servera znamená vytvorenie triedy, ktorá bude implementovať triedu org.example.ParkingService.

DefaultParkingService.java
package com.github.novotnyr.soap;

import jakarta.jws.WebService;
import org.example.parking.ParkingRequest;
import org.example.parking.ParkingService;
import org.example.parking.ParkingTicket;

@WebService(endpointInterface = "org.example.parking.ParkingService") (2)
public class DefaultParkingService implements ParkingService { (1)
    @Override
    public ParkingTicket getTicket(ParkingRequest part) {
        return new ParkingTicket(); (3)
    }
}
1 Implementujeme interfejs, ktorý vznikol generovaním kódu.
2 V atribúte endpointInterface uvedieme interfejs s kontraktom webovej služby pre JAX-WS.
3 Pripravíme implementáciu metódy — v tomto prípade veľmi jednoduchú.

Interfejs ParkingService sa uvádza na dvoch miestach:

  1. V implements, kde určuje metódy, ktoré v Jave naprogramujeme.

  2. V endpointInterface, kde spárujeme implementáciu s nagenerovaným kontraktom webovej služby.

Ak vynecháme atribút endpointInterface, môže sa stať, že server nageneruje kontrakt a WSDL na základe implementácie — teda „v protismere“ od kódu k WSDL, čo rozhodne nechceme!

Spustenie servera

Server môžeme spustiť jednoducho:

ParkingSoapServer.java
package com.github.novotnyr.soap;

import jakarta.xml.ws.Endpoint;

public class ParkingSoapServer {
    public static void main(String[] args) {
        Endpoint.publish("http://localhost:8888/parking", new DefaultParkingService());
    }
}

Server beží na porte 8888 a spĺňa špecifikáciu WSDL.

Táto služba zverejňuje WSDL na adrese http://localhost:8888/parking?wsdl. Toto WSDL je však autogenerované, čo popiera zmysel ručnej tvorby.

Spustenie servera s naším WSDL

Ak chceme použiť existujúce WSDL a to zverejniť klientovi, musíme prispôsobiť nasadenie služby.

Budeme predpokladať, že v CLASSPATH máme aj WSDL (parking.wsdl) aj XSD (parking.xsd).

public static void main(String[] args) {
    List<Source> metadata = new ArrayList<Source>(); (1)

    var wsdlSource = new StreamSource(DefaultParkingService.class.getResourceAsStream("/parking.wsdl")); (2)
    wsdlSource.setSystemId("http://www.example.org/parking/parking.wsdl"); (3)
    metadata.add(wsdlSource); (4)

    var xsdSource = new StreamSource(DefaultParkingService.class.getResourceAsStream("/parking.xsd"));(2)
    xsdSource.setSystemId("http://www.example.org/parking/parking.xsd");(3)
    metadata.add(xsdSource);(4)

    var filter = new HashMap<String, Object>();
    filter.put(Endpoint.WSDL_SERVICE, new QName("http://www.example.org/parking", "ParkingServices")); (5)
    filter.put(Endpoint.WSDL_PORT, new QName("http://www.example.org/parking", "ParkingService")); (6)

    var endpoint = Endpoint.create(new DefaultParkingService()); (7)
    endpoint.setProperties(filter); (8)
    endpoint.setMetadata(metadata); (9)
    endpoint.publish("http://localhost:8888/parking"); (10)
}
1 Musíme si pripraviť zoznam pre metadáta: teda WSDL a XSD.
2 Vytvoríme objekt Source reprezentujúci XML súbor pre WSDL či schému. Tento objekt načítame z CLASSPATH: to je reprezentované lomkou v argumente getResourceAsStream.
3 Každý takýto objekt Source potrebuje jednoznačný identifikátor v tvare URL s použitím protokolu HTTP alebo file. Keďže na konkrétnej hodnote nezáleží, vytvoríme si vymyslený ukážkový identifikátor.
4 Súbor dodáme do metadát.
5 Pomocou properties definujeme filter na službu service a port z WSDL, na ktorý použijeme naše metadáta, teda na ktorom zmeníme WSDL a XML schému XSD. Pomocou WSDL_SERVICE určíme kvalifikované meno (menný priestor a názov elementu) pre element wsdl:service z WSDL.
6 Pomocou WSDL_PORT určíme kvalifikované meno (menný priestor a názov elementu) pre element wsdl:port z WSDL.
7 Vytvoríme nový endpoint nad našou triedou s implementáciou servera.
8 Nastavíme filter cez properties.
9 Nastavíme nové metadáta služby.
10 Endpoint vypublikujeme na danej adrese.

Backend teraz môžeme spustiť ako Java aplikáciu!

Na adrese http://localhost:8888/parking?wsdl uvidíme našej ručne písané WSDL!

Pri deklarovaní filtra (properties) sa musia kvalifikované názvy presne zhodovať s názvami vo WSDL.

  • Menný priestor v kvalifikovanom mene QName sa preberá z atribútu targetNamespace vo WSDL.

  • Lokálne meno z atribútu name v elemente wsdl:service, resp. wsdl:port.

Výsledný repozitár

Výsledný repozitár je na GitHube, v repozitári novotnyr/jaxws-wsdl-server-2022.

Zdrojáky

XML Schéma

parking.xsd
<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
        targetNamespace="http://www.example.org/parking"
        elementFormDefault="qualified">

    <element name="parkingRequest">
        <complexType>
            <sequence>
                <element name="carId" type="string" />
                <element name="zone" type="int" />
            </sequence>
        </complexType>
    </element>

    <element name="parkingTicket">
        <complexType>
            <sequence>
                <element name="id" type="string" />
                <element name="validUntil" type="dateTime" />
            </sequence>
        </complexType>
    </element>
</schema>

WSDL

parking.wsdl
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<definitions
        name="parking"

        xmlns="http://schemas.xmlsoap.org/wsdl/"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"

        targetNamespace="http://www.example.org/parking"
        xmlns:p="http://www.example.org/parking"
>

    <types>
        <xsd:schema targetNamespace="http://www.example.org/parking">
            <xsd:include schemaLocation="parking.xsd" />
        </xsd:schema>
    </types>

    <message name="parkingRequest">
        <part name="part" element="p:parkingRequest" />
    </message>

    <message name="parkingResponse">
        <part name="part" element="p:parkingTicket" />
    </message>

    <portType name="ParkingPortType">
        <operation name="getTicket">
            <input message="p:parkingRequest" />
            <output message="p:parkingResponse" />
        </operation>
    </portType>

    <binding name="ParkingBinding" type="p:ParkingPortType">
        <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>

        <operation name="getTicket">
            <input>
                <soap:body use="literal"/>
            </input>
            <output>
                <soap:body use="literal"/>
            </output>
        </operation>
    </binding>

    <service name="ParkingServices">
        <port name="ParkingService" binding="p:ParkingBinding">
            <soap:address location="http://localhost:8888/parking"/>
        </port>
    </service>
</definitions>

Deskriptor pre Maven

pom.xml
<?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/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.github.novotnyr</groupId>
    <artifactId>jaxws-wsdl-server</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.glassfish.metro</groupId>
            <artifactId>webservices-rt</artifactId>
            <version>4.0.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>com.sun.xml.ws</groupId>
                <artifactId>jaxws-maven-plugin</artifactId>
                <version>4.0.0</version>
                <configuration>
                    <wsdlDirectory>src/main/resources</wsdlDirectory>
                    <wsdlFiles>
                        <wsdlFile>parking.wsdl</wsdlFile>
                    </wsdlFiles>
                    <wsdlLocation>/parking.wsdl</wsdlLocation>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
>> Home