Ibland så hamnar man i situationer när man behöver ladda ner eller ladda upp stora filer, tex dokument eller bilder från/till en server via webbtjänster. I den här artikeln tänkte jag visa hur man kan göra detta om man använder sig utav JAX-WS och även hur man kan optimera det. Jag kommer använda följande verktyg; JDK 6, Maven 3.0 och JAX-WS 2.2.0.2.Jag börjar med att skapa ett nytt projekt med hjälp av Maven:
mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DgroupId=se.stacktrace -DartifactId=jaxws-large-example -Dversion=1.0-SNAPSHOT -DinteractiveMode=false
Jag hoppar ner i den nya mappen, jaxws-large-example, som ovanstående Maven kommando skapade. Kommandot skapar även 2 java klasser, App och AppTest, dessa tar jag bort.
cd jaxws-large-example
rm src/main/java/se/stacktrace/App.java src/test/java/se/stacktrace/AppTest.java
Jag börjar med att skapa serverdelen, där behöver jag en klass som exponerar en metod som jag kan ladda ner data från. Jag kallar klassen för Server och metoden för download
package se.stacktrace.server;
import javax.activation.DataHandler;
public class Server {
public DataHandler download(int size) {
return null;
}
}
Download metoden returnerar en javax.activation.DataHandler, denna typ är att föredra istället för en byte[] när man skall skicka stora datamängder. Metoden tar även ett argument, size, detta är storleken på datat som jag vill att min metod skall returnera. Nu implementerar jag metoden download:
public DataHandler download(final int size) {
return new DataHandler(new javax.activation.DataSource() {
@Override
public java.io.InputStream getInputStream() throws java.io.IOException {
return new java.io.InputStream() {
int i;
@Override
public int read() throws java.io.IOException {
return i<size ? 'A'+(i++%26) : -1;
}
};
}
@Override
public String getContentType() {
return "application/octet-stream";
}
@Override
public java.io.OutputStream getOutputStream() throws java.io.IOException {
return null;
}
@Override
public String getName() {
return null;
}
});
}
Eftersom det här är ett exempel så returnerar metoden download bara dummy data. Jag implementerar en javax.activation.DataSource, där metoden getInputStream returnerar en java.io.InputStream som bara returnerar den mängd data som size argumentet begär. Eftersom argumentet size används av en anonym innerklass så måste jag även deklarera den som final. Jag implementerar metoden getContentType så att den returnerar strängen “application/octet-stream”, eftersom detta är MIME-typen för en godtycklig byteström. Det enda jag har kvar nu är att exponera klassen Server en webbtjänst, detta gör jag med en annotering, javax.jws.WebService.
package se.stacktrace.server;
import javax.activation.DataHandler;
import javax.jws.WebService;
@WebService
public class Server {
...
}
Nu är jag klar med min serverdel. Nu ska jag bara deploya min webbtjänst så att jag kan anropa den. Istället för att packa ihop min server till en war fil och deploya den på någon webcontainer så använder jag mej utav klassen javax.xml.ws.Endpoint. Jag lägger till följande main metod i min klass Server.java:
public static void main(String[] args) {
String addr = "http://localhost:2113/jaxws-example";
javax.xml.ws.Endpoint.create(new Server()).publish(addr);
}
Jag startar min server med följande Maven kommand:
mvn compile exec:java -Dexec.mainClass="se.stacktrace.server.Server"
Med en browser går jag till följande adress, http://localhost:2113/jaxws-example?wsdl och tar en titt på wsdl:en som publiceras. Där kan jag se att det är JAX-WS RI 2.1.6 som används.
<!-- Published by JAX-WS RI at http://jax-ws.dev.java.net. RI's version is JAX-WS RI 2.1.6. -->
Det är inte så konstigt eftersom det är den versionen som kommer med JDK6. Jag vill använda en senare version av JAX-WS RI, eftersom 2.1.6 känns lite gammal, så jag lägger till följande repository och beroende i min pom.xml fil:
...
<repositories>
<repository>
<id>dev.java.net</id>
<url>http://download.java.net/maven/2/</url>
</repository>
</repositories>
...
<dependency>
<groupId>com.sun.xml.ws</groupId>
<artifactId>jaxws-rt</artifactId>
<version>2.2.0.2</version>
</dependency>
...
Jag stänger ner min server med Ctrl^C och startar den på nytt med samma kommando som tidigare och gör sedan en refresh i min browser för att verifiera att en nyare JAX-WS RI används:
<!– Published by JAX-WS RI at http://jax-ws.dev.java.net. RI’s version is JAX-WS RI 2.2.0.2-04/14/2010 11:50 PM(ramkris)-. –>
Då var det dags att skapa en klient också. Jag börjar med att generera lite klientstubbar med hjälp av JAX-WS verktyget wsimport. Jag lägger till och konfigurerar följande plugin i min pom.xml:
...
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxws-maven-plugin</artifactId>
<configuration>
<!-- var jag vill att de genererade java filerna skall hamna -->
<sourceDestDir>${basedir}/src/main/java</sourceDestDir>
<!-- stänger av generering av klass filer -->
<destDir />
<!-- packetnamnet för mina genererade java filer -->
<packageName>se.stacktrace.client.gen</packageName>
<wsdlUrls>
<!-- wsdl:en som skall användas för att skapa stubbarna -->
<wsdlUrl>http://localhost:2113/jaxws-example?wsdl</wsdlUrl>
</wsdlUrls>
</configuration>
</plugin>
</plugins>
</build>
...
Jag öppnar ett nytt terminal fönster (eftersom servern ligger och kör i det andra) och kör följande Maven kommand:
mvn jaxws:wsimport
Detta kommando skapar bland annat följande klasser i packetet se.stacktrace.client.gen; Download.java, DownloadResponse.java, Server.java och ServerService.java som jag nu i min klient kan använda mej utav för att anropa min webbtjänst. Nu är det dags för själva klienten, jag skapar klassen Client i paketet se.stacktrace.client:
package se.stacktrace.client;
import se.stacktrace.client.gen.Server;
import se.stacktrace.client.gen.ServerService;
public class Client {
public static void main(String[] args) {
Server proxy = new ServerService().getServerPort();
int size = 10; // begär 10 byte data
byte[] resp = proxy.download(size);
System.out.println(size == resp.length);
}
}
Klassen Client innehåller bara en main metod där jag skapar en instans av proxyn som genererades av wsimport och sen så anropar jag download metoden och till sist så jämför jag att mängden data jag får tillbaka från webbtjänsten är lika med vad jag begärde. För att köra klienten kör jag jag följande Maven kommando:
mvn compile exec:java -Dexec.mainClass="se.stacktrace.client.Client"
...
[INFO] --- exec-maven-plugin:1.2:java (default-cli) @ jaxws-large-example ---
true
...
En sak man kan reagera på är att jag i klassen Server deklarerade att metoden download skulle returnera en javax.activation.DataHandler, men i klienten så returnerar metoden download på proxyn en byte[]. Detta beror på att datatypen DataHandler mappas per default mot XML datatypen xs:base64Binary, och XML datatypen xs:base64Binary mappas default mot java typen byte[]. Om jag tittar på XML schemat som har generats på min server (http://localhost:2113/jaxws-example?xsd=1) så ser jag följande:
...
<xs:complexType name="downloadResponse">
<xs:sequence>
<xs:element name=”return” type=”xs:base64Binary” minOccurs=”0″ />
</xs:sequence>
</xs:complexType>
...
Så när jag kör wsimport skapas klassen DownloadResponse med instansvariabel return av typen byte[], för så är den defaulta mappningen i JAXB (som JAX-WS använder sig utav för Java<->XML mappning). För att JAXB skall veta att XML datatypen xs:base64Binary i det här fallet faktiskt skall mappas mot Java-typen javax.activation.DataHandler så måste jag annotera schemaelementet med xmime:expectedContentTypes attributet för att indikera vilken typ av binärdata som är förväntat. Detta gör jag enkelt genom att lägga till en annotering, javax.xml.bind.annotation.XmlMimeType, på metoden download’s signatur i klassen Server.
public @javax.xml.bind.annotation.XmlMimeType(value = “application/octet-stream”) DataHandler download(final int size) {
...
}
Jag startar om min server och tittar på nytt på det genererade schemat i min browser:
…
<xs:complexType name="downloadResponse">
<xs:sequence>
<xs:element xmlns:ns1=”http://www.w3.org/2005/05/xmlmime”
name=”return” ns1:expectedContentTypes=”application/octet-stream”
type=”xs:base64Binary” minOccurs=”0″ />
</xs:sequence>
</xs:complexType>
...
Nu måste generera om det genererade klasserna som klienten använder sig utav, så i terminalfönstret där jag kör min klient kör jag följande kommando på nytt:
mvn jaxws:wsimport
Om jag nu försöker köra min klient så kommer jag att få kompileringsfel:
mvn compile exec:java -Dexec.mainClass=”se.stacktrace.client.Client”
...
[INFO] BUILD FAILURE
...
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
...
Jag implementerar om min main metod i Client klassen så att den använder sig utav DataHandler istället för byte[]
public static void main(String[] args) {
try {
Server proxy = new ServerService().getServerPort();
int size = 10;
javax.activation.DataHandler resp = proxy.download(size);
byte[] b = new byte[8192];
int total = 0;
int len;
java.io.InputStream in = resp.getInputStream();
while((len = in.read(b, 0, b.length)) != -1) {
total += len;
}
in.close();
System.out.println(size == total);
} catch(java.io.IOException e) {
e.printStackTrace();
}
}
För att komma åt datan som skickas med en DataHandler så läser man direkt från input-strömmen. Jag kör på nytt min klient, bara för att verifiera att det funkar:
mvn compile exec:java -Dexec.mainClass=”se.stacktrace.client.Client”
...
[INFO] --- exec-maven-plugin:1.2:java (default-cli) @ jaxws-large-example ---
true
...
Nu är det dags att förbättra den här implementationen, för hur skicka egentligen binärdatan i SOAP-meddelandet? För att se detta så lägger jag till följande rad först i main-metoden i Server.java:
public static void main(String[] args) {
System.setProperty(”com.sun.xml.ws.transport.http.HttpAdapter.dump”,”true”);
…
}
Jag startar om servern och nu kommer HTTP request/responsen att dumpas i konsolen där jag kör min server. Jag kör min klient på nytt och ser bland annat följande i serverkonsolen:
---[HTTP response 200]---
Transfer-encoding: chunked
Content-type: text/xml;charset=utf-8
Date: Fri, 15 Oct 2010 12:15:40 GMT
<?xml version='1.0' encoding='UTF-8'?>
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<ns2:downloadResponse xmlns:ns2="http://server.stacktrace.se/">
<return>QUJDREVGR0hJSg==</return>
</ns2:downloadResponse>
</S:Body>
</S:Envelope>
Datat som skickas från servern ligger “inline” i SOAP-meddelandet. Eftersom SOAP använder sig utav XML så måste all binärdata som skickas i SOAP-meddelandet kodas som text, detta görs vanligtvis med Base64 kodning vilket i sin tur ökar storleken på binärdatat med cirka 33%. Men som tur är finns det lösningar för detta. Istället för att skicka binärdatan “inline” i SOAP-meddelandet, så kan man använda sig utav MTOM, Message Transmission Optimization Mechanism. Med MTOM skickas binärdatat som ett MIME multipart meddelande (precis på samma sätt som e-post skickar bilagor), och det behöver inte Base64 kodas vilket i sin tur innebär att vi inte drabbas av en storleksökning på 33% av datat som skickas. För att aktivera så att min server skickar binärdatat med MTOM så behöver jag bara annotera min Server.java klass med annoteringen javax.xml.ws.soap.MTOM
@WebService
@javax.xml.ws.soap.MTOM
public class Server {
...
}
Jag måste även aktivera MTOM på klienten, detta gör jag med följande tillägg i Client.java:
(läser man dokumentationen så står det att man även måste aktivera MTOM på klienten, men använder man sig av JAX-WS RI 2.1.6 eller 2.2.0.2 så funkar det utan att aktivera på klienten, men JAX-WS RI 2.1.7 kräver att man aktiverar det på klienten för att servern skall skicka det med MTOM)
Server proxy = new ServerService().getServerPort(new javax.xml.ws.soap.MTOMFeature());
Jag startar om servern och kör klienten på nytt och fångar upp följande från severkonsolen:
---[HTTP response 200]---
Transfer-encoding: chunked
Content-type: multipart/related;start="<rootpart*34eac0a4-e1a8-4707-8fd9-3bef609447cc@example.jaxws.sun.com>";type="application/xop+xml";boundary="uuid:34eac0a4-e1a8-4707-8fd9-3bef609447cc";start-info="text/xml"
Date: Fri, 15 Oct 2010 12:40:29 GMT
--uuid:34eac0a4-e1a8-4707-8fd9-3bef609447cc
Content-Id: <rootpart*34eac0a4-e1a8-4707-8fd9-3bef609447cc@example.jaxws.sun.com>
Content-Type: application/xop+xml;charset=utf-8;type="text/xml"
Content-Transfer-Encoding: binary
<?xml version='1.0' encoding='UTF-8'?>
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<ns2:downloadResponse xmlns:ns2="http://server.stacktrace.se/">
<return>
<xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" href="cid:ac7f331f-fabb-4a84-b2e0-06753a05987f@example.jaxws.sun.com"/>
</return>
</ns2:downloadResponse>
</S:Body>
</S:Envelope>
--uuid:34eac0a4-e1a8-4707-8fd9-3bef609447cc
Content-Id: <ac7f331f-fabb-4a84-b2e0-06753a05987f@example.jaxws.sun.com>
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary
ABCDEFGHIJ
--uuid:34eac0a4-e1a8-4707-8fd9-3bef609447cc----------------------
Nu skickas datat som en bilaga istället för inline i XML:en, dessutom så är inte datat Base64-kodat heller.
En sista grej jag tänkte göra är att optimera läsningen lite när det är en stor mängd data som skickas. Jag ändrar Client.java så att jag begär en stor mängd data, ca 100 MB och lägger till lite tidtagning:
public class Client {
public static void main(String[] args) {
long start = System.currentTimeMillis();
try {
Server proxy = new ServerService().getServerPort(new javax.xml.ws.soap.MTOMFeature());
int size = 100000000; // ~100MB
...
} catch (java.io.IOException e) {
e.printStackTrace();
} finally {
System.out.println((System.currentTimeMillis() - start) + " ms");
}
}
}
Innan jag kör klienten så stänger jag av HTTP-dumpningen i servern (för nu kommer det skickas massor med data).
public static void main(String[] args) {
System.setProperty("com.sun.xml.ws.transport.http.HttpAdapter.dump", "false");
...
}
Jag startar om servern:
Ctrl^C
mvn compile exec:java -Dexec.mainClass=”se.stacktrace.server.Server”
Och så kör jag klienten på nytt:
mvn compile exec:java -Dexec.mainClass=”se.stacktrace.client.Client”
...
[INFO] [exec:java {execution: default-cli}]
true
9829 ms
...
Det tog nästan 10 sekunder på min dator att läsa ~100MB data. För att optimera den här läsningen så kan jag använda mej utav en klass som kommer med JAX-WS RI, StreamingDataHandler. Den kan användas för att läsa datat effektivare. Jag ändrar min kod i Client.java för att nyttja StreamingDataHandler:
...
import com.sun.xml.ws.developer.StreamingDataHandler;
...
public class Client {
public static void main(String[] args) {
long start = System.currentTimeMillis();
try {
...
java.io.InputStream in = ((StreamingDataHandler) resp).readOnce();
...
in.close();
((StreamingDataHandler) resp).close();
System.out.println(size == total);
} catch (java.io.IOException e) {
e.printStackTrace();
} finally {
System.out.println((System.currentTimeMillis() - start) + " ms");
}
}
}
Nu kör jag klienten på nytt och ser om det går snabbare att läsa datan:
mvn compile exec:java -Dexec.mainClass=”se.stacktrace.client.Client”
...
[INFO] [exec:java {execution: default-cli}]
true
3750 ms
Se där, det gick ju lite snabbare att läsa datan.
För mer läsning/referenser: