On a recent iOS app project, we had a scenario in which we needed some “fake” services post-haste so we could start integration/network testing. FYI, the real web-service development team was lagging behind quite a ways due to poor resource planning by management (same old story). A teammate took about a day and stood up a full suite of mock web-services via Apache and PHP. Another teammate joked he could’ve stood them up in half the time with Node.js which got me wondering how quickly and easily I could stand up a Java REST API with Apache Camel and Spark.

Standing up a Java REST API

I really didn’t want to go the route of using an application server and using JAX-RS or JAX-WS. I didn’t want to do all that boilerplate nor did I want to install WebLogic (which is what I’m most familiar with) on my personal computer. After reading around a bit, I was most intrigued by the concept of an embedded server. Instead of having to build a WAR file and deploying it to the server, I could run my plain old Java SE application and it would launch the server for me. Sounds awesome! As it turns out, Spark makes it really easy to implement in just a few lines of code. Now while I could’ve used Spark directly, I also wanted to use Apache Camel for some of the features it provides. Lo and behold Apache Camel has a spark-rest component!

Firing up my Java IDE:

I started a new Maven Java Application and I modify my pom.xml to look like this:

<?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.cf</groupId>
    <artifactId>java-rest-api-starterkit</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.javadoc.skip>true</maven.javadoc.skip>
    </properties>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                            <mainClass>com.cf.helloworld.Main</mainClass>
                        </manifest>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <dependencies>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.8.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.8.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-core</artifactId>
            <version>2.16.3</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-spark-rest</artifactId>
            <version>2.16.3</version>       
        </dependency>
        <dependency>
            <groupId>org.apache.camel</groupId>
            <artifactId>camel-gson</artifactId>
            <version>2.16.3</version>
        </dependency>
    </dependencies>
    <name>java-rest-api-starterkit</name>
</project>

As you can see, I’ve added Camel libraries (camel-core, camel-spark-rest, and came-gson) and logging (log4j2) as well as the maven-assembly-plugin to bundle everything into a single jar. When I build the project, the application is created as /target/java-rest-api-starterkit-1.0-SNAPSHOT-jar-with-dependencies.jar

Now for the code. To keep this example simple, my entry point (com.cf.helloworld.Main.java) simply creates the CamelContext, adds Routes to it via a custom RouteBuildImpl class (see below), and then starts the CamelContext. I’m using a for loop that sleeps for a second per iteration so my application runs for 10 minutes.

package com.cf.helloworld;

import com.cf.helloworld.route.RouteBuilderImpl;
import org.apache.camel.CamelContext;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Main {

    public static void main(String[] args) throws InterruptedException  {
        Logger logger = LogManager.getLogger();

        try {

            logger.info("Starting HelloWorld");
            CamelContext context = new DefaultCamelContext();
            context.addRoutes(new RouteBuilderImpl());
            
            context.start();
      
            final long sleepDurationInMs = 1000;
            for (int i = 0; i < 600; i++)
            {                
                TimeUnit.SECONDS.sleep(sleepDurationInMs);
            }
                 
            context.stop();
            logger.info("Stopping HelloWorld");
            
        }catch (InterruptedException iex) {
            throw iex;
        }
        catch (Exception ex) {
            logger.error("Main failed - " + ex.getMessage(), ex);
        }
    }
}

Now about that RouteBuilderImpl class:

package com.cf.helloworld.route;

import com.cf.helloworld.processor.DefaultGetProcessor;
import com.cf.helloworld.processor.DefaultPostProcessor;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.dataformat.JsonLibrary;
import org.apache.camel.model.rest.RestBindingMode;

public class RouteBuilderImpl extends RouteBuilder
{
    @Override
    public void configure() throws Exception
    {
        restConfiguration().component("spark-rest").scheme("http").host("localhost").port(9091).bindingMode(RestBindingMode.auto).enableCORS(true);

        rest("/api/v1/")
                .get("customers/{id}").to("direct:customerDetail")
                .get("customers/{id}/orders").to("direct:customerOrders")
                .post("/register").to("direct:register");
        
        from("direct:customerDetail")
                .process(new DefaultGetProcessor())
                .marshal().json(JsonLibrary.Gson)
                .log("customerDetail body: ${body}");
        
         from("direct:customerOrders")
                .process(new DefaultGetProcessor())
                 .marshal().json(JsonLibrary.Gson)
                .log("customerOrders body: ${body}");
         
         from("direct:register")
                .process(new DefaultPostProcessor())
                 .marshal().json(JsonLibrary.Gson)
                .log("register body: ${body}");
    }
}

There are a few things going on here. As you can see, RouteBuilderImpl extends Camel’s RouteBuilder class which in turn gives us the configure() method to override. The routes defined in this configure() method will be added to our CamelContext since back in our Main class we specified:

            CamelContext context = new DefaultCamelContext();
            context.addRoutes(new RouteBuilderImpl());

While I could’ve defined the routes in-line, breaking it out into it’s own RouteBuilder derived class makes it a bit more modular and easier to locate.

Line 14:

restConfiguration().component("spark-rest").scheme("http").host("localhost").port(9091).bindingMode(RestBindingMode.auto).enableCORS(true);

basically says, “I’m going to launch an embedded Spark (Jetty) server listening on http://localhost:9091 with CORS enabled.” Pretty cool as I configured my server in one line of code! For additional functionality, you can find the Apache Camel REST DSL documentation here.

I now proceed to define my endpoints and routes:

        rest("/api/v1/")
                .get("customers/{id}").to("direct:customerDetail")
                .get("customers/{id}/orders").to("direct:customerOrders")
                .post("/register").to("direct:register");

These 4 lines define 3 (2 GET and 1 POST) REST endpoints:

  • An HTTP GET request to http://localhost:9091/api/v1/customers/{id} will execute the route named “direct:customerDetail” with whatever passed in the {id} portion automatically assigned to a variable id
  • An HTTP GET request to http://localhost:9091/api/v1/orders/{id} will execute the route named “direct:customerOrders” with whatever passed in the {id} portion automatically assigned to a variable id
  • An HTTP POST request to http://localhost:9091/api/v1/register will execute the route named “direct:register”

Now for the routes mentioned above:

        from("direct:customerDetail")
                .process(new DefaultGetProcessor())
                .marshal().json(JsonLibrary.Gson)
                .log("customerDetail body: ${body}");
        
         from("direct:customerOrders")
                .process(new DefaultGetProcessor())
                 .marshal().json(JsonLibrary.Gson)
                .log("customerOrders body: ${body}");
         
         from("direct:register")
                .process(new DefaultPostProcessor())
                 .marshal().json(JsonLibrary.Gson)
                .log("register body: ${body}");

To err on the side of the explicit, you’ll see there are three routes defined when there probably could be one. Looking at the “direct:customerDetail” route definition, we process the incoming request via a new instance of DefaultGetProcessor. The output of that is marshaled to JSON string representation (provided by camel-gson), logged, and then returned to the client. Next let’s look at how the DefaultGetProcessor() transforms the client’s input to what we want to pass back.

package com.cf.helloworld.processor;

import java.util.Map;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class DefaultGetProcessor implements Processor
{

    private final Logger logger = LogManager.getLogger(DefaultGetProcessor.class);

    @Override
    public void process(Exchange ex) throws Exception
    {
        final long startTime = System.currentTimeMillis();
        logger.debug("Processing exchange - " + ex.toString());
        final String body = ex.getIn().getBody(String.class);
        final Map<String, Object> headers = ex.getIn().getHeaders();

        final Object id = headers.get("id");     
        logger.debug("ID: " + id);

        final Object httpQuery = headers.get("CamelHttpQuery");
        logger.debug("CamelHttpQuery: " + httpQuery);

        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, Object> header : headers.entrySet())
        {
            sb.append(header.getKey()).append(" = ").append(header.getValue()).append(System.lineSeparator());

        }
        logger.debug("Headers: " + sb.toString());

        ex.getOut().setHeaders(ex.getIn().getHeaders());
        ex.getOut().setBody("DefaultGetProcessor processing complete for id: " + id);
        logger.info("DefaultGetProcessor process() completed in " + (System.currentTimeMillis() - startTime) + " ms");
    }

}

What’s passed in (and out) by the route is an Exchange object. From looking at the code (and running it for the debug logs) you can understand what the relevant elements are. You can see we use the Exchange object’s setBody() to set the output. In this case, we set it to the String “DefaultGetProcessor processing complete”.

So now if we run the application we can see the Spark server is up and listening on 0.0.0.0:9091:

Running HelloWorld Java REST API

Running the application

I can call my endpoints using a REST client like Postman:

HelloWorld invoking Java REST API with Postman example

HelloWorld Postman example

What if we want to return a complex output? We can create a new CustomerDetailResponse class:

package com.cf.helloworld.response;

import java.util.Date;

public class CustomerDetailResponse
{
    public final String id;
    public final String name;
    public final String emailAddress;
    public final Date birthday;
    
    public CustomerDetailResponse(String id, String name, String emailAddress, Date birthday)
    {
        this.id = id;
        this.name = name;
        this.emailAddress = emailAddress;
        this.birthday = birthday;
    }
}

We modify our DefaultGetProcessor class:

package com.cf.helloworld.processor;
import com.cf.helloworld.response.CustomerDetailResponse;
import java.util.Date;
import java.util.Map;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class DefaultGetProcessor implements Processor
{

    private final Logger logger = LogManager.getLogger(DefaultGetProcessor.class);

    @Override
    public void process(Exchange ex) throws Exception
    {
        final long startTime = System.currentTimeMillis();
        logger.debug("Processing exchange - " + ex.toString());
        final String request = ex.getIn().getBody(String.class);
        final Map<String, Object> headers = ex.getIn().getHeaders();
        
        final Object id = headers.get("id");     
        logger.debug("ID: " + id);

        final Object httpQuery = headers.get("CamelHttpQuery");
        logger.debug("CamelHttpQuery: " + httpQuery);

        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, Object> header : headers.entrySet())
        {
            sb.append(header.getKey()).append(" = ").append(header.getValue()).append(System.lineSeparator());

        }
        logger.debug("Headers: " + sb.toString());
        
        // Construct output
        CustomerDetailResponse response = new CustomerDetailResponse(id.toString(), "Billy Bob", "b.bob@aol.com", new Date());

        ex.getOut().setHeaders(ex.getIn().getHeaders());
        ex.getOut().setBody(response);
        logger.info("DefaultPostProcessor process() completed in " + (System.currentTimeMillis() - startTime) + " ms");
    }

}

After these changes, we re-start the application (see how we don’t have to go through a deploy step with each dev iteration). Running Postman again:

Running Postman after complex response

Running Postman after complex response

The entire project source can be found at https://github.com/TheCookieLab/java-rest-api-starterkit