Ok, so there is the whole debate about EJB's vs. Web Services, at least at my company - we have a team of Java developers who want to use EJB's (well sort of) and the Architecture team who is pushing portability/compatibility with web services.
We use IBM's WebSphere here at work which is both good and bad I suppose - recently I was asked to help out with a Proof - Of - Concept application and wanted to leverage some of the services developed on WebSphere but from a non-WebSphere app server. Guess what... IBM won't let you access their services unless you are running under their JDK. What the heck is up with that IBM? We're wanting to play with some of the cool stuff that EJB 3/JPA and Annotations can give us but IBM doesn't support that in their JDK's so we can't play at all.
So, I'm playing with web services because I want a service that can be portable and compatible with other languages/frameworks - which comes at a bit of a performance hit - I understand - but it would be nice to have some services that could be accessed from other languages - don't get me wrong I love Java but I like to tinker with Python and Ruby as well....
My only complaint with Spring's Web Services is the build process...
I wrote my first Spring web service to authenticate user's against one of our LDAP servers, now I'm getting ready to write another web service from scratch and thought I'd write a tutorial on it as I go through the steps (note: I have adapted all of the code examples from a project I'm developing for another company so if you seen any misnamed xml namespaces, class files - or anything else let me know I'll fix them up for the next person).
Let's call our new web service...."ApplicationServices"
- First of all create a Web Project however you like to do that (I like Eclipse so I'm going to be creating a new "Dynamic Web Project" with as few faces as possible - no struts, no jsf, nothing, just a plain vanilla servlet project)
- My application will be running on GlassFish this is what the initial layout looks like:
- For my project I need to expose some functionality that was written to be included in another application. Luckily it can be bundled up in a Jar and included into our project very easily. Along with that let's go ahead and add all of our other supporting libraries. Here is a list of every jar I needed to add for the Spring Web Services framework to work (versioning shouldn't be too terribly critical just don't use anything less than the version listed), along with hibernate as well.:
- spring-ws-core-1.0.2.jar
- spring-xml-1.02.jar
- spring.jar
- commons-logging.jar
- commons-collections.jar(hibernate)
- log4j-1.2.15.jar
- jdom.jar - get this at JDOM
- WSDL4j.jar
- cglib-nodep-2.1_3.jar (hibernate)
- ehcache-1.2.4.jar (hibernate)
- hibernate3.jar (hibernate, duh)
- dom4j-1.6.1.jar (hibernate)
- Now that we have our libs all setup we need to configure our web.xml file. Basically we need to map the spring webservice servlet class to a url and load up our config file like this:
-
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>ApplicationServices</display-name>
<servlet>
<servlet-name>services-ws</servlet-name>
<servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>services-ws</servlet-name>
<url-pattern>*.wsdl</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>- So, what is going here? the <servlet> Tag let's our container know we have a servlet (you probably figured that much out) and then gives us a name to map it with and the class that needs to be loaded for it.
- Next is the servlet mapping which uses the mapping name we just created and maps that to any requests that come in with a URL that ends with *.wsdl
- Now this is the important part you MUST name your spring file the name of your servlet so for this example our spring file must be named: services-ws-servlet.xml and it must be in your WEB-INF directory (see the picture up above).
- To be convenient let's load up a welcome file to see if things are working (don't forget to create the index.jsp file in the WebContent root directory - we will be using it to test with. You can do something simple like "Hello World" if you want. I never seem to get tired of seeing that one....
- Let's go ahead and stub out a spring file and see if our application will even load. Create a new file called services-ws-servlet.xml in the WEB-INF directory you can fill it up with this:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.0.xsd
http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-2.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.0.xsd" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:lang="http://www.springframework.org/schema/lang" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:util="http://www.springframework.org/schema/util">
</beans>- That should be enough to get us started
- Go ahead and fire up your container and see if it works. You shouldn't see any startup exceptions in your logs. And if you go to the URL of your container probably something like http://localhost:8080/ApplicationServices/index.jsp you should see whatever you content you put in the jsp page - now if you were to jump ahead and go to http://localhost:8080/ApplicationServices/something.wsdl the servlet we configured will load up and you should get a 405 error.
- If you look at your logs you probably saw an error that said your log4j isn't configured correctly let's throw out a log4j.properties file in our classpath real quick this is the content I typically use:
log4j.rootLogger=DEBUG, stdout, logfile
log4j.rootCategory=DEBUG, stdout, logfile
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
# %d = date
# %p = priority
# %c = category (class)
# %m = message
# %C = class
# %M = method name
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c.%M] - %m %n
log4j.appender.logfile=org.apache.log4j.RollingFileAppender
log4j.appender.logfile.File=../logs/applications/applicationServices.log
log4j.appender.logfile.MaxFileSize=1024KB
log4j.appender.logfile.MaxBackupIndex=5
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.category.org.hibernate=WARN
log4j.category.net.sf.ehcache=WARN
log4j.category.org.apache.axis=WARN
log4j.category.org.apache.commons.beanutils=WARN
log4j.category.org.apache.commons.digester=WARN
log4j.category.org.apache.catalina.session.ManagerBase=WARN
log4j.category.org.apache.jasper=WARN
log4j.category.org.springframework=WARN
- Let's create an auto process request object that has an instance name
- And an auto process response that has a statusCode and a messages field my XML looks like this:
<autoProcessing xmlns="http://www.flyingspheres.com/tutorial/services">
<processRequest
<instanceName>someInstanceName</instanceName>
</processRequest>
<processResponse>
<statusCode></statusCode>
<messages></messages>
</processResponse>
</autoProcessing>
- You could definitely hand tool the XSD it isn't that difficult however... since we want to reduce the hand tooling as much as possible go to HitSoftware and load up your xml file and generate your xsd
- You should get a response that looks like this:
<?xml version="1.0" encoding="UTF-8" ?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="autoProcessing">
<xs:complextype>
<xs:sequence>
<xs:element ref="processRequest" />
<xs:element ref="processResponse" />
</xs:sequence>
</xs:complextype>
</xs:element>
<xs:element name="instanceName">
<xs:complextype mixed="true" />
</xs:element>
<xs:element name="messages" type="xs:string" />
<xs:element name="processRequest">
<xs:complextype>
<xs:sequence>
<xs:element ref="instanceName" />
</xs:sequence>
</xs:complextype>
</xs:element>
<xs:element name="processResponse">
<xs:complextype>
<xs:sequence>
<xs:element ref="statusCode" />
<xs:element ref="messages" />
</xs:sequence>
</xs:complextype>
</xs:element>
<xs:element name="statusCode" type="xs:string" />
</xs:schema>
<?xml version="1.0" encoding="UTF-8" ?>
<xs:schema targetNamespace="http://service.flyingspheres.com/autoProcessing"
elementFormDefault="qualified"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:auto="http://service.flyingspheres.com/autoProcessing">
<xs:element name="autoProcessing">
<xs:complexType>
<xs:choice>
<xs:element ref="auto:processResponse" />
<xs:element ref="auto:processRequest" />
</xs:choice>
</xs:complexType>
</xs:element>
<xs:element name="processRequest">
<xs:complexType>
<xs:sequence>
<xs:element ref="auto:instanceName" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="processResponse">
<xs:complexType>
<xs:sequence>
<xs:element ref="auto:messages" />
<xs:element ref="auto:statusCode" />
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="instanceName" type="xs:string" />
<xs:element name="messages" type="xs:string" />
<xs:element name="statusCode" type="xs:string" />
</xs:schema>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-2.0.xsd
http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-2.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-2.0.xsd" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:lang="http://www.springframework.org/schema/lang" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:util="http://www.springframework.org/schema/util">
<import resource="persistence_ioc.xml" />
<!-- WSDL Generation Information -->
<bean id="autoProcessing" class="org.springframework.ws.wsdl.wsdl11.DynamicWsdl11Definition">
<property name="builder">
<description>
The builder creates a WSDL from the a schema.
It detects all elements that ends with 'Request', finds corresponding
'Response' messages, and creates an operation based on that.
</description>
<bean class="org.springframework.ws.wsdl.wsdl11.builder.XsdBasedSoap11Wsdl4jDefinitionBuilder">
<property name="schema" value="/WEB-INF/mySchemaFile.xsd"/>
<property name="portTypeName" value="processing"/>
<property name="locationUri" value="http://localhost:8080/ApplicationServices/"/>
</bean>
</property>
</bean>
<bean id="servicesEndPoint" class="com.flyingspheres.service.endpoints.ServicesEndPoint">
<constructor-arg ref="autoProcessingService"/>
</bean>
<bean id="autoProcessingService" class="com.flyingspheres.service.AutoProcessingService">
</bean>
<bean class="org.springframework.ws.server.endpoint.mapping.PayloadRootQNameEndpointMapping">
<property name="mappings">
<props>
<prop key="{http://service.flyingspheres.com/processing}Processing">servicesEndPoint</prop>
</props>
</property>
<property name="interceptors">
<list>
<ref local="loggingInterceptor"/>
<ref local="validatingInterceptor"/>
</list>
</property>
</bean>
<bean id="loggingInterceptor" class="org.springframework.ws.server.endpoint.interceptor.PayloadLoggingInterceptor">
<description>
This interceptor logs the message payload.
</description>
</bean>
<bean id="validatingInterceptor"
class="org.springframework.ws.soap.server.endpoint.interceptor.PayloadValidatingInterceptor">
<description>
This interceptor validates both incoming and outgoing message contents according to the 'echo.xsd' XML
Schema file.
</description>
<property name="schema" value="/WEB-INF/mySchemaFile.xsd"/>
<property name="validateRequest" value="true"/>
<property name="validateResponse" value="true"/>
</bean>
</beans>
- Details:
- the autoProcessing bean has defines how the container / client will interact with our service you can access the wsdl by going to the url locationUri/portTypeName.wsdl: http://localhost:8080/ApplicationServices/processing.wsdl obviously this will probably be different for you.
- servicesEndPoint bean is the entry point for your application, our example uses a constructor arg so that we can control how it is created - this can be done a lot of different ways but this is how the Spring WS tutorial does it so we will follow suit.
- autoProcessingServices bean is where we will do most of our coding that is what will actually happen
- The next bean doesn't have a name but is really where the magic happens that kind of maps urls to our endpoints (which ends up calling our service bean). This line: <prop key="{http://service.flyingspheres.com/processing}processing">servicesEndPoint</prop> is where the url is mapped to our end point
- Next some interceptors are defined (we'll talk about interceptors and aspects some other day). For now they're there to do what they look like validation and logging
- the autoProcessing bean has defines how the container / client will interact with our service you can access the wsdl by going to the url locationUri/portTypeName.wsdl: http://localhost:8080/ApplicationServices/processing.wsdl obviously this will probably be different for you.
- Well if we did everything correctly we should be able to access a wsdl that looks something like this:
<?xml version="1.0" encoding="UTF-8"?><wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:schema="http://service.flyingspheres.com/processing" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" targetNamespace="http://service.flyingspheres.com/processing">
<wsdl:types>
<xs:schema xmlns:auto="http://service.flyingspheres.com/processing" xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://service.flyingspheres.com/processing">
<xs:element name="autoProcessing">
<xs:complexType>
<xs:choice>
<xs:element ref="auto:processResponse"/>
<xs:element ref="auto:processRequest"/>
</xs:choice>
</xs:complexType>
</xs:element>
<xs:element name="processRequest">
<xs:complexType>
<xs:sequence>
<xs:element ref="auto:instanceName"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="processResponse">
<xs:complexType>
<xs:sequence>
<xs:element ref="auto:messages"/>
<xs:element ref="auto:statusCode"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="instanceName" type="xs:string"/>
<xs:element name="messages" type="xs:string"/>
<xs:element name="statusCode" type="xs:string"/>
</xs:schema>
</wsdl:types>
<wsdl:message name="processRequest">
<wsdl:part element="schema:processRequest" name="processRequest">
</wsdl:part>
</wsdl:message>
<wsdl:message name="processResponse">
<wsdl:part element="schema:processResponse" name="processResponse">
</wsdl:part>
</wsdl:message>
<wsdl:portType name="autoProcessing">
<wsdl:operation name="process">
<wsdl:input message="schema:processRequest" name="processRequest">
</wsdl:input>
<wsdl:output message="schema:processResponse" name="processResponse">
</wsdl:output>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="autoProcessingBinding" type="schema:autoProcessing">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="process">
<soap:operation soapAction=""/>
<wsdl:input name="processRequest">
<soap:body use="literal"/>
</wsdl:input>
<wsdl:output name="processResponse">
<soap:body use="literal"/>
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
<wsdl:service name="autoProcessingService">
<wsdl:port binding="schema:autoProcessingBinding" name="autoProcessingPort">
<soap:address location="http://localhost:8080/ApplicationServices/"/>
</wsdl:port>
</wsdl:service>
</wsdl:definitions> - We can then take this wsdl and drop it into our client and have it generate the necessary files to call our web services.
- Before we close this tutorial for now let's talk real quick about the ServicesEndPoint class. So we told spring that we were going to create a class called com.flyingspheres.service.endpoints.ServicesEndPoint that would extend the AbstractJDomPayloadEndpoint class (by telling spring that it would be called from the org.springframework.ws.server.endpoint.mapping.PayloadRootQNameEndpointMapping class that meant we had to extend the AbstractJDomPayloadEndpoint. Since we also told spring that we would overload the constructor with our serviceBean we need to define that constructor as well as the methods the PayloadEndpoint forces us to implement. The Spring WS tutorial section 3.6 does a great job explaining what needs to happen in those methods - so I won't cover that here. For our needs lets put in some logging like this:
package com.flyingspheres.service.endpoints;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jdom.Element;
import org.springframework.ws.server.endpoint.AbstractJDomPayloadEndpoint;
import com.flyingspheres.service.AutoProcessingService;
public class ServicesEndPoint extends AbstractJDomPayloadEndpoint{
private static final Log LOG = LogFactory.getLog(ServicesEndPoint.class.getName());
public ServicesEndPoint(AutoProcessingService service){
LOG.debug("Entering the constructor with a: " + service.getClass().getName());
}
protected Element invokeInternal(Element authenticationRequest) throws Exception {
LOG.debug("Entering the invokeInternal method with a: " + authenticationRequest.getClass().getName());
return null;
}
}
Well that should be enough to get you started... Read the tutorial and all of the other documentation as well if you have any questions let me know Aaron at flyingspheres dot com
Happy coding!