Spring SOAP Web services – Add SOAP Fault Exception handling – Part III
In this post, we will learn how to add detail information to your exceptions. Spring WS enables easy translation of errors penetrated from business logic. Exceptions are automatically thrown up the chain with the use of EndpointExceptionResolvers
. Rather than exposing the internals of your application by giving an exception and stack trace, you can handle the exceptions programmatically and add an appropriate error code and description. This example shows you how to add detailed information to your SoapFault Exception.
In this post, we will implement
- MovieNotFoundException – Throw this exception when we know if a movie not present in the database
- ServiceFaultException – Add Custom code and error messages
Here are the list of posts on SOAP web services using Spring framework for your reference
- Publish SOAP web services – perform CRUD operation and consume SOAP web services using SOAP UI : We will explore these topic in this post – Publish SOAP Web services using Spring Boot – Part 1
- Consume SOAP web services using client application – Consume Spring SOAP web services using client application – Part II
- Exception handling in SOAP web services- We will learn about this topic in here
- Exception handling in CRUD SOAP web services – Spring SOAP Web services – Add SOAP Fault Exception handling for CRUD operations – Part IV
- Securing SOAP web services – In upcoming tutorial
- Testing SOAP web services – In upcoming tutorial
List of Contents
- Tools and Environment
- Project Structure
- Steps to create SOAP Server and SOAP Client with Exception handling
- Screenshots
- Download
- References
1. Tools and Environment
Following tools and environments are used to consume SOAP web services in Spring Boot
- Spring Tool Suite (version: 3.9.4.RELEASE)
- Spring Boot (version: 2.0.4)
- Java version (version 8)
- Maven (version 3.5.2)
- Mysql (version: 5.1.17)
2. Project Structure
3. Steps to create SOAP Server and SOAP Client with Exception handling
- Create sample data in the database
- New Spring Starter project
- Add “movies.xsd” under /src/main/resources/xsd folder
- Edit pom.xml
- Add “wsdl4j” dependency (The Web Services Description Language for Java Toolkit (WSDL4J) allows the creation, representation, and manipulation of WSDL documents)
- jaxb2-maven–plugin (to generate java source classes from XSD))
- Do Maven-> update project to generate java class files
- Create SOAP Server related files
- Create Movie Service class to perform CRUD operations on Movie object – “MovieService.java”
- Create SOAP Server configuration file – “SoapServerConfig.java”
- Create SOAP Endpoint file – “MovieEndpoint.java”
- Create SOAP Server Exceptions – “MovieNotFoundException”, “ServiceFaultException”, “DetailSoapFaultDefinitionExceptionResolver”
- Create an executable server class to run the Server – “RunServer.java”
- Run Server
- Right click on the “RunServer” and do Run As -> Java Application to start the server on default/specified port
- Test WDSL on web browser
- Test in SOAP UI or Create a Client application to test the exception
4. ScreenShots
Step 1: Create sample data in the database
CREATE TABLE movies ( movie_id BIGINT(5) NOT NULL AUTO_INCREMENT, title VARCHAR(45) NULL, category VARCHAR(45) NULL, PRIMARY KEY (movie_id)); INSERT INTO movies(movie_id, title, category) VALUES (101, 'Mama mia', 'drama'), (102, 'Mission Impossible', 'action'), (103, 'Coco', 'animation');
Step 2: New Spring Starter project
Step 3: Add “movies.xsd” under /src/main/resources/xsd folder
movies.xsd
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://www.javaspringclub.com/movies-ws" targetNamespace="http://www.javaspringclub.com/movies-ws" elementFormDefault="qualified"> <xs:element name="getMovieByIdRequest"> <xs:complexType> <xs:sequence> <xs:element name="movieId" type="xs:long" /> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="getMovieByIdResponse"> <xs:complexType> <xs:sequence> <xs:element name="movieType" type="tns:movieType" /> </xs:sequence> </xs:complexType> </xs:element> <xs:complexType name="movieType"> <xs:sequence> <xs:element name="movieId" type="xs:long" /> <xs:element name="title" type="xs:string" /> <xs:element name="category" type="xs:string" /> </xs:sequence> </xs:complexType> <xs:element name="getAllMoviesRequest"> <xs:complexType/> </xs:element> <xs:element name="getAllMoviesResponse"> <xs:complexType> <xs:sequence> <xs:element name="movieType" type="tns:movieType" maxOccurs="unbounded"/> </xs:sequence> </xs:complexType> </xs:element> <xs:complexType name="serviceStatus"> <xs:sequence> <xs:element name="statusCode" type="xs:string"/> <xs:element name="message" type="xs:string"/> </xs:sequence> </xs:complexType> <xs:element name="addMovieRequest"> <xs:complexType> <xs:sequence> <xs:element name="title" type="xs:string"/> <xs:element name="category" type="xs:string"/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="addMovieResponse"> <xs:complexType> <xs:sequence> <xs:element name="serviceStatus" type="tns:serviceStatus"/> <xs:element name="movieType" type="tns:movieType"/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="updateMovieRequest"> <xs:complexType> <xs:sequence> <xs:element name="title" type="xs:string"/> <xs:element name="category" type="xs:string"/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="updateMovieResponse"> <xs:complexType> <xs:sequence> <xs:element name="serviceStatus" type="tns:serviceStatus"/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="deleteMovieRequest"> <xs:complexType> <xs:sequence> <xs:element name="movieId" type="xs:long"/> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="deleteMovieResponse"> <xs:complexType> <xs:sequence> <xs:element name="serviceStatus" type="tns:serviceStatus"/> </xs:sequence> </xs:complexType> </xs:element> </xs:schema>
Step 4: Edit pom.xml to include wsdl4j and jaxb2-maven-plugin
Do Maven update project after saving pom.xml
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.javaspringclub</groupId> <artifactId>SpringBoot_SOAP_Fault_Exceptions</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>SpringBoot_SOAP_Fault_Exceptions</name> <description>WebService Example</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web-services</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>wsdl4j</groupId> <artifactId>wsdl4j</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>jaxb2-maven-plugin</artifactId> <version>2.3.1</version> <executions> <execution> <id>xjc-schema</id> <goals> <goal>xjc</goal> </goals> </execution> </executions> <configuration> <sources> <source>src/main/resources/xsd/movies.xsd</source> </sources> <packageName>com.javaspringclub.gs_ws</packageName> <clearOutputDir>false</clearOutputDir> </configuration> </plugin> </plugins> </build> </project>
Maven plugin will generate java source classes under “com.javaspringclub.gs_ws”. This package name is specified in pom.xml file
Alternatively, running the following command in parent directory, the Java Classes will be generated in the target/generated-sources/jaxb/<package-name> folder. <package-name> is specified in the pom.xml file
mvn package
Step 5: Create SOAP Server related files
- Create “MovieService.java”
- Create “SoapServerConfig.java”
- Create “MovieEndpoint.java”
- Create Custom Exceptions(optional)
- Create an executable java class “RunServer.java” to run server
MovieService.java
package com.javaspringclub.server; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.springframework.stereotype.Component; import com.javaspringclub.gs_ws.MovieType; import com.javaspringclub.gs_ws.ServiceStatus; @Component public class MovieService { public enum Status { SUCCESS, FAILURE; } private static List movies = new ArrayList<>(); static { MovieType movie1 = new MovieType(); movie1.setMovieId(101); movie1.setTitle("Mission Impossible"); movie1.setCategory("Action"); movies.add(movie1); MovieType movie2 = new MovieType(); movie2.setMovieId(102); movie2.setTitle("Argo"); movie2.setCategory("Drama"); movies.add(movie2); MovieType movie3 = new MovieType(); movie3.setMovieId(103); movie3.setTitle("Hang over"); movie3.setCategory("Comedy"); movies.add(movie3); } // Get Movie By Id - 1 public MovieType findById(long id) { for (MovieType movie : movies) { if (movie.getMovieId() == id) return movie; } return null; } // Get All movies public List findAll() { return movies; } public ServiceStatus deleteById(int id) { Iterator iterator = movies.iterator(); ServiceStatus status = new ServiceStatus(); while (iterator.hasNext()) { MovieType movie = iterator.next(); if (movie.getMovieId() == id) { iterator.remove(); // return Status.SUCCESS; status.setStatusCode("OK"); status.setMessage("SUCCESS"); return status; } } // return Status.FAILURE; status.setStatusCode("ERROR"); status.setMessage("FAILURE"); return status; } // updating course & new course }
SoapServerConfig.java
package com.javaspringclub.server; import java.util.Properties; import org.springframework.boot.web.servlet.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.config.annotation.WsConfigurerAdapter; import org.springframework.ws.soap.server.endpoint.SoapFaultDefinition; import org.springframework.ws.soap.server.endpoint.SoapFaultMappingExceptionResolver; 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; import com.javaspringclub.exception.DetailSoapFaultDefinitionExceptionResolver; import com.javaspringclub.exception.ServiceFaultException; @EnableWs @Configuration public class SoapServerConfig extends WsConfigurerAdapter { @SuppressWarnings({ "rawtypes", "unchecked" }) @Bean public ServletRegistrationBean messageDispatcherServlet(ApplicationContext appContext) { MessageDispatcherServlet servlet = new MessageDispatcherServlet(); servlet.setApplicationContext(appContext); servlet.setTransformWsdlLocations(true); return new ServletRegistrationBean(servlet, "/ws/*"); } @Bean(name = "movies") public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema schema) { DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition(); wsdl11Definition.setPortTypeName("MoviesPort"); wsdl11Definition.setLocationUri("/ws"); wsdl11Definition.setTargetNamespace(MovieEndpoint.NAMESPACE_URI); wsdl11Definition.setSchema(schema); return wsdl11Definition; } @Bean public XsdSchema moviesSchema() { return new SimpleXsdSchema(new ClassPathResource("/xsd/movies.xsd")); } @Bean public SoapFaultMappingExceptionResolver exceptionResolver() { SoapFaultMappingExceptionResolver exceptionResolver = new DetailSoapFaultDefinitionExceptionResolver(); SoapFaultDefinition faultDefinition = new SoapFaultDefinition(); faultDefinition.setFaultCode(SoapFaultDefinition.SERVER); exceptionResolver.setDefaultFault(faultDefinition); Properties errorMappings = new Properties(); errorMappings.setProperty(Exception.class.getName(), SoapFaultDefinition.SERVER.toString()); errorMappings.setProperty(ServiceFaultException.class.getName(), SoapFaultDefinition.SERVER.toString()); exceptionResolver.setExceptionMappings(errorMappings); exceptionResolver.setOrder(1); return exceptionResolver; } }
Note:
We will use Spring Java Configuration to configure our soap services in this example.
- The
@EnableWs
annotation enables support for the@Endpoint
and related Spring Ws annotations. - The
@Configuration
annotations tells spring that this Java Class is used as configuration file. - Spring WS uses a different servlet type for handling SOAP messages:
MessageDispatcherServlet
. It is important to inject and setApplicationContext
toMessageDispatcherServlet
. Without that, Spring WS will not detect Spring beans automatically. By naming this beanmessageDispatcherServlet
, it does not replace Spring Boot’s defaultDispatcherServlet
bean - Next, we map the service on the /ws/*URI. We do this by creating a
ServletRegistrationBean
with aMessageDispatcherServlet
which automatically transforms the WSDL location when deployed, together with the URI endpoint - We create a
DefaultWsdl11Definition
bean which we provide information about the portType, locationUri, targetNamespace and schema. It is important to notice that you need to specify bean name forDefaultWsdl11Definition
. Bean name determine the URL under which web service and the generated WSDL file is available. In this case, the WSDL will be available underhttp://<host>:<port>/ws/movies.wsdl
- For the soap exceptions to be propagated properly we must register our
SoapFaultMappingExceptionResolver
. We can define a defaultSoapFaultDefinition
. This default is used when theSoapFaultMappingExceptionResolver
does not have any appropriate mappings to handle the exception. We can map our Custom Exception by setting the mapping properties to the exception resolver. We do this by providing the fully qualified class name of the exception as the key andSoapFaultDefinition.SERVER
as the value. Finally, we have to specify an order, otherwise for some reason the mappings will not work.
MovieEndpoint.java
package com.javaspringclub.server; 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; import com.javaspringclub.exception.MovieNotFoundException; import com.javaspringclub.exception.ServiceFaultException; import com.javaspringclub.gs_ws.DeleteMovieRequest; import com.javaspringclub.gs_ws.DeleteMovieResponse; import com.javaspringclub.gs_ws.GetMovieByIdRequest; import com.javaspringclub.gs_ws.GetMovieByIdResponse; import com.javaspringclub.gs_ws.MovieType; import com.javaspringclub.gs_ws.ServiceStatus; @Endpoint public class MovieEndpoint { public static final String NAMESPACE_URI = "http://www.javaspringclub.com/movies-ws"; @Autowired MovieService service; @PayloadRoot(namespace = NAMESPACE_URI, localPart = "getMovieByIdRequest") @ResponsePayload public GetMovieByIdResponse getMovieById(@RequestPayload GetMovieByIdRequest request) { MovieType movie = service.findById(request.getMovieId()); if (movie == null) throw new MovieNotFoundException("Invalid Movie Id " + request.getMovieId()); return mapMovieDetails(movie); } private GetMovieByIdResponse mapMovieDetails(MovieType movie) { GetMovieByIdResponse response = new GetMovieByIdResponse(); response.setMovieType(movie); return response; } @PayloadRoot(namespace = NAMESPACE_URI, localPart = "deleteMovieRequest") @ResponsePayload public DeleteMovieResponse deleteMovieById(@RequestPayload DeleteMovieRequest request) { String errorMessage = "ERROR"; ServiceStatus serviceStatus = new ServiceStatus(); serviceStatus.setStatusCode("NOT_FOUND"); serviceStatus.setMessage("Movie with id: " + request.getMovieId() + " not found. Cannot delete Movie"); throw new ServiceFaultException(errorMessage, serviceStatus); } }
Note:
- We can produce Spring Ws Contract First Soap Endpoint by annotating the class with the
@Endpoint
annotation. This makes the class as a special sort of@Component
, suitable for handling XML messages in Spring WS and eligible for component scanning - The
@PayloadRoot
annotation informs Spring Ws that thegetMovieById
method is suitable for handling XML messages. This method can handle messages with localPart equal to getMovieByIdRequest with a namespace of http://www.javaspringclub.com/movies-ws @RequestPayload
tells Spring Ws that a XML Payload of typeGetMovieByIdRequest
is needed as a parameter of the request
MovieNotFoundException.java
package com.javaspringclub.exception; import org.springframework.ws.soap.server.endpoint.annotation.FaultCode; import org.springframework.ws.soap.server.endpoint.annotation.SoapFault; @SoapFault(faultCode = FaultCode.CUSTOM, customFaultCode = "{" + MovieNotFoundException.NAMESPACE_URI + "}custom_fault", faultStringOrReason = "@faultString") public class MovieNotFoundException extends Exception { private static final long serialVersionUID = 1L; public static final String NAMESPACE_URI = "http://javaspringclub.com/exception"; public MovieNotFoundException(String message) { super(message); } }
ServiceFaultException.java
package com.javaspringclub.exception; import com.javaspringclub.gs_ws.ServiceStatus; public class ServiceFaultException extends RuntimeException { private static final long serialVersionUID = 1L; private ServiceStatus serviceStatus; public ServiceFaultException(String message, ServiceStatus serviceStatus) { super(message); this.serviceStatus = serviceStatus; } public ServiceFaultException(String message, Throwable e, ServiceStatus serviceStatus) { super(message, e); this.serviceStatus = serviceStatus; } public ServiceStatus getServiceStatus() { return serviceStatus; } public void setServiceStatus(ServiceStatus serviceStatus) { this.serviceStatus = serviceStatus; } }
Note: The ServiceFaultException
extends from RuntimeException
, so we don’t need to catch or add it to our method signature. With the ServiceStatus
object we can add some detailed information about the error that occurred
ServiceStatus.java
This class either you implement on your own or specify in movies.xsd so that it generated by jaxb maven plugin. In our example, we took the second approach i.e specify in movies.xsd as shown below
<xs:complexType name="serviceStatus"> <xs:sequence> <xs:element name="statusCode" type="xs:string"/> <xs:element name="message" type="xs:string"/> </xs:sequence> </xs:complexType>
and here is the class, if you would like to write
import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlType; @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "ServiceStatus", propOrder = { "code", "description" }) public class ServiceStatus { private String code; private String description; public ServiceFault() { } public ServiceFault(String code, String description) { this.code = code; this.description = description; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } }
The ServiceStatus
class is used to propagate the appropriate error code and description up the chain. Typically the code is used to map the exception on the client side and the message is just a description indicating the user/developer what exactly went wrong.
DetailSoapFaultDefinitionExceptionResolver.java
package com.javaspringclub.exception; import javax.xml.namespace.QName; import org.springframework.ws.soap.SoapFault; import org.springframework.ws.soap.SoapFaultDetail; import org.springframework.ws.soap.server.endpoint.SoapFaultMappingExceptionResolver; import com.javaspringclub.gs_ws.ServiceStatus; public class DetailSoapFaultDefinitionExceptionResolver extends SoapFaultMappingExceptionResolver { private static final QName CODE = new QName("statusCode"); private static final QName MESSAGE = new QName("message"); @Override protected void customizeFault(Object endpoint, Exception ex, SoapFault fault) { logger.warn("Exception processed ", ex); if (ex instanceof ServiceFaultException) { ServiceStatus status = ((ServiceFaultException) ex).getServiceStatus(); SoapFaultDetail detail = fault.addFaultDetail(); detail.addFaultDetailElement(CODE).addText(status.getStatusCode()); detail.addFaultDetailElement(MESSAGE).addText(status.getMessage()); } } }
Note:
DetailSoapFaultDefinitionExceptionResolver
extends from theSoapFaultMappingExceptionResolver
and is used for enhancing theSoapFault
with detailed information about the error that occurred. We can override thecustomizeFault
method to enhance the exception with detail information. We do this by calling theaddFaultDetail
method of theSoapFault
class, and addingQName
indicating the element with theaddFaultDetailElement
.
RunServer.java
package com.javaspringclub.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class RunServer { public static void main(String[] args) { SpringApplication.run(RunServer.class); } }
Step 6: Run Server application.
Right click on “RunServer.java” and select Run As -> Java Application to run the Server and publish WSDL. In our application, Tomcat is started on port : 8080
Step 7: Test WSDL in web browser
You can view wsdl on http://localhost:8080/ws/movies.wsdl
Step 8: Test in SOAP UI or Create a Client application to test the exception
To run the client application:
All the required java classes are enclosed in the attached zip file in Download section
Right click on “RunClient.java” and click on Run As -> Java Application as shown below
5. Download
SpringBoot_SOAP_Fault_Exceptions
6. Reference
https://docs.spring.io/spring-ws/sites/1.5/reference/html/server.html
excellent article