AppBuilder
A framework for building Java and Scala applications with micro-services
Introduction
The Spals AppBuilder is a framework for constructing applications in Java or Scala using micro-services. Here are some important definitions which will help with understanding this concept:
- micro-service: A modular unit of functionality contained within a single Java or Scala class. In practice, micro-services are usually defined by an interface contract and implemented by a class.
- module: A group of related micro-services that are tested together and can be shared together within a build system.
- application: A container for a set of micro-services which together form a complete solution to a business problem.
- web application: An application which requires a web server. Usually, this is to support an HTTP-based API, such as REST or GraphQL.
- worker application: An application which does not require a web server.
Note that a full business solution need not be limited to a single application. In some cases, multiple applications may be created and state shared among them via a syncronizing data store or an asynchronous API (such as one implemented over a pub-sub message queue).
The Spals AppBuilder framework attempts to achieve 3 basic goals (in no particular order):
- Define and implement a set of micro-services that are common among many applications.
- Make it easy to define and implement custom micro-services.
- Make it easy to inject runtime configuration into both pre-defined and custom micro-services.
Quickstart
This quickstart imagines that we would like to create a calculator application. A natural part of such an application would be a micro-service which performs basic arithmetic functions.
So we are going to define a ArithmeticCalculator
micro-service to handle this piece of the application. We will then implement the ArithmeticCalculator
micro-service.
NOTE: These are not complete examples. Some parts of the quickstart are left as an exercise for the reader. However, the Spals AppBuilder test suite contains the following complete examples:
- A minimally viable Java application
- A sample Java application which uses pre-defined micro-services, configures their default implementations, and defines custom micro-services
- A sample Java application which uses pre-defined micro-services and configures their alternate implementations (plugins)
- A minimally viable Scala application
- A sample Scala application which uses pre-defined micro-services, configures their default implementations, and defines custom micro-services
- A sample Scala application which uses pre-defined micro-services and configures their alternate implementations (plugins)
Installation
All installation examples within this README show how to add Spals AppBuilder to a Maven build. However, this is not a pre-requisite for using the AppBuilder framework. All Spals AppBuilder artifacts are published to Maven Central and should be able to be used with any build system which integrates with it.
Whether we're using Java or Scala, we'll want to include the Spals AppBuilder BOM which defines core pieces of the framework as well as all pre-defined micro-services:
<dependencyManagement>
<dependencies>
...
<dependency>
<groupId>net.spals.appbuilder</groupId>
<artifactId>spals-appbuilder-bom</artifactId>
<version>${appbuilder.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
...
</dependencies>
</dependencyManagement>
Java
Spals AppBuilder integrates with Dropwizard to create Java web applications.
Installation
In addition to the Spals AppBuilder BOM, we'll add the plugins specifically for Dropwizard:
<dependencies>
<dependency>
<groupId>net.spals.appbuilder.plugins</groupId>
<artifactId>spals-appbuilder-app-dropwizard</artifactId>
<version>${appbuilder.version}</version>
</dependency>
</dependencies>
Define a Micro-Service
Micro-service definitions are made via Java interface contracts.
package com.example.calculator.arithmetic;
/**
* A mciro-service definition for an arthimetic calculator.
*/
public interface ArithmeticCalculator {
double add(double a, double b);
double divide(double a, double b);
double multiply(double a, double b);
double subtract(double a, double b);
}
Implement a Micro-Service
Micro-services are implemented via Java classes.
package com.example.calculator.arithmetic;
import net.spals.appbuilder.annotations.service.AutoBindSingleton;
/**
* A default implementation of the ArithmeticCalculator micro-service.
*/
@AutoBindSingleton(baseClass = ArithmeticCalculator.class)
class DefaultArithmeticCalculator implements ArithmeticCalculator {
@Override
public double add(final double a, final double b) {
return a + b;
}
@Override
public double divide(final double a, final double b) {
return a / b;
}
...
}
Use Micro-Service
Let's expose our ArithmeticCalculator
micro-service in a RESTful API endpoint.
package com.example.calculator.api;
import com.google.inject.Inject;
import net.spals.appbuilder.annotations.service.AutoBindSingleton;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@AutoBindSingleton
@Path("calculator")
@Produces(MediaType.TEXT_PLAIN)
public class CalculatorResource {
private final ArithmeticCalculator arithmeticCalculator;
@Inject
DefaultPaymentService(final ArithmeticCalculator arithmeticCalculator) {
this.arithmeticCalculator = arithmeticCalculator;
}
@GET
@Path("add/{a}/{b}")
public Response add(final double a, final double b) {
final double result = arithmeticCalculator.add(a, b);
return Response.ok(result).build();
}
@GET
@Path("divide/{a}/{b}")
public Response divide(final double a, final double b) {
final double result = arithmeticCalculator.divide(a, b);
return Response.ok(result).build();
}
...
}
Define Application
Finally, let's tie all of our micro-services together into a Dropwizard application.
package com.example.calculator.app;
public class CalculatorWebApp extends Application<Configuration> {
private static final Logger LOGGER = LoggerFactory.getLogger(CalculatorWebApp.class);
private static final String APP_CONFIG_FILE_NAME = "config/calculator-app.yml";
public static void main(final String[] args) throws Throwable {
new CalculatorWebApp().run("server", APP_CONFIG_FILE_NAME);
}
private DropwizardWebApp.Builder webAppDelegateBuilder;
private DropwizardWebApp webAppDelegate;
@Override
public void initialize(final Bootstrap<Configuration> bootstrap) {
this.webAppDelegateBuilder = new DropwizardWebApp.Builder(bootstrap, LOGGER)
.setServiceScan(new ServiceScan.Builder()
// Have the Appbuilder framework scan the com.example.calculator
// package for micro-services
.addServicePackages("com.example.calculator")
.build());
}
@Override
public void run(final Configuration configuration, final Environment env) throws Exception {
this.webAppDelegate = webAppDelegateBuilder.setEnvironment(env).build();
}
}
Scala
It is possible to translate the Dropwizard application code above into Scala, however the framework also integrates with Finatra for more native Scala support.
Installation
In addition to the Spals AppBuilder BOM, we'll add the plugins specifically for Finatra:
<dependencies>
<dependency>
<groupId>net.spals.appbuilder.plugins</groupId>
<artifactId>spals-appbuilder-app-finatra</artifactId>
<version>${appbuilder.version}</version>
</dependency>
</dependencies>
Define a Micro-Service
Micro-service definitions are made via Scala traits.
package com.example.calculator.arithmetic
/**
* A mciro-service definition for an arthimetic calculator.
*/
trait ArithmeticCalculator {
def add(a: Double, b: Double): Double
def divide(a: Double, b: Double): Double
def multiply(a: Double, b: Double): Double
def subtract(a: Double, b: Double): Double
}
Implement a Micro-Service
Micro-services are implemented via Scala classes.
package com.example.calculator.arithmetic
import net.spals.appbuilder.annotations.service.AutoBindSingleton
/**
* A default implementation of the ArithmeticCalculator micro-service.
*/
@AutoBindSingleton(baseClass = classOf[ArithmeticCalculator])
private[arithmetic] class DefaultArithmeticCalculator extends ArithmeticCalculator {
override def add(a: Double, b: Double): Double = a + b
override def divide(a: Double, b: Double): Double = a / b
...
}
Use Micro-Service
Let's expose our ArithmeticCalculator
micro-service in a RESTful API endpoint.
package com.example.calculator.api
import com.google.inject.Inject
import com.twitter.finagle.http.Request
import com.twitter.finatra.http.Controller
import net.spals.appbuilder.annotations.service.AutoBindSingleton
@AutoBindSingleton
private[finatra] class CalculatorController @Inject() (
arithmeticCalculator: ArithmeticCalculator
) extends Controller {
get("/add/:a/:b") { request: Request =>
val result = arithmeticCalculator.add(
request.params("a").toDouble, request.params("b").toDouble)
response.ok.body(result)
}
get("/divide/:a/:b") { request: Request =>
val result = arithmeticCalculator.divide(
request.params("a").toDouble, request.params("b").toDouble)
response.ok.body(result)
}
...
}
Define Application
Finally, let's tie all of our micro-services together into a Finatra application.
package com.example.calculator.app
import net.spals.appbuilder.app.finatra.FinatraWebApp
object CalculatorWebAppMain extends CalculatorWebApp
class CalculatorWebApp extends FinatraWebApp {
setServiceScan(new ServiceScan.Builder()
// Have the Appbuilder framework scan the com.example.calculator
// package for micro-services
.addServicePackages("com.example.calculator")
.build())
build()
}
Testing
AppBuilder includes a MockApp
which allows you to mix real micro-services with mocked micro-services for testing purposes. Consider that we want to write a test for our calculator application which uses the full micro-service graph except that we wish to replace the ArithmeticCalculator
service with a mocked implementation created in our test. Here's how that can be done using Mockito:
import static org.mockito.Mockito.mock;
public class CalculatorAppTest {
@Test
public void testArithmeticCalculator() {
// Build the full micro-service graph for the calculator application,
// but substitute in a mocked ArithmeticCalculator service
final MockApp app = new MockApp.Builder(MockAppTest.class)
.addMockSingleton(mock(ArithmeticCalculator.class), ArithmeticCalculator.class)
.setServiceScan(new ServiceScan.Builder()
.addServicePackages("com.example.calculator")
.build())
.build();
final Injector serviceInjector = app.getServiceInjector();
final ArithmeticCalculator mockedCalculator = serviceInjector.getInstance(ArithmeticCalculator.class);
...
}
AppBuilder also includes an inteface called MockSingleton
which emulates the @AutoBindSingleton
annotation. This allows testers to hand craft mock services:
import net.spals.appbuilder.app.mock.MockSingleton;
public class MockArithmeticCalculator implements ArithmeticCalculator, MockSingleton<ArithmeticCalculator> {
@Override
public Class<ArithmeticCalculator> baseClass() {
return ArithmeticCalculator.class;
}
...
}
These hand crafted mock services can also be included in a MockApp
instance:
final MockApp app = new MockApp.Builder(MockAppTest.class)
.addMockSingleton(new MockArithmeticCalculator())
.setServiceScan(new ServiceScan.Builder()
.addServicePackages("com.example.calculator")
.build())
.build();
Notes
- It is important to keep package names organized because the
ServiceScan
works by scanning for package prefixes. It is recommended that all package names at least start with [com|org|net].[organizationName].[applicationName] - Support for custom micro-services is complete, however certain predefined services are still in Beta. In particular, the asynchronous message producer and consumer services have not been fully tested.
- This README does not discuss all aspects of the Appbuilder framework. There is a TODO for a wiki which will go into greater detail about individual pieces of the framework