Sunday, January 1, 2017

Micro services with spring boot

1. Into

From wikipedia:
Microservices is a specialisation of an implementation approach for service-oriented architectures (SOA) used to build flexible, independently deployable software systems. Services in a microservice architecture (MSA)[1] are processes that communicate with each other over a network in order to fulfill a goal. These services use technology-agnostic protocols.[2][3][4] The microservices approach is a first realisation of SOA that followed the introduction of DevOps and is becoming more popular for building continuously deployed systems.[5][6]

Sometimes, when application is very big, it has a complicated logic. Component A is using component B, which is using C, which depends on D.... In such systems when we a changing, for example component D - it's hard to predict which components will be affected. Also, developers with just joined the project, have to spend a lot of time to understand how everything works.

Monolith architecture:

Microservice approach is about splitting big application into smaller units(services) which are independent, but may communicate with each others.

Microservice architecture:


2. Types of MicroServices

When we spitted big application into several smaller services, we got set of REGULAR services. But Also, for better and transparent communication we may need several INFRASTRUCTURE services.


Examples of INFRASTRUCTURE services:
 - config server: sometimes it's better to have all configuration settings "in one place": in one dedicated server. All our REGULAR services will be reading them from such server.
- discovery server: our REGULAR services can be switched to work on another port, or moved to another DNS name how can we handle that? For  such purposes we may have a DISCOVERY server: every REGULAR service have to register himself on such server by his "nick-name" for example as "user-service" and other service will be able to get his DNS and port number by asking for this nick-name from DISCOVERY service.
- edge server(edge service): interface on the "edge of the cloud". We may have some security checks in our infrastructure(in REGULAR services). But there are may services, should we "copy+paste" this security checks to all our REGULAR services? It's better to have one "proxy" server on the "edge of the cloud". Clients will be accessing this EDGE server and it will forwarding them to our REGULAR services.

Communication diagram:


Description:
Client want get some data from User service by it endpoint "/user/getDetails". For that, client just need to know DNS and port of our EDGE server(localhost:8080) and User service nick-name("user-service"). So, it can call EDGE server by URL: localhost:8080/user-service/user/getDetail - and that is it. EDGE server will call DISCOVERY server with request like "give me URI for user-service", DISCOVERY server will return URI: localhost:9003, and after that EDGE server will call localhost:9003/user/getDetails URI and return result to Client.





3. Our application structure

For test microservice-based application we are going to create an infrastructure for internet shop where people can login, find some interesting items, put them into cart and create an order.
We will have set of REGULAR services:
- user service
- item service
- cart service

And INFRASTRUCTURE services:
- config server
- discovery server
- egde server(edge serice)

4. Config server

Structure:

It's a simple SpringBoot application with several config files for our REGULAR services.
pom.xml: 

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>com.demien.services</groupId>
   <artifactId>config-server</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>jar</packaging>

   <name>config-server</name>
   <description>Demo project for Spring Boot</description>

   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>1.4.2.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->   </parent>

   <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
      <java.version>1.8</java.version>
   </properties>

   <dependencies>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-config-server</artifactId>
      </dependency>
   </dependencies>

   <dependencyManagement>
      <dependencies>
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Camden.SR2</version>
            <type>pom</type>
            <scope>import</scope>
         </dependency>
      </dependencies>
   </dependencyManagement>

   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>


</project>


ConfigServerApplication.java:


package com.demien.services;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;

@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {

   public static void main(String[] args) {
      SpringApplication.run(ConfigServerApplication.class, args);
   }
}

Beside regular SpringBoot annotation @SpringBootApplication we have in addition @EnableConfigServer which makes all stuff related with configs working(makes files in /config directory accessible by corresponding servers).

application.properties:
spring.profiles.active=native
server.port=8888

- here we are defining server port - it has to be static. profile=native is needed, because config files are located in filesystem(by default - GIT repository).

Configuration files for REGULAR services are almost the same, I'm configuring just a port number. But in real life we can put much more settings.

cart-service.properties:
server.port=${PORT:9001}

item-service.properties:
server.port=${PORT:9002}

user-service.properties:
server.port=${PORT:9003}

Now we can start our service by running mvn spring-boot:run
And check how it works by opening in a browser URL: http://localhost:8888/item-service/default
Result should be something like this:
{"name":"item-service",
 "profiles":["default"],
 "label":null,
 "version":null,
 "state":null,
 "propertySources":[
    {"name":"file:config/item-service.properties",
     "source":{"server.port":"${PORT:9002}"}
    },    
    {"name":"file:./config/item-service.properties",
     "source":{"server.port":"${PORT:9002}"}
    }
 ]
}

5. Discovery server

We will use most popular implementation of Discovery server: EUREKA by Netflix. To turn spring boot application to EUREKA server we need just one annotation.

Project structure:



pom.xml

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>com.demien.services</groupId>
   <artifactId>eureka-server</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>jar</packaging>

   <name>eureka-server</name>
   <description>Demo project for Spring Boot</description>

   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>1.4.2.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->   </parent>

   <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
      <java.version>1.8</java.version>
   </properties>

   <dependencies>

      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-config</artifactId>
      </dependency>

      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-eureka-server</artifactId>
      </dependency>
   </dependencies>

   <dependencyManagement>
      <dependencies>
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Camden.SR3</version>
            <type>pom</type>
            <scope>import</scope>
         </dependency>
      </dependencies>
   </dependencyManagement>

   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>


</project>

EurekaServerApplication.java

package com.demien.services;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

   public static void main(String[] args) {
      SpringApplication.run(EurekaServerApplication.class, args);
   }
}

- as I mentioned before, we need just one additional annotation to turn our application to Eureka server: @EnableEurekaServer.

bootstrap.properties


spring.application.name=eureka-serverspring.cloud.config.uri=http://localhost:8888server.port=8761eureka.client.register-with-eureka=falseeureka.client.fetch-registry=falseeureka.instance.hostname=localhosteureka.instance.prefer-ip-address=true

Now we can start our application the same way mvn spring-boot:run
And open URL: http://localhost:8761/
Result should be something like this:



Not a one REGULAR service is running now, so list of registered service is empty now(no instances available).


6. Edge server(edge service)

Edge-server is just a "proxy". Spring boot is providing implementation of  ZUUL proxy by Netflix.
In similar way to DISCOVERY server, we just need one annotation.

Project structure:



pom.xml

<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>com.demien.services</groupId>
   <artifactId>edge-service</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>jar</packaging>

   <name>edge-service</name>
   <description>Demo project for Spring Boot</description>

   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>1.4.2.RELEASE</version>
      <relativePath/> <!-- lookup parent from repository -->   </parent>

   <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
      <java.version>1.8</java.version>
   </properties>

   <dependencies>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-eureka</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.cloud</groupId>
         <artifactId>spring-cloud-starter-zuul</artifactId>
      </dependency>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
   </dependencies>

   <dependencyManagement>
      <dependencies>
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Camden.SR3</version>
            <type>pom</type>
            <scope>import</scope>
         </dependency>
      </dependencies>
   </dependencyManagement>

   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>


</project>


EdgeServiceApp.java

package com.demien.services;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class EdgeServiceApp {

    public static void main(String[] args) {
        SpringApplication.run(EdgeServiceApp.class, args);
    }

}

We just added one annotation @EnableZuulProxy and this is it! Now all requests to EDGE server in format /server-name/server-resource will be forwarder to corresponding server using DISCOVERY server.

bootstrap.properties
spring.application.name=edge-servicespring.cloud.config.url=http:/localhost:8888

7. Regular services

Regular services are not so interesting - it's just a regular SpringBoot applications and they looks similar to EDGE service. Later I'll show some code fragments of one of them. Of course all source can be downloaded from link on the bottom. 

As I mentioned before, there are 3 regular services: CartService, ItemService, UserService. All of them have to be registered in EUREKA, so then all these services are running EUREKA server will show all of them together with EDGE service. 


8. Regular services : Cart Service

Regular services are not interesting - it's just a regular SpringBoot applications, so I will show only most interesting files of one them: Cart Service. This service is communicating with the others: UserService and ItemService.

First of all, for communication we need a RestTemplate:

package com.demien.services.cart;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class AppConfig {

    @LoadBalanced
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}


And now, we can use this RestTemplate for communication with the others services:

package com.demien.services.cart.controller;

import com.demien.services.cart.domain.CartItem;
import com.demien.services.cart.repository.CartItemRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.websocket.server.PathParam;
import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.List;

@EnableDiscoveryClient@RestController@RequestMapping(value = "/cart")
public class CartController {

    public final static String ITEM_SERVICE_PATH="http://item-service/item";
    public final static String USER_SERVICE_PATH="http://user-service/user";
    public final static String USER_BY_TOKEN=USER_SERVICE_PATH+"/byToken";

    @Autowired
    private CartItemRepository cartItemRepository;

    @Autowired
    private RestTemplate restTemplate;


    @RequestMapping(method = RequestMethod.GET, value="/{tokenId}")
    public List<CartItem> getCardItemsByToken(@PathParam("tokenId") String tokenId) {
        return cartItemRepository.getCardItemsByToken(tokenId);
    }

    @RequestMapping(method = RequestMethod.POST)
    public void addUserItem(@RequestParam String tokenId, @RequestParam String itemId, @RequestParam String amount) {
        CartItem cartItem =new CartItem(itemId, Integer.parseInt(amount));
        cartItemRepository.addCardItemByToken(tokenId, cartItem);
    }

    @RequestMapping(value = "/order", method = RequestMethod.POST)
    public String getCartOrderByToken(@RequestParam("tokenId") String tokenId) {
        if (tokenId == null) {
            throw new RuntimeException("TokenId is null");
        }
        StringBuilder result=new StringBuilder();
        Object userResponse = getUserDetailsByToken(tokenId);
        String userName = (String)getValueFromResponse(userResponse, "name");
        String userAddress = (String)getValueFromResponse(userResponse, "address");

        result.append("User:"+userName+"\n");
        result.append("Address:"+userAddress+"\n");

        List<CartItem> cartItems = cartItemRepository.getCardItemsByToken(tokenId);
        BigDecimal total=BigDecimal.ZERO;
        int index=0;
        for (CartItem cartItem:cartItems) {
            index++;
            Object itemResponse = getItemDetails(cartItem.getItemId());
            String itemName = (String)getValueFromResponse(itemResponse, "itemName");
            BigDecimal price = new BigDecimal( (Double) getValueFromResponse(itemResponse, "price"));
            BigDecimal itemTotal = price.multiply(new BigDecimal(cartItem.getAmount()));
            total = total.add(itemTotal);
            result.append("  "+index+". item:"+itemName+", price:"+price+", amount:"+cartItem.getAmount()+", itemTotal:"+itemTotal +"\n");
        }
        result.append("Total:"+total);

        return result.toString();
    }

    public Object getValueFromResponse(Object response, String value) {
        return ((LinkedHashMap)response).get(value);

    }

    public Object getItemDetails(String itemId) {
        return restTemplate.getForObject(ITEM_SERVICE_PATH+"/"+itemId, Object.class);
    }

    public Object getUserDetailsByToken(String tokenId) {
        return restTemplate.getForObject(USER_BY_TOKEN+"/" + tokenId, Object.class);
    }


}


And we need to define "nick-name" of our service in boottrap.properties:
spring.application.name=cart-servicespring.cloud.config.uri=http://localhost:8888ribbon.http.client.enabled=true

As you can see, all we need to know for communication with another services is "nick-names"  :
    public final static String ITEM_SERVICE_PATH="http://item-service/item";
    public final static String USER_SERVICE_PATH="http://user-service/user";
We don't need to know the exact server name and port, just service name from DISCOVERY service.

9. Working with regular services. 

We have an EDGE service which is running on localhost:8080, so to call any service we want we need to call EDGE service by pattern: localhost:8080/service-name/service-resource

For example, to call endpoint /cart/order from Cart-service described above, we have to call:
http://localhost:8080/cart-service/cart/order

"cart-service" is a "nick-name" defined in service bootstrap.properties file:
spring.application.name=cart-service

To login, using UserService, we have to call:
http://localhost:8080/user-service/user/login

To get list of all items form ItemService:
http://localhost:8080/item-service/item/getAll

10. The end. 

All source code can be downloaded from here.