We will use the domain-driven implementation and approach described in the last chapter to implement the µServices using Spring Cloud. Let's revisit the key artifacts:
Entities have traits such as identity, a thread of continuity, and attributes that do not define their identity. Value Objects (VO) just have the attributes and no conceptual identity. A best practice is to keep Value Objects as immutable objects. In the Spring framework, entities are pure POJOs, therefore we'll also use them as VO.
Downloading the example code
Detailed steps to download the code bundle are mentioned in the Preface of this book. Please have a look.
The code bundle for the book is also hosted on GitHub at https://github.com/PacktPublishing/Mastering-Microservices-with-Java. We also have other code bundles from our rich catalog of books and videos available at https://github.com/PacktPublishing/. Check them out!
The Restaurant µService will be exposed to the external world using REST endpoints for consumption. We'll find the following endpoints in the Restaurant µService example. One can add as many endpoints as per the requirements:
|
| |
Parameters | ||
Name |
Description | |
|
Path parameter that represents the unique restaurant associated with this ID | |
Request | ||
Property |
Type |
Description |
None | ||
Response | ||
Property |
Type |
Description |
|
Restaurant object |
Restaurant object that is associated with the given ID |
|
| |
Parameters | ||
Name |
Description | |
None | ||
Request | ||
Property |
Type |
Description |
|
String |
Query parameter that represents the name, or substring of the name, of the restaurant |
Response | ||
Property |
Type |
Description |
|
Array of restaurant objects |
Returns all the restaurants whose names contain the given name value |
|
| |
Parameters | ||
Name |
Description | |
None | ||
Request | ||
Property |
Type |
Description |
|
Restaurant object |
A JSON representation of the restaurant object |
Response | ||
Property |
Type |
Description |
|
Restaurant object |
A newly created Restaurant object |
Similarly, we can add various endpoints and their implementations. For demonstration purposes, we'll implement the preceding endpoints using Spring Cloud.
The Restaurant Controller uses the @RestController
annotation to build the restaurant service endpoints. We have already gone through the details of @RestController
in Chapter 2, Setting Up the Development Environment. @RestController
is a class-level annotation that is used for resource classes. It is a combination of @Controller
and @ResponseBody
. It returns the domain object.
As we move forward, I would like to share with you that we are using the v1
prefix on our REST endpoint. That represents the version of the API. I would also like to brief you on the importance of API versioning. Versioning APIs is important, because APIs change over time. Your knowledge and experience improves with time, which leads to changes to your API. A change of API may break existing client integrations.
Therefore, there are various ways of managing API versions. One of these is using the version in path or some use the HTTP header. The HTTP header can be a custom request header or an Accept header to represent the calling API version. Please refer to RESTful Java Patterns and Best Practices by Bhakti Mehta, Packt Publishing, https://www.packtpub.com/application-development/restful-java-patterns-and-best-practices, for more information.
@RestController @RequestMapping("/v1/restaurants") public class RestaurantController { protected Logger logger = Logger.getLogger(RestaurantController.class.getName()); protected RestaurantService restaurantService; @Autowired public RestaurantController(RestaurantService restaurantService) { this.restaurantService = restaurantService; } /** * Fetch restaurants with the specified name. A partial case-insensitive * match is supported. So <code>http://.../restaurants/rest</code> will find * any restaurants with upper or lower case 'rest' in their name. * * @param name * @return A non-null, non-empty collection of restaurants. */ @RequestMapping(method = RequestMethod.GET) public ResponseEntity<Collection<Restaurant>> findByName(@RequestParam("name") String name) { logger.info(String.format("restaurant-service findByName() invoked:{} for {} ", restaurantService.getClass().getName(), name)); name = name.trim().toLowerCase(); Collection<Restaurant> restaurants; try { restaurants = restaurantService.findByName(name); } catch (Exception ex) { logger.log(Level.WARNING, "Exception raised findByName REST Call", ex); return new ResponseEntity< Collection< Restaurant>>(HttpStatus.INTERNAL_SERVER_ERROR); } return restaurants.size() > 0 ? new ResponseEntity< Collection< Restaurant>>(restaurants, HttpStatus.OK) : new ResponseEntity< Collection< Restaurant>>(HttpStatus.NO_CONTENT); } /** * Fetch restaurants with the given id. * <code>http://.../v1/restaurants/{restaurant_id}</code> will return * restaurant with given id. * * @param retaurant_id * @return A non-null, non-empty collection of restaurants. */ @RequestMapping(value = "/{restaurant_id}", method = RequestMethod.GET) public ResponseEntity<Entity> findById(@PathVariable("restaurant_id") String id) { logger.info(String.format("restaurant-service findById() invoked:{} for {} ", restaurantService.getClass().getName(), id)); id = id.trim(); Entity restaurant; try { restaurant = restaurantService.findById(id); } catch (Exception ex) { logger.log(Level.SEVERE, "Exception raised findById REST Call", ex); return new ResponseEntity<Entity>(HttpStatus.INTERNAL_SERVER_ERROR); } return restaurant != null ? new ResponseEntity<Entity>(restaurant, HttpStatus.OK) : new ResponseEntity<Entity>(HttpStatus.NO_CONTENT); } /** * Add restaurant with the specified information. * * @param Restaurant * @return A non-null restaurant. * @throws RestaurantNotFoundException If there are no matches at all. */ @RequestMapping(method = RequestMethod.POST) public ResponseEntity<Restaurant> add(@RequestBody RestaurantVO restaurantVO) { logger.info(String.format("restaurant-service add() invoked: %s for %s", restaurantService.getClass().getName(), restaurantVO.getName()); Restaurant restaurant = new Restaurant(null, null, null); BeanUtils.copyProperties(restaurantVO, restaurant); try { restaurantService.add(restaurant); } catch (Exception ex) { logger.log(Level.WARNING, "Exception raised add Restaurant REST Call "+ ex); return new ResponseEntity<Restaurant>(HttpStatus.UNPROCESSABLE_ENTITY); } return new ResponseEntity<Restaurant>(HttpStatus.CREATED); } }
RestaurantController
uses RestaurantService
. RestaurantService
is an interface that defines CRUD and some search operations and is defined as follows:
public interface RestaurantService { public void add(Restaurant restaurant) throws Exception; public void update(Restaurant restaurant) throws Exception; public void delete(String id) throws Exception; public Entity findById(String restaurantId) throws Exception; public Collection<Restaurant> findByName(String name) throws Exception; public Collection<Restaurant> findByCriteria(Map<String, ArrayList<String>> name) throws Exception; }
Now, we can implement the RestaurantService
we have just defined. It also extends the BaseService
you created in the last chapter. We use @Service
Spring annotation to define it as a service:
@Service("restaurantService") public class RestaurantServiceImpl extends BaseService<Restaurant, String> implements RestaurantService { private RestaurantRepository<Restaurant, String> restaurantRepository; @Autowired public RestaurantServiceImpl(RestaurantRepository<Restaurant, String> restaurantRepository) { super(restaurantRepository); this.restaurantRepository = restaurantRepository; } public void add(Restaurant restaurant) throws Exception { if (restaurant.getName() == null || "".equals(restaurant.getName())) { throw new Exception("Restaurant name cannot be null or empty string."); } if (restaurantRepository.containsName(restaurant.getName())) { throw new Exception(String.format("There is already a product with the name - %s", restaurant.getName())); } super.add(restaurant); } @Override public Collection<Restaurant> findByName(String name) throws Exception { return restaurantRepository.findByName(name); } @Override public void update(Restaurant restaurant) throws Exception { restaurantRepository.update(restaurant); } @Override public void delete(String id) throws Exception { restaurantRepository.remove(id); } @Override public Entity findById(String restaurantId) throws Exception { return restaurantRepository.get(restaurantId); } @Override public Collection<Restaurant> findByCriteria(Map<String, ArrayList<String>> name) throws Exception { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } }
The RestaurantRepository
interface defines two new methods: the containsName
and findByName
methods. It also extends the Repository
interface:
public interface RestaurantRepository<Restaurant, String> extends Repository<Restaurant, String> { boolean containsName(String name) throws Exception; Collection<Restaurant> findByName(String name) throws Exception; }
The Repository
interface defines three methods: add
, remove
, and update
. It also extends the ReadOnlyRepository
interface:
public interface Repository<TE, T> extends ReadOnlyRepository<TE, T> { void add(TE entity); void remove(T id); void update(TE entity); }
The ReadOnlyRepository
interface definition contains the get
and getAll
methods, which return Boolean values, Entity, and collection of Entity respectively. It is useful if you want to expose only a read-only abstraction of the repository:
public interface ReadOnlyRepository<TE, T> { boolean contains(T id); Entity get(T id); Collection<TE> getAll(); }
Spring framework makes use of the @Repository
annotation to define the repository bean that implements the repository. In the case of RestaurantRepository
, you can see that a map is used in place of the actual database implementation. This keeps all entities saved in memory only. Therefore, when we start the service, we find only two restaurants in memory. We can use JPA for database persistence. This is the general practice for production-ready implementations:
@Repository("restaurantRepository") public class InMemRestaurantRepository implements RestaurantRepository<Restaurant, String> { private Map<String, Restaurant> entities; public InMemRestaurantRepository() { entities = new HashMap(); Restaurant restaurant = new Restaurant("Big-O Restaurant", "1", null); entities.put("1", restaurant); restaurant = new Restaurant("O Restaurant", "2", null); entities.put("2", restaurant); } @Override public boolean containsName(String name) { try { return this.findByName(name).size() > 0; } catch (Exception ex) { //Exception Handler } return false; } @Override public void add(Restaurant entity) { entities.put(entity.getId(), entity); } @Override public void remove(String id) { if (entities.containsKey(id)) { entities.remove(id); } } @Override public void update(Restaurant entity) { if (entities.containsKey(entity.getId())) { entities.put(entity.getId(), entity); } } @Override public Collection<Restaurant> findByName(String name) throws Exception { Collection<Restaurant> restaurants = new ArrayList(); int noOfChars = name.length(); entities.forEach((k, v) -> { if (v.getName().toLowerCase().contains(name.subSequence(0, noOfChars))) { restaurants.add(v); } }); return restaurants; } @Override public boolean contains(String id) { throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. } @Override public Entity get(String id) { return entities.get(id); } @Override public Collection<Restaurant> getAll() { return entities.values(); } }
The Restaurant
entity, which extends BaseEntity
, is defined as follows:
public class Restaurant extends BaseEntity<String> { private List<Table> tables = new ArrayList<>(); public Restaurant(String name, String id, List<Table> tables) { super(id, name); this.tables = tables; } public void setTables(List<Table> tables) { this.tables = tables; } public List<Table> getTables() { return tables; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(String.format("id: {}, name: {}, capacity: {}", this.getId(), this.getName(), this.getCapacity())); return sb.toString(); } }
The Table
entity, which extends BaseEntity
, is defined as follows:
public class Table extends BaseEntity<BigInteger> { private int capacity; public Table(String name, BigInteger id, int capacity) { super(id, name); this.capacity = capacity; } public void setCapacity(int capacity) { this.capacity = capacity; } public int getCapacity() { return capacity; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(String.format("id: {}, name: {}", this.getId(), this.getName())); sb.append(String.format("Tables: {}" + Arrays.asList(this.getTables()))); return sb.toString(); } }
The Entity
abstract class is defined as follows:
public abstract class Entity<T> { T id; String name; public T getId() { return id; } public void setId(T id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
The BaseEntity
abstract class is defined as follows. It extends the Entity
abstract class:
public abstract class BaseEntity<T> extends Entity<T> { private T id; private boolean isModified; private String name; public BaseEntity(T id, String name) { this.id = id; this.name = name; } public T getId() { return id; } public void setId(T id) { this.id = id; } public boolean isIsModified() { return isModified; } public void setIsModified(boolean isModified) { this.isModified = isModified; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
We can use the RestaurantService
implementation to develop the Booking and User services. The User service can offer the endpoint related to the User resource with respect to CRUD operations. The Booking service can offer the endpoints related to the Booking resource with respect to CRUD operations and the availability of table slots. You can find the sample code of these services on the Packt website.
Spring Cloud provides state-of-the-art support to Netflix Eureka, a service registry and discovery tool. All services executed by you get listed and discovered by Eureka service, which it reads from the Eureka client Spring configuration inside your service project.
It needs a Spring Cloud dependency as shown here and a startup class with the @EnableEurekaApplication
annotation in pom.xml
:
Maven dependency:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency>
Startup class:
The startup class App would run the Eureka service seamlessly by just using the @EnableEurekaApplication
class annotation:
package com.packtpub.mmj.eureka.service; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } }
Spring configurations:
Eureka Service also needs the following Spring configuration for Eureka Server configuration (src/main/resources/application.yml
):
server: port: ${vcap.application.port:8761} # HTTP port eureka: instance: hostname: localhost client: registerWithEureka: false fetchRegistry: false server: waitTimeInMsWhenSyncEmpty: 0
Similar to Eureka Server, each OTRS service should also contain the Eureka Client configuration, so that a connection between Eureka Server and the client can be established. Without this, the registration and discovery of services is not possible.
Eureka Client: your services can use the following spring configuration to configure Eureka Server:
eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/
To see how our code works, we need to first build it and then execute it. We'll use Maven clean package to build the service JARs.
Now to execute these service JARs, simply execute the following command from the service home directory:
java -jar target/<service_jar_file>
For example:
java -jar target/restaurant-service.jar java -jar target/eureka-service.jar