Tuesday, 4 October 2016

Apache CXF- Contract First Web Services

I wrote a post a few years ago detailing a step by step guide to building a contract first web service using Spring.  I recently started working with Apache CXF and thought it would be worth putting together a more up to date post, this time using CXF.
We'll create a fictitious Account Service that takes a single account number parameter and returns associated account information. Although the sample service will be very simple, the approach taken should provide you with a solid foundation upon which to build real world services.

Source Code

The full source code for this post is available on GitHub so feel free to have a look. You may find it useful to have the code locally as you work through this post.

Domain Model Definition

We'll begin by defining the service domain model as an XML Schema Definition (XSD). The schema will define the entities that our Account service will use. We'll begin by defining the core Account entity - the diagram below shows the XSD snippet that defines the Account as well as a visual representation taken from XML Spy.

Next we'll define the AccountDetailsRequest type that will be used as a parameter to the Account service.

Finally we'll define the AccountDetailsResponse type that will be used as the return type from the Account service.

The full schema definition is shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://com/blog/samples/webservices/accountservice" xmlns:account="http://webservices.samples.blog.com" targetNamespace="http://com/blog/samples/webservices/accountservice" elementFormDefault="qualified">
 <xsd:complexType name="Account">
  <xsd:sequence>
   <xsd:element name="AccountNumber" type="xsd:string"/>
   <xsd:element name="AccountName" type="xsd:string"/>
   <xsd:element name="AccountBalance" type="xsd:double"/>
   <xsd:element name="AccountStatus" type="EnumAccountStatus"/>
  </xsd:sequence>
 </xsd:complexType> 
 <xsd:simpleType name="EnumAccountStatus">
  <xsd:restriction base="xsd:string">
   <xsd:enumeration value="Active"/>
   <xsd:enumeration value="Inactive"/>
  </xsd:restriction>
 </xsd:simpleType>
 <xsd:element name="AccountDetailsRequest">
  <xsd:complexType>
   <xsd:sequence>
    <xsd:element name="accountNumber" type="xsd:string"/>
   </xsd:sequence>
  </xsd:complexType>
 </xsd:element>
 <xsd:element name="AccountDetailsResponse">
  <xsd:complexType>
   <xsd:sequence>
    <xsd:element name="AccountDetails" type="Account"/>
   </xsd:sequence>
  </xsd:complexType>
 </xsd:element>
</xsd:schema>

Service Contract Definition

Next we'll define the public facing service contract using a WSDL. A WSDL is an XML document that describes a SOAP Web Service and how clients can interact with it. WSDLs are a great way to describe service contracts because they are easy to interpret by developers and can be used by tooling to generate many useful artefacts, some of which we'll look at later.  The Account Service WSDL is defined as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<wsdl:definitions xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap12/"
 xmlns:tns="http://www.briansjavablog.com/Accounts/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/"
 xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="Accounts"
 targetNamespace="http://www.briansjavablog.com/Accounts/"
 xmlns:accounts="http://com/blog/samples/webservices/accountservice">
 <wsdl:types>
  <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <xsd:import namespace="http://com/blog/samples/webservices/accountservice"
    schemaLocation="../schema/AccountsService.xsd">
   </xsd:import>
  </xsd:schema>
 </wsdl:types>
 <wsdl:message name="AccountDetailsRequest">
  <wsdl:part element="accounts:AccountDetailsRequest" name="parameters" />
 </wsdl:message>
 <wsdl:message name="AccountDetailsResponse">
  <wsdl:part element="accounts:AccountDetailsResponse" name="parameters" />
 </wsdl:message>
 <wsdl:portType name="Accounts">
  <wsdl:operation name="GetAccountDetails">
   <wsdl:input message="tns:AccountDetailsRequest" />
   <wsdl:output message="tns:AccountDetailsResponse" />
  </wsdl:operation>
 </wsdl:portType>
 <wsdl:binding name="AccountsServiceSoapBinding" type="tns:Accounts">
  <soap:binding style="document"
   transport="http://schemas.xmlsoap.org/soap/http" />
  <wsdl:operation name="GetAccountDetails">
   <soap:operation
    soapAction="http://www.briansjavablog.com/Accounts/GetAccountDetails" />
   <wsdl:input>
    <soap:body use="literal" />
   </wsdl:input>
   <wsdl:output>
    <soap:body use="literal" />
   </wsdl:output>
  </wsdl:operation>
 </wsdl:binding>
 <wsdl:service name="AccountsService">
  <wsdl:port binding="tns:AccountsServiceSoapBinding" name="AccountsPort">
   <soap:address
    location="http://localhost:8080/apache-cfx-demo/services/accounts" />
  </wsdl:port>
 </wsdl:service>
</wsdl:definitions>
                                                                                    
The WSDL is composed of 5 main parts
  • Types - the <wsdl:types> section defines the domain model associated with the service. The types are defined using XSD and can be defined in the WSDL itself or imported from a separate XSD. On line 9 above we import the schema definition file we created earlier.
  • Message - the <wsdl:message> is used to define the request and response messages handled by the service. The nested <wsdl:part> section defines the domain type that will be used to form the messages.
  • PortType - the <wsdl:portType> defines the service operations that are exposed to clients, parameters required to invoke the operations and response types returned. 
  • Binding - the <wsdl:binding> section defines the protocol and data format. 
    • The binding type attribute refers to the portType defined earlier in the WSDL. 
    • The soap binding style can be either RPC or document. In this instance we've chosen document. 
    • The transport attribute indicates that the service will be exposed over HTTP. Other less common options include JMS and SMTP.
    • The operation element defines each operation that we exposed through the portType.
    • Binding - the <wsdl:binding> section defines the protocol and data format. 
  • Service - the <wsd;:service> defines the exposed service using the portType and binding we defined above.  

POM Configuration

Now that we've defined the WSDL its time to configure the POM to use the Apache CXF code generation plugin. The plugin invokes a WSDL2Java process which parses the WSDL and generates a service endpoint interface and associated domain objects. The plugin configuration is shown below. Note that I've omitted the rest of the POM definition for brevity. To see the full definition pull the sample code from github.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
          <plugins>
      <plugin>
  <groupId>org.apache.cxf</groupId>
  <artifactId>cxf-codegen-plugin</artifactId>
  <version>3.1.7</version>
  <executions>
     <execution>
        <id>generate-sources</id>
   <phase>generate-sources</phase>
   <configuration>
      <sourceRoot>src/generated/java</sourceRoot>
      <wsdlOptions>
         <wsdlOption>
            <wsdl>${basedir}/src/main/resources/wsdl/Accounts.wsdl</wsdl>
         </wsdlOption>
      </wsdlOptions>
   </configuration>
   <goals>
      <goal>wsdl2java</goal>
   </goals>
     </execution>
  </executions>
      </plugin>
          </plugins>
                                                                          
The WSDL path on line 14 tells CXF what WSDL to run WSDL2Java with. The sourceRoot configuration on line 11 is the fully qualified package name that the generated class will be copied to. Its a good idea to put these in src/generated/java so that its obvious to other developers that the package contains generated code.

Code Generation

We're now ready to generate the Service Endpoint Interface and domain objects from the WSDL. To run the code generation simply open a command window and run mvn generate-sources



Refresh the IDE workspace and you will see 2 new packages. The package names are based on the namespaces specified in the WSDL. The contents of both packages is described below.
  • com.blog.samples.webservices.accountservice - contains 4 service domain objects, Account, AccountDetailsRequest, AccountDetailsResponse and EnumAccountStatus. These 4 types represent the service request and response. ObjectFactory is a helper class for creating new instances of the domain types and package-info.java applies XML namespace metadata to all domain classes in the package.
  • com.briansjavablog.accounts - contains the Service Endpoint Interface Accounts.java. This interface is a Java representation of the service operation we defined in the WSDL. The contents of this class are covered in detail below. AccountsService.java is a web service client and can be used to invoke the service.

Service Endpoint Interface

The Service endpoint interface that was generated in the previous step is defined below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
 * This class was generated by Apache CXF 3.1.7
 * 2016-09-29T07:57:42.236+01:00
 * Generated source version: 3.1.7
 * 
 */
@WebService(targetNamespace = "http://www.briansjavablog.com/Accounts/", name = "Accounts")
@XmlSeeAlso({com.blog.samples.webservices.accountservice.ObjectFactory.class})
@SOAPBinding(parameterStyle = SOAPBinding.ParameterStyle.BARE)
public interface Accounts {

    @WebMethod(operationName = "GetAccountDetails", action = "http://www.briansjavablog.com/Accounts/GetAccountDetails")
    @WebResult(name = "AccountDetailsResponse", targetNamespace = "http://com/blog/samples/webservices/accountservice", partName = "parameters")
    public com.blog.samples.webservices.accountservice.AccountDetailsResponse getAccountDetails(
        @WebParam(partName = "parameters", name = "AccountDetailsRequest", targetNamespace = "http://com/blog/samples/webservices/accountservice")
        com.blog.samples.webservices.accountservice.AccountDetailsRequest parameters
    );
} 
  • @WebService - Marks the class as defining a Web Service interface from a WSDL. The namespace should match the namespace defined in the WSDL and the name should match the WSDL PortType.
  • @XmlSeeeAlso - Lets JAXB know what other classes need to be registered with the JAXB context for serialization and deserialization. 
  • @SoapBinding - Describes mapping from web service operations to SOAP protocol
  • @WebMethod - Maps a service operation to a Java method. The operation name references the operation defined in the WSDL and the target namespace uses the namespace associated with the WSDL operation.  
  • @WebResult - Maps a service operation response message to a Java return type. The name refers to the response message name defined in the WSDL. The target namespace uses the namespace associated with the WSDL message and the partName refers to wdl:part name in the WSDL.
  • @WebParam - Maps a service operation request message to a Java parameter type. The name refers to the request message name defined in the WSDL. The target namespace uses the namespace associated with the WSDL message and the partName refers to wdl:part name in the WSDL. 

Service Endpoint Implementation

Now that we've generated the Service Endpoint Interface, its time to create an implementation. The endpoint implementation class will be called by the CXF framework after it has deserialized the incoming SOAP body and figured out which endpoint method should handle the request. Our simple service exposes a single operation, but for services with multiple operations CXF uses the request payload to determine which endpoint method to call. Creating an endpoint implementation is straight forward and simply requires a class that implements the Service Endpoint Interface as shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@WebService(portName = "Accounts", serviceName = "Accounts", 
            endpointInterface = "com.briansjavablog.accounts.Accounts", 
            targetNamespace = "http://www.briansjavablog.com/Accounts/")
public class AccountServiceEndpoint implements Accounts {

    private AccountService accountService;

    @Autowired
    public AccountServiceEndpoint(AccountService accountService) {
        this.accountService = accountService;
    }

    @Override
    public AccountDetailsResponse getAccountDetails(AccountDetailsRequest parameters) {

        return accountService.getAccountDetails(parameters);
    }
}

The @WebService annotation marks this class as implementing a web service endpoint. The attributes portName, serviceName and targetNamespace should all match their equivalent in the WSDL definition. The enpointInterface refers to the generated Service Endpoint Interface class we looked at earlier. The AccountServiceEndpoint constructor takes a Spring injected AccountService as an argument. The getAccountDetails method is an implementation of the interface method defined on the Service Endpoint Interface class and uses the injected service to get the required account details.

Account Service 

The Account Service injected into the endpoint implementation above is trivial and simply returns hard coded values. I've split this out into a separate service class to demonstrate that its good practice to decouple endpoint processing logic from business logic. The only thing to note in this class is that we use the JAXB generated ObjectFactory to create an instance of the response type.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Service
public class AccountServiceImpl implements AccountService {

    @Override
    public AccountDetailsResponse getAccountDetails(AccountDetailsRequest parameters) {

        ObjectFactory factory = new ObjectFactory();
        AccountDetailsResponse response = factory.createAccountDetailsResponse();
  
        Account account = factory.createAccount();
        account.setAccountNumber("12345");
        account.setAccountStatus(EnumAccountStatus.ACTIVE);
        account.setAccountName("Joe Bloggs");
        account.setAccountBalance(3400);
  
        response.setAccountDetails(account);  
        return response;
    }
}
                                                                                      

Spring XML Configuration 

The guys at CXF have made a considerable effort to ensure that CXF integrates nicely with the Spring framework. Of course CXF can be configured programatically without Spring, but if you're already using Spring for dependency injection (or anything else for that matter), it makes sense to use CXFs Spring support. This section describes how to configure the endpoint we created earlier so that it can be published to a Servlet container like Tomcat.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:jaxws="http://cxf.apache.org/jaxws"
    xsi:schemaLocation="http://www.springframework.org/schema/context
          http://www.springframework.org/schema/context/spring-context.xsd
          http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
          http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">

   <context:component-scan base-package="com.briansjavablog.accounts" />

   <jaxws:endpoint id="accountService"
                   implementor="com.briansjavablog.accounts.service.AccountServiceEndpoint"
                   address="/services/accounts">
    <jaxws:inInterceptors>
      <ref bean="loggingInInterceptor" />
    </jaxws:inInterceptors>
    <jaxws:outInterceptors>
      <ref bean="loggingOutInterceptor" />
    </jaxws:outInterceptors>
   </jaxws:endpoint>

   <bean id="loggingInInterceptor" class="org.apache.cxf.interceptor.LoggingInInterceptor" />
   <bean id="loggingOutInterceptor" class="org.apache.cxf.interceptor.LoggingOutInterceptor" />

</beans>
  • Line 11 - Enable component scanning so that beans annotated as @Component and @Service are registered as Spring components. 
  • Lines 13 to 15 - Define endpoint configuration by specifying endpoint implementation class for implementor and the address of the exposed endpoint. Note that the address is relative to the application root.
  •  Lines 16 to 21 - Register endpoint interceptors for logging incoming SOAP request payload and outgoing SOAP response payload.
  • Line 24 - Create interceptor for logging inbound SOAP payloads. This interceptor is provided by CXF out of the box and is executed before the JAXB deserialization and endpoint implementation class is called. 
  • Line 24 - Create interceptor for logging outbound SOAP payloads. This interceptor is provided by CXF out of the box and is executed after endpoint service method is executed and the JAXB serialization and endpoint implementation class is called. 

Web.xml 

The final piece of configuration we need to run our service in a Servlet container is the web.xml. If you've done any kind of Java web development the web.xml definition below will look familiar.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
    <context-param>
 <param-name>contextConfigLocation</param-name>
 <param-value>WEB-INF/beans.xml</param-value>
    </context-param>

    <listener>
 <listener-class>
     org.springframework.web.context.ContextLoaderListener
 </listener-class>
    </listener>

    <servlet>
 <servlet-name>CXFServlet</servlet-name>
 <display-name>CXF Servlet</display-name>
 <servlet-class>
     org.apache.cxf.transport.servlet.CXFServlet
 </servlet-class>
 <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
 <servlet-name>CXFServlet</servlet-name>
 <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>
  • Line 8 to 11 - Provides a path to the Spring beans.xml configuration file. This file is used by the ContextLoaderListener described below.
  • Lines 13 to 17 - ContextLoaderListener creates the Spring application context on container startup. It uses the bean definitions defined in contextConfigLocation.
  • Lines 19 to 26 - Configures CXF provided Servlet to process incoming HTTP requests. 
  • Lines 28 to 31 -  Servlet mapping configures CXF Servlet to process all requests received at the application root.

Integration Test 

The next step is to add an integration test. Most projects now a days use some sort of continuous integration so its important to have integration test coverage that is run on every code push. We're going to create a simple happy path integration test that will deploy the service to a specified endpoint using Jetty, make a HTTP call to the endpoint and then perform some assertions on the response. This is a simple but effective end to end test that we can run locally or on a continuous integration environment, without having to worry about deploying to a standalone Servlet container.
We'll begin by looking at the Spring configuration that describes the dependencies we need to run our test.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
 xmlns:jaxws="http://cxf.apache.org/jaxws"
 xsi:schemaLocation="
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd">

 
 <context:component-scan base-package="com.briansjavablog.accounts" />

 <jaxws:server id="accountService"
        address="http://localhost:8080/apache-cxf-demo/services/accounts">
     <jaxws:serviceBean>
  <bean class="com.briansjavablog.accounts.service.AccountServiceEndpoint" />
     </jaxws:serviceBean>
     <jaxws:inInterceptors>
  <ref bean="loggingInInterceptor" />
     </jaxws:inInterceptors>
     <jaxws:outInterceptors>
                <ref bean="loggingOutInterceptor" />
     </jaxws:outInterceptors>
 </jaxws:server>

 <jaxws:client id="testAccountServiceClient"
        address="http://localhost:8080/apache-cxf-demo/services/accounts"
        serviceClass="com.briansjavablog.accounts.Accounts">
     <jaxws:inInterceptors>
         <ref bean="loggingInInterceptor" />
     </jaxws:inInterceptors>
     <jaxws:outInterceptors>
                <ref bean="loggingOutInterceptor" />
     </jaxws:outInterceptors>
 </jaxws:client>

 <bean id="abstractLoggingInterceptor" abstract="true">
     <property name="prettyLogging" value="true" />
 </bean>
 <bean id="loggingInInterceptor" class="org.apache.cxf.interceptor.LoggingInInterceptor" parent="abstractLoggingInterceptor" />
 <bean id="loggingOutInterceptor" class="org.apache.cxf.interceptor.LoggingOutInterceptor" parent="abstractLoggingInterceptor" />

</beans>
  • Line 12 - Configures component scanning so that beans annotated with @Component and @Service are registered with the bean factory.
  • Lines 14 to 15 - jaxws:server defines a server configuration that will launch Jetty to host the web service for the duration of the test. The address attribute specifies the local URL where the service will be deployed.
  • Lines 16 to 18 - Defines the endpoint class that will service incoming requests. This is the service endpoint implementation class we defined earlier.
  • Lines 19 to 24 - Logging interceptors are registered with the server to log inbound SOAP requests and outbound SOAP responses.
  •  Lines 27 to 29 - jaxws:client defines a web service client that we'll use to call the service. This client will be injected into our test class. The address attribute is the target URL of the service and the serviceClass is the Service Endpoint Interface that was generated earlier.
  • Lines 30 to 35 - Logging interceptors are registered with Client to log outbound SOAP requests and inbound SOAP responses.
  • Lines 38 to 42 - Defines inbound and outbound logging interceptors that are used by both the client and the server, The abstract interceptor is used to define a common property (prettyLogging) that is used by both logging interceptors.     
Finally we'll look at the test class definition. This class uses the test configuration above to create a web service client, a test server and deploy the application to the server.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration( {"classpath:beans-config-test.xml"} )
public class AccountServiceEndpointTest {

    @Autowired
    private Accounts accountsService;
    private AccountDetailsRequest accountDetailsRequest;
 
    @Before
    public void setUp() throws Exception {
  
        ObjectFactory objectFactory = new ObjectFactory();
 accountDetailsRequest = objectFactory.createAccountDetailsRequest();
 accountDetailsRequest.setAccountNumber("12345");
    }

    @Test
    public void testGetAccountDetails() throws Exception {
        AccountDetailsResponse response = accountsService.getAccountDetails(accountDetailsRequest);
        assertTrue(response.getAccountDetails()!= null);
        assertTrue(response.getAccountDetails().getAccountNumber().equals("12345"));
        assertTrue(response.getAccountDetails().getAccountName().equals("Joe Bloggs"));
        assertTrue(response.getAccountDetails().getAccountBalance() == 3400);
        assertTrue(response.getAccountDetails().getAccountStatus().equals(EnumAccountStatus.ACTIVE));
    }

}

  • Lines 1 & 2 - Define this as test that relies on Spring managed dependencies. The test application context is loaded form the beans-config-test.xml file we defined earlier. 
  • Lines 5 & 6 - Spring injects an instance of the Accounts Endpoint Service Interface that was generated from the WSDL. The injected instance is a client side proxy to the remote service and is constructed by Spring using the jaxws:client definition we defined in beans-config-test.xml. This is essentially a bridge out to the remote service deployed on Jetty.
  • Lines 9 to 15 - Setup method is called before the test method and is used to create a request object.
  • Lines 17 to 25 - Test method uses injected AccountService client side proxy to call the remote service deployed  on Jetty and performs a number of assertions on the deserialized response.

Running the Test

Running the test is straight forward. Simply fire up a command window and run the mvn test command. You should see the SOAP request and response payloads logged by both the client and the server as follows.

Running on Tomcat 

The service can be deployed to Tomcat manually or you can use the Tomcat maven plugin by running mvn tomcat:run-war from the command line. When the service is deployed we can test it on port 8080 using a SOAP client. We'll use the Eclipse Web Services Explorer to run a quick manual test to ensure the service is up and running.
  • In Eclipse launch the Web Services Explorer, enter the path to the WSDL as shown below and click Go.

  • Click the GetAccountDetails operation

  • Populate the account number parameter and click Go to call the service. In the bottom panel click the Source link to see the raw SOAP request and response payloads.

Wrapping Up

In this post we took a fairly detailed look at building and testing a contract first web service using Apache CXF. The service itself is very simple but the approach used should provide you with a solid grounding for building more complex services. The full source code is available on Github. As always, feel free to post comments, questions or suggestions below. Feedback is always welcome.

Thursday, 11 August 2016

Docker - Multi Container App

In my last post I used Docker to build and run a simple Spring Boot application. This post will take things a little further by introducing a second container, showing you how distinct components can be deployed in separate containers and how those containers can communicate.

We'll build a simple Spring Boot app with a REST endpoint that takes an incoming message and adds it to an ActiveMQ message queue. A second endpoint will use a receiver component to consume the next message from the queue and return it to the client.  The application itself is simple but it'll provide a pretty realistic use case for how you might use multiple Docker containers during development.
Fig 1.0 - Boot & ActiveMQ - Multi Container App

Application Components

I'm not going to cover the application code in a huge amount of detail but I'll describe the fundamentals components and how they fit together. The MessageController below exposes 2 endpoints, one for posting a message and one for retrieving a message.  I've injected a MessageSender and MessageReceiver which are used to publish messages to and retrieve messages from an ActiveMQ message broker.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RestController
public class MessageController {

    private final MessageSender messageSender;
    private final MessageReceiver messageReceiver;

    @Autowired
    public MessageController(MessageSender messageSender, MessageReceiver messageReceiver){
       this.messageSender = messageSender;
       this.messageReceiver = messageReceiver;
    }
 
    @RequestMapping(value = "/message/", method = RequestMethod.GET)
    public String retrieveMessage() {
 
       return messageReceiver.retrieveMessage();
    }
  
    @RequestMapping(value = { "/message" }, method = { RequestMethod.POST })
    public void publishMessage(@RequestBody String message) {

       SimpleMessage simpleMessage = new SimpleMessage(message, new Date());
       messageSender.publishMessage(simpleMessage);
    }

}

MessageSender uses a jmsTemplate to send a message to the queue. This method is called from the REST controller to publish the message received via the HTTP endpoint.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Component
@Slf4j
public class MessageSender {

    private final JmsTemplate jmsTemplate;

    @Autowired
    public MessageSender(JmsTemplate jmsTemplate, MessageConverter messageConverter) {
       this.jmsTemplate = jmsTemplate;
       this.jmsTemplate.setMessageConverter(messageConverter);  
    }

    public void publishMessage(SimpleMessage simpleMessage){
  
       log.info("Sending message to queue: {}", simpleMessage.toString());
       jmsTemplate.convertAndSend("TestQueue", simpleMessage);
    }
 
}

MesssageReceiver uses a JmsTemplate to retrieve a message from the queue. The retrieveMessage method is invoked by the REST controller which will return the message to the client.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@Component
@Slf4j
public class MessageReceiver {

    private final JmsTemplate jmsTemplate;

    @Autowired
    public MessageReceiver(JmsTemplate jmsTemplate, MessageConverter messageConverter) {
       this.jmsTemplate = jmsTemplate;
       this.jmsTemplate.setMessageConverter(messageConverter);
    }

    public String retrieveMessage() {

       String message = (String) jmsTemplate.receiveAndConvert("TestQueue");
       log.info("Retrieved message from queue: {}", message);
       return message;
    }
}

SimpleMessageConverter is used by the MessageSender and MessageReceiver components to do some simple transformation. MessageSender uses this class to convert a simple message POJO to a String before publishing to the queue. MessageReceiver uses this class to extract the message String from the Message returned from the queue.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Component
public class SimpleMessageConverter implements MessageConverter {

    @Override
    public Message toMessage(Object messageObject, Session session) throws JMSException, MessageConversionException {

       SimpleMessage simpleMessage = (SimpleMessage) messageObject;
       MapMessage message = session.createMapMessage();
       message.setString("message", simpleMessage.toString());
       return message;
    }

    @Override
    public Object fromMessage(Message message) throws JMSException, MessageConversionException {

       MapMessage mapMessage = (MapMessage) message;
       return mapMessage.getString("message");
    }

The application.properties below contains a single property that defines the message broker URL. We'll see later that the host name value (activemq) is significant when we come to linking Docker containers.

Running ActiveMQ

Now that we've defined the app lets look at setting up ActiveMQ. We need an ActiveMQ instance with a single queue definition called TestQueue. Remember that we reference TestQueue  as the destination in MessageSender and MessageReceiver. To save you time I've already created such a Docker image and pushed it to DockerHub.  Running the command below will pull the image from DockerHub and start a container.

1
docker run --name activemq -p 8161:8161 briansjavablog/activemq

The --name flag allows us to provide a name for the container we're starting. We'll use this name later when we are linking the app container to this ActiveMQ container.  The -p 8161:8161 tells Docker to expose port 8161 to the host machine on port 8161. This is required so that we can access the message broker admin console when it starts and check that the message queue is configured and ready to use. When the container starts up open a browser and go to http://localhost:8161/admin/queues.jsp. You should see a single queue defined as shown below.

Fig 1.1 - ActiveMQ Queue Definition

Building the Application Image

We'll start by defining a simple Dockerfile that takes a pre built JAR from the target directory and adds it to the image. The ENTRYPOINT tells Docker to run the executable JAR on container startup.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
FROM anapsix/docker-oracle-java8

# Creating working directory
WORKDIR /app

# Add src to working directory
ADD target/docker-boot-activemq-0.1.0.jar /app/docker-boot-activemq-0.1.0.jar

# Start app
ENTRYPOINT ["java","-jar","/app/docker-boot-activemq-0.1.0.jar"]     

To create an image form the Dockerfile simply run docker build -t "docker-activemq" . as shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
brianh@brianh-VirtualBox:~/apps/docker-boot-activemq$ docker build -t "docker-activemq" .
Sending build context to Docker daemon 19.98 MB
Step 1 : FROM anapsix/docker-oracle-java8
 ---> a8a9dcb0ac64
Step 2 : WORKDIR /app
 ---> Using cache
 ---> 4a5482ed5d76
Step 3 : ADD target/docker-boot-activemq-0.1.0.jar /app/docker-boot-activemq-0.1.0.jar
 ---> 4dd383ddc5aa
Removing intermediate container b720dc09ee95
Step 4 : ENTRYPOINT java -jar /app/docker-boot-activemq-0.1.0.jar
 ---> Running in 398ca91a7205
 ---> 74e632bb7020
Removing intermediate container 398ca91a7205
Successfully built 74e632bb7020


Linking Containers

If you've followed the steps so far, you should have an ActiveMQ container up and running and an application image built and ready to run. We're now going to start a second container to run our application and link it to the ActiveMQ container we started earlier.

1
docker run -p 8080:8080 --link activemq:activemq docker-activemq
  • -p 8080:8080 exposes port 8080 on the container to port 8080 on the host machine. This is required as the Boot app will start on container port 8080 as soon as the image is started.
  • --link activemq:activemq defines the container we want to link to and an alias that we can use to reference it. In this case we are linking to the activemq container we started earlier.
  • docker-activemq is the name of the application Docker image we built earlier.,
When we defined the message broker URL earlier in application.properties, we set the URL as follows.

1
spring.activemq.brokerurl=tcp://activemq:61616

So how does the app container know how to resolve the above URL to a service exposed in another container? To see how, we're going to open a shell in the app container and have a look. After the app container has started run the following from another terminal window. Note that the value inside quotes is the ID of the application container. To get the container ID run docker ps.

1
docker exec -it "1a4db2b586c3" bash

You should now have shell access to the app image and your current working directory should be /app. Remember /app is the working directory we created earlier to add the JAR to. Now lets take a look at the hosts file. Simply run vi /etc/hosts as shown below.

1
2
3
root@1a4db2b586c3:/app# ls
docker-boot-activemq-0.1.0.jar
root@1a4db2b586c3:/app# vi /etc/hosts

On container startup when we specified the --link command we supplied the container we wanted to link to and an alias that we'd use to reference it. Docker used that alias to create a hosts entry (line 7) to map the container alias activemq to the IP of the activemq container.

1
2
3
4
5
6
7
8
127.0.0.1       localhost
::1     localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
172.17.0.2      activemq fa0b6f943d62
172.17.0.3      1a4db2b586c3

When the application in one container attempts to connect to ActivceMQ in the other container, the broker URL tcp://activemq:61616 will resolve to tcp://172.17.0.2:61616. To check that this IP address is indeed the IP address of the ActiveMQ container run the following command.

1
2
brianh@brianh-VirtualBox:~$ docker inspect --format '{{ .NetworkSettings.IPAddress }}' fa0b6f943d62
172.17.0.2


Testing the Application

Now that we've started and established a link between the application and ActiveMQ containers, its time to test it. Post a message to the MessageController endpoint using CURL as follows.

1
curl -i -H "Content-Type: application/json" -X POST -d "Test Message 1" localhost:8080/message/

In the app logs you should see a message saying that the message was added to the queue.

1
2016-08-10 17:44:52.789  INFO 1 --- [nio-8080-exec-1] com.blog.sample.app.mq.MessageSender     : Sending message to queue: SimpleMessage(message=Test Message 1, creationTime=Wed Aug 10 17:44:52 UTC 2016)

You can confirm that the message is indeed on the queue by opening the ActiveMQ admin console.
To retrieve the message from the queue, run curl -i localhost:8080/message/.

1
2
3
4
5
6
7
brianh@brianh-VirtualBox:~$ curl -i localhost:8080/message/
HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 80
Date: Thu, 11 Aug 2016 06:44:51 GMT

SimpleMessage(message=Test Message 1, creationTime=Wed Aug 10 17:44:52 UTC 2016)brianh@brianh-VirtualBox:~$ 


Source Code

The full source code for this post can be found on Github and the ActiveMQ image is available on DockerHub. As always, if you have any questions, comments or suggestions please leave a note below.

Friday, 22 July 2016

Docker & Spring Boot

Docker allows you to package an application with its dependencies, into a light weight, portable container that can run on almost any environment.  You can think of a Docker container as a run time, a mini virtual machine that encapsulates your application and its dependencies.
In order to run a container you need a Docker image. An image is like a template that defines everything that will exist within the container. You can almost think of an container as a run time instance of the image it was created from. In this post we'll define and build 3 slightly different Docker images that run a simple Java app.

Installing Docker 

If you haven't already done so you'll need to install Docker. The official documentation is pretty good so following it step by step should see you up and running in about 15 minutes.
I've installed Docker on Windows and Ubuntu but to be honest I prefer running it on Ubuntu and have found it a bit more reliable than with Docker Toolbox on Windows.    
Follow the links above to install docker on your OS of choice. Once you're done, open a terminal window and run docker run hello-world to check that your docker install is working as expected.   

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
brianh@brianh-VirtualBox:~/apps$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world

c04b14da8d14: Already exists 
Digest: sha256:0256e8a36e2070f7bf2d0b0763dbabdd67798512411de4cdcf9431a1feb60fd9
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker Hub account:
 https://hub.docker.com

For more examples and ideas, visit:
 https://docs.docker.com/engine/userguide/

This command pulls the hello-world image from the Docker hub repository and uses the image to start a container. If you see the output shown above you'll know your docker installation was successful.

Sample Code 

We're going to look at 3 slightly different ways of building a docker image to run a simple Spring Boot app. We'll start a container from each of the 3 images and call the application health check to make sure the app is up and running.

The Boot app itself couldn't be simpler, consisting of just an Application class annotated with @SpringBootApplication. This is enough to enable auto configuration and act as the application entry point. We don't even need to define our own health check as Boot provides one out of the box. 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.blog.samples.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
   
}



Dockerfile Definition

A Docker file is a set of instructions or steps that tells Docker how to build an image. The Dockerfile below defines steps to build, package and run our app.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
FROM anapsix/docker-oracle-java8

# Install maven
RUN apt-get update -y
RUN apt-get install -y maven

# Creating working directory
WORKDIR /app

# Add src to working directory
ADD pom.xml /app/pom.xml
ADD src /app/src

# Build JAR
RUN mvn package -DskipTests=true

# Start app
ENTRYPOINT ["java","-jar","/app/target/docker-sample-1-0.1.0.jar"]

We'll walk through the Dockerfile  line by line and explain whats happening.
  • Line 1 - FROM instruction tells Docker what base image we want to use as a starting point for our image. I've used anapsix/docker-oracle-java8 which is a lightweight image for Java 8 running on Ubuntu.
  • Line 4 - RUN instruction tells Docker to run a command, in this case apt-get update -y to update the apt package list in preparation for the next step.  
  • Line 5 - tells Docker to run apt-get install -y to download and install maven on the image. The image will use Maven to build our app.
  • Line 8 - WORKDIR command tells Docker to create a working directory on the image. This directory will be used by the ADD and RUN commands that are defined below.
  • Line 10 - ADD command tells Docker to add the application POM from the host machine, to the app directory on the image.
  • Line 11 - tells Docker to add the application source from the host machine to /app/src on the image. At this point the image has everything it need to build the project.
  • Line 15 - tells Docker to run the mvn package -DskipTests=true command from the /app directory on the image. Maven will download all required dependencies and build an executable jar in the /app/target directory.  
  • Line 18 - ENTRYPOINT tells Docker what command to run when the container is started. The comma separated list of values consists of an executable (java in our instance) and a number of parameters. The entry point defined here tells Docker to run the executable JAR from the /app/target directory.       



Creating the Image 

Now that we've defined a Dockerfile lets put it to work by building an image. Run the docker build command specifying an image name and the location of the Dockerfile. For example docker build -t "docker-sample-1" .

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
brianh@brianh-VirtualBox:~/apps/docker-spring-boot/docker-sample-1$ docker build -t "docker-sample-1" .
Sending build context to Docker daemon 12.83 MB
Step 1 : FROM anapsix/docker-oracle-java8
 ---> a8a9dcb0ac64
Step 2 : RUN apt-get update -y
 ---> Running in 5d477ccb8f46
Ign http://archive.ubuntu.com trusty InRelease
Get:1 http://ppa.launchpad.net trusty InRelease [15.5 kB]
Get:2 http://archive.ubuntu.com trusty-updates InRelease [65.9 kB]
Get:3 http://archive.ubuntu.com trusty-security InRelease [65.9 kB]
Hit http://archive.ubuntu.com trusty Release.gpg
Hit http://archive.ubuntu.com trusty Release

// Lots of output from apt update and maven build removed for brevity

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 9:06:42.103s
[INFO] Finished at: Thu Jul 21 16:24:04 UTC 2016
[INFO] Final Memory: 21M/107M
[INFO] ------------------------------------------------------------------------
 ---> b362beb8e90d
Removing intermediate container 512fd61457e6
Step 8 : RUN ls /app/target
 ---> Running in 7eba00207f50
classes
docker-sample-1-0.1.0.jar
docker-sample-1-0.1.0.jar.original
generated-sources
maven-archiver
maven-status
 ---> 613a5f6ef5bb
Removing intermediate container 7eba00207f50
Step 9 : ENTRYPOINT java -jar /app/target/docker-sample-1-0.1.0.jar
 ---> Running in e43707589d45
 ---> 2e4f625b102e
Removing intermediate container e43707589d45
Successfully built 2e4f625b102e

Docker runs each instruction in the Dockerfile step by step. Step 1 in this instance runs quickly because I already have this image cached locally. When you're building this for the first time you likely wont have the anapsix/docker-oracle-java-8 image, so Docker will pull it from the Docker Hub repository. Subsequent builds will use the local cached image and as a result will run much quicker. For each step Docker does the following
  • creates a new intermediate container
  • runs the command inside that container 
  • commits the change as a new image layer 
  • removes the intermediate container and moves to the next step
The new image consists of multiple layers stacked one on top of the other, one for each instruction in the Dockerfile. Run the docker images command to see the newly created image.

1
2
3
4
brianh@brianh-VirtualBox:~/apps$ docker images
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
docker-sample-1               latest              2e4f625b102e        33 minutes ago      936 MB
anapsix/docker-oracle-java8   latest              a8a9dcb0ac64        3 weeks ago         784.5 MB
                                                              
Note the Image ID is the same as that output at the end of the build. To see the various layers that make up the new image run the docker history command as follows. 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
brianh@brianh-VirtualBox:~/apps$ docker history docker-sample-1
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
2e4f625b102e        37 minutes ago      /bin/sh -c #(nop) ENTRYPOINT ["java" "-jar" "   0 B                 
613a5f6ef5bb        37 minutes ago      /bin/sh -c ls /app/target                       0 B                 
b362beb8e90d        37 minutes ago      /bin/sh -c mvn package -DskipTests=true         37.51 MB            
0f3103ac18be        9 hours ago         /bin/sh -c #(nop) ADD dir:82830cfed5011783b44   1.276 kB            
73ccd6348460        9 hours ago         /bin/sh -c #(nop) ADD file:cbad7ca7f8efa76f28   1.349 kB            
22f2ab199dd7        9 hours ago         /bin/sh -c #(nop) WORKDIR /app                  0 B                 
5e3e8435f2b4        9 hours ago         /bin/sh -c apt-get install -y maven             92.07 MB            
6c352c184d38        9 hours ago         /bin/sh -c apt-get update -y                    21.9 MB             
a8a9dcb0ac64        3 weeks ago         /bin/sh -c #(nop) ENV JAVA_HOME=/usr/lib/jvm/   0 B                 
<missing>           3 weeks ago         /bin/sh -c apt-get update && DEBIAN_FRONTEND=   583.6 MB            
<missing>           3 weeks ago         /bin/sh -c apt-key adv --keyserver keyserver.   25.18 kB            
<missing>           3 weeks ago         /bin/sh -c echo "deb http://ppa.launchpad.net   65 B                
<missing>           3 weeks ago         /bin/sh -c echo "oracle-java8-installer share   2.677 MB            
<missing>           3 weeks ago         /bin/sh -c #(nop) ENV LC_ALL=en_US.UTF-8        0 B                 
<missing>           3 weeks ago         /bin/sh -c #(nop) ENV LANG=en_US.UTF-8          0 B                 
<missing>           3 weeks ago         /bin/sh -c locale-gen en_US.UTF-8               1.621 MB            
<missing>           3 weeks ago         /bin/sh -c #(nop) MAINTAINER Anastas Dancha "   0 B                 
<missing>           3 weeks ago         /bin/sh -c #(nop) CMD ["/bin/bash"]             0 B                 
<missing>           3 weeks ago         /bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$/   1.895 kB            
<missing>           3 weeks ago         /bin/sh -c rm -rf /var/lib/apt/lists/*          0 B                 
<missing>           3 weeks ago         /bin/sh -c set -xe   && echo '#!/bin/sh' > /u   8.841 MB 
   
Note that lines 3 to 11 list the image layers that were added as a result of each instruction executed in our Dockerfile.

Running the Container 

Now that we've built the image we're ready to start a container using the docker run command. When the container starts it will launch the Java app on container port 8080. We need to tell the Docker container to map port 8080 to a port on the host machine so that we can access the application running inside the container. We do this using the -p 8080:8080 argument as part of the docker run command.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
brianh@brianh-VirtualBox:~/apps/docker-spring-boot/docker-sample-1$ docker run -p 8080:8080 docker-sample-1

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.7.RELEASE)

3051 [main] INFO  com.blog.samples.boot.Application - Starting Application v0.1.0 on 81828366ca4e with PID 1 (/app/target/docker-sample-1-0.1.0.jar started by root in /app) 
3382 [main] INFO  o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@66429cee: startup date [Thu Jul 21 17:15:31 UTC 2016]; root of context hierarchy 
6707 [main] INFO  o.s.b.f.s.DefaultListableBeanFactory - Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]] 
8176 [main] INFO  o.h.validator.internal.util.Version - HV000001: Hibernate Validator 5.1.3.Final 
10328 [main] INFO  o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http) 
11235 [main] INFO  o.a.catalina.core.StandardService - Starting service Tomcat 

When the container starts it runs the ENTRYPOINT command java -jar /app/target/docker-sample-1-0.1.o.jar specified in the Dockerfile,  The application will start on container port 8080 and Docker will bind to port 8080 on the host.

Testing the Application

To test that the app is up and running we can call the app health check using a simple curl command. We should see some activity in the logs and receive a HTTP 200 response.

1
2
3
4
5
6
7
brianh@brianh-VirtualBox:~/apps/docker-spring-boot/docker-sample-1$ curl -i localhost:8080/health
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
X-Application-Context: application
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Thu, 21 Jul 2016 17:15:54 GMT



Other Examples  

Next we'll look at 2 more Docker examples that are slight variations of the one above. I won't describe these in the same detail but feel free to pull them from Github and have a play around.

Example two is a simplified version of the first example, and simply adds the app JAR to the container and runs the app. In this instance you build and package the app on the host and simply copy the JAR into the container. This keeps the image slightly lighter as it doesn't have to create a maven repository like the first example did.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
FROM anapsix/docker-oracle-java8

# Creating working directory
WORKDIR /app

# Add src to working directory
ADD target/docker-smaple-2-0.1.0.jar /app/docker-sample-2-0.1.0.jar

# Start app
ENTRYPOINT ["java","-jar","/app/docker-sample-2-0.1.0.jar"]

Example 3 is a slightly different variation again. Rather than using the ADD command to copy the application artifact from the host machine, we use pass a URL to the ADD command to pull the artifact from a repository. I've used S3 in this example but you could pull your app from a CI server like Team City, Jenkins or anywhere else you please.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
FROM anapsix/docker-oracle-java8

# Creating working directory
WORKDIR /app

# Pull artifact from repo and add to working directory
ADD https://s3-us-west-2.amazonaws.com/docker-boot-artifact/docker-sample-3-0.1.0.jar /app/docker-sample-3-0.1.0.jar

# Start app
ENTRYPOINT ["java","-jar","/app/docker-sample-3-0.1.0.jar"]



Source Code  

The source code for each of these examples is on Github and split into 3 separate projects. Pull the code down, play around with it and if you have any comments, questions or suggestions just leave a note below.

Tuesday, 17 May 2016

An Introduction to Wiremock

This post provides a brief introduction to Wiremock, showing how it can be used to to quickly and easily mock remote API calls. We'll use Wiremock to write some integration tests for a simple Dropwizard app and show you how it can be put to use in a real world scenario.

Why would I need to mock external API calls? 

There are a number of scenarios where it makes sense to mock an external API rather than call a live service.
  • The external API may still be in development and not yet available for integration. In this instance as long as a data contract has been defined (e.g. Swagger spec, WSDL), the remote API can be stubbed based on the data contract. Stubbed endpoints allows a team to continue development even when external APIs isn't fully implemented.
  • You may have little or no control over the external API uptime in development or test.  As a result you cannot guarantee it will be available to call when running integration tests. In this instance it makes sense to use mocked responses so that your tests don't fail because an external dependency is down. This is especially important if integration tests are being run as part of your Continuous Integration pipeline.  
  • You may want to test fault tolerance scenarios that aren't particularly easy to produce in a live API.  You may want the API to behave badly, but in a very specific way, in order to test how your application deals with a remote failure. Remote calls timing out is an example of a fault tolerance scenario that isn't that easy to set up on a live API. Using a mocked responses allows you to easily test a variety of failure scenarios and ensure your application behaves as expected. 

How does it work? 

Wiremock uses a Jetty Servlet container to expose HTTP endpoints that can be configured to behave in a specific way. Stubbed endpoints are configured to return any HTTP response code, header and body, allowing you to test a wide variety of integration scenarios. Even though stubbed responses are being returned, from the client applications perspective the remote calls appear authentic. As a result the client behaves in exactly the same way it would integrating with a live API.
Wiremock can be deployed as a standalone server, returning mocked responses for preconfigured endpoints. It can also be started and stopped on the fly as part of an integration test suit. This is the approach we're going to take in this post. I like the idea of being able to start the server, configure a stubbed response, run an integration test and then tear down the stub when we're done.  

Sample Code

The sample code is a small Dropwizard app that exposes a single endpoint. An integration test will call the test endpoint, triggering a remote API call which will be serviced by a Wiremock stub.  
  

Customer Resource

The CustomerResource class is a Jersey managed resource that exposes a simple endpoint. In the constructor we pass the external API URL and configure the Client that will be used for the remote call.
On line 22 the Customer that was sent to the endpoint is used to issue a HTTP POST to the credit check API. A CreditCheckResult JSON response is expected from the API, which is then returned to the client.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Path("/customer")
public class CustomerResource {

    private String creditCheckServerUrl;
    private Client client;
 
    public CustomerResource(String creditCheckServerUrl) {
     
        this.creditCheckServerUrl = creditCheckServerUrl; 
        client = ClientBuilder.newClient();
        client.property(ClientProperties.CONNECT_TIMEOUT, 1000);
        client.property(ClientProperties.READ_TIMEOUT,    3000);
    }
    
    @POST
    @Path("/perform-customer-credit-check")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes({MediaType.APPLICATION_JSON})
    public Response performCustomerCreditCheck(Customer customer) throws Exception {
     
        WebTarget webTarget = client.target(creditCheckServerUrl + "/credit-check-api");      
        Response response = webTarget.request().post(Entity.entity(customer, MediaType.APPLICATION_JSON));
     
        if(response.getStatus() == 200){
            return Response.ok(response.readEntity(CreditCheckResult.class), MediaType.APPLICATION_JSON).build();      
        }
     
        throw new CreditCheckFailedException("Error occurred calling Check Service");
    }
}
Figure 1.0 - customer credit check endpoint

Another component worth mentioning is ApplicationExceptionMapper. This class is used to translate application exceptions into HTTP responses. If a SocketTimeoutException is thrown a 503 Service Unavailable is returned along with a message to say the credit check service call timed out. We'll see this in action later with a fault tolerance integration test. Other exception types result in a 500 Internal Server Error and a generic error message.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Provider
public class ApplicationExceptionMapper implements ExceptionMapper<Throwable>
{  
    @Override
    public Response toResponse(Throwable exception)
    {          
     if(exception.getCause() instanceof SocketTimeoutException){
      return Response.status(Response.Status.SERVICE_UNAVAILABLE).
                      entity("Credit Check Service Timed Out").
                      type(MediaType.APPLICATION_JSON).build();
     }
     else{
      return Response.status(Response.Status.INTERNAL_SERVER_ERROR).
                      entity("Error occurred calling Check Service").
                      type(MediaType.APPLICATION_JSON).build();      
     }      
    }
}

                                                                             Figure 1.1 - exception mapper


Integration Test Configuration

To see Wiremock in action we're going to create 3 integration tests, all of which will call the performCustomerCreditCheck endpoint defined above. Before any tests can be written we need to do some general configuration.
  • DropWizardAppRule starts the Dropwizard application before tests are run and stops it again after they finish. This is handy as it saves us having to manually start and stop the server each time we want to run our tests.
  • WireMockRule starts a Jetty container so that Wiremock can serve the mock HTTP responses defined in our tests. Jetty is started on the port specified in the WireMockRule constructor. 
  • Client object is used to call the Jersey endpoint.
  • ObjectMapper is used to serialize/deserialize JSON. 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class CustomerResourceTest {

 @ClassRule    
 public static final DropwizardAppRule<WireMockDemoAppConfig> RULE = 
                 new DropwizardAppRule<WireMockDemoAppConfig>(WireMockDemoApp.class, 
                    ResourceHelpers.resourceFilePath("config.yml"));
 @Rule
 public WireMockRule wireMockRule = new WireMockRule(8090);
 
 private Client client = ClientBuilder.newClient();;     
 private ObjectMapper mapper = new ObjectMapper();


Test 1 - Credit Check Success

Lets start with a happy path test that expects a successful response from the credit check API.  Lines 4 to 10 configure a stub that will expose an endpoint at /credit-check-api . The stub expects a HTTP POST request with an application/json content type and a request body containing Customer JSON. If WireMock receives a request matching this criteria it will return a HTTP 200, an application/json content type and a response body containing the supplied CreditCheckResponse JSON. Note that the getCustomerJson and getCreditCheckResult methods are local helper methods used to build mock JSON for the expected request and response.
 
Now that a stub has been configured for the remote API call, we can call the endpoint we created earlier at /customer/perform-customer-credit-check. On line 13 we do a HTTP POST with a request body containing Customer json.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    @Test
    public void testCustomerCreditCheckSuccessResponse() throws Exception {

     stubFor(post(urlEqualTo("/credit-check-api"))
                .withHeader("Content-Type", WireMock.equalTo("application/json"))
                .withRequestBody(WireMock.equalTo(getCustomerJson()))
                .willReturn(aResponse()
                    .withStatus(200)
                    .withHeader("Content-Type", "application/json")
                    .withBody(getCreditCheckJson())));
         
     WebTarget webTarget = client.target("http://localhost:8080/customer/perform-customer-credit-check");     Response response = webTarget.request(MediaType.APPLICATION_JSON).
                                  post(Entity.entity(getCustomer(), MediaType.APPLICATION_JSON));
     
     assertThat(response.getStatus(), equalTo(200));     
     assertThat(response.readEntity(CreditCheckResult.class), equalTo(getCreditCheckResult()));
    }

Lets quickly revisit the endpoint we created earlier (figure 1.0 above). The HTTP POST on line 13 above will be handled by the performCustomerCreditCheck endpoint. This endpoint will then make a call out to the credit check API (figure 1.0 line 8), which we configured above as a Wiremock stub. The stub will return a CreditCheckResult response and a HTTP 200, resulting in a similar response being returned by performCustomerCreditCheck.

Test 2 - Credit Check Failure

Next we'll look at a failure scenario that expects an error response from the credit check API. This time the Wiremock stub is configured to return a HTTP 503, testing how our endpoint handles an error response from the remote API.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
    @Test
    public void testCustomerCreditCheckErrorResponse() throws Exception {

     stubFor(post(urlEqualTo("/credit-check-api")).withHeader("Content-Type", WireMock.equalTo("application/json"))
                .withRequestBody(WireMock.equalTo(getCustomerJson()))
                .willReturn(aResponse()
                    .withStatus(503)
                    .withHeader("Content-Type", "application/json")));
                    
     WebTarget webTarget = client.target("http://localhost:8080/customer/perform-customer-credit-check");     Response response = webTarget.request(MediaType.APPLICATION_JSON).
                                  post(Entity.entity(getCustomer(), MediaType.APPLICATION_JSON));
     
     assertThat(response.getStatus(), equalTo(500));     
     assertThat(response.readEntity(String.class), equalTo("Error occurred calling Check Service"));
    }

This test sends a HTTP POST to /customer/perform-customer-credit-checkwhich results in an external call out to the credit check API (figure 1.0 line 8). The stub will return a HTTP 503 which will result in the endpoint throwing a CreditCheckFailedException. The ApplicationExceptionMapper defined earlier (Figure 1.1) will translate the CreditCheckFailedException into a HTTP 500 response which is returned to the client.

Test 3 - Credit Check Service Timeout

Our final test will look at a fault tolerance scenario. The Wiremock stub is configured to return a successful HTTP 200, but this time the response is delayed by 6 seconds. This simulates a slow or unresponsive API call and allows us to test how our application handles such a scenario. It's good practice to terminate external calls if the remote endpoint does not response within a reasonable period.
In the CustomerResource defined earlier (figure 1.0) we configured the Client with a read time out of 3 seconds. This means that if the credit check API doesn't respond within 3 seconds the call will time out and fail. The stub configuration below tests this behaviour by mocking the endpoint to wait 6 seconds before responding, more than enough time for the client application to time out.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
    public void testCustomerCreditCheckServiceTimeout() throws Exception {

     int creditCheckServiceDelayMillis = 6000;
     
     stubFor(post(urlEqualTo("/credit-check-api"))                .withHeader("Content-Type", WireMock.equalTo("application/json"))
                .withRequestBody(WireMock.equalTo(getCustomerJson()))
                .willReturn(WireMock.aResponse()
                    .withStatus(200)
                    .withHeader("Content-Type", "application/json")                    
                    .withBody(getCreditCheckJson())
                    .withFixedDelay(creditCheckServiceDelayMillis)));
     
     WebTarget webTarget = client.target("http://localhost:8080/customer/perform-customer-credit-check");     
     long startMillis = DateTime.now().getMillis();
     Response response = webTarget.request(MediaType.APPLICATION_JSON).
              post(Entity.entity(getCustomer(), MediaType.APPLICATION_JSON));
     long endMillis = DateTime.now().getMillis();
     
     assertThat((int)(endMillis - startMillis), is(lessThan(creditCheckServiceDelayMillis)));     
     assertThat(response.getStatus(), equalTo(503));
     assertThat(response.readEntity(String.class), equalTo("Credit Check Service Timed Out"));
    }

This test sends a HTTP POST to /customer/perform-customer-credit-check, which results in an external call out to the credit check API (figure 1.0 line 8). The stub will hang for 6 seconds causing the Client to time out and throw a SocketTimeOutException. The ApplicationExceptionMapper defined earlier (Figure 1.1) will translate the SocketTimeOutException into a HTTP 503 response and include a message saying that the credit check service timed out.

Running the Tests

To see the sample code in action you'll need to pull it down and run it as follows.
  • git clone https://github.com/briansjavablog/wiremock-demo.git
  • cd wiremock-demo
  • mvn test 

What else can Wiremock do?

Wiremock has a number of other interesting features that are worth looking at.
  • Standalone Deployment - Wiremock can be deployed as a standalone web app rather than being started and stopped with each integration test.
  • Mapping files can be used to configure stubs as an alternative to using just the API. This is useful is you have lots of stubs and you want to decouple their configuration from your tests. Mappings can be placed in a directory on your deployed Wiremock instance or registered by posting them to a Wiremock endpoint.
  • Proxying - Wiremock can be used to proxy calls to a live API. Say for example you want to run the majority of your integration tests against a live API, but you'd like to use Wiremock stubs for testing fault tolerance scenarios. Wiremock can be used as a proxy so that it forwards some requests to the live API and responds with stubs for other. This is a powerful feature as it allows you to mix stubbed and real responses in the same suit of tests.   

Wrapping Up 

This post looked at how Wiremock can be used to stub remote endpoints and provide a simple way to test a variety of integration scenarios. From what I've seen Wiremock isn't an alternative to live end to end integration testing, but rather something that can compliment it.  As always, if you have any questions or suggestions, please leave a comment below.