Showing posts with label cloud. Show all posts
Showing posts with label cloud. Show all posts

Monday, July 27, 2020

Kafka and Spring Boot(Spring Cloud Stream)

0.Intro

We can interact with Kafka in different ways. Default is to use official Kafka client. By using this client we have to create Kafka Producer and Kafka Consumer and after that we're ready to go. I had a post on this topic here. But if we're using Spring Boot we can try much easier option: Spring Cloud Stream. It is more like "high level" way in comparison to native Kafka client. 

For this demo I created in traditional way 2 applications: Kafka Producer and Kafka Consumer. 
Let's start! 


1. pom.xml 

These files are almost the same for these 2 projects. The only difference is artefact name. 



<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
<relativePath/>
</parent>
<groupId>com.demien</groupId>
<artifactId>spboot-kafka-producer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spboot-kafka-producer</name>
<description>Demo project for Spring Boot and Kafka</description>

<properties>
<java.version>11</java.version>
<spring-cloud.version>Hoxton.SR6</spring-cloud.version>
</properties>

<dependencies>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-kafka-streams</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</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>




2. Producer application

This application will be sending messages to Kafka. 

2.1. Entity 

Let's start from entity class which we will be sending to Kafka. I used Lombok to make it more simple: 
import lombok.Setter;

@Getter
@Setter
@AllArgsConstructor
@ToString
public class TestEntity {
private String id;
private String name;
private String description;
}



2.2. Channel

Ok, we have entity for sending, now we have to configure channel for sending: 
package com.demien.spbootkafkaproducer.domain.message;

import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;

public interface TestMessageChannels {
@Output("myTestChannel")
MessageChannel myTestChannel();
}



- as you can see it's just an interface - Spring will create an implementation for it in runtime. We just have to define the "id" for this channel, which is "myTestChannel" in this particular case. 

2.3. Sender service

Now we can use our channel for message sending: we should enable bindings for it, autowire it and we're ready to go. 

package com.demien.spbootkafkaproducer.domain.service;

import com.demien.spbootkafkaproducer.domain.message.TestMessageChannels;
import com.demien.spbootkafkaproducer.domain.model.TestEntity;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.messaging.Message;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.stereotype.Service;

import java.util.logging.Logger;

@Service("testProducerService")
@EnableBinding(TestMessageChannels.class)
public class TestProducerService {

private final static Logger log = Logger.getLogger(TestProducerService.class.getName());

private final TestMessageChannels testMessageChannels;

@Autowired
public TestProducerService(TestMessageChannels testMessageChannels) {
this.testMessageChannels = testMessageChannels;
}

public void sendTestMessages() {
for (int i=0; i<10; i++) {
var idString = Integer.toString(i);
var testEntity = new TestEntity(idString, "Name " + idString, "Description " + idString);
log.info("Sending to kafka entitiy: " + testEntity);
final Message<TestEntity> message = MessageBuilder.withPayload(testEntity).build();
testMessageChannels.myTestChannel().send(message);
}
}
} 

Not complicated, right? We just have to wrap our test entity into Message object and send it into channel. 

2.4. Application runner

And now we can run our application, let the Spring to create out service (and autowire channel) and we can use it: 

package com.demien.spbootkafkaproducer;

import com.demien.spbootkafkaproducer.domain.service.TestProducerService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class SpbootKafkaProducerApplication {

public static void main(String[] args) {
final var applicationContext = SpringApplication.run(SpbootKafkaProducerApplication.class, args);
new SpbootKafkaProducerApplication().run(applicationContext);
}

public void run(ApplicationContext applicationContext) {
TestProducerService testProducerService = applicationContext.getBean(TestProducerService.class);
testProducerService.sendTestMessages();
}

}

  

2.5. Application Config

Remember, we defined in #2.2 channel with id "myTestChannel"? Now we have to point it to some Kafka topic. And, basically, configure Kaffka as well. This should be done in application config file(application.properties under resources folder)

spring:
application:
name: test-producer
cloud:
stream:
bindings:
myTestChannel:
destination: myTestTopic
kafka:
binder:
zkNodes: localhost
binder:
brokers: localhost


- thus, we will be sending data to some channel with id "myTestChannel" which is linked to Kafka topic "myTestTopic".  



3. Consumer Application

Consumer will be receiving messages from Kafka. To make consumer simpler I omitted entity class: we will be just printing JSONs with our entities received from Kafka. 

3.1. Input Channel

In a similar way: 
package com.demien.spbootkafkaconsumer.domain.message;

import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.MessageChannel;

public interface TestMessageChannels {
@Input("myTestChannel")
MessageChannel myTestChannel();
} 


3.2 Consumer Service

Also, in a similar way but in addition we have to annotate some method as "Stream Listener":

package com.demien.spbootkafkaconsumer.domain.service;

import com.demien.spbootkafkaconsumer.domain.message.TestMessageChannels;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.stereotype.Service;

import java.util.logging.Logger;

@Service
@EnableBinding(TestMessageChannels.class)
public class TestConsumerService {

private final static Logger log = Logger.getLogger(TestConsumerService.class.getName());

@StreamListener("myTestChannel")
public void consumeTestEntity(byte[] testEntity) {
log.info("Received from kafka: " + new String(testEntity));
}
}

- as you can see, we're defining listener for our channel and it will triggered every time we're sending messages to Kafka topic linked with this channel. Of course, in real life, here we have to convert JSONs into our entity classes. 

3.3. Application runner

It even simpler than it was for Producer: 

package com.demien.spbootkafkaconsumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpbootKafkaConsumerApplication {

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

}

3.4. Application configuration

And again we have to link out channel with Kafka topic. 

spring:
application:
name: test-producer
cloud:
stream:
bindings:
myTestChannel:
destination: myTestTopic
consumer:
resetOffsets: true
group:
myTestConsumerGroup
kafka:
binder:
zkNodes: localhost
binder:
brokers: localhost




4. Running

First we have to run Consumer: 

java -jar target/spboot-kafka-consumer-0.0.1-SNAPSHOT.jar


And when it was started - Producer: 

java -jar target/spboot-kafka-producer-0.0.1-SNAPSHOT.jar


Producer should print something like this: 

20-07-27 21:49:44.776  INFO 14016 --- [           main] c.d.s.d.service.TestProducerService      : Sending to kafka entitiy: TestEntity(id=1, name=Name 1, description=Description 1)

2020-07-27 21:49:44.788  INFO 14016 --- [           main] c.d.s.d.service.TestProducerService      : Sending to kafka entitiy: TestEntity(id=2, name=Name 2, description=Description 2)

2020-07-27 21:49:44.789  INFO 14016 --- [           main] c.d.s.d.service.TestProducerService      : Sending to kafka entitiy: TestEntity(id=3, name=Name 3, description=Description 3)

2020-07-27 21:49:44.790  INFO 14016 --- [           main] c.d.s.d.service.TestProducerService      : Sending to kafka entitiy: TestEntity(id=4, name=Name 4, description=Description 4)

2020-07-27 21:49:44.791  INFO 14016 --- [           main] c.d.s.d.service.TestProducerService      : Sending to kafka entitiy: TestEntity(id=5, name=Name 5, description=Description 5)

2020-07-27 21:49:44.793  INFO 14016 --- [           main] c.d.s.d.service.TestProducerService      : Sending to kafka entitiy: TestEntity(id=6, name=Name 6, description=Description 6)

2020-07-27 21:49:44.794  INFO 14016 --- [           main] c.d.s.d.service.TestProducerService      : Sending to kafka entitiy: TestEntity(id=7, name=Name 7, description=Description 7)

2020-07-27 21:49:44.795  INFO 14016 --- [           main] c.d.s.d.service.TestProducerService      : Sending to kafka entitiy: TestEntity(id=8, name=Name 8, description=Description 8)

2020-07-27 21:49:44.796  INFO 14016 --- [           main] c.d.s.d.service.TestProducerService      : Sending to kafka entitiy: TestEntity(id=9, name=Name 9, description=Description 9)




And Consumer: 

2020-07-27 21:47:46.351  INFO 13839 --- [container-0-C-1] c.d.s.d.service.TestConsumerService      : Received from kafka: {"id":"0","name":"Name 0","description":"Description 0"}

2020-07-27 21:47:46.365  INFO 13839 --- [container-0-C-1] c.d.s.d.service.TestConsumerService      : Received from kafka: {"id":"1","name":"Name 1","description":"Description 1"}

2020-07-27 21:47:46.367  INFO 13839 --- [container-0-C-1] c.d.s.d.service.TestConsumerService      : Received from kafka: {"id":"2","name":"Name 2","description":"Description 2"}

2020-07-27 21:47:46.367  INFO 13839 --- [container-0-C-1] c.d.s.d.service.TestConsumerService      : Received from kafka: {"id":"3","name":"Name 3","description":"Description 3"}

2020-07-27 21:47:46.369  INFO 13839 --- [container-0-C-1] c.d.s.d.service.TestConsumerService      : Received from kafka: {"id":"4","name":"Name 4","description":"Description 4"}

2020-07-27 21:47:46.369  INFO 13839 --- [container-0-C-1] c.d.s.d.service.TestConsumerService      : Received from kafka: {"id":"5","name":"Name 5","description":"Description 5"}

2020-07-27 21:47:46.370  INFO 13839 --- [container-0-C-1] c.d.s.d.service.TestConsumerService      : Received from kafka: {"id":"6","name":"Name 6","description":"Description 6"}

2020-07-27 21:47:46.371  INFO 13839 --- [container-0-C-1] c.d.s.d.service.TestConsumerService      : Received from kafka: {"id":"7","name":"Name 7","description":"Description 7"}

2020-07-27 21:47:46.372  INFO 13839 --- [container-0-C-1] c.d.s.d.service.TestConsumerService      : Received from kafka: {"id":"8","name":"Name 8","description":"Description 8"}

2020-07-27 21:47:46.372  INFO 13839 --- [container-0-C-1] c.d.s.d.service.TestConsumerService      : Received from kafka: {"id":"9","name":"Name 9","description":"Description 9"}


It works! :)



5. The end

As you can see, with Spring Boot we can interact with Kafka on much higher level, so it more simple. 
Source code can be downloaded from here. 



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.