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
@Features
annotation that can be used on either the service endpoint interface (SEI) or the SEI implementation class. In this example, we have already added theLoggingFeature
to the CXF bus but if we wanted to use the annotation instead it would need to be applied to theTicketAgentImpl
class 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.