Статьи

Сборка игры Mancala в микросервисах с использованием Spring Boot (Часть 3: Внедрение микросервиса в веб-клиент с использованием Vaadin)

Манкала для всех! 

В первой статье « Создание игры Mancala на микросервисах с использованием Spring Boot (часть 1: Архитектура решения) » я объяснил общую архитектуру решения, принятого для реализации игры Mancala с использованием подхода Microservices.


Вам также может понравиться:
Сборка игры Mancala на микросервисах с использованием Spring Boot (Часть 1: Архитектура решения)

Затем я подробно обсудил реализацию микросервиса mancala-api ‘ в отдельной статье: Создание игры Mancala в микросервисах с использованием Spring Boot (часть 2: Реализация API Mancala) .

Теперь в этой статье я собираюсь обсудить детальную реализацию микросервиса « mancala-web », который представляет собой простое веб-приложение, разработанное для этой игры на основе Spring Boot и Vaadin .

Эта статья представляет собой пошаговое руководство по внедрению микросервиса веб-интерфейса Mancala с использованием современной веб-среды под названием Vaadin . Вывод будет выглядеть так:

Полный исходный код этого проекта доступен в моем репозитории GitHub  .

Чтобы создать и запустить это приложение, пожалуйста, следуйте инструкциям, которые я предоставил здесь .

1 — пружинный инициализатор

Как и в предыдущем проекте, мы начнем с запуска start.spring.io :

Заполните информацию о проекте, как показано ниже:

Группа: com.dzone.mancala.game

Артефакт: mancala-web

зависимости:

2 — MancalaWebApplication

Так же, как и проектmancala-api ‘, мы используем API-интерфейс Spring Cloud Consul Discovery, чтобы сделать наш микросервис доступным для сервера реестра службы Consul  . Чтобы включить эту функцию, мы уже добавили следующую зависимость в наш файл pom.xml:


<!-- Consul  -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

Затем мы добавляем @EnableDiscoveryClient  в наше загрузочное приложение Spring:

@SpringBootApplication
@EnableDiscoveryClient
@RibbonClient("mancala-service")
public class MancalaWebApplication {

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

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

Нам нужно будет указать конфигурации Consul в файле application.properties :

#consul configurations
spring.cloud.consul.host=consul
spring.cloud.consul.port=8500
spring.cloud.consul.discovery.preferIpAddress=true
spring.cloud.consul.discovery.instanceId=${spring.application.name}:${spring.application.instance_id:${random.value}}
spring.cloud.consul.ribbon.enabled=true

Нам также нужно добавить следующую конфигурацию в наш файл bootstrap.properties :

spring.application.name=mancala-web

3 — Балансировка нагрузки клиента ленты

Лента , это балансировщик нагрузки на стороне клиента, предоставляемый Netflix, который дает большой контроль над поведением клиентов HTTP и TCP:

  • Балансировка нагрузки.
  • Отказоустойчивость.
  • Поддержка нескольких протоколов (HTTP, TCP, UDP) в асинхронной и реактивной модели.
  • Кеширование и пакетирование.

Spring boot обеспечивает интеграцию реализации Ribbon для вашего загрузочного приложения Spring, добавляя в файл pom.xml следующую зависимость:

<!-- Ribbon   -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

и добавьте аннотацию @RibbonClient в класс запуска приложения Spring Boot, как показано здесь:

@SpringBootApplication
@EnableDiscoveryClient
@RibbonClient("mancala-service")
public class MancalaWebApplication {

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

@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

И нам нужно предоставить имя для нашего ленточного клиента, например, « mancala-service », потому что Ribbon является именованным клиентом. Каждый балансировщик нагрузки является частью ансамбля компонентов, которые работают вместе для связи с удаленным сервером по требованию, и у ансамбля есть имя, которое мы даем ему как разработчику приложения. Вы также можете настроить клиента ленты на основе различных конфигураций. Смотрите документацию .

Теперь мы можем использовать интерфейс LoadBalancerClient, как показано  ниже:

@Component
public class MancalaClientConfig {

    private LoadBalancerClient loadBalancer;

    @Value("${mancala.api.service.id}")
    private final String apiServiceId = "mancala-api";

    @Autowired
    public void setLoadBalancer(LoadBalancerClient loadBalancer) {
        this.loadBalancer = loadBalancer;
    }

    public String getNewMancalaGameUrl(){
        ServiceInstance instance = this.loadBalancer.choose(apiServiceId);

        String url = String.format("http://%s:%s/games", instance.getHost(), instance.getPort());

        return url;
    }

    public String getSowMancalaGameUrl(String gameId, Integer pitIndex){
        ServiceInstance instance = this.loadBalancer.choose(apiServiceId);

        String url = String.format("http://%s:%s/games/%s/pits/%s", instance.getHost(), instance.getPort(), gameId, pitIndex);

        return url;
    }
}

LoadBalancerClient.choose ( «сервис-идентификатор») возвращает ServiceInstance на основе наличия экземпляров в рамках нашего консула Service Registry. Алгоритм по умолчанию — циклический перебор. В этом приложении вы можете добавить больше экземпляров для нашей службыmancala-api ‘, выполнив следующую команду оболочки с помощью Docker-compose:

docker-compose scale mancala-api=3

4 — MVC реализация веб-клиента с использованием Vaadin

Vaadin  — это веб-инфраструктура с открытым исходным кодом, которая помогает разработчикам Java создавать отличные пользовательские интерфейсы с минимальными усилиями. Этот учебник не предназначен для того, чтобы вдаваться в детали этой структуры. Для получения дополнительной информации, пожалуйста, посетите веб-сайт vaadin.com .

Мы использовали подход Model View Controller  (MVC) для реализации нашего веб-клиентского приложения. Это помогает нам отделить логику, используемую для каждого слоя: модель, вид и контроллер.

Модельные классы

Ниже приведены классы моделей, которые мы определили в проекте ‘ mancala-api ‘, и мы используем их также в этом проекте.

KalahaConstants

Этот класс включает в себя все константы, которые мы определили для этой игры:

public class KalahaConstants {
    public static final int emptyStone = 0;
    public static final int leftPitHouseId = 14;
    public static final int rightPitHouseId = 7;

    public static final int firstPitPlayerA = 1;
    public static final int secondPitPlayerA = 2;
    public static final int thirdPitPlayerA = 3;
    public static final int forthPitPlayerA = 4;
    public static final int fifthPitPlayerA = 5;
    public static final int sixthPitPlayerA = 6;

    public static final int firstPitPlayerB = 8;
    public static final int secondPitPlayerB = 9;
    public static final int thirdPitPlayerB = 10;
    public static final int forthPitPlayerB = 11;
    public static final int fifthPitPlayerB = 12;
    public static final int sixthPitPlayerB = 13;

}

MancalaGame

@Data
public class KalahaGame {
    private String id;
    private String playerTurn;
    private List<KalahaPit> pits;

    @JsonIgnore
    public Integer getPlayerAStones(){
        return getPit(firstPitPlayerA).getStones() +
                getPit(secondPitPlayerA).getStones() +
                getPit(thirdPitPlayerA).getStones()+
                getPit(forthPitPlayerA).getStones()+
                getPit(fifthPitPlayerA).getStones()+
                getPit(sixthPitPlayerA).getStones();
    }

    @JsonIgnore
    public Integer getPlayerBStones() {
        return getPit(firstPitPlayerB).getStones() +
                getPit(secondPitPlayerB).getStones() +
                getPit(thirdPitPlayerB).getStones()+
                getPit(forthPitPlayerB).getStones()+
                getPit(fifthPitPlayerB).getStones()+
                getPit(sixthPitPlayerB).getStones();
    }

    public KalahaPit getPit (Integer pitIndex){
        return this.pits.stream().filter(p -> p.getId() == pitIndex).findAny().get();
    }

    @JsonIgnore
    public Integer getLeftHouseStones (){
        return getPit(leftPitHouseId).getStones();
    }

    @JsonIgnore
    public Integer getRightHouseStones (){
        return getPit(rightPitHouseId).getStones();
    }

    @JsonIgnore
    public Integer getPitStones (Integer pitIndex){
        return getPit(pitIndex).getStones();
    }
}

KalahaPit

@AllArgsConstructor
@NoArgsConstructor
@Data
public class KalahaPit implements Serializable {

    private Integer id;
    private Integer stones;

    @JsonIgnore
    public Boolean isEmpty (){
        return this.stones == 0;
    }

    public void clear (){
        this.stones = 0;
    }

    public void sow () {
        this.stones++;
    }

    public void addStones (Integer stones){
        this.stones+= stones;
    }

    @Override
    public String toString() {
        return  id.toString() +
                ":" +
                stones.toString() ;
    }
}

KalahaHouse

public class KalahaHouse extends KalahaPit {
    public KalahaHouse(Integer id) {
        super(id , emptyStone);
    }
}

Классы контроллера

Контроллер отвечает за перехват пользовательских запросов и делегирует их соответствующему внутреннему API и предоставляет ответ клиенту. Ниже мы определили определение для этой цели:

MancalaClient

Этот класс выполняет все вызовы вызовов REST для микросервиса mancala-api ‘ на основе реализации балансировщика нагрузки ленты клиента, предоставленной в классе MancalaClientConfig  :

@Component
@Slf4j
public class MancalaClient {

    private RestTemplate restTemplate;

    @Autowired
    private MancalaClientConfig mancalaClientConfig;

    public MancalaClient(@Autowired RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public KalahaGame startNewMancalaGame() throws Exception {

        String url = mancalaClientConfig.getNewMancalaGameUrl();

        log.info("calling:" + url);

        ResponseEntity<KalahaGame> gameResponse = this.restTemplate.postForEntity(url, null, KalahaGame.class);

        log.info("response: " + new ObjectMapper().writerWithDefaultPrettyPrinter().
                writeValueAsString(gameResponse.getBody()));

        return gameResponse.getBody();
    }

    public KalahaGame sowMancalaGame(String gameId, Integer pitIndex) throws Exception {

        String url = mancalaClientConfig.getSowMancalaGameUrl(gameId, pitIndex);

        log.info("calling: " + url);

        ResponseEntity<KalahaGame> response = restTemplate.exchange(url, HttpMethod.PUT, null, KalahaGame.class);

        log.info("response: " + new ObjectMapper().writerWithDefaultPrettyPrinter().
                writeValueAsString(response.getBody()));

        return response.getBody();
    }
}

Игровой контроллер

Этот класс действует в качестве основного класса контроллера в шаблоне MVC, который взаимодействует с пользовательским интерфейсом Vaadin с одной стороны и вызывает соответствующий REST-API, предоставляемый микросервисом mancala-api ‘ через класс MancalaClient  с другой стороны:

@Component
public class GameController {

    private KalahaGame game;

    @Autowired
    private MancalaClient mancalaClient;

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    public KalahaGame startNewGame() throws ApiConnectionException {
        try {
            this.game = mancalaClient.startNewMancalaGame();

            return this.game;

        }catch (Exception e){
            throw new ApiConnectionException("Error connecting to Mancala service!");
        }
    }

    public void sow(Integer pitIndex) throws ApiConnectionException {
        try {
            this.game = mancalaClient.sowMancalaGame(this.game.getId(), pitIndex);

            this.eventPublisher.publishEvent(new SowEvent(this, this.game, pitIndex));

        }catch (Exception ex){
            throw new ApiConnectionException("Error connecting to Mancala service!");
        }
    }

    public boolean hasGameStarted () {
        return this.game != null;
    }
}

Посмотреть классы

Создание классов пользовательского интерфейса в Vaadin очень просто. Это очень похоже на API Java Swings. Сначала нам нужно будет создать наш игровой макет и поместить наши компоненты пользовательского интерфейса в эти макеты. В Vaadin компоненты пользовательского интерфейса размещаются в двух разных макетах: вертикальные  и горизонтальные .

Чтобы добавить Vaadin в наш проект, все, что нам нужно, это добавить приведенную ниже зависимость в наш файл pom.xml:

<!-- vaadin UI -->
<dependency>
  <groupId>com.vaadin</groupId>
  <artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>

Вот то, что я разработал как простой подход к доказательству концепции для моей реализации микросервиса:

ПитКомпонент Класс

этот класс представляет собой конкретную яму в игре Mancala. Он состоит из двух внутренних компонентов Vaadin: TextField  и Button.

@UIScope
@SpringComponent
@Getter
@Slf4j
public class PitComponent extends VerticalLayout {

    private static final Integer defaultPitStones = 0;

    private final TextField pit = new TextField();
    private final Button btn = new Button();

    private GameController gameController;

    public PitComponent() {
        pit.getElement().setAttribute("theme", "align-center");
        pit.setReadOnly(true);
        pit.setValue(defaultPitStones.toString());
        pit.getStyle().set("font-size", "15px");
        pit.getStyle().set("font-weight", "bold");
        pit.setMaxLength(30);
        pit.setMinLength(30);
        btn.getElement().setAttribute("theme", "align-center");
        add(btn, pit);
        setAlignItems(Alignment.CENTER);

        pit.addValueChangeListener(e -> {
            pit.getStyle().set("background-color", "#ff9933");
            new ChangeColorThread(UI.getCurrent(), pit).start();
        });
    }

    private static class ChangeColorThread extends Thread{

        private UI ui;
        private TextField textField;
        public ChangeColorThread(UI ui, TextField textField) {
            this.ui = ui;
            this.textField = textField;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
            ui.access(() -> {
                textField.getStyle().set("background-color", "#ffffff");
            });
        }
    }

    public PitComponent(Integer pitIndex, GameController gameController) {
        this();
        this.gameController = gameController;
        pit.setId(pitIndex.toString());

        btn.setText(pitIndex.toString());
        btn.setTabIndex(pitIndex);
        btn.addClickListener(e -> {
            if (!this.gameController.hasGameStarted()){
                Notification.show("Please click on 'Start Game' button to start the game first!");
                return;
            }

            Notification.show(e.getSource().getTabIndex() + " Clicked");
            try {
                this.gameController.sow(e.getSource().getTabIndex());
            } catch (ApiConnectionException ex) {
                log.error(ex.getMessage(), ex);
                Notification.show("Error connecting to the server!. Try later");
            }
        });
    }

    public void setStones(String stones) {
        this.pit.setValue(stones);
    }
}

As you can see, LeftHouse (14), RightHouse (7), and all Pits components (1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13) are instances of PitComponent class.

PitLayoutComponent Class

PitLayoutComponent is a Horizontal Layout designed to include all pits components.


@SpringComponent
@UIScope
public class PitLayoutComponent extends VerticalLayout implements KeyNotifier {

    private PitComponent pit1 ;
    private PitComponent pit2 ;
    private PitComponent pit3 ;
    private PitComponent pit4 ;
    private PitComponent pit5 ;
    private PitComponent pit6 ;

    private PitComponent pit8 ;
    private PitComponent pit9 ;
    private PitComponent pit10;
    private PitComponent pit11 ;
    private PitComponent pit12 ;
    private PitComponent pit13 ;

    public PitLayoutComponent(GameController gameController) {
        this.pit1 = new PitComponent(firstPitPlayerA, gameController);
        this.pit2 = new PitComponent(secondPitPlayerA, gameController);
        this.pit3 = new PitComponent(thirdPitPlayerA, gameController);
        this.pit4 = new PitComponent(forthPitPlayerA, gameController);
        this.pit5 = new PitComponent(fifthPitPlayerA, gameController);
        this.pit6 = new PitComponent(sixthPitPlayerA, gameController);

        this.pit8 = new PitComponent(firstPitPlayerB, gameController);
        this.pit9 = new PitComponent(secondPitPlayerB, gameController);
        this.pit10 = new PitComponent(thirdPitPlayerB, gameController);
        this.pit11 = new PitComponent(forthPitPlayerB, gameController);
        this.pit12 = new PitComponent(fifthPitPlayerB, gameController);
        this.pit13 = new PitComponent(sixthPitPlayerB, gameController);
        HorizontalLayout playerAPits = new HorizontalLayout(pit1, pit2, pit3, pit4, pit5, pit6);
        HorizontalLayout playerBPits = new HorizontalLayout(pit13, pit12, pit11, pit10, pit9, pit8);

        add(playerBPits, playerAPits);
    }

    public void fillPitStones (KalahaGame game){
        this.pit1.setStones(game.getPitStones(firstPitPlayerA).toString());
        this.pit2.setStones(game.getPitStones(secondPitPlayerA).toString());
        this.pit3.setStones(game.getPitStones(thirdPitPlayerA).toString());
        this.pit4.setStones(game.getPitStones(forthPitPlayerA).toString());
        this.pit5.setStones(game.getPitStones(fifthPitPlayerA).toString());
        this.pit6.setStones(game.getPitStones(sixthPitPlayerA).toString());
        this.pit8.setStones(game.getPitStones(firstPitPlayerB).toString());
        this.pit9.setStones(game.getPitStones(secondPitPlayerB).toString());
        this.pit10.setStones(game.getPitStones(thirdPitPlayerB).toString());
        this.pit11.setStones(game.getPitStones(forthPitPlayerB).toString());
        this.pit12.setStones(game.getPitStones(fifthPitPlayerB).toString());
        this.pit13.setStones(game.getPitStones(sixthPitPlayerB).toString());
    }
}

KalahaGameComponent Class

KalahaGameComponent is a Horizontal Layout designed to include the three main layouts of the game including PlayerTurnLayout, GameLayout, and ActionsLayout.


@UIScope
@SpringComponent
@Slf4j
@Component
public class KalahaGameComponent extends VerticalLayout implements KeyNotifier {

    private final PitLayoutComponent pitLayoutComponent;

    private final PitComponent rightHouse ;
    private final PitComponent leftHouse ;

    final Label playerTurnLabel;
    final TextField playerTurnTextField;

    final Label winLabel;


    public KalahaGameComponent(PitLayoutComponent pitLayoutComponent, @Autowired GameController gameController) {
        this.pitLayoutComponent = pitLayoutComponent;

        this.playerTurnLabel = new Label("Player turn:");
        this.playerTurnTextField = new TextField("");
        this.playerTurnTextField.setReadOnly(true);

        // build layout for game information
        HorizontalLayout turnLayout = new HorizontalLayout(playerTurnLabel, playerTurnTextField);
        turnLayout.setAlignItems(Alignment.CENTER);
        add(turnLayout);

        rightHouse = new PitComponent(7 , gameController);
        rightHouse.setAlignItems(Alignment.CENTER);
        rightHouse.add(new Label("Player A"));

        leftHouse = new PitComponent(14, gameController);
        leftHouse.setAlignItems(Alignment.CENTER);
        leftHouse.add(new Label("Player B"));

        HorizontalLayout gameLayout = new HorizontalLayout(leftHouse, pitLayoutComponent, rightHouse);
        gameLayout.setAlignItems(Alignment.CENTER);

        add(gameLayout);

        // Adding the win layout
        winLabel = new Label("");
        winLabel.setVisible(false);
        winLabel.getStyle().set("font-size", "50px");
        winLabel.getStyle().set("color", "#ff0000");

        HorizontalLayout winLayout = new HorizontalLayout(winLabel);
        winLayout.setAlignItems(Alignment.CENTER);

        add(winLayout);

        setAlignItems(Alignment.CENTER);
    }

    public TextField getPlayerTurnTextField() {
        return playerTurnTextField;
    }

    public void fillMancala(KalahaGame game) {
        this.leftHouse.setStones(game.getLeftHouseStones().toString());
        this.rightHouse.setStones(game.getRightHouseStones().toString());
        this.pitLayoutComponent.fillPitStones(game);
    }

    public void newGame(KalahaGame game) {
        this.fillMancala(game);
        this.winLabel.setVisible(false);
    }

    @EventListener
    public void handleFlushEvent (SowEvent event) {
        KalahaGame game = event.getGame();
        this.fillMancala(game);
        this.playerTurnTextField.setValue(event.getGame().getPlayerTurn());

        Integer playerARemainingStones = game.getPlayerAStones();
        Integer playerBRemainingStones = game.getPlayerBStones();

        if (playerARemainingStones == 0 || playerBRemainingStones ==0){
            Integer totalA = playerARemainingStones + game.getRightHouseStones();
            Integer totalB = playerBRemainingStones + game.getLeftHouseStones();

            this.leftHouse.setStones(totalB.toString());
            this.rightHouse.setStones(totalA.toString());

            if (totalA > totalB)
                this.winLabel.setText("Game Over!. Player A Won!!!");
            else
                this.winLabel.setText("Game Over!. Player B Won!!!");

            this.winLabel.setVisible(true);
        }
    }
}

MainView Class

MainView is a Vertical Layout which includes all components of the game. It also provides the default routing path for the web application called ‘mancala‘.


@Route ("mancala")
@Slf4j
public class MainView extends VerticalLayout {

private final Button startGameBtn;

final Label gameIdLabel;
final TextField gameIdTextField;

@Autowired
private GameController gameController;

final KalahaGameComponent kalahaGameComponent;

public MainView(KalahaGameComponent kalahaGameComponent, GameController gameController) {
this.kalahaGameComponent = kalahaGameComponent;
this.gameController = gameController;

// build the game information layout
this.startGameBtn = new Button("Start Game");
this.gameIdLabel = new Label("Game Id:");
this.gameIdTextField = new TextField("", "", "");
this.gameIdTextField.setReadOnly(true);
this.gameIdTextField.setMinLength(50);

// build layout for game id
HorizontalLayout gameIdLayout = new HorizontalLayout(gameIdLabel, gameIdTextField);
gameIdLayout.setAlignItems(Alignment.CENTER);
add(gameIdLayout);

// adding the game itself
add(kalahaGameComponent);

// build layout for actions
HorizontalLayout actions = new HorizontalLayout(startGameBtn);
add(actions);

        // Instantiate and edit new Customer the new button is clicked
        startGameBtn.addClickListener(e -> {
            try {
                KalahaGame game = this.gameController.startNewGame();
                kalahaGameComponent.newGame(game);
                this.gameIdTextField.setValue(game.getId());
                this.kalahaGameComponent.getPlayerTurnTextField().setValue("");

                Notification.show("New Game started. id:" + game.getId(), 3000, Notification.Position.MIDDLE);

            } catch (ApiConnectionException ex) {
                Notification.show("Error!. Message:" + ex.getMessage());
                log.error(ex.getMessage(), ex);
            }
        });
}
}

5 — Exception Handling

We have defined a custom exception class to handle exceptions might throw while interacting with ‘mancala-api‘ service:

public class ApiConnectionException extends RuntimeException {
    public ApiConnectionException(String message) {
        super(message);
    }
}

6 — Custom Events

Using Events is the most appropriate way of handling various situations might happen while users are interacting with our user interface. We have defined a custom Event class called SowEvent representing a single sow event while user clicks on specific pit index:

/*
    This event is fired when user clicks on any pit to sow the game. As a result of this event, a call is made to

    Mancala Api and the application is filled with the results of sowing the pit for selected index
 */

@Getter
@Setter
public class SowEvent extends ApplicationEvent {

    private KalahaGame game;
    private Integer pitIndex;
    public SowEvent(Object source, KalahaGame game, Integer pitIndex) {
        super(source);
        this.game = game;
        this.pitIndex = pitIndex;
    }
}

7 — Spring Sleuth and Zipkin Configuration

To enable Spring Cloud Sleuth functioning for this application, we have to add below property in our application.properties file:

#Sleuth configurations
spring.sleuth.sampler.probability=1

To enable Spring Cloud Zipkin functioning, we have to add below properties in our application.properties file:


#Zipkin configurations
spring.zipkin.base-url=http://localhost:9411/

Since we are using Docker-compose to build and run the Zipkin server, we will override the above property with the actual value in our docker-compose.yml file as below:

 mancala-web:
    build: ../mancala-microservice/mancala-web
    links:
      - consul-server
      - zipkin-server
    environment:
      - SPRING_CLOUD_CONSUL_HOST=consul-server
      - SPRING_APPLICATION_NAME=mancala-web
      - MANCALA_API_SERVICE_ID= mancala-api
      - SPRING_ZIPKIN_BASE_URL=http://zipkin-server:9411/

We also need to have below dependency in our pom.xml file:


<!-- Zipkin -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

<!-- Sleuth-->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

9 — Spring Actuator, Micrometer and Prometheus Server Integration

As I explained in the previous article, Spring Actuator collects metrics about the health of the application at runtime and expose them through well-defined endpoints. We can enhance this feature by adding Micrometer API to our Spring boot application and even further enabling our Spring boot application to send those collected metrics to Prometheus server by adding below dependencies to pom.xml:


<!-- Spring boot actuator to expose metrics endpoint -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- Micrometer core  -->
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-core</artifactId>
</dependency>

<!-- Micrometer Prometheus registry  -->
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

10 — ELK-Stack

The same as our ‘mancala-api‘ project, in order to add Elasticsearch integration for our spring boot application, we need to add below dependencies into pom.xml file:

<!-- Dependencies for LogStash -->
<dependency>
  <groupId>ch.qos.logback</groupId>
  <artifactId>logback-core</artifactId>
  <version>1.2.3</version>
</dependency>
<dependency>
  <groupId>net.logstash.logback</groupId>
  <artifactId>logstash-logback-encoder</artifactId>
  <version>6.1</version>
</dependency>

We also need to configure the spring logger to store all logs into specific folder «/logs» through our logback-spring.xml file and then we configure the Filebeat module to ingest logs from that folder and send it to Elasticsearch server where we can search and query about the details through its web interface.

The complete configuration of ELK-Stack servers is provided within the docker-compose.yml file.

11 — Integration Tests

To implement a complete set of integration tests for our two microservices, I have used Wiremock to provide comprehensive testing by mocking our ‘mancala-api‘ API through Wiremock service virtualization facility.

I have also used Spring Cloud Contract to implement Consumer Contract Testing between our two microservices, ‘mancala-web‘ and ‘mancala-api‘.

12 — Source Code

The complete source code for this project is available in my GitHub repository.

13 — How to Build and Run

To build and run this application, you will need to have Docker-compose installed in your system and follow the instructions I have provided here.

In my next article, I will explain various Testing strategies developed for above Mancala Game implementation: Building Mancala Game in Microservices using Spring Boot Part 4 — Testing.

Your valuable feedback and comments are highly appreciated!

Further Reading

Building an AI App That Learns to Play a Game

Keys to Success Game Development

The Future of Game Development