見出し画像

【マイクロサービスの統合】コミュニケーション、Netflix Eureka、API Gateway - Springboot3 第5回


はじめに


こんにちは、今日はマイクロサービスの統合方法三つについてまとめてみます。マイクロサービスアーキテクチャはモジュール化されたコンポーネントで構築された分散システムを可能にしますが、これらのサービス間の円滑な通信と管理が重要な課題です。 このブログでは、EurekaサービスディスカバリーとAPIゲートウェイを通じてマイクロサービス間の効率的なコミュニケーションをどのように実現するかについて探求します。


マイクロサービスの動機的コミュニケーション


RestTemplate

<Employee>

    private String departmentCode; 

<DepartmentDto>

employee-serviceのdtoにDepartmentDtoを生成
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class DepartmentDto {
    private Long id;
    private String departmentName;
    private String departmentDescription;
    private String departmentCode;
}

これらのフィールドは、DepartmentDto のサービスと同じです。

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

RestTemplateの関数に「Bean」アノテーションをつきます、

RestTemplateはスプリングフレームワークで提供するHTTP通信を簡単に行えるクラスです。 主にRESTfulウェブサービスと相互作用するために使用されます。 HTTPリクエストの送信、リクエスト、および応答マッピングに利用されます。

<EmployeeServiceImpl>

 // for microservices communication
    private RestTemplate restTemplate;

    @Override
    public EmployeeDto saveEmployee(EmployeeDto employeeDto) {

        Employee employee = new Employee(
                employeeDto.getId(),
                employeeDto.getFirstName(),
                employeeDto.getLastName(),
                employeeDto.getEmail(),
                // for microservices communication
                employeeDto.getDepartmentCode() 
        );
        Employee savedEmployee = employeeRepository.save(employee);

        EmployeeDto savedEmployeeDto = new EmployeeDto(
                savedEmployee.getId(),
                savedEmployee.getFirstName(),
                savedEmployee.getLastName(),
                savedEmployee.getEmail(),
                // for microservices communication
                savedEmployee.getDepartmentCode()
        );
        return savedEmployeeDto;
    }

    @Override
    public EmployeeDto getEmployeeById(Long employeeId) {

        Employee employee = employeeRepository.findById(employeeId).get();

        // for microservices communication
        ResponseEntity<DepartmentDto> responseEntity = restTemplate.getForEntity("http://localhost:8080/api/departments/" + employee.getDepartmentCode(),
                DepartmentDto.class);

        EmployeeDto employeeDto = new EmployeeDto(
                employee.getId(),
                employee.getFirstName(),
                employee.getLastName(),
                employee.getEmail(),
                // for microservices communication
                employee.getDepartmentCode()
        );
        return employeeDto;
    }

マイクロサービスの通信のため、「getDepartmentCode」と「restTemplate.getForEntity」を追加します。


<APIResponseDTO>

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class APIResponseDto {
    private EmployeeDto employee;
    private DepartmentDto department;
}

APIResponseDTOを生成して上記のコードを作成すます。

<EmployeeService>

APIResponseDto getEmployeeById(Long employeeId);

<EmployeeServiceImpl>

 @Override
    public APIResponseDto getEmployeeById(Long employeeId) {

        Employee employee = employeeRepository.findById(employeeId).get();

        // for microservices communication
        ResponseEntity<DepartmentDto> responseEntity = restTemplate.getForEntity("http://localhost:8080/api/departments/" + employee.getDepartmentCode(),
                DepartmentDto.class);

        DepartmentDto departmentDto = responseEntity.getBody();

        EmployeeDto employeeDto = new EmployeeDto(
                employee.getId(),
                employee.getFirstName(),
                employee.getLastName(),
                employee.getEmail(),
                // for microservices communication
                employee.getDepartmentCode()
        );

        APIResponseDto apiResponseDto = new APIResponseDto();
        apiResponseDto.setEmployee(employeeDto);
        apiResponseDto.setDepartment(departmentDto);

        return apiResponseDto;
    }

<EmployeeController>

public ResponseEntity<APIResponseDto> getEmployee(@PathVariable("id") Long employeeId) {
        APIResponseDto apiResponseDto = employeeService.getEmployeeById(employeeId);
        return new ResponseEntity<>(apiResponseDto, HttpStatus.OK);
    }

全体的にタイプを「EmployeeDto」から「APIResponseDto」で修正します。


POSTで社員を生成
GETで社員を照会

社員とともに、部署のデータも応答に表示されていることがわかります。社員サービスから部署サービスへのREST API Call が成功したということです。


WebClient


		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webflux</artifactId>
		</dependency>

「webflux」の依存性を追加します。

@SpringBootApplication
public class EmployeeServiceApplication {

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

	@Bean
	public WebClient webClient() {
		return WebClient.builder().build();
	}
	
	public static void main(String[] args) {
		SpringApplication.run(EmployeeServiceApplication.class, args);
	}

}

restTemplateの代わりにWebClientをbeanで設定します。

<EmployeeServiceImpl>

   @Override
    public APIResponseDto getEmployeeById(Long employeeId) {

        Employee employee = employeeRepository.findById(employeeId).get();

//        // for microservices communication
//        ResponseEntity<DepartmentDto> responseEntity = restTemplate.getForEntity("http://localhost:8080/api/departments/" + employee.getDepartmentCode(),
//                DepartmentDto.class);
//
//        DepartmentDto departmentDto = responseEntity.getBody();

        DepartmentDto departmentDto = webClient.get()
                .uri("http://localhost:8080/api/departments/" + employee.getDepartmentCode())
                .retrieve()
                .bodyToMono(DepartmentDto.class)
                .block();

        EmployeeDto employeeDto = new EmployeeDto(
                employee.getId(),
                employee.getFirstName(),
                employee.getLastName(),
                employee.getEmail(),
                // for microservices communication
                employee.getDepartmentCode()
        );

        APIResponseDto apiResponseDto = new APIResponseDto();
        apiResponseDto.setEmployee(employeeDto);
        apiResponseDto.setDepartment(departmentDto);

        return apiResponseDto;
    }

restTemplateをコメント処理し、WebClientを利用してdepartmentDtoを再定義します。

GETリクエスト成功

では、なぜWebClientが推薦されますかという疑問点ができるかもしれないですね。
非同期およびリアクティブサポート、関数型プログラミングスタイル、リアクティブストリームサポートなどの理由でWebClientを使用することを推奨しています。



Spring Cloud Open Feign

Spring Cloud OpenFeignは、スプリングベースのマイクロサービスアーキテクチャにおいて、異なるマイクロサービス間のHTTP通信を簡単に行うためのライブラリです。 HTTP要求を送信するクライアント コードをインターフェイスとして定義する方法を使用して、より高いレベルの抽象化を提供します。 また、Netflix Eurekaのようなサービスディスカバリークライアントと一緒に使用できます。また、サーキットブレーカーをサポートしてサービス間の通信に障害が発生した場合、サーキットブレーカーを使用して障害を処理し、サービスの可用性を維持することができます。


	<properties>
		<java.version>17</java.version>
		<spring-cloud.version>2022.0.4</spring-cloud.version>
	</properties>
...

 <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-openfeign</artifactId>
    </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>

Spring InitializrでSpring Cloud OpenFeignの依存性を探して、pom.xmlに追加します。<properties>, <dependency>, <dependencyManagement> で分けて追加します。

@SpringBootApplication
@EnableFeignClients
public class EmployeeServiceApplication {

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

//	@Bean
//	public WebClient webClient() {
//		return WebClient.builder().build();
//	}

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

}

webClientはコメント処理して、EmployeeServiceApplicationのクラスに@EnableFeignClientsアノテーションを付きます。

<APIClient>

@FeignClient(url = "http://localhost:8080", value= "DEPARTMENT-SERVICE")
public interface APIClient {

    @GetMapping("api/departments/{department-code}")
   DepartmentDto getDepartment(@PathVariable("department-code") String DepartmentCode);
}

serviceパッケージの中にAPIClientクラスを作ります。@FeignClientアノテーションを付き、DepartmentControllerのgetDepartmentを追加し、urlを修正して、ビジネスロジックをなくします。


@Service
@AllArgsConstructor
public class EmployeeServiceImpl implements EmployeeService {

    private EmployeeRepository employeeRepository;

    // for microservices communication
    //private RestTemplate restTemplate;

    //private WebClient webClient;
    private APIClient apiClient;

    ...

    @Override
    public APIResponseDto getEmployeeById(Long employeeId) {

        Employee employee = employeeRepository.findById(employeeId).get();

//        // for microservices communication
//        ResponseEntity<DepartmentDto> responseEntity = restTemplate.getForEntity("http://localhost:8080/api/departments/" + employee.getDepartmentCode(),
//                DepartmentDto.class);
//
//        DepartmentDto departmentDto = responseEntity.getBody();

//        DepartmentDto departmentDto = webClient.get()
//                .uri("http://localhost:8080/api/departments/" + employee.getDepartmentCode())
//                .retrieve()
//                .bodyToMono(DepartmentDto.class)
//                .block();

       DepartmentDto departmentDto = apiClient.getDepartment(employee.getDepartmentCode());

        EmployeeDto employeeDto = new EmployeeDto(
                employee.getId(),
                employee.getFirstName(),
                employee.getLastName(),
                employee.getEmail(),
                // for microservices communication
                employee.getDepartmentCode()
        );

        APIResponseDto apiResponseDto = new APIResponseDto();
        apiResponseDto.setEmployee(employeeDto);
        apiResponseDto.setDepartment(departmentDto);

        return apiResponseDto;
    }
}

webClientで定義したdepartmentDtoをapiClientを通じて再定義します。より簡単でしょう。

EmployeeServiceApplicationを再起動して、Postmanでテストしましょう。

GETリクエストの成功


Netflix Eureka - Service Registry and Discovery


EurekaプロジェクトをModuleで読み込む

Eureka Server 依存性追加
springboot-microservicesのフォルダにダウンロードして
圧縮されたデータを元に戻す
service-registryが springboot-microservicesのModuleで
読み込まれた。


Eurekaサーバー実装

@SpringBootApplication
@EnableEurekaServer
public class ServiceRegistryApplication {

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

}

@EnableEurekaServerアノテーションを追加します。

<application.properties> 

spring.application.name=SERVICE-REGISTRY
server.port=8761
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
アプリの起動時(http://localhost:8761/), Spring Eureka画面


Eureka Clientをdepartment-serviceの Microserviceに登録

department-serviceの<pom.xml>

	<properties>
		<java.version>17</java.version>
		<spring-cloud.version>2022.0.4</spring-cloud.version>
	</properties>

...
	    <dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</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>

依存性を追加。


department-serviceの<application.properties>

spring.application.name=DEPARTMENT-SERVICE
eureka.instance.client.serverUrl.defaultZone=http://localhost:8761/eureka

アプリケーションの名称を入力
service-registryのポート番号を含めたURLを入力


department-serviceの インスタンスを再鼓動すると、
Eureka Serverにアプリケーションが表示された。


Eureka Clientをemployee-serviceの Microserviceに登録

employee-serviceの<pom.xml>

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

Eureka Clienのemployee-serviceのpom.xmlに依存性を追加します。

再起動すると、EMPLOYEE-SERVICEのインスタンスが表示される


department-serviceの複数インスタンスの実行


Jarファイルが生成されました。
Buildの完了



-Dserver.port=8082ができなったので、
--server.port=8082で起動しました。

二つのインスタンスが表示される


ロードバランシング

//@FeignClient(url = "http://localhost:8080;http://localhost:8082", value= "DEPARTMENT-SERVICE")
@FeignClient(name= "DEPARTMENT-SERVICE")
public interface APIClient {

    @GetMapping("api/departments/{department-code}")
   DepartmentDto getDepartment(@PathVariable("department-code") String DepartmentCode);
}

}

  employee-serviceのAPIClientに「http://localhost:8082」をアノテーションに追加しても、できますが、nameにアプリケーションの名称を記入したら、より簡単になります。


EurekaクライアントはSpringクラウドロードバランサモジュールを内部的に提供します。


 employee-serviceを再起動して、ポート番号8081で始まる。


 employee-serviceのGETリクエストの成功

もし、department-serviceの中で一つが止まっちゃったら?

アプリケーションのAPを止める。
Eurekaで department-serviceの8081サービスだけで作動してる。
8081のリクエストはリスポンスがうまくできた。
8080のサービスの確認:できない。
8082のサービスの確認:うまくできる。


Spring Cloud Gateway



クライアントは、それぞれが消費したいすべてのマイクロサービスのホスト名とポートを記憶する必要はありません。

API Gatewayはマイクロサービスのための統合されたインターフェースを提供します。 おかげで、クライアントはマイクロサービスの内部的な詳細について詳しく知る必要がありません。

また、セキュリティ、モニタリング、トラフィックに制限をかけるレート制限 などを集中化します。Spring Cloud Gatewayがその役割を果たします。

API Gateway プロジェクトの生成

Spring Initializrで「Gateway」、「Eureka Discovory Client」、「Actuator」の依存性を追加
api-gatewayプロジェクトをImportModule


api-gatewayをEureka ClientとしてEurekaサーバに登録

api-gatewayの<application.properties>

spring.application.name=API-GATEWAY
server.port=9191
eureka.instance.client.serverUrl.defaultZone=http://localhost:8761/eureka
localhost:8761のEurekaサーバーでAPI-GATEWAYの「UP」確認


API-Gatewayのルートの設定とPostmanテスト

api-gatewayの<application.properties>

spring.application.name=API-GATEWAY
server.port=9191
eureka.instance.client.serverUrl.defaultZone=http://localhost:8761/eureka
management.endpoints.web.exposure.include=*

## Routes for Employee Service
spring.cloud.gateway.routes[0].id=EMPLOYEE-SERVICE
spring.cloud.gateway.routes[0].uri=lb://EMPLOYEE-SERVICE
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/employees/**

## Routes for Department Service
spring.cloud.gateway.routes[1].id=DEPARTMENT-SERVICE
spring.cloud.gateway.routes[1].uri=lb://DEPARTMENT-SERVICE
spring.cloud.gateway.routes[1].predicates[0]=Path=/api/departments/**

 「lb」はロード バランサの略で、その後にサービス レジストリ サービス ID が続きます。
「predicates[0]=Path=」の後はControllerの「@RequestMapping」のエンドポイントです。「**」は全てのエンドポイントを意味すます。

9191ポート番号で部署照会リクエストの成功
9191ポート番号で社員照会リクエストの成功


Spring Cloud Gatewayを使用したルートの自動作成

API Gatewayの構造例
 Spring Cloud Gateway Propertiesで「spring.cloud.gateway.discovery.locator.enabled」を探す
「 spring.cloud.gateway.discovery.locator.lower-case-service-id」も。
spring.application.name=API-GATEWAY
server.port=9191
eureka.instance.client.serverUrl.defaultZone=http://localhost:8761/eureka
management.endpoints.web.exposure.include=*
spring.cloud.gateway.discovery.locator.enabled=true
logging.level.org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping=DEBUG

### Routes for Employee Service
#spring.cloud.gateway.routes[0].id=EMPLOYEE-SERVICE
#spring.cloud.gateway.routes[0].uri=lb://EMPLOYEE-SERVICE
#spring.cloud.gateway.routes[0].predicates[0]=Path=/api/employees/**
#
### Routes for Department Service
#spring.cloud.gateway.routes[1].id=DEPARTMENT-SERVICE
#spring.cloud.gateway.routes[1].uri=lb://DEPARTMENT-SERVICE
#spring.cloud.gateway.routes[1].predicates[0]=Path=/api/departments/**

「spring.cloud.gateway.discovery.locator.enabled」と「 spring.cloud.gateway.discovery.locator.lower-case-service-id」を「true」で設定します。先ほど設定したコードはコメント処理します。

ログレベルの設定も「logging.level.org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping=DEBUG」でします。

9191ポート番号で社員照会リクエストの成功
「localhost:9191/employee-service/api/employees/{id}」
真ん中にアプリケーションの名称がはいっている。


このようにSprint Cloud APIは、特定のリクエストのルートを自動的に作成します。


9191ポート番号で部署照会リクエストの成功「localhost:9191/department-service/api/departments/{departmentCode}」真ん中にアプリケーションの名称がはいっている。

通常、ルートは手動で設定するため、要件に基づいて手動で設定します。新しいメンバーがチームに入ってくるたびに、 新しいメンバーは簡単にルートを理解できます。

そのためコメント処理したものを解除し、自動設定をコメント処理します。

spring.application.name=API-GATEWAY
server.port=9191
eureka.instance.client.serverUrl.defaultZone=http://localhost:8761/eureka
management.endpoints.web.exposure.include=*

#spring.cloud.gateway.discovery.locator.enabled=true
#spring.cloud.gateway.discovery.locator.lower-case-service-id=true
#logging.level.org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping=DEBUG


## Routes for Employee Service
spring.cloud.gateway.routes[0].id=EMPLOYEE-SERVICE
spring.cloud.gateway.routes[0].uri=lb://EMPLOYEE-SERVICE
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/employees/**

## Routes for Department Service
spring.cloud.gateway.routes[1].id=DEPARTMENT-SERVICE
spring.cloud.gateway.routes[1].uri=lb://DEPARTMENT-SERVICE
spring.cloud.gateway.routes[1].predicates[0]=Path=/api/departments/**


今日の作業、GithubにCommitとPushまでスムーズに!!!


私のGithubのレポジトリです~。https://github.com/Commonerd/springboot-microservices


最後に


マイクロサービスアーキテクチャの成功的な実装は、サービス間の通信と管理を効果的に扱うことに依存します。 EurekaとAPI Gatewayによるサービスディスカバリーと通信は、このようなアーキテクチャをサポートし、改善する重要なツールの一つです。 これらのツールを適切に活用すると、マイクロサービスベースのアプリケーションをより安定的かつ拡張可能に構築し、ユーザーエクスペリエンスとシステムパフォーマンスを向上させることができます。



エンジニアファーストの会社 株式会社CRE-CO
ソンさん



【参考】


  • [Udemy] Building Microservices with Spring Boot & Spring Cloud


この記事が気に入ったらサポートをしてみませんか?