QBit Microservices Java Lib RESTful

Richard Hightower edited this page Aug 31, 2015 · 12 revisions

If you are new to QBit. It might make more sense to skim the overview. We suggest reading the landing page of the QBit Microservices Lib's wiki for a background on QBit. This will let you see the forrest while the tutorials are inspecting the trees. There is also a lot of documents linked to off of the wiki landing page as well as in the footer section of the tutorials.

QBit Microservices Java Lib RESTful - Part 3

Home <<Part 1, <Part 2, Part 3, Part 4 -- -example code- -qbit docs-

Mammatus Tech

QBit provides three way to remotely talk to microservices, WebSocket, HTTP REST and the QBit event bus. (Communication is also pluggable so it is easy to send QBit calls or events over a message bus, or other means.)

This tutorial is going to focus on QBit and its REST support. Here is an example program that we are going to examine.

REST API for Microservices

package com.mammatustech.todo;

import io.advantageous.qbit.annotation.RequestMapping;
import io.advantageous.qbit.annotation.RequestMethod;
import io.advantageous.qbit.annotation.RequestParam;

import java.util.*;

@RequestMapping(value = "/todo-service", description = "Todo service")
public class TodoService {


    private final Map<String, Todo> todoMap = new TreeMap<>();


    @RequestMapping(value = "/todo", method = RequestMethod.POST,
            description = "add a todo item to the list", summary = "adds todo",
            returnDescription = "returns true if successful")
    public boolean add(final Todo todo) {

        todoMap.put(todo.getId(), todo);
        return true;
    }



    @RequestMapping(value = "/todo", method = RequestMethod.DELETE,
            description = "Deletes an item by id",  summary = "delete a todo item")
    public void remove(@RequestParam(value = "id", description = "id of Todo item to delete")
                           final String id) {

        todoMap.remove(id);
    }



    @RequestMapping(value = "/todo", method = RequestMethod.GET,
            description = "List all items in the system",  summary = "list items",
            returnDescription = "return list of all items in system")
    public List<Todo> list() {
        return new ArrayList<>(todoMap.values());
    }

}

Let's start with the addTodo method.

Add TODO

    @RequestMapping(value = "/todo", method = RequestMethod.POST,
            description = "add a todo item to the list", summary = "adds todo",
            returnDescription = "returns true if successful")
    public boolean add(final Todo todo) {

        todoMap.put(todo.getId(), todo);
        return true;
    }

The RequestMapping is inspired from Spring MVC's REST support. In fact, if you use Spring's annotation, QBit can use it as is.

RequestMapping annotation

package io.advantageous.qbit.annotation;
...
/**
 * Used to map Service method to URIs in an HTTP like protocol.
 * @author Rick Hightower
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.TYPE})
public @interface RequestMapping {


    /**
     * Primary mapping expressed by this annotation.
     * For HTTP, this would be the URI. Or part of the URI after the parent URI context
     * be it ServletApp Context or some other parent context.
     *
     * @return a request mapping, URIs really
     */
    String[] value() default {};

    /**
     * HTTP request methods must be:
     * GET, or POST or WebSocket.
     *
     * @return or RequestMethods that are supported by this end point
     */
    RequestMethod[] method() default {RequestMethod.GET};

    /**
     * Used to document endpoint
     * @return description
     */
    String description() default "no description";


    /**
     * Used to document endpoint
     * @return description
     */
    String returnDescription() default "no return description";

    /**
     * Used to document endpoint
     * @return summary
     */
    String summary() default "no summary";
}

Both Boon and QBit have the same philosophy on annotations. You don't have to use our annotations. You can create your own and as long as the class name and attributes match, we can use the annotations that you provide. This way if you decide to switch away from Boon or QBit, it is easier and you can keep your annotations.

QBit added description, summary and returnDescription attributes to our RequestMapping annotation. QBit does this to capture the meta-data and expose it for API-Gateway style client generation and documentation using tools like Swagger. QBit is not tied to Swagger. It has its own service meta-data format, but QBit converts its service meta-data to Swagger format to get access to the wealth of Swagger tools and clients. Swagger, and tools like Swagger, makes documenting your API easy and accessible. QBit fully embraces generating service meta-data and Swagger because it embraces the concepts behind building Microserivce API Gateways, which is essential part of Microservices Architecture to support web and mobile Microservices.

The add method specifies the HTTP method as POST, and the URI is mapped to "/todo"

Add TODO

    @RequestMapping(value = "/todo", method = RequestMethod.POST, ...)
    public boolean add(final Todo todo) {

        todoMap.put(todo.getId(), todo);
        return true;
    }

You can specify the HTTP methods POST, GET, DELETE, PUT, etc. Generally speaking you use GET to read, POST to create new, PUT to update or add to a list, and DELETE to destroy, delete or remove. It is a bad idea to use a GET to modify something (just in case a tool crawls your web service).

The next method is a DELETE operation which removes a Todo item.

Remove TODO

    @RequestMapping(value = "/todo", method = RequestMethod.DELETE ...)
    public void remove(@RequestParam(value = "id", description = "id of Todo item to delete")
                           final String id) {

        todoMap.remove(id);
    }

Notice that the remove method also takes an id of the Todo item, which we want to remove from the map. The @RequestParam is also modeled after the one from Spring MVC as that is the one that most people are familiar with. It just pulls the id from a request parameter.

Notice that the method returns void. Whenever you provide a method that provides a void, it does not wait for the method to return to send a HTTP response code 202 Accepted. If you want the service to capture any exceptions that might occur and send the exception message, then return anything but void from a method call. Returning void means fire and forget which is very fast, but does not have a way to notify the client of errors.

Let's show an example that would report errors.

Remove TODO with error handling and a return

    @RequestMapping(value = "/todo", method = RequestMethod.DELETE)
    public boolean remove(final @RequestParam("id") String id) {

        Todo remove = todoMap.remove(id);
        return remove!=null;

    }

If this operation for any reason throws an exception, then the client with get the exception message and an HTTP response code of 500 Internal Server Error.

Briefly, as you know you might have to call a downstream service to save the actual Todo item in Cassandra or a relational database. In this case, you would use a Callback as follows:

Callback version of remove

    @RequestMapping(value = "/todo", method = RequestMethod.DELETE)
    public void remove(final Callback<Boolean> callback, 
                       final @RequestParam("id") String id) {

        Todo remove = todoMap.remove(id);
        callback.accept(remove!=null);

    }

A Callback allows you to response async to a request so that the method call does not block the IO threads. We will cover callbacks more later in the tutorial series.

Side Note: To learn more about Callbacks go to the QBit wiki and search for Callback in the Pages sidebar, to see some Cassandra async examples and SOLR examples of Callbacks go to reactively handling async callbacks with QBit Reactive Microservices.

Lastly in this example we have list todos.

List Todos

    @RequestMapping(value = "/todo", method = RequestMethod.GET ... )
    public List<Todo> list() {
        return new ArrayList<>(todoMap.values());
    }

Since the list method is not modifying the Todo items but merely returning them, then we can return the entire list.

Using Callbacks

In general you use a Callback to handle multiple downstream services that may be doing additional processing or IO but in seperate services and then in a non-blocking way return the result to the original client.

This service could be rewritten using Callbacks as follows:

REST API with callback for Microservices

package com.mammatustech.todo;

import io.advantageous.qbit.annotation.RequestMapping;
import io.advantageous.qbit.annotation.RequestMethod;
import io.advantageous.qbit.annotation.RequestParam;
import io.advantageous.qbit.reactive.Callback;

import java.util.*;


@RequestMapping("/todo-service")
public class TodoService {


    private final Map<String, Todo> todoMap = new TreeMap<>();


    @RequestMapping(value = "/todo", method = RequestMethod.POST)
    public void add(final Callback<Boolean> callback, final Todo todo) {
        todoMap.put(todo.getId(), todo);
        callback.accept(true);
    }


    @RequestMapping(value = "/todo", method = RequestMethod.DELETE)
    public void remove(final Callback<Boolean> callback, 
                       final @RequestParam("id") String id) {

        Todo remove = todoMap.remove(id);
        callback.accept(remove!=null);

    }

    @RequestMapping(value = "/todo", method = RequestMethod.GET)
    public void list(final Callback<ArrayList<Todo>> callback) {
        callback.accept(new ArrayList<>(todoMap.values()));
    }


}

Again callbacks would make more sense if we were talking to a downstream service that did additional IO as follows:

Callback


    @RequestMapping(value = "/todo", method = RequestMethod.POST)
    public void add(final Callback<Boolean> callback, final Todo todo) {
        todoMap.put(todo.getId(), todo);
        todoRepo.add(callback, todo);
    }

Of course there are a lot more details to cover then this and we will cover them in due course.

*Remember: To learn more about Callbacks go to the QBit wiki and search for Callback in the Pages sidebar, to see some Cassandra async examples and SOLR examples of Callbacks go to reactively handling async callbacks with QBit Reactive Microservices. Or just continue reading this tutorial series.

TODO Info how to run the example

The complete code for the microservice Todo service without Callbacks is in github.

You can find the complete code listing for the microservice Todo service with Callbacks.

Running the todo service with gradle is shown as follows:

Running the todo service

$ gradle run
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:run
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
log4j:WARN No appenders could be found for logger (io.netty.util.internal.logging.InternalLoggerFactory).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
Todo Server and Admin Server started

The TODO service has a gradle build file that uses the Gradle application plugin as follows:

group 'qbit-ex'
version '1.0-SNAPSHOT'

apply plugin: 'java'


apply plugin: 'application'


mainClassName = "com.mammatustech.todo.TodoServiceMain"


compileJava {
    sourceCompatibility = 1.8
}

repositories {
    mavenCentral()
    mavenLocal()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.11'
    compile group: 'io.advantageous.qbit', name: 'qbit-admin', version: '0.9.0.M2'
    compile group: 'io.advantageous.qbit', name: 'qbit-vertx', version: '0.9.0.M2'

}

In the main method that launches the app we setup some meta-data about the service so we can export it later via Swagger.

TodoServiceMain

package com.mammatustech.todo;


import io.advantageous.qbit.admin.ManagedServiceBuilder;
import io.advantageous.qbit.meta.builder.ContextMetaBuilder;

public class TodoServiceMain {


    public static void main(final String... args) throws Exception {

        /* Create the ManagedServiceBuilder which manages a clean shutdown, health, stats, etc. */
        final ManagedServiceBuilder managedServiceBuilder =
                ManagedServiceBuilder.managedServiceBuilder()
                        .setRootURI("/v1") //Defaults to services
                        .setPort(8888); //Defaults to 8080 or environment variable PORT

        /* Context meta builder to document this endpoint.  */
        ContextMetaBuilder contextMetaBuilder = managedServiceBuilder.getContextMetaBuilder();
        contextMetaBuilder.setContactEmail("lunati-not-real-email@gmail.com");
        contextMetaBuilder.setDescription("A great service to show building a todo list");
        contextMetaBuilder.setContactURL("http://www.bwbl.lunati/master/of/rodeo");
        contextMetaBuilder.setContactName("Buffalo Wild Bill Lunati");
        contextMetaBuilder.setLicenseName("Commercial");
        contextMetaBuilder.setLicenseURL("http://www.canttouchthis.com");
        contextMetaBuilder.setTitle("Todo Title");
        contextMetaBuilder.setVersion("47.0");


        managedServiceBuilder.getStatsDReplicatorBuilder().setHost("192.168.59.103");
        managedServiceBuilder.setEnableStatsD(true);


        /* Start the service. */
        managedServiceBuilder.addEndpointService(new TodoService()) //Register TodoService
                .getEndpointServerBuilder()
                .build().startServer();

        /* Start the admin builder which exposes health end-points and swagger meta data. */
        managedServiceBuilder.getAdminBuilder().build().startServer();

        System.out.println("Todo Server and Admin Server started");

    }
}

The Todo class is just a POJO.

Todo class

package com.mammatustech.todo;

public class Todo {

    private  String id;

    private final String name;
    private final String description;
    private final long createTime;

    public Todo(String name, String description, long createTime) {
        this.name = name;
        this.description = description;
        this.createTime = createTime;

        this.id = name + "::" + createTime;
    }


    public String getId() {
        if (id == null) {
            this.id = name + "::" + createTime;
        }
        return id;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }

    public long getCreateTime() {
        return createTime;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Todo todo = (Todo) o;

        if (createTime != todo.createTime) return false;
        return !(name != null ? !name.equals(todo.name) : todo.name != null);

    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + (int) (createTime ^ (createTime >>> 32));
        return result;
    }
}

The TodoService is as follows.

TodoService without callbacks

package com.mammatustech.todo;

import io.advantageous.qbit.annotation.RequestMapping;
import io.advantageous.qbit.annotation.RequestMethod;
import io.advantageous.qbit.annotation.RequestParam;

import java.util.*;


/**
 * Default port for admin is 7777.
 * Default port for main endpoint is 8080.
 *
 * <pre>
 * <code>
 *
 *     Access the service:
 *
 *    $ curl http://localhost:8888/v1/...
 *
 *
 *     To see swagger file for this service:
 *
 *    $ curl http://localhost:7777/__admin/meta/
 *
 *     To see health for this service:
 *
 *    $ curl http://localhost:8888/__health
 *     Returns "ok" if all registered health systems are healthy.
 *
 *     OR if same port endpoint health is disabled then:
 *
 *    $ curl http://localhost:7777/__admin/ok
 *     Returns "true" if all registered health systems are healthy.
 *
 *
 *     A node is a service, service bundle, queue, or server endpoint that is being monitored.
 *
 *     List all service nodes or endpoints
 *
 *    $ curl http://localhost:7777/__admin/all-nodes/
 *
 *
 *      List healthy nodes by name:
 *
 *    $ curl http://localhost:7777/__admin/healthy-nodes/
 *
 *      List complete node information:
 *
 *    $ curl http://localhost:7777/__admin/load-nodes/
 *
 *
 *      Show service stats and metrics
 *
 *    $ curl http://localhost:8888/__stats/instance
 * </code>
 * </pre>
 */
@RequestMapping(value = "/todo-service", description = "Todo service")
public class TodoService {


    private final Map<String, Todo> todoMap = new TreeMap<>();


    @RequestMapping(value = "/todo", method = RequestMethod.POST,
            description = "add a todo item to the list", summary = "adds todo",
            returnDescription = "returns true if successful")
    public boolean add(final Todo todo) {

        todoMap.put(todo.getId(), todo);
        return true;
    }



    @RequestMapping(value = "/todo", method = RequestMethod.DELETE,
            description = "Deletes an item by id",  summary = "delete a todo item")
    public void remove(@RequestParam(value = "id", description = "id of Todo item to delete")
                           final String id) {

        todoMap.remove(id);
    }



    @RequestMapping(value = "/todo", method = RequestMethod.GET,
            description = "List all items in the system",  summary = "list items",
            returnDescription = "return list of all items in system")
    public List<Todo> list() {
        return new ArrayList<>(todoMap.values());
    }

}

QBit comes with a lib to easily (and quickly) make REST calls.

Calling todo service from Java using QBit's http client support

package com.mammatustech.todo;

import io.advantageous.boon.json.JsonFactory;
import io.advantageous.qbit.http.HTTP;

public class HttpClient {

    public static void main(final String... args) throws Exception {

        for (int index = 0; index < 100; index++) {

            HTTP.postJSON("http://localhost:8888/v1/todo-service/todo",
                    JsonFactory.toJson(new Todo("name" + index,
                            "desc" + index, System.currentTimeMillis() )));
            System.out.print(".");
        }
    }

}

You can also make async calls with QBit's http client lib.

Swagger generation

You can import the JSON meta data into Swagger.

$ curl http://localhost:7777/__admin/meta/
{
    "swagger": "2.0",
    "info": {
        "title": "application title goes here",
        "description": "A great service to show building a todo list",
        "contact": {
            "name": "Buffalo Wild Bill Lunati",
            "url": "http://www.bwbl.lunati/master/of/rodeo",
            "email": "lunati-not-real-email@gmail.com"
        },
        "version": "47.0",
        "license": {
            "name": "Commercial",
            "url": "http://www.canttouchthis.com"
        }
    },
    "host": "localhost:8888",
    "basePath": "/v1",
    "schemes": [
        "http",
        "https",
        "wss",
        "ws"
    ],
    "consumes": [
        "application/json"
    ],
    "definitions": {
        "Todo": {
            "properties": {
                "id": {
                    "type": "string"
                },
                "name": {
                    "type": "string"
                },
                "description": {
                    "type": "string"
                },
                "createTime": {
                    "type": "integer",
                    "format": "int64"
                }
            }
        }
    },
    "produces": [
        "application/json"
    ],
    "paths": {
        "/todo-service/todo": {
            "get": {
                "operationId": "list",
                "summary": "list items",
                "description": "List all items in the system",
                "produces": [
                    "application/json"
                ],
                "responses": {
                    "200": {
                        "description": "return list of all items in system",
                        "schema": {
                            "type": "array",
                            "items": {
                                "$ref": "#/definitions/Todo"
                            }
                        }
                    }
                }
            },
            "post": {
                "operationId": "add",
                "summary": "adds todo",
                "description": "add a todo item to the list",
                "produces": [
                    "application/json"
                ],
                "parameters": [
                    {
                        "name": "body",
                        "in": "body",
                        "required": true,
                        "schema": {
                            "$ref": "#/definitions/Todo"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "returns true if successful",
                        "schema": {
                            "type": "boolean"
                        }
                    }
                }
            },
            "delete": {
                "operationId": "remove",
                "summary": "delete a todo item",
                "description": "Deletes an item by id",
                "parameters": [
                    {
                        "name": "id",
                        "in": "query",
                        "description": "id of Todo item to delete",
                        "type": "string"
                    }
                ],
                "responses": {
                    "202": {
                        "description": "returns success",
                        "schema": {
                            "type": "string"
                        }
                    }
                }
            }
        }
    }
}

The Swagger JSON meta-data can be imported into the swagger editor.

swagger: '2.0'
info:
  title: Todo Title
  description: A great service to show building a todo list
  contact:
    name: Buffalo Wild Bill Lunati
    url: 'http://www.bwbl.lunati/master/of/rodeo'
    email: lunati-not-real-email@gmail.com
  version: '47.0'
  license:
    name: Commercial
    url: 'http://www.canttouchthis.com'
host: 'localhost:8888'
basePath: /v1
schemes:
  - http
  - https
  - wss
  - ws
consumes:
  - application/json
definitions:
  Todo:
    properties:
      id:
        type: string
      name:
        type: string
      description:
        type: string
      createTime:
        type: integer
        format: int64
produces:
  - application/json
paths:
  /todo-service/todo:
    get:
      operationId: list
      summary: list items
      description: List all items in the system
      produces:
        - application/json
      responses:
        '200':
          description: return list of all items in system
          schema:
            type: array
            items:
              $ref: '#/definitions/Todo'
    post:
      operationId: add
      summary: adds todo
      description: add a todo item to the list
      produces:
        - application/json
      parameters:
        - name: body
          in: body
          required: true
          schema:
            $ref: '#/definitions/Todo'
      responses:
        '200':
          description: returns true if successful
          schema:
            type: boolean
    delete:
      operationId: remove
      summary: delete a todo item
      description: Deletes an item by id
      parameters:
        - name: id
          in: query
          description: id of Todo item to delete
          type: string
      responses:
        '202':
          description: returns success
          schema:
            type: string

You can see the descriptions, summary and returnDescription from the @RequestMapping and @RequestParam annotations are exposed in the Swagger generation for documentation.

You can also generate working client libs from Swagger using the JSON that QBit provides. We did this and it works for both the Callback version of the TODO list as well as the non-callback version. The source code for the Swagger REST TODO Client which was generated is in github.

Using Swagger generated client to talk to TODO service

        DefaultApi defaultApi = new DefaultApi();

        Todo todo = new Todo();
        todo.setDescription("Show demo to group");
        todo.setName("Show demo");
        todo.setCreateTime(123L);

        defaultApi.add(todo);

        List<Todo> list = defaultApi.list();

        list.forEach(new Consumer<Todo>() {
            @Override
            public void accept(Todo todo) {
                System.out.println(todo);
            }
        });

Curl, Stats and health

You can access the service via curl commands.

Getting a list of TODO items using REST curl call

$ curl http://localhost:8888/v1/todo-service/todo
[{"id":"name0::1441040038414","name":"name0","description":"desc0",
"createTime":1441040038414}, ...

You can inquire about the health of its nodes using the admin port.

Using admin port to check Todo services health

$ curl http://localhost:7777/__admin/load-nodes/
[{"name":"TodoService","ttlInMS":10000,"lastCheckIn":1441040291968,"status":"PASS"}]

Remember that this will list all service actors (ServiceQueues) and ServiceServerEndpoints (REST and WebSocket services).

If you are looking for a yes/no answer to health for Nagios or Consul or some load balancer, then you can use.

Health status yes or no?

 $ curl http://localhost:8888/__health
"ok"

Of the admin port version of this with:

Health status yes or no? on admin port

$ curl http://localhost:7777/__admin/ok
true

To get the stats (after I ran some load testing):

Getting runtime stats of the TODO microservice

$  curl http://localhost:8888/__stats/instance

Output of getting runtime stats of the TODO microservice

{
    "MetricsKV": {
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.heap.free": 219638816,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.thread.count.std": 0.5,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.os.load.level.count": 8,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.non.heap.used.median": 21867208,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.up.time.seconds.min": 60,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.non.heap.used.std": 1023408.06,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.up.time.seconds.median": 300,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.thread.peak.count": 32,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.non.heap.used.mean": 21437416,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.heap.free.std": 18688268,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.heap.max": 3817865216,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.heap.used.mean": 61136300,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.thread.count": 31,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.up.time.minutes.count": 9,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.os.load.level.std": 1.1659224,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.heap.free.min": 173839288,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.os.load.level": 4,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.up.time.minutes.std": 2.5819888,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.heap.used.min": 19162464,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.up.time.minutes": 9,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.up.time.seconds.mean": 300,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.total": 257425408,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.thread.started.count": 35,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.thread.count.median": 32,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.thread.count.count": 2,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.thread.started.count.max": 35,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.up.time.minutes.mean": 5,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.os.load.level.median": 4,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.heap.free.median": 191758000,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.up.time.seconds.std": 154.91933,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.heap.used.max": 83586120,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.up.time.seconds.count": 9,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.thread.count.max": 32,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.os.load.level.mean": 4.125,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.up.time.seconds": 540,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.heap.used": 37786592,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.heap.used.median": 65667408,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.non.heap.used": 22026992,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.heap.used.count": 10,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.up.time.minutes.median": 5,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.non.heap.used.max": 22026992,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.thread.count.mean": 31.5,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.thread.count.min": 31,
        "TodoService": 1,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.heap.free.max": 238262944,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.up.time.minutes.min": 1,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.heap.free.mean": 196289104,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.non.heap.used.count": 10,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.thread.started.count.median": 35,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.thread.started.count.min": 34,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.non.heap.used.min": 18501664,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.thread.started.count.count": 2,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.os.load.level.max": 6,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.up.time.minutes.max": 9,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.thread.daemon.count": 11,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.heap.free.count": 10,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.thread.started.count.std": 0.5,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.non.heap.max": -1,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.os.load.level.min": 2,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.up.time.seconds.max": 540,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.mem.heap.used.std": 18688268,
        "todo.title.millenniumfalcon.mammatustech.com.jvm.thread.started.count.mean": 34.5
    },
    "MetricsMS": {
        "TodoService.callTimeSample": [
            167643,
            9502
        ],
        "todo.title.millenniumfalcon.mammatustech.com.jvm.gc.collector.ps.scavengecollection.time": [
            11,
            11
        ]
    },
    "MetricsC": {
        "todo.title.millenniumfalcon.mammatustech.com.jvm.gc.collector.ps.scavengecollection.count": 2,
        "TodoService.startBatchCount": 175,
        "TodoService.receiveCount": 260
    },
    "version": 1
}

Remember that you can disable this local collection of stats (read the batteries included tutorial for more information).

Conclusion

We covered how to expose a service by mapping the service and its methods to URIs.

Next up we will show how to create a resourceful REST scheme.