見出し画像

【マイクロサービスの拡張実装】API構築から有機的結合まで - Springboot3 第9回


 1. はじめに


こんにちは、今日は既存のマイクロサービスに一つのサービスを追加して、マイクロサービスがどうやって拡張されるのか、勉強・実習します!
今まで勉強したマイクロサービスの要素を全部復習します。

 Oraganization Serviceの追加構築になるます。



2. API構築過程


サービス作成とDB設定

 Spring initializrで依存性追加して、「Generate」ボタンを押す。
フォルダに解凍します。
モジュールをインポートします。
モジュールがうまくインポートされたようです。
MysqlでDBを生成します。
orginization-serviceのapplication.propertiesで下記の設定コードを入力してdb名とサーバーポート番号を別に設定します。
spring.datasource.url=jdbc:mysql://localhost:3306/department_db
spring.datasource.username=root
spring.datasource.passwod=自分の暗号

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.hibernate.ddl-auto=update

server.port=8083


JPA EntityとJPA Repositoryの作成

1. entity, repository, service, controller, dto, mapperパッケージを生成します。
2. Organizationエンティティを作成し、アノテーションとテーブルやフィールドを定義するコードを入力します。
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "organizations")
public class Organization {

    @Id
    @GeneratedValue(strategy =  GenerationType.IDENTITY)
    private Long id;
    @Column(nullable = false)
    private String organizationName;
    private String organizationDescription;
    @Column(nullable = false, unique = true)
    private String organizationCode;
    @CreationTimestamp
    private LocalDateTime createdDate;
}


OrganizationRepository インターフェースを生成し、JpaRepository を継承します。
public interface OrganizationRepository extends JpaRepository<Organization, Long> {
    
}


orginization-service アプリケーションを実行します。


Mysql workbenceを確認すると、
テーブルとカラムがうまく生成されたことが確認できます。


DTOとMapperの生成

OrganizationDtoを生成します。
Lombokのアノテーションを追加します。
Orgaizationにある同じフィールドを追加します。
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class OrganizationDto {
    private Long id;
    private String organizationName;
    private String organizationDescription;
    private String organizationCode;
    private LocalDateTime createdDate;
}


OrgaizationMapperを生成し、
OrgaizationをOrgaizationDtoに、
OrgaizationDtoをOrganizationに変えるコードを作成します。
public class OrganizationMapper {

    public static OrganizationDto mapToOrganizationDto(Organization organization) {
        OrganizationDto organizationDto = new OrganizationDto(
                organization.getId(),
                organization.getOrganizationName(),
                organization.getOrganizationDescription(),
                organization.getOrganizationCode(),
                organization.getCreatedDate()
        );
        return organizationDto;
    }

    public static Organization mapToOrganization(OrganizationDto organizationDto) {
        Organization organization = new Organization(
                organizationDto.getId(),
                organizationDto.getOrganizationName(),
                organizationDto.getOrganizationDescription(),
                organizationDto.getOrganizationCode(),
                organizationDto.getCreatedDate()
        );
        return organization;
    }
}



REST API(登録・照会)

1.登録

OrganizationServiceで登録メソッドのためのインタフェースを定義します。
public interface OrganizationService {
    OrganizationDto saveOrganization(OrganizationDto organizationDto);
}


OrganizationServiceImplでビジネスロジックを実装します。
@Service
@AllArgsConstructor
public class OrganizationServiceImpl implements OrganizationService {

    private OrganizationRepository organizationRepository;

    @Override
    public OrganizationDto saveOrganization(OrganizationDto organizationDto) {
        //convert OrganizationDto into Organization jpa entity
        Organization organization = OrganizationMapper.mapToOrganization(organizationDto);

        Organization savedOrganization = organizationRepository.save(organization);

        return OrganizationMapper.mapToOrganizationDto(savedOrganization);


    }
}

注意すべき点は、OrganizationServiceImplクラスに@AllArgsConstructorアノテーションを付けないと、OrganizationRepositoryが使えなくなるので、必ず忘れずに入力することです。


OrganizationControllerも下記のように作成します。
@RestController
@RequestMapping("api/organizations")
@AllArgsConstructor
public class OrganizationController {

    private OrganizationService organizationService;


    // Build Save Organization REST API
    @PostMapping
    public ResponseEntity<OrganizationDto> saveOrganization(@RequestBody OrganizationDto organizationDto) {
        OrganizationDto savedOrganization = organizationService.saveOrganization(organizationDto);
        return new ResponseEntity<>(savedOrganization, HttpStatus.CREATED);
    }

}

ここにもOrganizationControllerクラスに@AllArgsConstructorアノテーションを付けないとOrganizationServiceが使えなくなるので、必ず忘れずに入力する。


Postmanで登録APIテスト成功
DBにもスムーズに。

2.照会

Repository、Service、ServiceImpl、Controllerの順にコードを書く


OrganizationRepository

public interface OrganizationRepository extends JpaRepository<Organization, Long> {

    Organization findByOrganizationCode(String organizationCode);

}

OrganizationServcie

    OrganizationDto getOrganizationByCode(String organizationCode);

OrganizationServcieImpl

    @Override
    public OrganizationDto getOrganizationByCode(String organizationCode) {
        Organization organization = organizationRepository.findByOrganizationCode(organizationCode);
        return OrganizationMapper.mapToOrganizationDto(organization);
    }

OrganizationController

    // Build Get Organization REST API
    @GetMapping("{code}")
    public ResponseEntity<OrganizationDto> getOrganizationByCode(@PathVariable("code") String organizationCode) {
        OrganizationDto organizationDto = organizationService.getOrganizationByCode(organizationCode);
        return ResponseEntity.ok(organizationDto);
    }


Postmanで照会APIテスト成功


employee-serviceからREST API呼び出し

もし、クライアントがEmployeeがorganizationに属し、employeeが唯一のorganization codeを持つことを要求したらどうしたらいいでしょうか。

Get Employee APIをorganizationと一緒にemployeeを返すように修正する必要があります。

まず、Employeeエンティティにorganization codeフィールドを追加します。
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "employees")
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String firstName;
    private String lastName;
    @Column(nullable = false, unique = true)
    private String email;
    private String departmentCode;
    private String organizationCode;
}


EmployeeDtoにもorganization code追加
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class EmployeeDto {
    private Long id;
    private String firstName;
    private String lastName;
    private String email;
    private String departmentCode;
    private String organizationCode;


}


EmployeeMapperにもorganization codeフィールドを取得するコードを追加します。
public class EmployeeMapper {

    public static EmployeeDto mapToEmployeeDto(Employee employee) {
        EmployeeDto employeeDto = new EmployeeDto(
                employee.getId(),
                employee.getFirstName(),
                employee.getLastName(),
                employee.getEmail(),
                employee.getDepartmentCode(),
                employee.getOrganizationCode()
        );
        return employeeDto;
    }

    public static Employee mapToEmployee(EmployeeDto employeeDto) {
        Employee employee = new Employee(
                employeeDto.getId(),
                employeeDto.getFirstName(),
                employeeDto.getLastName(),
                employeeDto.getEmail(),
                employeeDto.getDepartmentCode(),
                employeeDto.getOrganizationCode()
        );
                return employee;
    }
}
organization-serviceのdtoと同じように、
employee-serviceでdtoを作成します。
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class OrganizationDto {
    private Long id;
    private String organizationName;
    private String organizationDescription;
    private String organizationCode;
    private LocalDateTime createdDate;
}


APIResponseDtoで該当コードをOrganizationコードを追加。
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class APIResponseDto {
    private EmployeeDto employee;
    private DepartmentDto department;
    private OrganizationDto organization;
}


EmployeeServiceImplでwebclientを使ってdepartment情報を取得したのと同じ方法で、organization情報を取得します。
    //@CircuitBreaker(name = "${spring.application.name}", fallbackMethod = "getDefaultDepartment")
    @Retry(name = "${spring.application.name}", fallbackMethod = "getDefaultDepartment")
    @Override
    public APIResponseDto getEmployeeById(Long employeeId) {

        LOGGER.info("inside getEmployeeById() method");
        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());

        OrganizationDto organizationDto = webClient.get()
                .uri("http://localhost:8083/api/organizations/" + employee.getOrganizationCode())
                .retrieve()
                .bodyToMono(OrganizationDto.class)
                .block();

        EmployeeDto employeeDto = EmployeeMapper.mapToEmployeeDto(employee);

        APIResponseDto apiResponseDto = new APIResponseDto();
        apiResponseDto.setEmployee(employeeDto);
        apiResponseDto.setDepartment(departmentDto);
        apiResponseDto.setOrganization(organizationDto);
        return apiResponseDto;
    }

ここまでは一つのサービス(employee-service)からe別のサービス(organization-service)へapiを呼び出す方法を実装したコードでした。


テストのためにapi-gateway, config-server, service-registry, employee-service, department-serve, organization-serviceを全て実行してみましょう。


DBでemployeeテーブルを照会した後、organization_codeフィールドに「ABC_ORG」値を入れて、「apply」ボタンを押します。


大成功!


ここまで作業を一旦コミット/プッシュしました。
かなり大変ですね!


3. サービスの有機的結合


Eureka Clientに登録

organization-service の pom.xml に依存関係を追加。
<properties>
		<java.version>17</java.version>
		<spring-cloud.version>2022.0.4</spring-cloud.version>

	</properties>
	<dependencies>
		
		</dependency>
		<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>

	<build>
		...


department-service の application.properties のアプリケーション名と設定値を organization-serviceのapplication.properties にも適用します。当たり前ですが、アプリケーション名は変更します。
spring.application.name=ORGANIZATION-SERVICE
eureka.instance.client.serverUrl.defaultZone=http://localhost:8761/eureka


6つのサービスをオンにして動作させる。


localhost:8761に接続してみると、
ORGANIZATION-SERVICEがうまく動作しています。
今回の作業もコミット/プッシュしました。
可能であれば一つのコミットには一つの単位を入れるようにしています。
変更された部分が一目瞭然です。


Config Serverに登録

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


spring.application.nameだけを除いて、organization-serviceのapplicaton.propertiesのコードをorganization-service.propertiesに貼り付ける。organization-serviceのapplicaton.propertiesのコードはコメントアウト(spring.application.nameを除く)
 


application.properteisファイルでconfigサーバーから呼び出す設定コード値を追加します。
spring.config.import=optional:configserver:http://localhost:8888


コミットしてプッシュしてconfig-serverのorganization-service.propertiesの値の変更をGitHubに反映します。


config-serverとorganization-serviceをそれぞれ再起動すると、organization-serviceもconfig-serverから設定値を取得します。


Cloud Bus(RabbitMQ, Actuator)とAPI Gatewayの設定

organization-service/pom.xml

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-bus-amqp</artifactId>
		</dependency>
         <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-actuator</artifactId>
		</dependency>

organization-serviceのapplicatiop.properties 

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest

management.endpoints.web.exposure.include=*

api-gatewayのapplicatiop.properties

## Routes for Organization Service
spring.cloud.gateway.routes[2].id=ORGANIZATION-SERVICE
spring.cloud.gateway.routes[2].uri=lb://ORGANIZATION-SERVICE
spring.cloud.gateway.routes[2].predicates[0]=Path=/api/organizations/**

api-gateway, organization-service を再起動します。

9191ポート番号に「api/organizations/ABC_ORG」エンドポイントが入ると、正常にルーティングされたことが確認できます。


分散トレーシング(Zipkin)の設定

organization-serviceのpom.xml

<dependency>
			<groupId>io.micrometer</groupId>
			<artifactId>micrometer-observation</artifactId>
		</dependency>
		<dependency>
			<groupId>io.micrometer</groupId>
			<artifactId>micrometer-tracing-bridge-brave</artifactId>
		</dependency>
		<dependency>
			<groupId>io.zipkin.reporter2</groupId>
			<artifactId>zipkin-reporter-brave</artifactId>
		</dependency>
		<dependency>
			<groupId>io.github.openfeign</groupId>
			<artifactId>feign-micrometer</artifactId>
		</dependency>

orginization-serviceのapplication.properties

spring.zipkin.base-url=http://localhost:9411/
management.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spans

config-serverのorginization-service.properties

management.tracing.sampling.probability=1.0
logging.pattern.level=%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]
logging.level.org.springframework.web=DEBUG

management.zipkin.tracing.endpoint=http://localhost:9411/api/v2/spans
management.endpoints.web.exposure.include=*


zipkinを実行して、organization-serviceを再起動します。
Postmanでテストしてみるとうまく動作します。


application.name, traceId, spanId の順に表示されます。


Zipkin-serverのUI画面でもよく表示されます。


今日の作業もコミット/プッシュ

https://github.com/Commonerd/springboot-microservices/commits/main


4. 終わりに


今までdepartment-service, employee-serviceの二つのメインサービスだったマイクロサービスシステムに、organization-serviceを一つ追加してシステムを拡張してみました。

それぞれのサービスが内部的に完結性があって実際に実装してみる味がありますね!本当に面白いです!そして、思ったより一度システムを構築しておけば、Eureka ClientやSpring Cloudなどは似たような原理で適用されるので、再利用性も高く、開発の生産性も良いようですね。 もちろん、これは技術スタックがほぼ同じように適用されたので、そう思うかもしれません。もし、全く違うフレームワークと言語、DBを使うと全く違う状況が展開されるかもしれません。 でも、それでも、 

今までバックエンドロジックだけに集中していたら、次回は簡単にフロントエンドの領域をReactフレームワーク(ライブラリで見る人もいますが…)を使って簡単に実装してみようと思います。 かなり有名で人気もありますが、実際に使ったことがないのでワクワクしますね!


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



【参考】


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

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