Java Web Service Client
There are numerous
ways to create web service clients in java. The simplest way is of course to
create a proxy class from the wdsl (see http://sourceforge.net/projects/wsdl2javawizard/,
http://axis.apache.org/axis2/java/core/tools/eclipse/wsdl2java-plugin.html).
There are times that
this is fine usually when you need to support only one web service and it is
static. The downside is that a web service is just a protocol for
communication, and we have now coupled our code to this specific web service
and its interface.
First level of abstraction: spring templates
Spring has a template class
for multiple services. The class for this is: WebServiceTemplate. This class supports defaultUri if all
your calls are to the same web service.
The simplest function of this class is:
boolean sendSourceAndReceiveToResult(String
uri, Source requestPayload, WebServiceMessageCallback requestCallback, final Result
responseResult);
Through this function
you can send to any web service. You define the url and the xml payload for the
service. Spring will add all the soap headers needed (except for the soap
action – we will see this later on).
An example:
String url = "http://www.w3schools.com/webservices/tempconvert.asmx";
String xml =” <CelsiusToFahrenheit
xmlns="http://tempuri.org/"> <Celsius>22</Celsius>
</CelsiusToFahrenheit>”;
StreamSource webServiceInput = new
StreamSource(new StringReader(xmlDoc));
StringWriter finalResponseWriter = new
StringWriter();
StreamResult webServiceOutput = new
StreamResult(finalResponseWriter);
webServiceTemplate.sendSourceAndReceiveToResult(url,
webServiceInput,
webServiceOutput);
If succeeded the result of the
soap request is stored in the finalResponseWriter object.
The soap action needs to be added
to the soap request, and cannot be automatically added. To add support for this
there is the callback method. For example:
new WebServiceMessageCallback()
{
public void
doWithMessage(WebServiceMessage message) {
// Please see the WSDL for more
details.
((SoapMessage) message).setSoapAction(“http://tempuri.org/CelsiusToFahrenheit”);
}
A more generic approach can be added
by the following function:
public boolean
sendSourceAndReceiveToResult(String uri,
Source
requestPayload, final Result responseResult,
final String soapAction) {
return super.sendSourceAndReceiveToResult(uri,
requestPayload,
new
WebServiceMessageCallback() {
public void
doWithMessage(WebServiceMessage message) {
// Please see the
WSDL for more details.
((SoapMessage)
message).setSoapAction(soapAction);
}
},
responseResult);
}
The spring template will allow
the beginning of decoupling. We do not create specific classes for the web
service, and if the provider is changed the we can paramitize the web service
properties to an ini file and the xml request to another class to generate per
web service interface.
Second level of abstraction: spring integration
With the soap template we still
need write an abstraction level to create the xml requests per web service
client. In addition if the web service
needs to be changed with another protocol the code now needs to be redone. To
get full decoupling model we will use spring integration. To fully understand
spring integration see http://www.springsource.org/spring-integration.
To make the code as nice as
possible as in the example above we want to create a class CelsiusToFahrenheit and have the option to send a request to a service to get
the result. What needs to be done is to serialize the class to the specific
service protocol. This abstraction can be done with spring integration.
First we will look at
the code that sends the class to get the Celsius to Fahrenheit conversion.
CelsiusToFahrenheit cToF = new CelsiusToFahrenheit(22);
MessagingTemplate messagingTemplate = new MessagingTemplate(celsiusToFahrenheit);
Message<?>
response = messagingTemplate.sendAndReceive(new GenericMessage<CelsiusToFahrenheit>(cToF));
CelsiusToFahrenheitResponse
celsiusToFahrenheitResponse =
(CelsiusToFahrenheitResponse)response.getPayload();
To accomplish this we
need to convert the CelsiusToFahrenheit
class to xml using jaxb. We then send the xml on the web service. The result of
the web service is then unmarshalled from xml to the class CelsiusToFahrenheitResponse.
The spring integration
for this is:
<bean id="c2fMarshallingTransformer"
class="org.springframework.integration.xml.transformer.MarshallingTransformer">
<constructor-arg>
<bean class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="classesToBeBound">
<array>
<value> CelsiusToFahrenheit
</value>
</array>
</property>
</bean>
</constructor-arg>
<constructor-arg>
<bean
class="org.springframework.integration.xml.transformer.ResultToDocumentTransformer"
/>
</constructor-arg>
</bean>
<bean id="c2fUnmarshallingTransformer"
class="org.springframework.integration.xml.transformer.UnmarshallingTransformer">
<constructor-arg>
<bean class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="classesToBeBound">
<array>
<value>CelsiusToFahrenheitResponse
</value>
</array>
</property>
</bean>
</constructor-arg>
</bean>
<int:channel id="celsiusToFahrenheit"
/>
<int:chain input-channel="celsiusToFahrenheit">
<int:transformer ref="c2fMarshallingTransformer"
/>
<ws:header-enricher>
<ws:soap-action value="http://tempuri.org/CelsiusToFahrenheit"
/>
</ws:header-enricher>
<ws:outbound-gateway
uri="http://www.w3schools.com/webservices/tempconvert.asmx">
</ws:outbound-gateway>
<int:transformer ref="c2fUnmarshallingTransformer"
/>
</int:chain>
Other issues with Soap Requests
Whether using spring integration or spring template there
are issues with web services that need to be overcome.
SSL – with or without credentials
The first is to allow
sending web services over ssl and might or might not include using user
credentials. To do this spring has two implementations for the sending of the
web service. One is HttpComponentsMessageSender.
This implementation uses HttpClient
as the underlying connection. The HttpClient has many features that we will
use.
By using the url of
https, the HttpComponentsMessageSender
knows to send all requests via ssl. To use user credentials the following needs
to be configured on the message sender object:
<bean id="messageSender"
class="org.springframework.ws.transport.http.HttpComponentsMessageSender">
<property name="connectionTimeout"
value="3000" />
<property name="readTimeout"
value="5000" />
<property name="credentials">
<bean class="org.apache.http.auth.UsernamePasswordCredentials">
<constructor-arg value="username:password"
/>
</bean>
</property>
</bean>
Timeout
To add timeout
parameters we also use the HttpClient as above and will configure:
<bean id="messageSender"
class="org.springframework.ws.transport.http.HttpComponentsMessageSender">
<property name="connectionTimeout"
value="3000" />
<property name="readTimeout"
value="5000" />
</bean>
Retries
We will again use the
built in options of the HttpClient.
HttpClient support
retry by using the following interface:
public interface HttpRequestRetryHandler {
boolean
retryRequest(IOException exception, int executionCount, HttpContext context);
}
You can implement this in your own way
if needed. There is a default retry handler: DefaultHttpRequestRetryHandler.
The default handler will retry 3 times
if not succeeded. The default handler knows to distinguish between a failed
connection (and will retry) to other failures that retry will not help (like UnknownHostException,
ConnectException, SSLException, InterruptedIOException). In the case that the exception is not
in the list above the default handler calls the function handleAsIdempotent to
determine if it is a read only request.
In the case of web services all http
requests are post, and the protocol does not notify if the request is read only
or not. So we will add a new class SoapHttpRequestRetryHandler that will override the handleAsIdempotent and
allow to configure in the xml file the read only soap actions.
public class SoapHttpRequestRetryHandler
extends
DefaultHttpRequestRetryHandler {
private List<String> readOnlySoapActions = new
LinkedList<String>();
public
SoapHttpRequestRetryHandler(int retryCount, boolean requestSentRetryEnabled,
List<String> readOnlySoapActions){
super(retryCount,
requestSentRetryEnabled);
this.readOnlySoapActions =
readOnlySoapActions;
}
private String
getSoapAction(HttpRequest request){
return
request.getFirstHeader("SOAPAction").getValue();
}
@Override
protected boolean
handleAsIdempotent(HttpRequest request) {
if (readOnlySoapActions.indexOf(getSoapAction(request))!=-1)
return true;
return super.handleAsIdempotent(request);
}
}
Untrusted SSL Clients
In the case of a trusted site,
spring soap template knows to use the sites certificate to encrypt all data. If
the site is not trusted, then spring will raise an exception:
SunCertPathBuilderException:
unable to find valid certification path to requested target.
In the case you do not want this,
you need to register a new class SSLSocketFactory with the
httpclient, and add the method:
public boolean isTrusted(final X509Certificate[]
chain, String authType) throws CertificateException {
return true;
}
For example:
private static DefaultHttpClient
trustEveryoneSslHttpClient() {
try {
SchemeRegistry registry = new SchemeRegistry();
SSLSocketFactory socketFactory = new SSLSocketFactory(new TrustStrategy() {
public boolean isTrusted(final X509Certificate[]
chain, String authType) throws CertificateException {
// Oh, I am easy...
return true;
}
}, org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
registry.register(new Scheme("https", 443, socketFactory));
registry.register(new Scheme("http", 80, new PlainSocketFactory()));
PoolingClientConnectionManager mgr = new
PoolingClientConnectionManager(registry);
DefaultHttpClient client = new DefaultHttpClient(mgr, new
DefaultHttpClient().getParams());
return client;
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
No comments:
Post a Comment