3

Microservices with Spring Boot 3 and Spring Cloud

 1 year ago
source link: https://piotrminkowski.com/2023/03/13/microservices-with-spring-boot-3-and-spring-cloud/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Microservices with Spring Boot 3 and Spring Cloud

Screenshot-2023-03-12-at-17.20.22.png?fit=2070%2C1164&ssl=1

This article will teach you how to build microservices with Spring Boot 3 and the Spring Cloud components. It’s a tradition that I describe this topic once a new major version of Spring Boot is released. As you probably know, Spring Boot 3.0 is generally available since the end of November 2022. In order to compare changes, you can read my article about microservices with Spring 2 written almost five years ago.

In general, we will cover the following topics in this article:

  • Using Spring Boot 3 in cloud-native development
  • Provide service discovery for all microservices with Spring Cloud Netflix Eureka. Anticipating your questions – yes, Eureka is still there. It’s the last of Netflix microservices components still available in Spring Cloud
  • Spring Cloud OpenFeign in inter-service communication
  • Distributed configuration with Spring Cloud Config
  • API Gateway pattern with Spring Cloud Gateway including a global OpenAPI documentation with the Springdoc project
  • Collecting traces with Micrometer OpenTelemetry and Zipkin

Fortunately, the migration from Spring Boot 2 to 3 is not a painful process. You can even check it out in my example repository, which was originally written in Spring Boot 2. The list of changes is not large. However, times have changed during the last five years… And we will begin our considerations from that point.

Running Environment

Here are the results of my quick 1-day voting poll run on Twitter. I assume that those results are meaningful since around 900 people voted. As you probably expect, currently, the first-choice platform for running your Spring Boot microservices is Kubernetes. I don’t have a survey conducted five years ago, but the results would probably be significantly different. Even if you had Kubernetes in your organization 5 years ago, you were probably starting a migration of your apps or at least it was in progress. Of course, there might be some exceptions, but I’m thinking about the vast majority.

Screenshot-2023-03-10-at-11.10.18.png?resize=541%2C252&ssl=1

You could migrate to Kubernetes during that time, but also Kubernetes ecosystem has changed a lot. There are many useful tools and platform services you may easily integrate with your apps. We can at least mention Kubernetes native solutions like service mesh (e.g. Istio) or serverless (e.g. Knative). The main question here is: if I’m running microservices on Kubernetes are Spring Cloud components still relevant? The answer is: in most cases no. Of course, you can still use Eureka for service discovery, Spring Cloud Config for a distributed configuration, or Spring Cloud Gateway for the API gateway pattern. However, you can easily replace them with Kubernetes built-in mechanisms and additional platform services.

To conclude, this article is not aimed at Kubernetes users. It shows how to easily run microservices architecture anywhere. If you are looking for staff mainly related to Kubernetes you can read my articles about the best practices for Java apps and microservices there.

Source Code

If you would like to try it by yourself, you may always take a look at my source code. In order to do that you need to clone my GitHub repository. Then you should just follow my instructions.

Before we proceed to the source code, let’s take a look at the following diagram. It illustrates the architecture of our sample system. We have three independent Spring Boot 3 microservices, which register themself in service discovery, fetch properties from the configuration service, and communicate with each other. The whole system is hidden behind the API gateway. Our Spring Boot 3 microservices send traces to the Zipkin instance using the Micrometer OTEL project.

spring-boot-3-microservices-arch

Currently, the newest version of Spring Cloud is 2022.0.1. This version of spring-cloud-dependencies should be declared as a BOM for dependency management.

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

Step 1: Configuration Server with Spring Cloud Config

To enable Spring Cloud Config feature for an application, we should first include spring-cloud-config-server to your project dependencies.

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

Then enable running the embedded configuration server during application boot use @EnableConfigServer annotation.

@SpringBootApplication
@EnableConfigServer
public class ConfigApplication {

   public static void main(String[] args) {
      new SpringApplicationBuilder(ConfigApplication.class).run(args);
   }

}

By default Spring Cloud Config Server stores the configuration data inside the Git repository. We will change that behavior by activating the native mode. In this mode, Spring Cloud Config Server reads property sources from the classpath. We place all the YAML property files inside src/main/resources/config. Here’s the config server application.yml file. It activates the native mode and overrides a default port to 8088.

server:
  port: 8088
spring:
  profiles:
    active: native

The YAML filename will be the same as the name of the service. For example, the YAML file of discovery-service is located here: src/main/resources/config/discovery-service.yml. Besides a default profile, we will also define the custom docker profile. Therefore the name of the config file will contain the docker suffix. On the default profile, we are connecting services through localhost with dynamically assigned ports. So, the typical configuration file for the default profile will look like that:

server:
  port: 0

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8061/eureka/

Here’s the typical configuration file for the default profile:

server:
  port: 8080

eureka:
  client:
    serviceUrl:
      defaultZone: http://discovery-service:8061/eureka/

In order to connect the config server on the client side we need to include the following module in Maven dependencies:

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

Depending on the running environment (localhost or docker) we need to provide different addresses for the config server:



application.yml

spring: config: import: "optional:configserver:http://config-service:8088" activate: on-profile: docker --- spring: application: name: discovery-service config: import: "optional:configserver:http://localhost:8088"

Step 2: Discovery Server with Spring Cloud Netflix Eureka

Of course, you can replace Eureka with any other discovery server supported by Spring Cloud. It can be Consul, Alibaba Nacos, or Zookeeper. The best way to run the Eureka server is just to embed it into the Spring Boot app. In order to do that, we first need to include the following Maven dependency:

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

Then we need to set the @EnableEurekaServer annotation on the main class.

@SpringBootApplication
@EnableEurekaServer
public class DiscoveryApplication {

   public static void main(String[] args) {
      new SpringApplicationBuilder(DiscoveryApplication.class).run(args);
   }

}

There is nothing new with that. As I already mentioned, the configuration files, discovery-service.yml or discovery-service-docker.yml, should be placed inside config-service module. We have changed Eureka’s running port from the default value (8761) to 8061. For the standalone Eureka instance, we have to disable registration and omit to fetch the registry. We just want to activate a single-node, demo discovery server.

server:
  port: 8061

eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

Once you have successfully started the application you may visit Eureka Dashboard available under the address http://localhost:8061/.

spring-boot-3-microservices-eureka

Step 3: Build Apps with Spring Boot 3 and Spring Cloud

Let’s take a look at a list of required Maven modules for our microservices. Each app has to get a configuration from the config-service and needs to register itself in the discovery-service. It also exposes REST API, automatically generates API documentation, and export tracing info to the Zipkin instance. We use the springdoc-openapi v2 library dedicated to Spring Boot 3. It generates documentation in both JSON and YAML formats available under the v3/api-docs path (or /v3/api-docs.yaml for the YAML format). In order to export traces to the Zipkin server, we will include the opentelemetry-exporter-zipkin module.

<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>
  <dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-otel</artifactId>
  </dependency>
  <dependency>
    <groupId>io.opentelemetry</groupId>
    <artifactId>opentelemetry-exporter-zipkin</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
    <version>2.0.2</version>
  </dependency>
</dependencies>

For the apps that call other services, we also need to include a declarative REST client. We will use Spring Cloud OpenFeign.

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

OpenFeign client automatically integrates with the service discovery. We just to set the name under which it is registered in Eureka inside the @FeingClient annotation. In order to create a client, we need to define an interface containing all the endpoints it has to call.

@FeignClient(name = "employee-service")
public interface EmployeeClient {

   @GetMapping("/organization/{organizationId}")
   List<Employee> findByOrganization(@PathVariable("organizationId") Long organizationId);
	
}

During the demo, we will send all the traces to Zipkin. It requires setting the value of the probability parameter to 1.0. In order to override the default URL of Zipkin we need to use the management.zipkin.tracing.endpoint property.

management:
  tracing:
    sampling:
      probability: 1.0
  zipkin:
    tracing:
      endpoint: http://zipkin:9411/api/v2/spans

Here’s the implementation of the @RestController in department-service. It injects the repository bean to interact with the database, and the Feign client bean to communicate with employee-service. The rest of the code is pretty simple.

@RestController
public class DepartmentController {

  private static final Logger LOGGER = LoggerFactory.getLogger(DepartmentController.class);

  DepartmentRepository repository;
  EmployeeClient employeeClient;

  public DepartmentController(DepartmentRepository repository, EmployeeClient employeeClient) {
    this.repository = repository;
    this.employeeClient = employeeClient;
  }

  @PostMapping("/")
  public Department add(@RequestBody Department department) {
    LOGGER.info("Department add: {}", department);
    return repository.add(department);
  }
	
  @GetMapping("/{id}")
  public Department findById(@PathVariable("id") Long id) {
    LOGGER.info("Department find: id={}", id);
    return repository.findById(id);
  }
	
  @GetMapping("/")
  public List<Department> findAll() {
    LOGGER.info("Department find");
    return repository.findAll();
  }
	
  @GetMapping("/organization/{organizationId}")
  public List<Department> findByOrganization(@PathVariable("organizationId") Long organizationId) {
    LOGGER.info("Department find: organizationId={}", organizationId);
    return repository.findByOrganization(organizationId);
  }
	
  @GetMapping("/organization/{organizationId}/with-employees")
  public List<Department> findByOrganizationWithEmployees(@PathVariable("organizationId") Long organizationId) {
    LOGGER.info("Department find: organizationId={}", organizationId);
    List<Department> departments = repository.findByOrganization(organizationId);
    departments.forEach(d -> d.setEmployees(employeeClient.findByDepartment(d.getId())));
    return departments;
  }
	
}

As you see there are almost no differences in the app implementation between Spring Boot 2 and 3. The only thing you would have to do is to change all the javax.persistence to the jakarta.persistance.

Step 4: API Gateway with Spring Cloud Gateway

A gateway-service is the last app in our microservices architecture with Spring Boot 3. Beginning from Spring Boot 2 Spring Cloud Gateway replaced Netflix Zuul. We can also install it on Kubernetes using, for example, the Helm chart provided by VMWare Tanzu.

We will create a separate application with the embedded gateway. In order to do that we need to include Spring Cloud Gateway Starter in the Maven dependencies. Since our gateway has to interact with discovery and config services, it also includes Eureka Client Starter and Spring Cloud Config Starter. We don’t want to use it just as a proxy to the downstream services, but also we expose there OpenAPI documentation generated by all the apps. Since Spring Cloud Gateway is built on top of Spring WebFlux, we need to include Springdoc starters dedicated to that project.

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
  <groupId>io.micrometer</groupId>
  <artifactId>micrometer-tracing-bridge-otel</artifactId>
</dependency>
<dependency>
  <groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-zipkin</artifactId>
</dependency>
<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-starter-webflux-api</artifactId>
  <version>2.0.2</version>
</dependency>
<dependency>
  <groupId>org.springdoc</groupId>
  <artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
  <version>2.0.2</version>
</dependency>

In order to expose OpenAPI documentation from multiple v3/api-docs endpoints we need to use the GroupedOpenApi object. It should provide a way to switch between documentation generated by employee-service, department-service and organization-service. Those services run on dynamic addresses (or at least random ports). In that case, we will use the RouteDefinitionLocator bean to grab the current URL of each service. Then we just need to filter a list of routes to find only those related to our three microservices. Finally, we create the GroupedOpenApi containing a service name and path.

@SpringBootApplication
public class GatewayApplication {

   private static final Logger LOGGER = LoggerFactory
      .getLogger(GatewayApplication.class);

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

   @Autowired
   RouteDefinitionLocator locator;

   @Bean
   public List<GroupedOpenApi> apis() {
      List<GroupedOpenApi> groups = new ArrayList<>();
      List<RouteDefinition> definitions = locator
         .getRouteDefinitions().collectList().block();
      assert definitions != null;
      definitions.stream().filter(routeDefinition -> routeDefinition
         .getId()
         .matches(".*-service"))
         .forEach(routeDefinition -> {
            String name = routeDefinition.getId()
               .replaceAll("-service", "");
            groups.add(GroupedOpenApi.builder()
               .pathsToMatch("/" + name + "/**").group(name).build());
         });
      return groups;
   }

}

Here’s the configuration of gateway-service. We should enable integration with the discovery server by setting the property spring.cloud.gateway.discovery.locator.enabled to true. Then we may proceed to define the route rules. We use the Path Route Predicate Factory for matching the incoming requests, and the RewritePath GatewayFilter Factory for modifying the requested path to adapt it to the format exposed by downstream services. The uri parameter specifies the name of the target service registered in the discovery server. For example, organization-service is available on the gateway under the /organization/** path thanks to the predicate Path=/organization/**, and the rewrite path from /organization/** to the /**.



application.yml

spring: output: ansi: enabled: always cloud: gateway: discovery: locator: enabled: true routes: - id: employee-service uri: lb://employee-service predicates: - Path=/employee/** filters: - RewritePath=/employee/(?<path>.*), /$\{path} - id: department-service uri: lb://department-service predicates: - Path=/department/** filters: - RewritePath=/department/(?<path>.*), /$\{path} - id: organization-service uri: lb://organization-service predicates: - Path=/organization/** filters: - RewritePath=/organization/(?<path>.*), /$\{path} - id: openapi uri: http://localhost:${server.port} predicates: - Path=/v3/api-docs/** filters: - RewritePath=/v3/api-docs/(?<path>.*), /$\{path}/v3/api-docs springdoc: swagger-ui: urls: - name: employee url: /v3/api-docs/employee - name: department url: /v3/api-docs/department - name: organization url: /v3/api-docs/organization

As you see above, we are also creating a dedicated route for Springdoc OpenAPI. It rewrites the path for the /v3/api-docs context to serve it properly in the Swagger UI.

Step 5: Running Spring Boot 3 Microservices

Finally, we can run all our microservices. With the current configuration in the repository, you can start them directly on your laptop or with Docker containers.

Option 1: Starting directly on the laptop

In total, we have 6 apps to run: 3 microservices, a discovery server, a config server, and a gateway. We also need to run Zipkin to collect and store traces from communication between the services. In the first step, we should start the config-service. We can use Spring Boot Maven plugin for that. Just go to the config-service directory and the following command. It is exposed on the 8088 port.

$ mvn spring-boot:run

We should repeat the same step for all the other apps. The discovery-service is listening on the 8061 port, while the gateway-service on the 8060 port. Microservices will start on the dynamically generated port number thanks to the server.port=0 property in config. In the final step, we can run Zipkin using its Docker container with the following command:

$ docker run -d --name zipkin -p 9411:9411 openzipkin/zipkin

Option 2: Build images and run them with Docker Compose

In the first step, we will build the whole Maven project and Docker images for all the apps. I created a profile build-image that needs to be activated to build images. It mostly uses the build-image step provided by the Spring Boot Maven Plugin. However, for config-service and discovery-service I’m using Jib because it is built on top of the base image with curl installed. For both these services Docker compose needs to verify health checks before starting other containers.

$ mvn clean package -Pbuild-image

The docker-compose.yml is available in the repository root directory. The whole file is visible below. We need to run config-service before all other apps since it provides property sources. Secondly, we should start discovery-service. In both these cases, we are defining a health check that tests the HTTP endpoint using curl inside the container. Once we start and verify config-service and discovery-service we may run gateway-service and all the microservices. All the apps are running with the docker Spring profile activated thanks to the SPRING_PROFILES_ACTIVE environment variable. It corresponds to the spring.profiles.active param that may be defined in configuration properties.



docker-composer.yml

version: "3.7" services: zipkin: container_name: zipkin image: openzipkin/zipkin ports: - "9411:9411" config-service: image: piomin/config-service:1.1-SNAPSHOT ports: - "8088:8088" healthcheck: test: curl --fail http://localhost:8088/employee/docker || exit 1 interval: 5s timeout: 2s retries: 3 discovery-service: image: piomin/discovery-service:1.1-SNAPSHOT ports: - "8061:8061" depends_on: config-service: condition: service_healthy links: - config-service healthcheck: test: curl --fail http://localhost:8061/eureka/v2/apps || exit 1 interval: 4s timeout: 2s retries: 3 environment: SPRING_PROFILES_ACTIVE: docker employee-service: image: piomin/employee-service:1.2-SNAPSHOT ports: - "8080" depends_on: discovery-service: condition: service_healthy links: - config-service - discovery-service - zipkin environment: SPRING_PROFILES_ACTIVE: docker department-service: image: piomin/department-service:1.2-SNAPSHOT ports: - "8080" depends_on: discovery-service: condition: service_healthy links: - config-service - discovery-service - employee-service - zipkin environment: SPRING_PROFILES_ACTIVE: docker organization-service: image: piomin/organization-service:1.2-SNAPSHOT ports: - "8080" depends_on: discovery-service: condition: service_healthy links: - config-service - discovery-service - employee-service - department-service - zipkin environment: SPRING_PROFILES_ACTIVE: docker gateway-service: image: piomin/gateway-service:1.1-SNAPSHOT ports: - "8060:8060" depends_on: discovery-service: condition: service_healthy environment: SPRING_PROFILES_ACTIVE: docker links: - config-service - discovery-service - employee-service - department-service - organization-service - zipkin

Finally, let’s run all the apps using Docker Compose:

$ docker-compose up

Try it out

Once you start all the apps you can perform some test calls to the services through the gateway-service. It listening on the 8060 port. There is some test data automatically generated during startup. You can call the following endpoint to test all the services and communication between them:

$ curl http://localhost:8060/employee/
$ curl http://localhost:8060/department/organization/1
$ curl http://localhost:8060/department/organization/1/with-employees
$ curl http://localhost:8060/organization/
$ curl http://localhost:8060/organization/1/with-departments

Here are the logs generated by the apps during the calls visible above:

Screenshot-2023-03-13-at-11.13.46.png?resize=696%2C269&ssl=1

Let’s display Swagger UI exposed on the gateway. You can easily switch between contexts for all three microservices as you see below:

spring-boot-3-microservices-swagger

We can go to the Zipkin dashboard to verify the collected traces:

Screenshot-2023-03-13-at-11.38.58.png?resize=692%2C458&ssl=1

Final Thoughts

Treat this article as a quick guide to the most common components related to microservices with Spring Boot 3. I focused on showing you some new features since my last article on this topic. You could read how to implement tracing with Micrometer OpenTelemetry, generate API docs with Springdoc, or build Docker images with Spring Boot Maven Plugin.

Like this:

Loading...

Related


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK