Building Java Microservices from Scratch

Written by sergeidzeboev | Published 2023/01/31
Tech Story Tags: java | microservices | spring-framework | spring | spring-boot | docker | docker-compose | eureka

TLDRMicroservices architecture is a commonly used approach to building a system. In this article, I will show you how to build your microservices using Java and Spring Framework. The basic idea is to avoid forwarding requests to a service and let the Discovery Service decide which one is available. The complete code can be found here.via the TL;DR App

Today, microservices architecture is a common approach to building a system. In this article, I will show you how to build your microservices using Java and Spring Framework.

Let’s build a system that will contain the basic infrastructure services:

  • Gateway Service - redirects requests from a client to Business Services

  • Discovery Service - resolves the address of available service

  • Business Service - does the main job

The basic idea is to avoid forwarding requests to a service and let the discovery service decide which one is available, this will allow you to launch of as many instances as needed and make the system scalable.

What will be used to build the system:

  • Java 17
  • Spring boot 3.0.1
  • Maven
  • IntelliJ Idea
  • Docker-Compose

The complete code can be found here.

Creating the parent module

The first step in building the services is the parent module. The parent module will compose the services and will be a handy tool for building the services from one entry point. However, the main module is not required, and actually, for a production, you don’t want to compose services, since each of the services would be deployed independently.

In our environment, we will use the Spring Framework. Spring Framework is the most popular framework for building java back-end services, it provides many useful tools. So, to create the main module, visit https://start.spring.io/.

The website generates a project for you with all the necessary dependencies. For the initial service let’s create a project without any dependencies, we will use it as a parent module for other services:

<?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>3.0.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>ser.hkrn</groupId>
    <artifactId>microservice-package-hkrn</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>microservice-package-hkrn</name>
    <packaging>pom</packaging>
    <description>microservice-package-hkrn</description>

    <properties>
        <java.version>17</java.version>
    </properties>

    <modules>
        <module>discovery-service</module>
        <module>business-service</module>
        <module>gateway-service</module>
    </modules>

</project>

Note that we already added dependent modules that we are about to create:

   <modules>
        <module>discovery-service</module>
        <module>business-service</module>
        <module>gateway-service</module>
    </modules>

Creating Services: The Discovery Service

Discovery Service is a service that will help to find the physical address of a business with its name. It also handles balancing between several instances of the same service.

To create a service, choose the following dependencies:

  • Spring Security

  • Eureka Server

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

The Eureka Server will do the main job here. It is a tool from Netflix which does everything that the discovery service is expected to do. Spring security will restrict unauthorized access.

Now we need to configure the service. First of all, let’s update the parent project:

<parent>
    <groupId>ser.hkrn</groupId>
    <artifactId>microservice-package-hkrn</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>

The next step is to add some properties. To do this, we have to configure spring security by adding a name and a password:

spring.security.user.name=${eureka_login:login}
spring.security.user.password=${eureka_password:pass}

Using Spring SpEL here makes the properties configurable and allows you to add default values in order to launch the service locally. Since the Eureka Server has a client as well, we have to configure the client to exclude registering and fetching the registry:

eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

For Discovery Service use 8761 port:

server.port=8761

Then, create SecurityFilterChain which is a bean to configure HTTP security:

@Configuration
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf()
                .disable()
                .authorizeHttpRequests()
                .anyRequest()
                .authenticated()
                .and()
                .httpBasic()
                .and()
                .formLogin();

        return http.build();
    }

In short, the configuration:

  • Disables CSRF,
  • Allows only authenticated requests
  • Allows basic and form login

Finally, put @EnableEurekaServer annotation over the main class:

@EnableEurekaServer
@SpringBootApplication
public class DiscoveryServiceApplication {

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

}

Now we can launch the service and check if it works. If you did everything correctly you see the following Eureka page:

Creating Services: A Business Service

The next step is to create a business service. The service will do some tasks and will not be a part of the infrastructure system. So let’s create a new service with these dependencies:

  • Eureka Discovery Client

  • Spring Web

  • Spring Boot Actuator

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

Eureka Discovery Client connects to the Eureka Server. Spring Web will allow for the creation of controllers. Spring Boot Actuator is needed to allow Eureka Server to monitor the health of the service.

So, updating the parent project:

<parent>
    <groupId>ser.hkrn</groupId>
    <artifactId>microservice-package-hkrn</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>

Don't forget to change the group id to match the name of your service. The next step is to update properties.

Application name is used to register in the Eureka Server:

spring.application.name=business-service

URL to Eureka Server:

eureka.client.serviceUrl.defaultZone=http://${eureka_login:login}:${eureka_password:pass}@${eureka_url:localhost}:${eureka_port:8761}/eureka/

Application port:

server.port=${business_service_port:8081}

Now, let’s create a controller and put some logic into it. To keep it simple, let’s create a controller that returns the current version of the service:

@Controller
public class VersionController {

    @GetMapping("/resolve/version")
    public ResponseEntity<?> resolveVersion() {
        return ResponseEntity.ok(new Object() {
            public String version = "0.0.1-SNAPSHOT";
        });
    }
}

The business layer is done, now we can launch the Discovery Service and then the Business Service to see if it works.

On the Eureka webpage, you can see the registered Business Service:

Creating Services: A Gateway Service

The last service to create is the Gateway Service, which will redirect requests from a client to necessary services using Eureka. It will contain the same dependencies as Business Service does:

  • Eureka Discovery Client

  • Spring Web

  • Spring Boot Actuator

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

Update parent project:

<parent>
    <groupId>ser.hkrn</groupId>
    <artifactId>microservice-package-hkrn</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>

Add properties

Application name:

spring.application.name=gateway-service

Eureka service URL:

eureka.client.serviceUrl.defaultZone=http://${eureka.login:login}:${eureka.password:pass}@${eureka.url:localhost}:${eureka.port:8761}/eureka/

Server port:

server.port=8082

it's important to create RestTemplate bean:

@Configuration
public class RestTemplateConfig {

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

Note the @LoadBalanced annotation. It allows you to use the Eureka Server to balance the requests. Otherwise, requests will be sent to the given URL avoiding Eureka.

Now let’s create a controller class that will be used to redirect all the requests to a necessary web service using Eureka:

@Controller
public class BusinessController {

    private final RestTemplate restTemplate;

    public BusinessController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @GetMapping("/business")
    public ResponseEntity<?> getBusinessVersion() {
        return ResponseEntity.ok(
                restTemplate.getForObject("http://business-service/resolve/version", Object.class)
        );
    }
}

Basically, the controller receives requests and redirects them to the Business Service. Note that the name of the service is used in the URL: http://business-service/resolve/version. Eureka proceeds with the name and changes it to the IP address. Thus for communicating, it is enough to know only the service’s name.

Docker-Compose to automate deployment

The package of services is done. To automate deployment, let’s wrap everything in Docker. To do that, we will prepare some Dockerfiles.

Discovery-service Dockerfile:

FROM openjdk
COPY ./target/service-discovery-0.0.1-SNAPSHOT.jar /usr/app/
WORKDIR /usr/app
EXPOSE 8761
ENTRYPOINT ["java", "-jar", "service-discovery-0.0.1-SNAPSHOT.jar"]

Business-service Dockerfile:

FROM openjdk
COPY ./target/business-service-0.0.1-SNAPSHOT.jar /usr/app/
WORKDIR /usr/app
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "business-service-0.0.1-SNAPSHOT.jar"]

Gateway-service Dockerfile:

FROM openjdk
COPY ./target/gateway-service-0.0.1-SNAPSHOT.jar /usr/app/
WORKDIR /usr/app
EXPOSE 8082
ENTRYPOINT ["java", "-jar", "gateway-service-0.0.1-SNAPSHOT.jar"]

As can be seen from the files, there are a couple of similar actions for each service.

When Dockerfiles are done, we need to prepare Docker-Compose as a main script to launch all created services. Since the Docker-Compose script will launch all the services, it must be located in the parent service:

version: '3.1'

services:
  discovery-service:
    build: ./discovery-service
    restart: always
    environment:
      - eureka.login=${eureka_login}
      - eureka.password=${eureka_password}
      - server.port=${eureka_port}
    ports:
      - 8080:${eureka_port}

  business-service:
    build: ./business-service
    restart: always
    environment:
      - eureka.login=${eureka_login}
      - eureka.password=${eureka_password}
      - eureka.url=${eureka_url}
      - eureka.port=${eureka_port}
      - server.port=${business_service_port}
    ports:
      - 8081:${business_service_port}
    links:
      - discovery-service:${eureka_url}

  gateway-service:
    build: ./gateway-service
    restart: always
    environment:
      - eureka.login=${eureka_login}
      - eureka.password=${eureka_password}
      - eureka.url=${eureka_url}
      - eureka.port=${eureka_port}
      - server.port=${gateway_service_port}
    ports:
      - 8082:${gateway_service_port}
    links:
      - discovery-service:${eureka_url}

As can be seen from the script, it contains all the necessary configurations for each service, and particular values are used. Using the variables allows deploying the microservices pack to any environment. To provide particular values we will use .env files. For the production environment, we will create an .env file, and an .env-dev file for a development stand.

.env:

eureka_login=admin
eureka_password=CjPjaM97TybVR9O
eureka_url=eureka
eureka_port=8080
business_service_port=8081
gateway_service_port=8082

.env-dev:

eureka_login=user
eureka_password=123
eureka_url=eureka
eureka_port=8080
business_service_port=8081
gateway_service_port=8082

Launching

Finally, the project is done. In order to start it the only command we need to use is:

docker-compose up -d

Conclusion

In order to create a microservice system using Spring Framework we need to:

  • Create a parent project
  • Create Discovery Service
  • Create Gateway Service
  • Create Business Services
  • Create Dockerfiles for each service
  • Create Docker-Compose on the parent-level service
  • Create .env files for each stand


Written by sergeidzeboev | Senior Java Developer
Published by HackerNoon on 2023/01/31