Saturday, November 21, 2015

Spring WS with Spring Boot

Related posts :   Simple Spring boot application

1. Intro 

Usually, to make a web service we had to configure web server (like oracle weblogic, glassfish) and deploy there our web service. With SpringWS  - everything is much more simple : our SpringWS-based web service can be deployed just to Tomcat - we don't need web servers. But with using of SpringBoot for running SpringWS-based web service  - we even don't need a Tomcat : we can just run our application using main(String[] args)  function and that's it!


2. Project structure

SpringWS use "contract first" approach for web services. That means: we have to start with "contract" : our web services have to be "explained" using XSD schemes for domain entities and operations. In this example we have only one entity class : Category.xsd and operation file CategoryOperations.xsd  with 2 operations: save category and getCategoryById.

After that, based on XSD files we have to generate .java classes. Highlighted files on next picture were generated - we were not creating them manually :





3. pom.xml 

Most interesting thing here located in the bottom of file  - it's a plugin for generating java src files based on our XSD schemes.
<?xml version="1.0"?><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/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <artifactId>com.demien</artifactId>
   <groupId>springws</groupId>
   <version>1.0</version>
   <packaging>jar</packaging>

   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
                <version>1.2.7.RELEASE</version>
      <relativePath/>
   </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-ws</artifactId>
        </dependency>
        <dependency>
            <groupId>wsdl4j</groupId>
            <artifactId>wsdl4j</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.ws.xmlschema</groupId>
            <artifactId>xmlschema-core</artifactId>
            <version>2.0.1</version>
        </dependency>
    </dependencies>


   <build>
      <plugins>
         <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>jaxb2-maven-plugin</artifactId>
            <version>1.4</version>
            <executions>
               <execution>
                  <goals>
                     <goal>xjc</goal>
                  </goals>
                  <phase>generate-sources</phase>
               </execution>
            </executions>
            <configuration>
               <clearOutputDir>false</clearOutputDir>
               <outputDirectory>src/main/java</outputDirectory>
               <schemaDirectory>src/main/resources</schemaDirectory>
               <enableIntrospection>false</enableIntrospection>
            </configuration>
         </plugin>
      </plugins>
   </build>

</project>


4. Entity schema (Category.xsd)

Just regular java POJO class "transalted" to XSD language:

<?xml version="1.0" encoding="UTF-8"?><xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"            xmlns="http://com/demien/springws/domain" targetNamespace="http://com/demien/springws/domain" elementFormDefault="qualified" attributeFormDefault="unqualified">
   <xs:element name="Category" type="Category"/>
   <xs:complexType name="Category">
      <xs:sequence>
         <xs:element name="CategoryId" type="xs:integer"/>
         <xs:element name="CategoryName" type="xs:string"/>
         <xs:element name="CategoryDescription" type="xs:string"/>
         <xs:element name="CategoryParentId" type="xs:integer"/>
      </xs:sequence>
   </xs:complexType>
</xs:schema>


5. Operations schema (CategoryOperations.xsd)

Here we have 2 operations : save and getbyId. For both of them we have to define data types for request and response. So, in total we have 4 types :


<?xml version="1.0" encoding="UTF-8"?><xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"             xmlns="http://com/demien/springws/domain/operation"            xmlns:category="http://com/demien/springws/domain" targetNamespace="http://com/demien/springws/domain/operation" elementFormDefault="qualified">
   <xsd:import namespace="http://com/demien/springws/domain" schemaLocation="Category.xsd"/>
   <xsd:element name="CategorySaveRequest">
      <xsd:complexType>
         <xsd:sequence>
            <xsd:element name="category" type="category:Category"/>
         </xsd:sequence>
      </xsd:complexType>
   </xsd:element>
   <xsd:element name="CategorySaveResponse">
      <xsd:complexType>
         <xsd:sequence>
            <xsd:element name="category" type="category:Category"/>
         </xsd:sequence>
      </xsd:complexType>
   </xsd:element>

   <xsd:element name="CategoryGetByIdRequest">
      <xsd:complexType>
         <xsd:sequence>
            <xsd:element name="categoryId" type="xsd:integer"/>
         </xsd:sequence>
      </xsd:complexType>
   </xsd:element>
   <xsd:element name="CategoryGetByIdResponse">
      <xsd:complexType>
         <xsd:sequence>
            <xsd:element name="category" type="category:Category"/>
         </xsd:sequence>
      </xsd:complexType>
   </xsd:element>

</xsd:schema>



6. Web service configuration(WSConfig.java)

Here we have 4 beans :
 - messageDispatcherServlet - it will intercept our calls to url /ws/ and interpret them as web service calls
- Wsdl11Definition - it is responsible for showing WSDL - description of our web service. This WSDL file will be automatically generated based on CategoryOperations.xsd file. After all, it will be accessable by path ws/beanName.wsdl  - so in our case :  /ws/CategoryService.wsdl.
- Category schema - it will be accessable by path /ws/Category.xsd
- Category operation schema - it will be accessable by path /ws/CategoryOperations.xsd

package com.demien.springws;

import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ws.config.annotation.EnableWs;
import org.springframework.ws.transport.http.MessageDispatcherServlet;
import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition;
import org.springframework.xml.xsd.SimpleXsdSchema;
import org.springframework.xml.xsd.XsdSchema;


@Configuration@EnableWspublic class WSConfig {

    @Bean    public ServletRegistrationBean messageDispatcherServlet(ApplicationContext applicationContext) {
        MessageDispatcherServlet servlet = new MessageDispatcherServlet();
        servlet.setApplicationContext(applicationContext);
        servlet.setTransformWsdlLocations(true);
        return new ServletRegistrationBean(servlet, "/ws/*");
    }

    @Bean(name = "CategoryService")
    public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema CategoryOperations) {
        DefaultWsdl11Definition result = new DefaultWsdl11Definition();
        result.setPortTypeName("CategoryWS");
        result.setLocationUri("/ws");
        result.setTargetNamespace("http://com/demien/springws/domain/operation");

        result.setSchema(CategoryOperations);
        return result;
    }

    @Bean    public XsdSchema Category() {
        return new SimpleXsdSchema(new ClassPathResource("Category.xsd"));
    }

    @Bean    public XsdSchema CategoryOperations() {
        return new SimpleXsdSchema(new ClassPathResource("CategoryOperations.xsd"));
    }

}



7. App main file(App.java)

Here everything is pretty simple - just running :
package com.demien.springws;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplicationpublic class App {
    public static void main(String[] args) {
        SpringApplication.run(new Class<?>[]{App.class}, args);
    }
}


8. Service(business logic) class(CategoryService.java)

Here I made "emulation" of service implementation : my services is storing data in HashMap instead of saving in DB and generate ID by increment of static variable :

package com.demien.springws.service;

import com.demien.springws.domain.Category;
import org.springframework.stereotype.Service;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;

@Servicepublic class CategoryService {

    private Map<BigInteger,Category> storage=new HashMap<BigInteger,Category>();
    private int id=1;

    public Category save(Category category) {
        category.setCategoryId(BigInteger.valueOf(id));
        id++;
        storage.put(category.getCategoryId(), category);
        return category;
    }

    public Category getById(BigInteger categoryId) {
        Category result=storage.get(categoryId);
        return result;
    }
}

9. Build app  - for generating sources from schema files

Now development part is almost over  and we need to generate classes from XDS schemas. Everything is defined in our pom.xml, so, all we need is to run :
  mvn clean compile

During compiling we will see in output list of generated files :
[INFO] com\demien\springws\domain\operation\CategoryGetByIdRequest.java
[INFO] com\demien\springws\domain\operation\CategoryGetByIdResponse.java
[INFO] com\demien\springws\domain\operation\CategorySaveRequest.java
[INFO] com\demien\springws\domain\operation\CategorySaveResponse.java
[INFO] com\demien\springws\domain\operation\ObjectFactory.java
[INFO] com\demien\springws\domain\operation\package-info.java
[INFO] com\demien\springws\domain\Category.java
[INFO] com\demien\springws\domain\ObjectFactory.java
[INFO] com\demien\springws\domain\package-info.java


10. Web service endpoint(CategoryServiceEndpoint.java)

All files were generated, now one step more to go: create a web service endpoint  - "wrapper" for CategoryService with needed for endpoint annotations :

package com.demien.springws.service;

import com.demien.springws.domain.Category;
import com.demien.springws.domain.operation.CategoryGetByIdRequest;
import com.demien.springws.domain.operation.CategoryGetByIdResponse;
import com.demien.springws.domain.operation.CategorySaveRequest;
import com.demien.springws.domain.operation.CategorySaveResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;
import org.springframework.ws.server.endpoint.annotation.RequestPayload;
import org.springframework.ws.server.endpoint.annotation.ResponsePayload;

@Endpointpublic class CategoryServiceEndpoint {
    private static final String TARGET_NAMESPACE = "http://com/demien/springws/domain/operation";


    @Autowired    CategoryService categoryService;

    public CategoryServiceEndpoint() {
    }

    @PayloadRoot(localPart = "CategorySaveRequest", namespace = TARGET_NAMESPACE)
    public @ResponsePayload    CategorySaveResponse save(@RequestPayload CategorySaveRequest request)       {
        CategorySaveResponse response = new CategorySaveResponse();
        Category category=categoryService.save(request.getCategory());
        response.setCategory(category);
        return response;
     }

    @PayloadRoot(localPart = "CategoryGetByIdRequest", namespace = TARGET_NAMESPACE)
    public @ResponsePayload    CategoryGetByIdResponse save(@RequestPayload CategoryGetByIdRequest request)       {
        CategoryGetByIdResponse response = new CategoryGetByIdResponse();
        Category category=categoryService.getById(request.getCategoryId());
        response.setCategory(category);
        return response;
    }

}


11. Running and testing of our web service using SoapUI 

Now we can run our application(main procedure in App.java class) and check our services.

First of all we can check our resources - by next 2 links we have to be able to open our schemas :
http://localhost:8080/ws/Category.xsd
http://localhost:8080/ws/CategoryOperations.xsd

Next - we can check generation of WSDL file(from CategoryOperations.xsd) by SpringWS:
http://localhost:8080/ws/CategoryService.wsdl

And if everything is OK  - we can call web service from client, for example using SoapUI.
After import of WSDL link to SoapUI - it creates 2 operations : CategorySave and CategoryGetById.

Execution of CategorySave :
In request I filled only CategoryName field:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:oper="http://com/demien/springws/domain/operation" xmlns:dom="http://com/demien/springws/domain">
   <soapenv:Header/>
   <soapenv:Body>
      <oper:CategorySaveRequest>
         <oper:category>
            <dom:CategoryId>?</dom:CategoryId>
            <dom:CategoryName>First Category</dom:CategoryName>
            <dom:CategoryDescription>?</dom:CategoryDescription>
            <dom:CategoryParentId>?</dom:CategoryParentId>
         </oper:category>
      </oper:CategorySaveRequest>
   </soapenv:Body>
</soapenv:Envelope>

Returned response - we can see that ID was generated :
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP-ENV:Header/>
   <SOAP-ENV:Body>
      <ns3:CategorySaveResponse xmlns:ns2="http://com/demien/springws/domain" xmlns:ns3="http://com/demien/springws/domain/operation">
         <ns3:category>
            <ns2:CategoryId>1</ns2:CategoryId>
            <ns2:CategoryName>First Category</ns2:CategoryName>
            <ns2:CategoryDescription>?</ns2:CategoryDescription>
         </ns3:category>
      </ns3:CategorySaveResponse>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>


Execution of CategoryGetById:
Request - I filled CategoryId :
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:oper="http://com/demien/springws/domain/operation">
   <soapenv:Header/>
   <soapenv:Body>
      <oper:CategoryGetByIdRequest>
         <oper:categoryId>1</oper:categoryId>
      </oper:CategoryGetByIdRequest>
   </soapenv:Body>
</soapenv:Envelope>


Returned response - category which was created on previous step :
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
   <SOAP-ENV:Header/>
   <SOAP-ENV:Body>
      <ns3:CategoryGetByIdResponse xmlns:ns2="http://com/demien/springws/domain" xmlns:ns3="http://com/demien/springws/domain/operation">
         <ns3:category>
            <ns2:CategoryId>1</ns2:CategoryId>
            <ns2:CategoryName>First Category</ns2:CategoryName>
            <ns2:CategoryDescription>?</ns2:CategoryDescription>
         </ns3:category>
      </ns3:CategoryGetByIdResponse>
   </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

12. The end. 

Source code can be downloaded from here.

1 comment:

  1. Hi, If i have few xsd import in my main xsd, do i need to create a simpleXsdSchema method and declare as bean for each one of them? Is there any simple way to do it?
    I tried using XsdSchemaCollection but it creates wsdl with message types and operations from reference xsd that were imported as well which is not required for client.

    ReplyDelete