- Purpose: Showing developers and architects how Red Hat jumpstarts modernization
- Difficulty:
intermediate
- Time:
60-70 minutes
In the previous scenarios, you learned how to take an existing monolithic app and refactor a single inventory service using WildFly Swarm. Since WildFly Swarm is using Java EE much of the technology from the monolith can be reused directly, like JPA and JAX-RS. The previous scenario resulted in you creating an inventory service, but so far we haven't started strangling the monolith. That is because the inventory service is never called directly by the UI. It's a backend service that is only used only by other backend services. In this scenario, you will create the catalog service and the catalog service will call the inventory service. When you are ready, you will change the route to tie the UI calls to new service.
To implement this, we are going to use the Spring Framework. The reason for using Spring for this service is to introduce you to Spring Development, and how Red Hat Runtimes helps to make Spring development on Kubernetes easy. In real life, the reason for choosing Spring vs. WF Swarm mostly depends on personal preferences, like existing knowledge, etc. At the core Spring and Java EE are very similar.
The goal is to produce something like:
Spring is one of the most popular Java Frameworks and offers an alternative to the Java EE programming model. Spring is also very popular for building applications based on microservices architectures. Spring Boot is a popular tool in the Spring ecosystem that helps with organizing and using 3rd-party libraries together with Spring and also provides a mechanism for boot strapping embeddable runtimes, like Apache Tomcat. Bootable applications (sometimes also called fat jars) fits the container model very well since in a container platform like OpenShift responsibilities like starting, stopping and monitoring applications are then handled by the container platform instead of an Application Server.
Another thing you will learn in this scenario is one of the techniques to aggregate services using service-to-service calls. Other possible solutions would be to use a microservices gateway or combine services using client-side logic.
To start in the right directory, from the CodeReady Workspaces Terminal, run the following command:
cd /projects/modernize-apps/catalog
For your convenience, this scenario has been created with a base project using the Java programming language and the Apache Maven build tool.
Initially, the project is almost empty and doesn't do anything. Start by reviewing the content in the file explorer.
The output should look something like this
As you can see, there are some files that we have prepared for you in the project. Under src/main/resources/static/index.html
we have for example prepared a simple html-based UI file for you. Except for the fabric8/
folder and index.html
, this matches very well what you would get if you generated an empty project from the Spring Initializr web page. For the moment you can ignore the content of the fabric8/
folder (we will discuss this later).
One that differs slightly is the pom.xml
. Please open the and examine it a bit closer (but do not change anything at this time)
As you review the content, you will notice that there are a lot of TODO comments. Do not remove them! These comments are used as a marker and without them, you will not be able to finish this scenario.
Notice that we are not using the default BOM (Bill of material) that Spring Boot projects typically use. Instead, we are using a BOM provided by Red Hat.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>me.snowdrop</groupId>
<artifactId>spring-boot-bom</artifactId>
<version>${spring-boot.bom.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
We use this bill of material to make sure that we are using the version of for example Apache Tomcat that Red Hat supports.
Adding web (Apache Tomcat) to the application
Since our applications (like most) will be a web application, we need to use a servlet container like Apache Tomcat or Undertow. Since Red Hat offers support for Apache Tomcat (e.g., security patches, bug fixes, etc.), we will use it.
NOTE: Undertow is another an open source project that is maintained by Red Hat and therefore Red Hat plans to add support for Undertow shortly.
To add Apache Tomcat to our project all we have to do is to add the following lines in modernize-apps/catalog/pom.xml
. Open the file to automatically add these lines at the <!-- TODO: Add web (tomcat) dependency here -->
marker:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
We will also make use of Spring's JDBC implementation, so we need to add the following to pom.xml
at the <!-- TODO: Add jdbc dependency here -->
marker:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
We will go ahead and add a bunch of other dependencies while we have the pom.xml open. These will be explained later. Add these at the
<!-- TODO: Add actuator, feign and hystrix dependency here -->
marker:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
Test the application locally
As we develop the application, we might want to test and verify our change at different stages. We can do that locally, by using the spring-boot
maven plugin.
Run the application by executing the below command:
mvn spring-boot:run
Wait for it to complete startup and report Started RestApplication in ***** seconds (JVM running for ******)
3. Verify the application
In the CodeReady workspace open a new terminal and run the below command:
curl http://localhost:8081
You should now see some HTML.
NOTE: The service calls to get products from the catalog doesn't work yet. Be patient! We will work on it in the next steps.
4. Stop the application
Before moving on, press CTRL-Z
on your terminal window to stop and send the running application to the background, then at the command prompt enter kill %1
to kill the application.
You have now successfully executed the first step in this scenario.
Now you've seen how to get started with Spring Boot development on Red Hat Runtimes.
In next step of this scenario, we will add the logic to be able to read a list of fruits from the database.
Before we create the database repository class to access the data it's good practice to create test cases for the different methods that we will use.
Create the file modernize-apps/catalog/src/test/java/com/redhat/coolstore/service/ProductRepositoryTest.java
and then copy the below code into the file:
package com.redhat.coolstore.service;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import com.redhat.coolstore.model.Product;
@RunWith(SpringRunner.class)
@SpringBootTest()
public class ProductRepositoryTest {
//TODO: Insert Catalog Component here
//TODO: Insert test_readOne here
//TODO: Insert test_readAll here
}
Next, inject a handle to the future repository class which will provide access to the underlying data repository. It is injected with Spring's @Autowired
annotation which locates, instantiates, and injects runtime instances of classes automatically, and manages their lifecycle (much like Java EE and it's CDI feature). Copy and paste the following code right below the comment //TODO: Insert Catalog Component here
:
@Autowired
private ProductRepository repository;
The ProductRepository
should provide a method called findById(String id)
that returns a product and collect that from the database. We test this by querying for a product with id "444434" which should have name "Pebble Smart Watch". The pre-loaded data comes from the modernize-apps/catalog/src/main/resources/schema.sql
file. Copy and paste the following code right below the comment //TODO: Insert test_readOne here
:
@Test
public void test_readOne() {
Product product = repository.findById("444434");
assertThat(product).isNotNull();
assertThat(product.getName()).as("Verify product name").isEqualTo("Pebble Smart Watch");
assertThat(product.getQuantity()).as("Quantity should be ZERO").isEqualTo(0);
}
The ProductRepository
should also provide a methods called readAll()
that returns a list of all products in the catalog. We test this by making sure that the list contains a "Red Fedora", "Forge Laptop Sticker" and "Oculus Rift".Again, copy and paste the following code below the comment //TODO: Insert test_readAll here
:
@Test
public void test_readAll() {
List<Product> productList = repository.readAll();
assertThat(productList).isNotNull();
assertThat(productList).isNotEmpty();
List<String> names = productList.stream().map(Product::getName).collect(Collectors.toList());
assertThat(names).contains("Red Fedora","Forge Laptop Sticker","Oculus Rift");
}
We are now ready to implement the database repository.
Create the file modernize-apps/catalog/src/main/java/com/redhat/coolstore/service/ProductRepository.java
.
Here is the base for the calls, insert the following code:
package com.redhat.coolstore.service;
import java.util.List;
import com.redhat.coolstore.model.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
@Repository
public class ProductRepository {
//TODO: Autowire the jdbcTemplate here
//TODO: Add row mapper here
//TODO: Create a method for returning all products
//TODO: Create a method for returning one product
}
NOTE: That the class is annotated with
@Repository
. This is a feature of Spring that makes it possible to avoid a lot of boiler plate code and only write the implementation details for this data repository. It also makes it very easy to switch to another data storage, like a NoSQL database.
Spring Data provides a convenient way for us to access data without having to write a lot of boiler plate code. One way to do that is to use a JdbcTemplate
. First we need to autowire that as a member to ProductRepository
. Copy and paste the following code under the comment //TODO: Autowire the jdbcTemplate here
:
@Autowired
private JdbcTemplate jdbcTemplate;
The JdbcTemplate
require that we provide a RowMapper
so that it can map between rows in the query to Java Objects. We are going to define the RowMapper
like this:
The JdbcTemplate
require that we provide a RowMapper
so that it can map between rows in the query to Java Objects. We are going to define the RowMapper
like this (copy and paste the following code under the comment //TODO: Add row mapper here
):
private RowMapper<Product> rowMapper = (rs, rowNum) -> new Product(
rs.getString("itemId"),
rs.getString("name"),
rs.getString("description"),
rs.getDouble("price"));
Now we are ready to create the methods that are used in the test. Let's start with the readAll()
. It should return a List<Product>
and then we can write the query as SELECT * FROM catalog
and use the rowMapper to map that into Product
objects. Our method should look like this (copy and paste the following code under the comment //TODO: Create a method for returning all products
):
public List<Product> readAll() {
return jdbcTemplate.query("SELECT * FROM catalog", rowMapper);
}
The ProductRepositoryTest
also used another method called findById(String id)
that should return a Product. The implementation of that method using the JdbcTemplate
and RowMapper
looks like this (copy and paste the following code under the comment //TODO: Create a method for returning one product
):
public Product findById(String id) {
return jdbcTemplate.queryForObject("SELECT * FROM catalog WHERE itemId = '" + id + "'", rowMapper);
}
The ProductRepository
should now have all the components, but we still need to tell spring how to connect to the database. For local development we will use the H2 in-memory database. When deploying this to OpenShift we are instead going to use the PostgreSQL database, which matches what we are using in production.
The Spring Framework has a lot of sane defaults that can always seem magical sometimes, but basically all we have todo to setup the database driver is to provide some configuration values. Open modernize-apps/catalog/src/main/resources/application-default.properties
and add the following properties where the comment says "#TODO: Add database properties"
Add the following:
spring.datasource.url=jdbc:h2:mem:catalog;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.username=sa
spring.datasource.password=sa
spring.datasource.driver-class-name=org.h2.Driver
The Spring Data framework will automatically see if there is a schema.sql in the class path and run that when initializing.
Now we are ready to run the test to verify that everything works. Because we created the ProductRepositoryTest.java
all we have todo is to run:
mvn verify
The test should be successful and you should see BUILD SUCCESS, which means that we can read that our repository class works as as expected.
You have now successfully executed the second step in this scenario.
Now you've seen how to use Spring Data to collect data from the database and how to use a local H2 database for development and testing.
In next step of this scenario, we will add the logic to expose the database content from REST endpoints using JSON format.
Now you are going to create a service class. Later on the service class will be the one that controls the interaction with the inventory service, but for now it's basically just a wrapper of the repository class.
Create a new class CatalogService
with the following path modernize-apps/catalog/src/main/java/com/redhat/coolstore/service/CatalogService.java
And then Open the file to implement the new service:
package com.redhat.coolstore.service;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
//import com.redhat.coolstore.client.InventoryClient;
import com.redhat.coolstore.model.Product;
@Service
public class CatalogService {
@Autowired
private ProductRepository repository;
//TODO: Autowire Inventory Client
public Product read(String id) {
Product product = repository.findById(id);
//TODO: Update the quantity for the product by calling the Inventory service
return product;
}
public List<Product> readAll() {
List<Product> productList = repository.readAll();
//TODO: Update the quantity for the products by calling the Inventory service
return productList;
}
//TODO: Add Callback Factory Component
}
As you can see there is a number of TODO in the code, and later we will use these placeholders to add logic for calling the Inventory Client to get the quantity. However for the moment we will ignore these placeholders.
Now we are ready to create the endpoints that will expose REST service. Let's again first start by creating a test case for our endpoint. We need to endpoints, one that exposes for GET calls to /services/products
that will return all product in the catalog as JSON array, and the second one exposes GET calls to /services/product/{prodId}
which will return a single Product as a JSON Object. Let's again start by creating a test case.
Create the test case by opening: modernize-apps/catalog/src/test/java/com/redhat/coolstore/service/CatalogEndpointTest.java
Add the following code to the test case and make sure to review it so that you understand how it works.
package com.redhat.coolstore.service;
import com.redhat.coolstore.model.Inventory;
import com.redhat.coolstore.model.Product;
import io.specto.hoverfly.junit.rule.HoverflyRule;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static io.specto.hoverfly.junit.dsl.HttpBodyConverter.json;
import static io.specto.hoverfly.junit.dsl.ResponseCreators.success;
import static io.specto.hoverfly.junit.dsl.ResponseCreators.serverError;
import static io.specto.hoverfly.junit.dsl.matchers.HoverflyMatchers.startsWith;
import static org.assertj.core.api.Assertions.assertThat;
import static io.specto.hoverfly.junit.core.SimulationSource.dsl;
import static io.specto.hoverfly.junit.dsl.HoverflyDsl.service;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CatalogEndpointTest {
@Autowired
private TestRestTemplate restTemplate;
//TODO: Add ClassRule for HoverFly Inventory simulation
@Test
public void test_retriving_one_proudct() {
ResponseEntity<Product> response
= restTemplate.getForEntity("/services/product/329199", Product.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody())
.returns("329199",Product::getItemId)
.returns("Forge Laptop Sticker",Product::getName)
//TODO: Add check for Quantity
.returns(8.50,Product::getPrice);
}
@Test
public void check_that_endpoint_returns_a_correct_list() {
ResponseEntity<List<Product>> rateResponse =
restTemplate.exchange("/services/products",
HttpMethod.GET, null, new ParameterizedTypeReference<List<Product>>() {
});
List<Product> productList = rateResponse.getBody();
assertThat(productList).isNotNull();
assertThat(productList).isNotEmpty();
List<String> names = productList.stream().map(Product::getName).collect(Collectors.toList());
assertThat(names).contains("Red Fedora","Forge Laptop Sticker","Oculus Rift");
Product fedora = productList.stream().filter( p -> p.getItemId().equals("329299")).findAny().get();
assertThat(fedora)
.returns("329299",Product::getItemId)
.returns("Red Fedora", Product::getName)
//TODO: Add check for Quantity
.returns(34.99,Product::getPrice);
}
}
Now we are ready to implement the CatalogEndpoint
.
Start by creating the file by opening: modernize-apps/catalog/src/main/java/com/redhat/coolstore/service/CatalogEndpoint.java
Then add the following content:
package com.redhat.coolstore.service;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.redhat.coolstore.model.Product;
@RestController
@RequestMapping("/services")
public class CatalogEndpoint {
private final CatalogService catalogService;
public CatalogEndpoint(CatalogService catalogService) {
this.catalogService = catalogService;
}
@GetMapping("/products")
public List<Product> readAll() {
return this.catalogService.readAll();
}
@GetMapping("/product/{id}")
public Product read(@PathVariable("id") String id) {
return this.catalogService.read(id);
}
}
The Spring MVC Framework uses Jackson by default to serialize or map Java objects to JSON and vice versa. Because Jackson extends upon JAXB and can automatically parse simple Java structures and parse them into JSON and vice verse and since our Product.java
is very simple and only contains basic attributes we do not need to tell Jackson how to parse between object and JSON.
Now you can run the CatalogEndpointTest
and verify that it works.
mvn verify -Dtest=CatalogEndpointTest
Since we now have endpoints that returns the catalog we can also start the service and load the default page again, which should now return the products.
Start the application by running the following command:
mvn spring-boot:run
Wait for the application to start. Then we can verify the endpoint by running the following command in a new terminal (Note the link below will execute in a second terminal)
curl http://localhost:8081/services/products ; echo
You should get a full JSON array consisting of all the products:
[{"itemId":"329299","name":"Red Fedora","desc":"Official Red Hat Fedora","price":34.99,"quantity":0},{"itemId":"329199","name":"Forge Laptop Sticker"},
...
]
You have now successfully executed the third step in this scenario.
Now you've seen how to create REST application in Spring MVC and create a simple application that returns product.
In the next scenario we will also call another service to enrich the endpoint response with inventory status.
Be sure to stop the service by clicking on the first Terminal window and typing CTRL-z
to stop and send the running application to the background. Then at the command line, enter kill %1
to stop the application.
Next, we'll add a call to the existing Inventory service to enrich the above data with Inventory information. On to the next challenge!
So far our application has been kind of straight forward, but our monolith code for the catalog is also returning the inventory status. In the monolith since both the inventory data and catalog data is in the same database we used a OneToOne mapping in JPA like this:
@OneToOne(cascade = CascadeType.ALL,fetch=FetchType.EAGER)
@PrimaryKeyJoinColumn
private InventoryEntity inventory;
When redesigning our application to Microservices using domain driven design we have identified that Inventory and ProductCatalog are two separate domains. However our current UI expects to retrieve data from both the Catalog Service and Inventory service in a singe request.
Service interaction
Our problem is that the user interface requires data from two services when calling the REST service on /services/products
. There are multiple ways to solve this like:
- Client Side integration - We could extend our UI to first call
/services/products
and then for each product item call/services/inventory/{prodId}
to get the inventory status and then combine the result in the web browser. This would be the least intrusive method, but it also means that if we have 100 of products, the client will make 101 request to the server. If we have a slow internet connection this may cause issues. - Microservices Gateway - Creating a gateway in-front of the
Catalog Service
that first calls the Catalog Service and then based on the response calls the inventory is another option. This way we can avoid lots of calls from the client to the server. Apache Camel provides nice capabilities to do this and if you are interested to learn more about this, please checkout the Coolstore Microservices example here. - Service-to-Service - Depending on use-case and preferences another solution would be to do service-to-service calls instead. In our case means that the Catalog Service would call the Inventory service using REST to retrieve the inventory status and include that in the response.
There are no right or wrong answers here, but since this is a workshop on application modernization using Red Hat Runtimes, we will not choose option 1 or 2 here. Instead we are going to use option 3 and extend our Catalog to call the Inventory service.
In the Test-Driven Development style, let's first extend our test to test the Inventory functionality (which doesn't exist).
Open modernize-apps/catalog/src/test/java/com/redhat/coolstore/service/CatalogEndpointTest.java
again.
Now at the markers //TODO: Add check for Quantity
add the following line:
.returns(9999,Product::getQuantity)
And add it to the second test as well at the remaining //TODO: Add check for Quantity
marker:
.returns(9999,Product::getQuantity)
Now if we run the test it should fail!. Run the bfollowing command to test :
mvn verify
It should fail:
Tests run: 4, Failures: 2, Errors: 0, Skipped: 0
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
Again the test fails because we are trying to call the Inventory service which is not running. We will soon implement the code to call the inventory service, but first we need a way to test this service without having the inventory service to be up an running. For that we are going to use an API Simulator called HoverFly and particular it's capability to simulate remote APIs. HoverFly is very convenient to use with Unit test and all we have to do is to add a ClassRule
that will simulate all calls to inventory. Open the file to insert the code at the //TODO: Add ClassRule for HoverFly Inventory simulation
marker:
@ClassRule
public static HoverflyRule hoverflyRule = HoverflyRule.inSimulationMode(dsl(
service("inventory:8080")
// .andDelay(2500, TimeUnit.MILLISECONDS).forMethod("GET")
.get(startsWith("/services/inventory"))
// .willReturn(serverError())
.willReturn(success(json(new Inventory("9999",9999))))
));
This ClassRule
means that if our tests are trying to call our inventory url Howeverfly will intercept this and respond with our hard coded response instead.
Implementing the Inventory Client
Since we now have a nice way to test our service-to-service interaction we can now create the client that calls the Inventory. Netflix has provided some nice extensions to the Spring Framework that are mostly captured in the Spring Cloud project, however Spring Cloud is mainly focused on Pivotal Cloud Foundry and because of that Red Hat and others have contributed Spring Cloud Kubernetes to the Spring Cloud project, which enables the same functionallity for Kubernetes based platforms like OpenShift.
The inventory client will use a Netflix project called Feign, which provides a nice way to avoid having to write boilerplate code. Feign also integrate with Hystrix which gives us capability for circuit breaking. We will discuss this more later, but let's start with the implementation of the Inventory Client. Using Feign all we have to do is to create a interface that details which parameters and return type we expect, annotate it with @RequestMapping
and provide some details and then annotate the interface with @Feign
and provide it with a name.
Create the file : modernize-apps/catalog/src/main/java/com/redhat/coolstore/client/InventoryClient.java
Add the following small code to the file:
package com.redhat.coolstore.client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import com.redhat.coolstore.model.Inventory;
import feign.hystrix.FallbackFactory;
@FeignClient(name="inventory")
public interface InventoryClient {
@GetMapping(path = "/services/inventory/{itemId}", consumes = {MediaType.APPLICATION_JSON_VALUE})
Inventory getInventoryStatus(@PathVariable("itemId") String itemId);
//TODO: Add Fallback factory here
}
There is one more thing that we need to do which is to tell Feign where the inventory service is running. Before that notice that we are setting the @FeignClient(name="inventory")
.
Open modernize-apps/catalog/src/main/resources/application-default.properties
and add these properties below the #TODO: Configure netflix libraries
marker:
inventory.ribbon.listOfServers=inventory:8080
feign.hystrix.enabled=true
By setting inventory.ribbon.listOfServers
we are hard coding the actual URL of the service to inventory:8080
. If we had multiple servers we could also add those using a comma. However using Kubernetes there is no need to have multiple endpoints listed here since Kubernetes has a concept of Services that will internally route between multiple instances of the same service. Later on we will update this value to reflect our URL when deploying to OpenShift.
Now that we have a client we can make use of it in our CatalogService
Open modernize-apps/catalog/src/main/java/com/redhat/coolstore/service/CatalogService.java
And autowire (e.g. inject) the client into it by inserting this at the //TODO: Autowire Inventory Client
marker:
@Autowired
private InventoryClient inventoryClient;
Next, update the read(String id)
method at the comment //TODO: Update the quantity for the product by calling the Inventory service
add the following:
product.setQuantity(inventoryClient.getInventoryStatus(product.getItemId()).getQuantity());
Also, don't forget to add the import statement by un-commenting the import statement //import com.redhat.coolstore.client.InventoryClient
near the top
import com.redhat.coolstore.client.InventoryClient;
Also in the readAll()
method replace the comment //TODO: Update the quantity for the products by calling the Inventory service
with the following:
productList.parallelStream()
.forEach(p -> {
p.setQuantity(inventoryClient.getInventoryStatus(p.getItemId()).getQuantity());
});
NOTE: The lambda expression to update the product list uses a
parallelStream
, which means that it will process the inventory calls asynchronously, which will be much faster than using synchronous calls. Optionally when we run the test you can test with bothparallelStream()
andstream()
just to see the difference in how long the test takes to run.
We are now ready to test the service
mvn verify
So even if we don't have any inventory service running we can still run our test. However to actually run the service using mvn spring-boot:run
we need to have an inventory service or the calls to /services/products/
will fail. We will fix this in the next step
You now have the framework for retrieving products from the product catalog and enriching the data with inventory data from an external service. But what if that external inventory service does not respond? That's the topic for the next step.
In the previous step we added a client to call the Inventory service. Services calling services is a common practice in Microservices Architecture, but as we add more and more services the likelihood of a problem increases dramatically. Even if each service has 99.9% update, if we have 100 of services our estimated up time will only be ~90%. We therefor need to plan for failures to happen and our application logic has to consider that dependent services are not responding.
In the previous step we used the Feign client from the Netflix cloud native libraries to avoid having to write boilerplate code for doing a REST call. However Feign also have another good property which is that we easily create fallback logic. In this case we will use static inner class since we want the logic for the fallback to be part of the Client and not in a separate class.
Open: modernize-apps/catalog/src/main/java/com/redhat/coolstore/client/InventoryClient.java
And paste this into it at the //TODO: Add Fallback factory here
marker:
@Component
class InventoryClientFallbackFactory implements FallbackFactory<InventoryClient> {
@Override
public InventoryClient create(Throwable cause) {
return itemId -> new Inventory(itemId,-1);
}
}
After creating the fallback factory all we have to do is to tell Feign to use that fallback in case of an issue, by adding the fallbackFactory property to the @FeignClient
annotation. Open the file to replace it at the @FeignClient(name="inventory")
line:
@FeignClient(name="inventory",fallbackFactory = InventoryClient.InventoryClientFallbackFactory.class)
Test the Fallback
Now let's see if we can test the fallback. Optimally we should create a different test that fails the request and then verify the fallback value, however in because we are limited in time we are just going to change our test so that it returns a server error and then verify that the test fails.
Open modernize-apps/catalog/src/test/java/com/redhat/coolstore/service/CatalogEndpointTest.java
and change the following lines:
@ClassRule
public static HoverflyRule hoverflyRule = HoverflyRule.inSimulationMode(dsl(
service("inventory:8080")
// .andDelay(2500, TimeUnit.MILLISECONDS).forMethod("GET")
.get(startsWith("/services/inventory"))
// .willReturn(serverError())
.willReturn(success(json(new Inventory("9999",9999))))
));
TO
@ClassRule
public static HoverflyRule hoverflyRule = HoverflyRule.inSimulationMode(dsl(
service("inventory:8080")
// .andDelay(2500, TimeUnit.MILLISECONDS).forMethod("GET")
.get(startsWith("/services/inventory"))
.willReturn(serverError())
// .willReturn(success(json(new Inventory("9999",9999))))
));
Notice that the Hoverfly Rule will now return serverError for all request to inventory.
Now if you run mvn verify -Dtest=CatalogEndpointTest
the test will fail with the following error message:
Failed tests: test_retriving_one_proudct(com.redhat.coolstore.service.CatalogEndpointTest): expected:<[9999]> but was:<[-1]>
So since even if our inventory service fails we are still returning inventory quantity -1. The test fails because we are expecting the quantity to be 9999.
Change back the class rule by re-commenting out the .willReturn(serverError())
line so that we don't fail the tests like this:
@ClassRule
public static HoverflyRule hoverflyRule = HoverflyRule.inSimulationMode(dsl(
service("inventory:8080")
// .andDelay(2500, TimeUnit.MILLISECONDS).forMethod("GET")
.get(startsWith("/services/inventory"))
// .willReturn(serverError())
.willReturn(success(json(new Inventory("9999",9999))))
));
Make sure the test works again by running mvn verify -Dtest=CatalogEndpointTest
Slow running services Having fallbacks is good but that also requires that we can correctly detect when a dependent services isn't responding correctly. Besides from not responding a service can also respond slowly causing our services to also respond slow. This can lead to cascading issues that is hard to debug and pinpoint issues with. We should therefore also have sane defaults for our services. You can add defaults by adding it to the configuration.
Open modernize-apps/catalog/src/main/resources/application-default.properties
And add this line to it at the #TODO: Set timeout to for inventory to 500ms
marker:
hystrix.command.inventory.execution.isolation.thread.timeoutInMilliseconds=500
Open modernize-apps/catalog/src/test/java/com/redhat/coolstore/service/CatalogEndpointTest.java
and un-comment the .andDelay(2500, TimeUnit.MILLISECONDS).forMethod("GET")
Now if you run mvn verify -Dtest=CatalogEndpointTest
the test will fail with the following error message:
Failed tests: test_retriving_one_proudct(com.redhat.coolstore.service.CatalogEndpointTest): expected:<[9999]> but was:<[-1]>
This shows that the timeout works nicely. However, since we want our test to be successful you should now comment out .andDelay(2500, TimeUnit.MILLISECONDS).forMethod("GET")
again and then verify that the test works by executing:
mvn verify -Dtest=CatalogEndpointTest
You have now successfully executed the fifth step in this scenario.
In this step you've learned how to add Fallback logic to your class and how to add timeout to service calls.
In the next step we now test our service locally before we deploy it to OpenShift.
As you have seen in previous steps, using the Spring Boot maven plugin (predefined in pom.xml
), you can conveniently run the application locally and test the endpoint.
Execute the following command to run the new service locally:
mvn spring-boot:run
INFO: As an uber-jar, it could also be run with
java -jar target/catalog-1.0-SNAPSHOT-swarm.jar
but you don't need to do this now
Once the application is done initializing you should see:
INFO [main] com.redhat.coolstore.RestApplication : Started RestApplication [...]
Running locally using spring-boot:run
will use an in-memory database with default credentials. In a production application you will use an external source for credentials using an OpenShift secret in later steps, but for now this will work for development and testing.
3. Test the application
To test the running application, navigate back to the CodeReady Workspaces and run the following in a new terminal:
curl http://localhost:8081
You should now see a html code deployed.
To see the raw JSON output using curl
, you can open an new terminal window by clicking on the plus (+) icon on the terminal toolbar and then choose Terminal. Enter the following command to run the test:
curl http://localhost:8081/services/product/329299 ; echo
You would see a JSON response like this:
{"itemId":"329299","name":"Red Fedora","desc":"Official Red Hat Fedora","price":34.99,"quantity":-1}
NOTE: Since we do not have an inventory service running locally the value for the quantity is -1, which matches the fallback value that we have configured.
The REST API returned a JSON object representing the inventory count for this product. Congratulations!
4. Stop the application
Before moving on, be sure to stop the service by clicking on the first Terminal window and typing CTRL-Z
to stop and send the running application to the background. Then at the command line, enter kill %1
to stop the application.
You have now successfully created your the Catalog service using Spring Boot and implemented basic REST API on top of the product catalog database. You have also learned how to deal with service failures.
In next steps of this scenario we will deploy our application to OpenShift Container Platform and then start adding additional features to take care of various aspects of cloud native microservice development.
We have already deployed our coolstore monolith and inventory to OpenShift. In this step we will deploy our new Catalog microservice for our CoolStore application
In this step we'll deploy your new microservice to OpenShift, so let's navigate back to ocpuser0XX-coolstore-dev
From the CodeReady Workspaces Terminal window, navigate back to ocpuser0XX-coolstore-dev
project by entering the following command:
oc project ocpuser0XX-coolstore-dev
Now that you've logged into OpenShift, let's deploy our new catalog microservice:
Deploy the Database
Our production catalog microservice will use an external database (PostgreSQL) to house inventory data. First, deploy a new instance of PostgreSQL by executing:
oc new-app -e POSTGRESQL_USER=catalog \
-e POSTGRESQL_PASSWORD=mysecretpassword \
-e POSTGRESQL_DATABASE=catalog \
openshift/postgresql:latest \
--name=catalog-database
NOTE: If you change the username and password you also need to update
modernize-apps/catalog/src/main/fabric8/credential-secret.yml
which contains the credentials used when deploying to OpenShift.
This will deploy the database to our new project. Wait for it to complete:
oc rollout status -w dc/catalog-database
Update configuration
Create the file : modernize-apps/catalog/src/main/resources/application-openshift.properties
Copy the following content to the file:
spring.datasource.url=jdbc:postgresql://${project.artifactId}-database:5432/catalog
spring.datasource.initialization-mode=always
inventory.ribbon.listOfServers=inventory:8080
NOTE: The
application-openshift.properties
does not have all values ofapplication-default.properties
, that is because on the values that need to change has to be specified here. Spring will fall back toapplication-default.properties
for the other values.
Build and Deploy
Build and deploy the project using the following command, which will use the maven plugin to deploy:
mvn package fabric8:deploy -Popenshift -DskipTests
The build and deploy may take a minute or two. Wait for it to complete. You should see a BUILD SUCCESS at the end of the build output.
After the maven build finishes it will take less than a minute for the application to become available. To verify that everything is started, run the following command and wait for it complete successfully:
oc rollout status -w dc/catalog
NOTE: If you recall in the WildFly Swarm lab Fabric8 detected the
health
fraction and generated health check definitions for us, the same is true for Spring Boot if you have thespring-boot-starter-actuator
dependency in our project.
3. Access the application running on OpenShift
This sample project includes a simple UI that allows you to access the Inventory API. This is the same UI that you previously accessed outside of OpenShift which shows the CoolStore inventory. Click on the route URL at
http://catalog-ocpuser0XX-coolstore-dev.{{ROUTE_SUFFIX}}
to access the sample UI.
/!\ Don't forget to change the user number in your route.
You can also access the application through the link on the OpenShift Web Console Overview page.
The UI will refresh the catalog table every 2 seconds, as before.
NOTE: Since we previously have a inventory service running you should now see the actual quantity value and not the fallback value of -1
You have deployed the Catalog service as a microservice which in turn calls into the Inventory service to retrieve inventory data. However, our monolih UI is still using its own built-in services. Wouldn't it be nice if we could re-wire the monolith to use the new services, without changing any code? That's next!
So far we haven't started strangling the monolith. To do this we are going to make use of routing capabilities in OpenShift. Each external request coming into OpenShift (unless using ingress, which we are not) will pass through a route. In our monolith the web page uses client side REST calls to load different parts of pages.
For the home page the product list is loaded via a REST call to http:///services/products. At the moment calls to that URL will still hit product catalog in the monolith. By using a path based route in OpenShift we can route these calls to our newly created catalog services instead and end up with something like:
Flow the steps below to create a path based route.
1. Obtain hostname of monolith UI from our Dev environment
oc get route/www -n ocpuser0XX-coolstore-dev
/!\ Change the project name according to your user number
The output of this command shows us the hostname:
NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD
www www-ocpuser0XX-coolstore-dev.{{ROUTE_SUFFIX}} coolstore <all> None
My hostname is www-ocpuser0XX-coolstore-dev.{{ROUTE_SUFFIX}}
but yours will be different.
**2. Open the OpenShift Console for "Coolstore Monolith Dev" and navigate to Applications -> Routes
3. Click on Create Route, and set
- Name:
catalog-redirect
- Hostname: the hostname from above
- Path:
/services/products
- Service:
catalog
Leave other values set to their defaults, and click Create
4. Test the route
Test the route by running curl http://www-ocpuser0XX-coolstore-dev.{{ROUTE_SUFFIX}}/services/products
You should get a complete set of products, along with their inventory.
5. Test the UI
Open the monolith UI at
http://www-ocpuser0XX-coolstore-dev.{{ROUTE_SUFFIX}}
and observe that the new catalog is being used along with the monolith:
The screen will look the same, but notice that the earlier product Atari 2600 Joystick is now gone, as it has been removed in our new catalog microservice.
You have now successfully begun to strangle the monolith. Part of the monolith's functionality (Inventory and Catalog) are now implemented as microservices, without touching the monolith. But there's a few more things left to do, which we'll do in the next steps.
In this scenario you learned a bit more about what Spring Boot and how it can be used together with OpenShift and OpenShift Kubernetes.
You created a new product catalog microservice representing functionality previously implemented in the monolithic CoolStore application. This new service also communicates with the inventory service to retrieve the inventory status for each product.