![見出し画像](https://assets.st-note.com/production/uploads/images/117250035/rectangle_large_type_2_26b2fc4346930f2c394b8c94febb1571.jpeg?width=1200)
【マイクロサービスの拡張実装】API構築から有機的結合まで - Springboot3 第9回
1. はじめに
こんにちは、今日は既存のマイクロサービスに一つのサービスを追加して、マイクロサービスがどうやって拡張されるのか、勉強・実習します!
今まで勉強したマイクロサービスの要素を全部復習します。
![](https://assets.st-note.com/img/1695691587746-jEMMfjfBgo.png?width=1200)
2. API構築過程
サービス作成とDB設定
![](https://assets.st-note.com/img/1695692320029-XFs1I8imB5.png?width=1200)
![](https://assets.st-note.com/img/1695692460774-4ZIpzabG08.png?width=1200)
![](https://assets.st-note.com/img/1695692905586-j3ggHsnt38.png?width=1200)
![](https://assets.st-note.com/img/1695692984397-De75lw9r6i.png)
![](https://assets.st-note.com/img/1695693250731-je6Vrz3Uv3.png?width=1200)
![](https://assets.st-note.com/img/1695693520401-JFaJ9uqq0s.png?width=1200)
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の作成
![](https://assets.st-note.com/img/1695694186112-hc2Yi4YJVd.png?width=1200)
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;
}
![](https://assets.st-note.com/img/1695694304910-ofD56cjKKJ.png?width=1200)
public interface OrganizationRepository extends JpaRepository<Organization, Long> {
}
![](https://assets.st-note.com/img/1695694484755-DxWzKlTg9E.png?width=1200)
![](https://assets.st-note.com/img/1695694524216-Nrl0ab4aTB.png)
テーブルとカラムがうまく生成されたことが確認できます。
DTOとMapperの生成
![](https://assets.st-note.com/img/1695694785653-g3Ij41FTtY.png?width=1200)
Lombokのアノテーションを追加します。
Orgaizationにある同じフィールドを追加します。
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class OrganizationDto {
private Long id;
private String organizationName;
private String organizationDescription;
private String organizationCode;
private LocalDateTime createdDate;
}
![](https://assets.st-note.com/img/1695695116048-2jNXf4SNs3.png?width=1200)
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.登録
![](https://assets.st-note.com/img/1695696987367-udfpSLhTA2.png?width=1200)
public interface OrganizationService {
OrganizationDto saveOrganization(OrganizationDto organizationDto);
}
![](https://assets.st-note.com/img/1695696986635-Ldf6Nwk1be.png?width=1200)
@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が使えなくなるので、必ず忘れずに入力することです。
![](https://assets.st-note.com/img/1695696987097-VihOSZDaYQ.png?width=1200)
@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が使えなくなるので、必ず忘れずに入力する。
![](https://assets.st-note.com/img/1695696987792-SBrlSJlH5k.png?width=1200)
![](https://assets.st-note.com/img/1695696986295-Biab9Q5URH.png?width=1200)
2.照会
![](https://assets.st-note.com/img/1695702421002-FLdV4zCJvO.png?width=1200)
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);
}
![](https://assets.st-note.com/img/1695702505399-g91eexOW7y.png?width=1200)
employee-serviceからREST API呼び出し
もし、クライアントがEmployeeがorganizationに属し、employeeが唯一のorganization codeを持つことを要求したらどうしたらいいでしょうか。
Get Employee APIをorganizationと一緒にemployeeを返すように修正する必要があります。
![](https://assets.st-note.com/img/1695703092821-cBtd6HzT25.png?width=1200)
@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;
}
![](https://assets.st-note.com/img/1695703150640-j51jAIfdue.png?width=1200)
@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;
}
![](https://assets.st-note.com/img/1695703232301-450FUigMaY.png?width=1200)
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;
}
}
![](https://assets.st-note.com/img/1695703454934-m2I3GYGEXJ.png)
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;
}
![](https://assets.st-note.com/img/1695704087296-m3auuA7znc.png?width=1200)
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class APIResponseDto {
private EmployeeDto employee;
private DepartmentDto department;
private OrganizationDto organization;
}
![](https://assets.st-note.com/img/1695704189406-CSJEuqLlDc.png?width=1200)
//@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を呼び出す方法を実装したコードでした。
![](https://assets.st-note.com/img/1695705693492-Rn94kgGHt8.png)
![](https://assets.st-note.com/img/1695705908005-8L6ygjxBXD.png?width=1200)
![](https://assets.st-note.com/img/1695706173924-COuMpZ6ES5.png?width=1200)
![](https://assets.st-note.com/img/1695706584868-0LW6ymAoAd.png?width=1200)
かなり大変ですね!
3. サービスの有機的結合
Eureka Clientに登録
![](https://assets.st-note.com/img/1695707096290-MCObp1x5QR.png?width=1200)
![](https://assets.st-note.com/img/1695707112189-8lrwa68UGH.png?width=1200)
<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>
...
![](https://assets.st-note.com/img/1695707498554-deHvfRYiM5.png?width=1200)
spring.application.name=ORGANIZATION-SERVICE
eureka.instance.client.serverUrl.defaultZone=http://localhost:8761/eureka
![](https://assets.st-note.com/img/1695707937055-R1LVhd8I6q.png?width=1200)
![](https://assets.st-note.com/img/1695708061457-Bta5cdLnuW.png?width=1200)
ORGANIZATION-SERVICEがうまく動作しています。
![](https://assets.st-note.com/img/1695708222072-JotXr6wsji.png?width=1200)
可能であれば一つのコミットには一つの単位を入れるようにしています。
![](https://assets.st-note.com/img/1695708360744-n9jYiObZkn.png?width=1200)
Config Serverに登録
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
![](https://assets.st-note.com/img/1695708780679-uQoGaA6Kbs.png?width=1200)
![](https://assets.st-note.com/img/1695708956165-DhW84KBSVU.png?width=1200)
spring.config.import=optional:configserver:http://localhost:8888
![](https://assets.st-note.com/img/1695709223767-riEj58X6bp.png?width=1200)
![](https://assets.st-note.com/img/1695709410630-LfLq2RmafE.png?width=1200)
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 を再起動します。
![](https://assets.st-note.com/img/1695710478343-WZVmz0y2eb.png?width=1200)
分散トレーシング(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=*
![](https://assets.st-note.com/img/1695710894107-IxeBDTc9uP.png?width=1200)
![](https://assets.st-note.com/img/1695711413043-n75mZvSkrB.png?width=1200)
![](https://assets.st-note.com/img/1695711542872-MCaEfVhyfR.png?width=1200)
![](https://assets.st-note.com/img/1695711690673-EphLa5Yzxj.png?width=1200)
![](https://assets.st-note.com/img/1695711852517-CnJjWC6n9W.png)
![](https://assets.st-note.com/img/1695711880279-QbF2ErNL2q.png)
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