Key Takeaways
- Microservices can decouple your code
- Microservices enable different teams to work on smaller pieces, using independent technologies, producing safer, more frequent deployments
- SpringBoot supports various implementations for producing REST APIs
- Service discovery and service invocation are independent of the service platform
- Swagger produces a robust API documentation and invocation interface
If you are not already using Microservices, you are safely out of the early adopter phase of the adoption curve, and it is probably time to get started. In this article, we will demonstrate the essential components for creating RESTful microservices, using Consul service registry, Spring Boot for all the scaffolding, dependency injection, and dependencies, Maven for the build, and both Spring REST and Jersey/JaxRS Java RESTful API.
Over the last two decades, enterprise has become very agile in our SDLC process, but our applications tend to still be rather monolith, with huge jars supporting all of the varied APIs and versions in play. But nowadays there is a push towards more Lean, DevOps-ey processes, and functionality is becoming “serverless”. Refactoring to Microservices can decouple code and resources, make builds smaller, releases safer, and APIs more stable.
In this article, we will build a simple stock market portfolio management application that clients can call to price their stock portfolio (stock tickers and quantities). The portfolio microservice will retrieve the clients portfolio, send it to a pricing microservice to apply the latest pricing, and then return the fully priced and subtotaled portfolio, exposing all that via a rest call.
Before we get to work on creating our microservices, let’s prepare our environment by setting up Consul.
Download Consul Service Registry
We will be using Hashicorp Consul for service discovery, so head over to https://www.consul.io/downloads.html and download Consul, for Windows, Linux, Mac, etc. That will provide an executable that you will want to add to your path.
Start Consul
From a shell prompt, launch Consul in dev mode:
consul agent -dev
To verify that it is running, head over to a browser and access the consul UI http://localhost:8500. If all is well, consul should report that it is alive and well. Clicking on the consul service (on the left) provides more information (on the right).
If there are any issues at this point, be sure you have added consul to your execution path, and that ports 8500 and 8600 are available.
Create the SpringBoot application
We will use Spring Initializr, which is integrated in the major IDEs, to create the scaffolding for our SpringBoot applications. The screenshots below use IntelliJ IDEA.
Choose File/New Project to bring up the new project template, and select Spring Initializr.
In fact, you can set up the scaffolding without an IDE by completing the online web-form via the SpringBoot Initializr website https://start.spring.io, which will produce a zip of your empty project ready for download.
Click next and complete the project metadata. Use the following configuration:
Click next to select dependencies, and enter Jersey and Consul Discovery in the Dependencies search. Add those dependencies:
Click next to specify your project name and location. Keep the default name “portfolio” and specify your prefered project location, then click “finish” to generate and open the project:
(Click on the image to enlarge it)
We can use the generated application.properties
, but SpringBoot also recognizes YAML format, and that’s a little easier to visualize, so let’s rename it application.yml
.
We will name the microservice “portfolio-service”. We can specify a port or use port 0 to have the application use an available port. In our case we will use 57116. If you deploy this service as a Docker container you will be able to map that to any port of your choosing. Let’s name the application and specify our port by adding the following to our application.yml:
spring:
application:
name: portfolio-service
server:
port: 57116
To make our service discoverable let’s add an annotation to our SpringBoot application class. Open PortfolioApplication and add @EnableDiscoveryClient
above the class declaration.
Accept the imports. The class should look like this:
package com.restms.demo.portfolio;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
. . .
@SpringBootApplication
@EnableDiscoveryClient
public class PortfolioApplication {
public static void main(String[] args) {
SpringApplication.run(PortfolioApplication.class, args);
}
}
(To demonstrate how microservices can be composed of independent platforms, we will use Jersey for this service and Spring REST for the next one).
To set up the Jersey RESTful Web Service, we need to specify a ResourceConfig
Configuration class. Add the class JerseyConfig (for the demo we’ll keep it in the same package as our application class.) It should look like this, plus proper package and imports:
@Configuration
@ApplicationPath("portfolios")
public class JerseyConfig extends ResourceConfig {
public JerseyConfig()
{
register(PortfolioImpl.class);
}
}
Note that it extends ResourceConfig to designate it as a Jersey configuration class. The @ApplicationPath("portfolios")
attribute specifies the call context, meaning that calls should begin with the path element “portfolios”. (If you omit it, the context defaults to “/”.)
The PortfolioImpl class will serve two requests - portfolios/customer/{customer-id}
returns all portfolios and portfolios/customer/{customer-id}/portfolio/{portfolio-id}
returns one portfolio. A portfolio consists of a set of tickers and the number of shares owned of that ticker.
(For the sake of the demo, there are three customers, with ids 0, 1, and 2, and each has three portfolios, with ids 0, 1, and 2).
Your IDE will ask you to create PortfolioImpl; do so now. For the demo, let’s add it to the same package. Enter the code below and accept all of the imports:
@Component
@Path("/")
public class PortfolioImpl implements InitializingBean {
private Object[][][][] clientPortfolios;
@GET
@Path("customer/{customer-id}")
@Produces(MediaType.APPLICATION_JSON)
// a portfolio consists of an array of arrays, each containing an array of
// stock ticker and associated shares
public Object[][][] getPortfolios(@PathParam("customer-id") int customerId)
{
return clientPortfolios[customerId];
}
@GET
@Path("customer/{customer-id}/portfolio/{portfolio-id}")
@Produces(MediaType.APPLICATION_JSON)
public Object[][] getPortfolio(@PathParam("customer-id") int customerId,
@PathParam("portfolio-id") int portfolioId) {
return getPortfolios(customerId)[portfolioId];
}
@Override
public void afterPropertiesSet() throws Exception {
Object[][][][] clientPortfolios =
{
{
// 3 customers, 3 portfolios each
{new Object[]{"JPM", 10201}, new Object[]{"GE", 20400}, new Object[]{"UTX", 38892}},
{new Object[]{"KO", 12449}, new Object[]{"JPM", 23454}, new Object[]{"MRK", 45344}},
{new Object[]{"WMT", 39583}, new Object[]{"DIS", 95867}, new Object[]{"TRV", 384756}},
}, {
{new Object[]{"GE", 38475}, new Object[]{"MCD", 12395}, new Object[]{"IBM", 91234}},
{new Object[]{"VZ", 22342}, new Object[]{"AXP", 385432}, new Object[]{"UTX", 23432}},
{new Object[]{"IBM", 18343}, new Object[]{"DIS", 45673}, new Object[]{"AAPL", 23456}},
}, {
{new Object[]{"AXP", 34543}, new Object[]{"TRV", 55322}, new Object[]{"NKE", 45642}},
{new Object[]{"CVX", 44332}, new Object[]{"JPM", 12453}, new Object[]{"JNJ", 45433}},
{new Object[]{"MRK", 32346}, new Object[]{"UTX", 46532}, new Object[]{"TRV", 45663}},
}
};
this.clientPortfolios = clientPortfolios;
}
}
The @Component
annotation designates this as a Spring component class, and exposes it as an endpoint. The @Path
annotation about the class declartion declares that the class is accessed via the “/” path element, and the two supported api calls are accessed via portfolios/customer/{customer-id} and portfolios/customer/{customer-id}/portfolio/{portfolio-id}
, as we see from the method annotations. Note that path("/") is the default but we leave it for reference. The methods are designated as HTTP GET via the @GET
annotation, and the method is declared to return array and is annotated to return Json, so it will return a Json array. Notice how the @PathParam
annotations are used in the method signature to extract the mapped parameters from the request mapping.
(For our demo we are returning hard coded values. Of course in vivo, the implementation would query a database or some other service or datasource here.)
Now build the project, and run it. If you’re using IntelliJ, it will create a default runnable, so just click the green “run” arrow. You can also use
mvn spring-boot:run
Or you can do a maven install, and run the application using java -jar, pointing to the generated jar in the target directory:
java -jar target\portfolio-0.0.1-SNAPSHOT.jar
We should now see this service in Consul, so let’s head back over to our browser, load up http://localhost:8500/ui/#/dc1/services (or refresh if you’re already there).
Hmm, we see our customer-service there, but it is showing as failing. That’s because Consul is expecting a “health” heartbeat from our service.
In order to generate the heartbeat, we can add a dependency ont the SpringBoot "Actuator" service to our application pom.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
While we are in the pom, please note that there is a version conflict with the Jersey version between the consul-starter and the jersey-starter. To alleviate this, move the jersey starter to be the first dependency.
Your pom should now contain the following dependencies:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Restarting Consul and then the Portfolio service displays a happy:
There are now two passing nodes under portfolio-service - one is our portfolio service implementation and the other is the heartbeat.
Let’s check the port that was assigned. You can see that in the application output:
INFO 19792 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 57116 (http)
You can also see the port directly in the consul UI. Click on customer-service, then choose the link “Service ‘customer-service’ check link, which displays the service port, in this case 57116.
Call http://localhost:57116/portfolios/customer/1/portfolio/2 and you should see the json array [["IBM",18343],["DIS",45673],["AAPL",23456]]
Our first microservice is open for business!
Pricing Service
Next we create our pricing service, this time using a Spring RestController instead of Jersey.
The pricing service will accept as parameters client id and portfolio id, and will use a RestTemplate to query the portfolio service to get the tickers and shares, and return the current prices. (I don’t have to tell you these values are fake news, so don’t use them to make trading decisions!)
Create a new project using the information below:
This time choose dependencies Web, Consul Discovery, and Actuator:
(Click on the image to enlarge it)
Leave the default project name “pricing”, and generate the project in the directory of your choosing.
This time we will use application.properties instead of application.yml.
Set the name and port in application.properties as:
spring.application.name=pricing
server.port=57216
Annotate PricingApplication with @EnableDiscoveryClient. The class should look like this, plus package and imports.
@SpringBootApplication
@EnableDiscoveryClient
public class PricingApplication {
public static void main(String[] args) {
SpringApplication.run(PricingApplication.class, args);
}
}
Next we will create the PricingEndpoint class. It is a little verbose because it is demonstrating several important features, including service discovery (looking up the portfolio service) and using the RestTemplate to make a query:
@RestController
@RequestMapping("/pricing")
public class PricingEndpoint implements InitializingBean {
@Autowired
DiscoveryClient client;
Map<String, Double> pricingMap = new HashMap<>();
RestTemplate restTemplate = new RestTemplate();
@GetMapping("/customer/{customer-id}/portfolio/{portfolio-id}")
public List<String> getPricedPortfolio(
@PathVariable("customer-id") Integer customerId,
@PathVariable("portfolio-id") Integer portfolioId)
{
List<ServiceInstance> instances
= client.getInstances("portfolio-service");
ServiceInstance instance
= instances.stream()
.findFirst()
.orElseThrow(() -> new RuntimeException("not found"));
String url = String.format("%s/portfolios/customer/%d/portfolio/%d",
instance.getUri(), customerId, portfolioId);
// query for the portfolios, returned as an array of List
// of size 2, containing a ticker and a position (# of shares)
Object[] portfolio = restTemplate.getForObject(url, Object[].class);
// Look up the share prices, and return a list of Strings, formatted as
// ticker, shares, price, total
List<String> collect = Arrays.stream(portfolio).map(position -> {
String ticker = ((List<String>) position).get(0);
int shares = ((List<Integer>) position).get(1);
double price = getPrice(ticker);
double total = shares * price;
return String.format("%s %d %f %f", ticker, shares, price, total);
}).collect(Collectors.toList());
return collect;
}
private double getPrice(String ticker)
{
return pricingMap.get(ticker);
}
@Override
public void afterPropertiesSet() throws Exception {
pricingMap.put("MMM",201.81);
pricingMap.put("AXP",85.11);
pricingMap.put("AAPL",161.04);
pricingMap.put("BA",236.32);
pricingMap.put("CAT",118.02);
pricingMap.put("CVX",111.31);
pricingMap.put("CSCO",31.7);
pricingMap.put("KO",46.00);
pricingMap.put("DIS",101.92);
pricingMap.put("XOM",78.7);
pricingMap.put("GE",24.9);
pricingMap.put("GS",217.62);
pricingMap.put("HD",155.82);
pricingMap.put("IBM",144.29);
pricingMap.put("INTC",35.66);
pricingMap.put("JNJ",130.8);
pricingMap.put("JPM",89.75);
pricingMap.put("MCD",159.81);
pricingMap.put("MRK",63.89);
pricingMap.put("MSFT",73.65);
pricingMap.put("NKE",52.78);
pricingMap.put("PFE",33.92);
pricingMap.put("PG",92.79);
pricingMap.put("TRV",117.00);
pricingMap.put("UTX",110.12);
pricingMap.put("UNH",198.00);
pricingMap.put("VZ",47.05);
pricingMap.put("V",103.34);
pricingMap.put("WMT", 80.05);
}
}
In order to discover the portfolio service, we need to have access to a DiscoveryClient. This is easily obtained via Spring’s @Autowired annotation.
@Autowired
DiscoveryClient client;
This DiscoveryClient instance is then used to locate out service, in the call:
List<ServiceInstance> instances = client.getInstances("portfolio-service");
ServiceInstance instance = instances.stream().findFirst().orElseThrow(() -> new RuntimeException("not found"));
Once the service is located we can use it to execute our request, which we compose according to the api call we created in our portfolio-service.
String url = String.format("%s/portfolios/customer/%d/portfolio/%d", instance.getUri(), customerId, portfolioId)
;
Finally, we use a RestTemplate to execute our GET request.
Object[] portfolio = restTemplate.getForObject(url, Object[].class);
Note that for RestControllers (as with SpringMVC RequestController), the path variables are extracted via the @PathVariable annotation, unlike Jersey that uses @PathParam, as we saw.
This completes out pricing service using a Spring RestController.
Documentation
We’ve gone to all this trouble to create our microservices, but they won’t do much good without letting the world know how to use them.
For this we use a handy tool called Swagger, a simple to use tool that not only documents our API calls, but also provides a handy web client for invoking them.
First, let’s specify Swagger in our pom:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
Next, we need to tell Swagger which of our classes we want to document. Let’s introduce a new class called SwaggerConfig containing the Swagger specification.
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.regex("/pricing.*"))
.build();
}
}
Let’s see what this class does. First we designated this as a Swagger configuration with the @EnableSwagger2
annotation.
Next we created a Docket bean that tells Swagger which APIs to expose. In the example above we told Swagger to expose any path that begins with “/pricing”. An alternative would have been to specify the classes to document rather than the paths:
.apis(RequestHandlerSelectors.basePackage("com.restms.demo"))
.paths(PathSelectors.any())
Restart the pricing microservice and from a browser call http://localhost:57216/swagger-ui.html
Click “List Operations” to see the service operations in detail.
Click Expand Operations to create a form based query invoker. Supply some parameters, click “Try it out!” and wait for a response:
(Click on the image to enlarge it)
You can add a lot more color by adding Swagger annotations to your methods.
For example, decorate the existing method PricingImpl.getPricedPortfolio using the @ApiOperation annotation, as follows:
@ApiOperation(value = "Retrieves a fully priced portfolio",
notes = "Retrieves fully priced portfolio given customer id and portfolio id")
@GetMapping("/customer/{customer-id}/portfolio/{portfolio-id}")
public List<String> getPricedPortfolio(@PathVariable("customer-id") Integer customerId, @PathVariable("portfolio-id") Integer portfolioId)
Restart and refresh the swagger-ui to see the new clarified documentation:
There is a lot more that you can do with Swagger, so check out the documentation there.
About the Author
Victor Grazi works at Nomura Securities on core platform tools, and as a technical consultant and Java evangelist. He is a frequent presenter at technical conferences, and hosts the "Java Concurrent Animated" and "Bytecode Explorer" open source projects. An Oracle Java Champion, Victor serves as an editor on the Java queue at InfoQ.