Let's start by sketching the context in which we encountered the problems. We already had a contract first approach (actually, our WSDL was generated using an AndroMDA cartridge), so we could start from an existing WSDL. We only had to move the types into a separate XSD file (for JAXB, see further). Our WSDL defines a lot of operations with input, output and fault messages. With axis2, these faults were represented as java exceptions (inheriting from java.lang.Exception). When serializing these java classes back to XML, they were represented as SOAP faults with as detail element of the soap fault a serialized version of the java exception (as defined in the XML schema). This is an example of a SOAP fault message sent to the client:
The first problem when migrating to spring web services and JAXB, is that JAXB generates java types from the XSD rather then from the WSDL. The types of the fault messages are off course known as an XSD element, but the XSD has no information to distinguish them from other types (input and output messages). Typically all input messages end with "Request", all output messages with "Response" and all exceptions with "Exception", but JAXB doesn't do anything special with these naming conventions. Apart from these names, there is thus nothing special about the java types that JAXB generates for them. For exceptions, this leads to the confusing situation of having something named FooException that doesn't inherit from java.lang.Exception (I'll call them "fake" exceptions from now on).<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> <soapenv:Body> <soapenv:Fault> <faultcode>soapenv:Client</faultcode> <faultstring>FooException: null</faultstring> <detail> <ns:FooException xsi:type="ns:FooException" xmlns:ns="http://our.namespace" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <ns:customField>0</ns:customField> </ns:FooException> </detail> </soapenv:Fault> </soapenv:Body> </soapenv:Envelope>
This means that in our AbstractMarshallingPayloadEndpoint implementation (which is what you use if you want to implement web services with spring and a marshalling technology like JAXB), we can't throw these exceptions (as they are not throwable).
The second problem is that these fake exceptions don't have the JAXB @XmlRootElement annotation on them. This means that they can not directly be serialized: you first have to create a valid JAXBElement for them using the ObjectFactory that is generated by JAXB, something like
The third problem is that spring's built in support for dealing with exceptions doesn't serialize the exception in the detail part of a SOAP fault. By default, spring only renders a fault code and error message in the SOAP fault.final JAXBElementelement = new ObjectFactory().createFooException((FooException) exception);
Serializing exceptions as SOAP fault detail messages
Let's forget about the first and second problems for now and assume all exceptions properly inherit from java.lang.Exception and have the JAXB @XmlRootElement annotation on them. By writing a custom SoapFaultMappingExceptionResolver (or directly implementing EndpointExceptionResolver if you want to), you can customize the way the SOAP fault is generated. This is how the customizeFault method could be implemented:Note how we use the marshaller from the given endpoint to marshal the exception into the detail of the SOAP fault (represented using the Result interface).@Override protected void customizeFault( final Object endpoint, final Exception exception, final SoapFault fault) { super.customizeFault(endpoint, exception, fault); // get the marshaller AbstractMarshallingPayloadEndpoint marshallingendEndpoint = (AbstractMarshallingPayloadEndpoint) endpoint; // get the result inside the fault detail to marshal to Result result = fault.addFaultDetail().getResult(); // marshal try { marshallingendEndpoint.getMarshaller().marshal(exception, result); } catch (IOException e) { throw new RuntimeException(e); } }
Using JAXB's ObjectFactory to solve the second problem
As explained already, the second of the three problems forces us to use JAXB's ObjectFactory for the exceptions. This means that, in the code example above, we can't directly pass the exception to the marshaller. We first have to create a JAXBElement from the exception. This forces us to known the exact type of the exception, and thus requires ugly casting, for example:As you can see, this code is no longer scalable. For every new exception, you have to add code.@Override protected void customizeFault( final Object endpoint, final Exception exception, final SoapFault fault) { super.customizeFault(endpoint, exception, fault); // get the marshaller AbstractMarshallingPayloadEndpoint marshallingendEndpoint = (AbstractMarshallingPayloadEndpoint) endpoint; // get the result inside the fault detail to marshal to Result result = fault.addFaultDetail().getResult(); // create the corresponding jaxb element final JAXBElement element; if (exception instanceof FooException) { element = new ObjectFactory().createFooException((FooException) exception); } else if (...) { // else if required for all possible exceptions } // marshal try { marshallingendEndpoint.getMarshaller().marshal(exception, result); } catch (IOException e) { throw new RuntimeException(e); } }
Wrapping fake exceptions in a real exception
The last problem to solve is that the fake exceptions are not really java exceptions. We've looked into possibilities to customize the JAXB generation of java types in such a way that everything that ends with "Exception" is actually a real exception, but that didn't seem to be possible.We ended up writing a solution that works, although I must say it is not very elegant. We've created a custom exception to wrap the "fake" exceptions generated by JAXB (called it UnmarshalledExceptionWrapperException). The wrapper wraps instances of type Object and verifies that their class names end with "Exception" (that really seems to be the best thing we can do). The custom SoapFaultMappingExceptionResolver now has to unwrap these fake exceptions and build a valid JAXB element from them:
@Override protected void customizeFault( final Object endpoint, final Exception exception, final SoapFault fault) { super.customizeFault(endpoint, exception, fault); // get the wrapper UnmarshalledExceptionWrapperException exception = (UnmarshalledExceptionWrapperException) ex; // unwrap the exception Object unwrappedException = exception.getWrappedException(); // get the marshaller AbstractMarshallingPayloadEndpoint marshallingendEndpoint = (AbstractMarshallingPayloadEndpoint) endpoint; // get the result inside the fault detail to marshal to Result result = fault.addFaultDetail().getResult(); // create the corresponding jaxb element final JAXBElement element; if (unwrappedException instanceof FooException) { element = new ObjectFactory().createFooException((FooException) unwrappedException); } else if (...) { // else if required for all possible exceptions } // marshal try { marshallingendEndpoint.getMarshaller().marshal(exception, result); } catch (IOException e) { throw new RuntimeException(e); } }