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

2021/10/01

Ú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 na platforme Java 11!

Použijeme na to štandard JAX-WS 2.0 a jeho tradičnú implementáciu Metro.

Java 11 je prvá verzia, ktorá už neobsahuje v základnej knižnici implementáciu JAX-WS 2.0. Zároveň je to posledná verzia, na ktorej referenčná implementácia Metro zatiaľ funguje. Keďže technická podpora tejto Javy končí v roku 2026, dúfajme, že sa to dovtedy podarí zladiť.

Príklad a štruktúra dát

Vybudujme si službu pre objednanie parkovania. V požiadavke pošlem 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

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. na GitHube[https://github.com/novotnyr/kopr-wsdl-server-2021/blob/main/src/main/resources/parking.xsd]

WSDL súbor

Vezmeme si hotový súbor pre WSDL.

Kompletný súbor schémy parking.wsdl nájdeme na konci článku, resp. na GitHube[https://github.com/novotnyr/kopr-wsdl-server-2021/blob/main/src/main/resources/parking.wsdl]

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 11

  • závislosť na knižnici Metro

  • Maven plugin pre generovanie kódu servera

Verzia kompilátora

Dodajme nasledovné projektové vlastnosti:

pom.xml
<properties>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</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>2.3.3</version>
</dependency>

Maven Plugin

V starších verziách Javy sme mali k dispozícii nástroj wsimport, ktorý však v takejto verzii Metra už nie je dostupný. Pomocou neho sme dokázali vygenerovať kód pre server ale i klienta.

Jeho funkcionalitu nahraďme pluginom pre Maven:

pom.xml
<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>jaxws-maven-plugin</artifactId>
    <version>2.6</version>
    <configuration>
        <wsdlDirectory>${project.basedir}/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

pom.xml
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>sk.upjs.ics.kopr</groupId>
	<artifactId>kopr-parking-server</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<properties>
		<maven.compiler.source>11</maven.compiler.source>
		<maven.compiler.target>11</maven.compiler.target>
	</properties>


	<dependencies>
		<dependency>
			<groupId>com.sun.xml.ws</groupId>
			<artifactId>jaxws-rt</artifactId>
			<version>2.3.3</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>jaxws-maven-plugin</artifactId>
				<version>2.6</version>
				<configuration>
					<wsdlDirectory>${project.basedir}/src/main/resources</wsdlDirectory>
					<wsdlFiles>
						<wsdlFile>parking.wsdl</wsdlFile>
					</wsdlFiles>
					<wsdlLocation>/parking.wsdl</wsdlLocation>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

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 verzie 11 — nie novšej (Metro tam nefunguje) a nie staršej (kompilácia očakáva verziu 11)!

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
      ├── ParkingPortType.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.

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

Implementácia servera

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

ParkingServiceImpl.java
package sk.upjs.ics.kopr;

import javax.jws.WebService;
import org.example.parking.ParkingPortType;
import org.example.parking.ParkingRequest;
import org.example.parking.ParkingTicket;

@WebService(endpointInterface = "org.example.parking.ParkingPortType") (2)
public class ParkingServiceImpl implements ParkingPortType { (1)

    public ParkingTicket getTicket(ParkingRequest part) {
        // implementacia metody
        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 ParkingPortType 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:

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

Toto je klasický jednoduchý spôsob, ktorý ale vygeneruje WSDL automaticky na základe zdrojového kódu.

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) throws Exception {
    List<Source> metadata = new ArrayList<Source>(); (1)

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

    var xsdSource = new StreamSource(ParkingServiceImpl.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 ParkingServiceImpl()); (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/kopr-wsdl-server-2021.

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"?>
<wsdl:definitions
        name="parking"

        xmlns:wsdl="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:tns="http://www.example.org/parking"
>

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

    <wsdl:message name="request">
        <wsdl:part name="part" element="tns:parkingRequest" />
    </wsdl:message>

    <wsdl:message name="response">
        <wsdl:part name="part" element="tns:parkingTicket" />
    </wsdl:message>

    <wsdl:portType name="ParkingPortType">
        <wsdl:operation name="getTicket">
            <wsdl:input message="tns:request" />
            <wsdl:output message="tns:response" />
        </wsdl:operation>
    </wsdl:portType>

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

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

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