Apache CXF - Logging SOAP Request Response Fault Messages Example
Table of Contents
Since Apache CXF 3.1, the message logging code was moved into a separate module and gathered a number of new features.
In this tutorial, we will demonstrate how to configure CXF to log the SOAP request, response and fault XML using a logging Interceptor and Feature. The example uses the Logback logging framework in addition to Apache CXF, Spring Boot, and Maven.
General Project Setup #
Tools used:
- Apache CXF 3.2
- Spring Boot 1.5
- Maven 3.5
The setup of the project is based on a previous CXF web service example in which we have swapped out the basic helloworld.wsdl for a more generic ticketagent.wsdl from the W3C WSDL 1.1 specification.
As the sample TicketAgent WSDL does not contain a SOAP fault we will add one in the context of this tutorial.
<?xml version="1.0" encoding="UTF-8"?>
<wsdl:definitions targetNamespace="http://example.org/TicketAgent.wsdl11"
xmlns:tns="http://example.org/TicketAgent.wsdl11" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsTicketAgent="http://example.org/TicketAgent.xsd"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
<wsdl:types>
<xs:schema xmlns:xsTicketAgent="http://example.org/TicketAgent.xsd"
targetNamespace="http://example.org/TicketAgent.xsd">
<xs:element name="listFlightsRequest" type="xsTicketAgent:tListFlights" />
<xs:complexType name="tListFlights">
<xs:sequence>
<xs:element name="travelDate" type="xs:date" />
<xs:element name="startCity" type="xs:string" />
<xs:element name="endCity" type="xs:string" />
</xs:sequence>
</xs:complexType>
<xs:element name="listFlightsResponse" type="xsTicketAgent:tFlightsResponse" />
<xs:complexType name="tFlightsResponse">
<xs:sequence>
<xs:element name="flightNumber" type="xs:integer"
minOccurs="0" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>
<xs:element name="listFlightsFault" type="xsTicketAgent:tFlightsFault" />
<xs:complexType name="tFlightsFault">
<xs:sequence>
<xs:element name="errorMessage" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:schema>
</wsdl:types>
<wsdl:message name="listFlightsRequest">
<wsdl:part name="body" element="xsTicketAgent:listFlightsRequest" />
</wsdl:message>
<wsdl:message name="listFlightsResponse">
<wsdl:part name="body" element="xsTicketAgent:listFlightsResponse" />
</wsdl:message>
<wsdl:message name="listFlightsFault">
<wsdl:part name="body" element="xsTicketAgent:listFlightsFault" />
</wsdl:message>
<wsdl:portType name="TicketAgent">
<wsdl:operation name="listFlights">
<wsdl:input message="tns:listFlightsRequest" />
<wsdl:output message="tns:listFlightsResponse" />
<wsdl:fault name="listFlightsFault" message="tns:listFlightsFault"></wsdl:fault>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="TicketAgentSoap" type="tns:TicketAgent">
<soap:binding style="document"
transport="http://schemas.xmlsoap.org/soap/http" />
<wsdl:operation name="listFlights">
<wsdl:input>
<soap:body parts="body" use="literal" />
</wsdl:input>
<wsdl:output>
<soap:body parts="body" use="literal" />
</wsdl:output>
<wsdl:fault name="listFlightsFault">
<soap:fault name="listFlightsFault" use="literal" />
</wsdl:fault>
</wsdl:operation>
</wsdl:binding>
</wsdl:definitions>
Maven is used to build the project. As the CXF message logging code was moved into a separate package since version 3.1, we need to include it by adding the cxf-rt-features-logging dependency to the project POM file as shown below.
<?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.codenotfound</groupId>
<artifactId>cxf-jaxws-logging-logback</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>cxf-jaxws-logging-logback</name>
<description>Apache CXF - Logging SOAP Request Response Fault Messages Example</description>
<url>https://www.codenotfound.com/apache-cxf-logging-soap-request-response-fault-messages-example.html</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<cxf.version>3.2.1</cxf.version>
</properties>
<dependencies>
<!-- spring-boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- cxf -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-spring-boot-starter-jaxws</artifactId>
<version>${cxf.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-features-logging</artifactId>
<version>${cxf.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- spring-boot-maven-plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!-- cxf-codegen-plugin -->
<plugin>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-codegen-plugin</artifactId>
<version>${cxf.version}</version>
<executions>
<execution>
<id>generate-sources</id>
<phase>generate-sources</phase>
<configuration>
<sourceRoot>${project.build.directory}/generated/cxf</sourceRoot>
<wsdlOptions>
<wsdlOption>
<wsdl>${project.basedir}/src/main/resources/wsdl/ticketagent.wsdl</wsdl>
<wsdlLocation>classpath:wsdl/ticketagent.wsdl</wsdlLocation>
</wsdlOption>
</wsdlOptions>
</configuration>
<goals>
<goal>wsdl2java</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Configure the CXF LoggingInInterceptor and CXF LoggingOutInterceptor #
Interceptors are the fundamental processing unit inside CXF. For more information checkout following post on the basic CXF interceptor architecture.
CXF ships with a LoggingInInterceptor that allows logging of the received (IN) messages. In addition, for logging sent (OUT) messages, a LoggingOutInterceptor is provided. These interceptors can be added to one of the CXF interceptor providers (Client, Endpoint, Service, Bus or Binding) that implement the InterceptorProvider interface.
In order to demonstrate this, we will add both LoggingInInterceptor and LoggingOutInterceptor to the TicketAgent client as illustrated below.
Note that for SOAP faults, there are separate error handling chains. In the case of a client, this is an inbound error handling chain to which we also need to add a
LoggingInInterceptor.
package com.codenotfound.client;
import org.apache.cxf.ext.logging.LoggingInInterceptor;
import org.apache.cxf.ext.logging.LoggingOutInterceptor;
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
import org.example.ticketagent_wsdl11.TicketAgent;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ClientConfig {
@Value("${client.ticketagent.address}")
private String address;
@Bean(name = "ticketAgentProxy")
public TicketAgent ticketAgentProxy() {
JaxWsProxyFactoryBean jaxWsProxyFactoryBean =
new JaxWsProxyFactoryBean();
jaxWsProxyFactoryBean.setServiceClass(TicketAgent.class);
jaxWsProxyFactoryBean.setAddress(address);
// add an interceptor to log the outgoing request messages
jaxWsProxyFactoryBean.getOutInterceptors()
.add(loggingOutInterceptor());
// add an interceptor to log the incoming response messages
jaxWsProxyFactoryBean.getInInterceptors()
.add(loggingInInterceptor());
// add an interceptor to log the incoming fault messages
jaxWsProxyFactoryBean.getInFaultInterceptors()
.add(loggingInInterceptor());
return (TicketAgent) jaxWsProxyFactoryBean.create();
}
@Bean
public LoggingOutInterceptor loggingOutInterceptor() {
return new LoggingOutInterceptor();
}
@Bean
public LoggingInInterceptor loggingInInterceptor() {
return new LoggingInInterceptor();
}
}
Configure the CXF LoggingFeature #
A Feature in CXF is a way of adding capabilities to a Client, Server or Bus. They provide a simple way to perform or configure a series of related tasks.
CXF includes a LoggingFeature which encapsulates the creation of the different logging interceptors and then subsequently adds them to all the different interceptor chains.
Let’s add a LoggingFeature to the CXF Bus that hosts our TicketAgent Endpoint. First, we create a new instance of the feature and enable formatting of the XML message by using the setPrettyLogging() method. We then add the feature by using setFeatures() on the bus.
package com.codenotfound.endpoint;
import java.util.ArrayList;
import java.util.Arrays;
import javax.xml.ws.Endpoint;
import org.apache.cxf.Bus;
import org.apache.cxf.ext.logging.LoggingFeature;
import org.apache.cxf.jaxws.EndpointImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class EndpointConfig {
@Autowired
private Bus cxfBus;
@Bean
public Endpoint endpoint() {
EndpointImpl endpoint =
new EndpointImpl(bus(), new TicketAgentImpl());
endpoint.publish("/ticketagent");
return endpoint;
}
@Bean
public Bus bus() {
cxfBus.setFeatures(
new ArrayList<>(Arrays.asList(loggingFeature())));
return cxfBus;
}
@Bean
public LoggingFeature loggingFeature() {
LoggingFeature loggingFeature = new LoggingFeature();
loggingFeature.setPrettyLogging(true);
return loggingFeature;
}
}
There is also an
@Featuresannotation that can be used on either the service endpoint interface (SEI) or the SEI implementation class. In this example, we have already added theLoggingFeatureto the CXF bus but if we wanted to use the annotation instead it would need to be applied to theTicketAgentImplclass as shown below.
package com.codenotfound.endpoint;
import java.math.BigInteger;
import org.example.ticketagent.ObjectFactory;
import org.example.ticketagent.TFlightsFault;
import org.example.ticketagent.TFlightsResponse;
import org.example.ticketagent.TListFlights;
import org.example.ticketagent_wsdl11.ListFlightsFault;
import org.example.ticketagent_wsdl11.TicketAgent;
@Features(features = "org.apache.cxf.ext.logging.LoggingFeature")
public class TicketAgentImpl implements TicketAgent {
@Override
public TFlightsResponse listFlights(TListFlights request) throws ListFlightsFault {
ObjectFactory factory = new ObjectFactory();
if ("XYZ".equals(request.getStartCity())) {
TFlightsFault tFlightsFault = factory.createTFlightsFault();
tFlightsFault.setErrorMessage("no city named XYZ");
throw new ListFlightsFault("unknown city", tFlightsFault);
}
TFlightsResponse response = factory.createTFlightsResponse();
response.getFlightNumber().add(BigInteger.valueOf(101));
return response;
}
}
CXF Logging Configuration #
Now that we have setup logging on both client and server we need to set the logging level of the 'org.apache.cxf.services' Logger to 'INFO' in order to have the XML SOAP messages appear.
The cxf-spring-boot-starter-jaxws Spring Boot starter automatically includes the Logback, Log4J and SLF4J dependencies. As such we just have to place a logback.xml configuration file on the classpath in order to activate Logback.
You can use the logger name to fine tune which services you want to log. The logger name is 'org.apache.cxf.services.<service name>.<type>'. Where the service name is the name of the generated interface class (in this example TicketAgent). The type can be one of the following depending on whether the message is sent (OUT) or received (IN):
- REQ_IN
- RESP_IN
- FAULT_IN
- REQ_OUT
- RESP_OUT
- FAULT_OUT
In this sample, we only want to log the messages that are sent. In other words, we don’t want to log the messages that are received (by either client or endpoint). As such we sett them to
'WARN'.
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.codenotfound" level="INFO" />
<logger name="org.apache.cxf" level="WARN" />
<logger name="org.springframework" level="WARN" />
<!-- INFO level needed to log the SOAP messages -->
<logger name="org.apache.cxf.services" level="INFO" />
<!-- fine tune individual service logging -->
<logger name="org.apache.cxf.services.TicketAgent.REQ_IN" level="WARN" />
<logger name="org.apache.cxf.services.TicketAgent.RESP_IN" level="WARN" />
<logger name="org.apache.cxf.services.TicketAgent.FAULT_IN" level="WARN" />
<root level="WARN">
<appender-ref ref="STDOUT" />
</root>
</configuration>
Test That the CXF Logging Interceptor and Feature Are Enabled #
As we also want to log faults, we extend the original SpringCxfApplicationTests class with an additional testListFlightsFault() unit test which triggers a SOAP fault to be returned in case an unknown city is supplied.
package com.codenotfound;
import static org.assertj.core.api.Assertions.assertThat;
import java.math.BigInteger;
import org.example.ticketagent_wsdl11.ListFlightsFault;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.context.junit4.SpringRunner;
import com.codenotfound.client.TicketAgentClient;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
public class SpringCxfApplicationTests {
@Autowired
private TicketAgentClient ticketAgentClient;
@Test
public void testListFlights() throws ListFlightsFault {
assertThat(ticketAgentClient.listFlights("LAX", "SFO").get(0))
.isEqualTo(BigInteger.valueOf(101));
}
@Test
public void testListFlightsFault() {
try {
ticketAgentClient.listFlights("XYZ", "SFO");
} catch (Exception exception) {
assertThat(exception.getMessage()).isEqualTo("unknown city");
}
}
}
Run the above tests, by executing following Maven command in the projects root folder:
mvn test
Maven will download the needed dependencies, compile the code and run the unit test cases during which the SOAP XML are logged as shown below:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.5.9.RELEASE)
07:27:54.753 [main] INFO c.c.SpringCxfApplicationTests - Starting SpringCxfApplicationTests on cnf-pc with PID 1000 (started by CodeNotFound in c:\codenotfound\code\cxf-jaxws\cxf-jaxws-logging-logback)
07:27:54.755 [main] INFO c.c.SpringCxfApplicationTests - No active profile set, falling back to default profiles: default
07:27:57.934 [main] INFO c.c.SpringCxfApplicationTests - Started SpringCxfApplicationTests in 3.486 seconds (JVM running for 4.123)
07:27:58.082 [main] INFO o.a.cxf.services.TicketAgent.REQ_OUT - REQ_OUT
Address: http://localhost:9090/codenotfound/ws/ticketagent
HttpMethod: POST
Content-Type: text/xml
ExchangeId: 2178e0db-4a64-4c9c-9467-dfdcfe1b3075
ServiceName: TicketAgentService
PortName: TicketAgentPort
PortTypeName: TicketAgent
Headers: {SOAPAction="", Accept=*/*}
Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:listFlightsRequest xmlns:ns2="http://example.org/TicketAgent.xsd"><startCity>LAX</startCity><endCity>
SFO</endCity></ns2:listFlightsRequest></soap:Body></soap:Envelope>
07:27:58.211 [http-nio-9090-exec-1] INFO o.a.c.services.TicketAgent.RESP_OUT - RESP_OUT
Content-Type: text/xml
ResponseCode: 200
ExchangeId: 9633cf96-f3b7-44f0-ab32-948da11eeab3
ServiceName: TicketAgentImplService
PortName: TicketAgentImplPort
PortTypeName: TicketAgent
Headers: {}
Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns2:listFlightsResponse xmlns:ns2="http://example.org/TicketAgent.xsd">
<flightNumber>101</flightNumber>
</ns2:listFlightsResponse>
</soap:Body>
</soap:Envelope>
07:27:58.257 [main] INFO o.a.cxf.services.TicketAgent.REQ_OUT - REQ_OUT
Address: http://localhost:9090/codenotfound/ws/ticketagent
HttpMethod: POST
Content-Type: text/xml
ExchangeId: f5f0520c-cc9c-479d-bb8f-188d7ad288ed
ServiceName: TicketAgentService
PortName: TicketAgentPort
PortTypeName: TicketAgent
Headers: {SOAPAction="", Accept=*/*}
Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:listFlightsRequest xmlns:ns2="http://example.org/TicketAgent.xsd"><startCity>XYZ</startCity><endCity>
SFO</endCity></ns2:listFlightsRequest></soap:Body></soap:Envelope>
07:27:58.270 [http-nio-9090-exec-2] INFO o.a.c.services.TicketAgent.FAULT_OUT - FAULT_OUT
Content-Type: text/xml
ResponseCode: 500
ExchangeId: db05096f-9cb8-49c9-9c3f-d5030c2ba582
ServiceName: TicketAgentImplService
PortName: TicketAgentImplPort
PortTypeName: TicketAgent
Headers: {}
Payload: <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>soap:Server</faultcode>
<faultstring>unknown city</faultstring>
<detail>
<ns2:listFlightsFault xmlns:ns2="http://example.org/TicketAgent.xsd">
<errorMessage>no city named XYZ</errorMessage>
</ns2:listFlightsFault>
</detail>
</soap:Fault>
</soap:Body>
</soap:Envelope>
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.919 sec - in com.codenotfound.SpringCxfApplicationTests
Results :
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6.555 s
[INFO] Finished at: 2017-07-22T07:27:58+02:00
[INFO] Final Memory: 21M/227M
[INFO] ------------------------------------------------------------------------
This concludes our example in which we programmatically added a CXF LoggingInInterceptor, LoggingOutInterceptor and LoggingFeature in order to log the sent/received SOAP messages.
Feel free to drop a comment in case of a question or if you just like the post.