728x90
반응형
회사에서 API 게이트웨이 서버를 만들게 되었다.
검색을 해도 중구난방 잘 안되어있어서 경험하면서 기본적인 틀을 기록하고자 한다.
1. build.gradle에 추가해준다.
ext {
set('snippetsDir', file("build/generated-snippets"))
set('springCloudVersion', "2022.0.4")
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'org.postgresql:postgresql'
runtimeOnly 'org.postgresql:r2dbc-postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
}
2. application-properties.yml 파일 수정
server:
port: 8070
spring:
jackson:
timezone: Asia/Seoul
data:
r2dbc:
repositories:
enabled: true
datasource:
url: r2dbc:postgresql://localhost:5433/postgres
username: postgres
password: postgres
driver-class-name: org.postgresql.Driver
r2dbc:
url: r2dbc:postgresql://localhost:5433/postgres
username: postgres
password: postgres
cloud:
gateway:
default-filters: # Gateway 공통 필터
- name: GlobalFilter
args:
baseMessage: hello world
routes:
- id: router-1
uri: http://localhost:3000
predicates:
- Path=/**
비동기 서버를 위한 r2dbc를 사용해야 한다.
3. GlobalFilter 생성
@Slf4j
@Component
public class GlobalFilter extends AbstractGatewayFilterFactory<FilterDto> {
public GlobalFilter() {
super(FilterDto.class);
}
@Override
public GatewayFilter apply(FilterDto dto) {
return (exchange, chain) -> {
log.info("GlobalFilter baseMessage: {}", dto.getMessage());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
log.info("GlobalFilter End: {}", exchange.getResponse());
}));
};
}
}
4. FilterDto 생성
@Getter
public class FilterDto {
private String message;
}
5. ApiRoute Entity 생성
@Entity
@Getter
@Table(name = "api_route")
@TableGenerator(name = "api_route", allocationSize = 1)
public class ApiRoute {
@Id
@GeneratedValue(strategy = GenerationType.AUTO, generator = "api_route_seq")
@SequenceGenerator(name = "api_route_seq", sequenceName = "api_route_seq", allocationSize = 1)
@Column(name = "id")
private String id;
@Column
private String routeIdentifier;
@Column
private String uri;
@Column
private String method;
@Column
private String path;
}
6. ApiRouteRepository 생성
@Repository
public interface ApiRouteRepository extends R2dbcRepository<ApiRoute, String> {
}
7. ApiRouteService 생성
Service Interface는 아래의 Override 한 메소드만 추가해주면 된다.
@Service
@RequiredArgsConstructor
public class ApiRouteServiceImpl implements ApiRouteService {
private final ApiRouteRepository apiRouteRepository;
@Override
public Flux<ApiRoute> getAll() {
return this.apiRouteRepository.findAll();
}
public Mono<ApiRoute> create(ApiRoute apiRoute) {
return this.apiRouteRepository.save(apiRoute);
}
public Mono<ApiRoute> getById(String id) {
return this.apiRouteRepository.findById(id);
}
}
8. ApiPathRouteLocatorImpl 생성
@AllArgsConstructor
public class ApiPathRouteLocatorImpl implements RouteLocator {
private final ApiRouteService apiRouteService;
private final RouteLocatorBuilder routeLocatorBuilder;
@Override
public Flux<Route> getRoutes() {
RouteLocatorBuilder.Builder routesBuilder = routeLocatorBuilder.routes();
return apiRouteService.getAll()
.map(apiRoute -> routesBuilder.route(String.valueOf(apiRoute.getRouteIdentifier()),
predicateSpec -> setPredicateSpec(apiRoute, predicateSpec)))
.collectList()
.flatMapMany(builders -> routesBuilder.build()
.getRoutes());
}
private Buildable<Route> setPredicateSpec(ApiRoute apiRoute, PredicateSpec predicateSpec) {
BooleanSpec booleanSpec = predicateSpec.path(apiRoute.getPath());
if (!StringUtils.isEmpty(apiRoute.getMethod())) {
booleanSpec.and()
.method(apiRoute.getMethod());
}
return booleanSpec.uri(apiRoute.getUri());
}
@Override
public Flux<Route> getRoutesByMetadata(Map<String, Object> metadata) {
return RouteLocator.super.getRoutesByMetadata(metadata);
}
}
9. GatewayConfig 생성
@Configuration
@Slf4j
public class GatewayConfig {
@Bean
public RouteLocator routeLocator(ApiRouteService routeService,
RouteLocatorBuilder routeLocationBuilder) {
return new ApiPathRouteLocatorImpl(routeService, routeLocationBuilder);
}
}
여기까지가 기본적인 프록시를 위한 라우터이다.
다음 부터는 API로 route를 CRUD 하기 위한 작업이다.
10. ApiRouteRouter Configuration 생성
@Configuration
public class ApiRouteRouter {
@Bean
public RouterFunction<ServerResponse> route(ApiRouteHandler apiRouteHandler) {
return RouterFunctions.route(POST("/routes")
.and(accept(MediaType.APPLICATION_JSON)), apiRouteHandler::create)
.andRoute(GET("/routes/{routeId}")
.and(accept(MediaType.APPLICATION_JSON)), apiRouteHandler::getById)
.andRoute(GET("/routes/refresh-routes")
.and(accept(MediaType.APPLICATION_JSON)), apiRouteHandler::refreshRoutes);
}
}
11. ApiROuteHandler 생성
@RequiredArgsConstructor
@Component
@Slf4j
public class ApiRouteHandler {
private final ApiRouteService routeService;
private final GatewayRoutesRefresher gatewayRoutesRefresher;
public Mono<ServerResponse> create(ServerRequest serverRequest) {
Mono<ApiRoute> apiRoute = serverRequest.bodyToMono(ApiRoute.class);
return apiRoute.flatMap(route ->
ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(routeService.create(route), ApiRoute.class));
}
public Mono<ServerResponse> getById(ServerRequest serverRequest) {
log.info("serverRequest.pathVariable(\"routeId\") = {}",
serverRequest.pathVariable("routeId"));
final String apiId = serverRequest.pathVariable("routeId");
Mono<ApiRoute> apiRoute = routeService.getById(apiId);
return apiRoute.flatMap(route -> ServerResponse.ok()
.body(fromValue(route)))
.switchIfEmpty(ServerResponse.notFound()
.build());
}
public Mono<ServerResponse> refreshRoutes(ServerRequest serverRequest) {
gatewayRoutesRefresher.refreshRoutes();
return ServerResponse.ok().body(BodyInserters.fromObject("Routes reloaded successfully"));
}
}
12. GatewayRoutesRefresher 생성
@Component
public class GatewayRoutesRefresher implements ApplicationEventPublisherAware {
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* Refresh the routes to load from data store
*/
public void refreshRoutes() {
applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
}
}
만약, domain Hostname에 따라 Proxy되는 서버의 주소를 변경하고자 하면 다음과 같은 방법을 쓸 수 있다.
@Override
public GatewayFilter apply(HostNameFilterDto hostNameFilterdto) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
request.getHeaders();
String[] split = request.getURI().getHost().split("\\.");
String siteId = split[0];
Mono<Site> siteMono = siteService.findById(siteId);
Site site = siteMono.share().block();
String uri = Objects.requireNonNull(site).getConnectHost();
int port = site.getConnectPort();
if (port != 80) {
uri += ":" + port;
}
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
Route newRoute = Route.async()
.id(site.getId())
.uri(uri)
.predicate(serverWebExchange -> false)
.order(Objects.requireNonNull(route).getOrder())
.filters(route.getFilters())
.build();
exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, newRoute);
return chain.filter(exchange);
};
}
728x90
반응형
'Back-End > Spring Boot' 카테고리의 다른 글
Spring boot | MFA( Multi Factor Auth ) OTP, Email 복합 인증 구현 (0) | 2024.05.17 |
---|---|
Spring boot | Restdocs-api-spec with Swagger, Docker 완전 정복 하기 (0) | 2024.05.08 |
Spring Boot | MQTT ( Mosquitto ) with Kafka | Kafka Mqtt Source Connector 생성하기 (0) | 2024.01.16 |
Spring Boot | MQTT ( Mosquitto ) with Kafka | MQTT 사용하기 (2) | 2024.01.10 |
Spring Boot | @AuthenticationPrincipal in Spring Security (0) | 2023.09.06 |