버전 정보
> Spring Boot 3.2.2
> OpenJDK 17.0.8
> spring-cloud-starter-gateway 4.1.0
> spring-cloud-starter-netflix-eureka-client 4.1.0
Gradle Dependecies
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
}
Eureka Client로 등록
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
application.yml 설정을 통한 라우팅
server:
port: 8200
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: first-service
uri: lb://first-service
predicates:
- Path=/first-service/**
filters:
- RewritePath=/first-service/?(?<segment>.*), /$\{segment}
# loadbalancer:
# use404: true
라우팅 정보 입력 필드 설명
- id: 라우팅 Id
- uri: 위에서는 lb://first-service를 사용했는데, 이는 유레카 서버에서 해당 서비스를 조회하여 라우팅 하는 것을 설정 (일반적인 http 경로로도 설정 가능)
- predicates - Path: 해당 경로로 오는 모든 요청을 라우팅
- filters - RewritePath: 경로를 수정함 (e.g. /first-service/hello -> /hello)
- 서비스에서는 /first-service 경로를 신경 쓰지 않고, /hello 경로로 구현하면 됨
- loadbalacers - use404: 유레카에서 경로 정보를 찾지 못했을 경우 기본적으로 503 에러코드를 응답하게 되는데, 404를 응답하고 싶다면 해당 필드를 true로 설정하면 됨
RouteLocator 통한 라우팅 구현
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("first-service", r -> r
.path("/first-service/**")
.filters(f -> f
.rewritePath("/first-service/(?<segment>.*)", "/${segment}")
)
.uri("lb://first-service")
)
.build();
}
요청 처리 과정
- http://localhost:8200/first-service/hello 경로를 통해 Gateway로 요청
- /first-service/** 요청이기 때문에 lb://first-service 정보를 유레카 서버에서 조회
- lb://first-service 정보 응답
- FIRST-SERVICE로 요청 전달 (Rewrite: /first-serivce/hello -> /hello)
- FIRST-SERVICE에서 /hello 요청 응답
- Gateway에서 응답 전달
GlobalFilter 통해 요청, 응답 정보 로깅하는 LoggingGlobalFilter 구현
@Slf4j
@Component
public class LoggingGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("PreFilter ===> requestId: {}", exchange.getRequest().getId());
ServerHttpRequest request = getDecoratedRequest(exchange);
ServerHttpResponse response = getDecoratedResponse(exchange);
return chain.filter(exchange.mutate().request(request).response(response).build())
.then(Mono.fromRunnable(() -> {
log.info("PostFilter ===> for requestId: {}, responseCode: {}", exchange.getRequest().getId(), response.getStatusCode());
}));
}
@Override
public int getOrder() {
return -1;
}
private ServerHttpRequest getDecoratedRequest(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
log.info("Request ===> for requestId: {}, method: {}, url: {}", request.getId(), request.getMethod(), request.getURI());
Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
if (route != null) {
log.info("Route ===> for requestId: {}, routeId: {}, routeUri: {}", request.getId(), route.getId(), route.getUri());
}
return new ServerHttpRequestDecorator(request) {
@Override
public Flux<DataBuffer> getBody() {
return super.getBody().publishOn(Schedulers.boundedElastic()).doOnNext(dataBuffer -> {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
Channels.newChannel(byteArrayOutputStream).write(dataBuffer.asByteBuffer().asReadOnlyBuffer());
String requestBody = removeWhiteSpacesFromJson(byteArrayOutputStream.toString(StandardCharsets.UTF_8));
log.info("Request Body ===> for requestId: {}, requestBody: {}", request.getId(), requestBody);
} catch (Exception e) {
log.error(e.getMessage());
}
});
}
};
}
private ServerHttpResponseDecorator getDecoratedResponse(ServerWebExchange exchange) {
ServerHttpResponse response = exchange.getResponse();
DataBufferFactory dataBufferFactory = response.bufferFactory();
return new ServerHttpResponseDecorator(response) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
DefaultDataBuffer joinedBuffers = new DefaultDataBufferFactory().join(dataBuffers);
byte[] content = new byte[joinedBuffers.readableByteCount()];
joinedBuffers.read(content);
log.info("Response Body ===> for requestId: {}, responseBody: {}", exchange.getRequest().getId(), new String(content, StandardCharsets.UTF_8));
return dataBufferFactory.wrap(content);
})).onErrorResume(err -> {
log.error(err.getMessage());
return Mono.empty();
});
}
return super.writeWith(body);
}
};
}
private String removeWhiteSpacesFromJson(String json) {
ObjectMapper om = new ObjectMapper();
try {
JsonNode jsonNode = om.readTree(json);
return om.writeValueAsString(jsonNode);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
Request, Response의 Body를 로깅하기 위해 Decorator로 ServerHttpRequest, ServerHttpResponse를 재정의 하여 exchange에 mutate 해줌
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("PreFilter ===> requestId: {}", exchange.getRequest().getId());
ServerHttpRequest request = getDecoratedRequest(exchange);
ServerHttpResponse response = getDecoratedResponse(exchange);
return chain.filter(exchange.mutate().request(request).response(response).build())
.then(Mono.fromRunnable(() -> {
log.info("PostFilter ===> for requestId: {}, responseCode: {}", exchange.getRequest().getId(), response.getStatusCode());
}));
}
Body 로깅이 필요 없는 경우 다음과 같이 간단하게 구현할 수 있음
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
log.info("PreFilter ===> requestId: {}, method: {}, url: {}", request.getId(), request.getMethod(), request.getURI());
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
log.info("PostFilter ===> for requestId: {}, responseCode: {}", request.getId(), response.getStatusCode());
})
);
}
로깅 정보
2024-02-06T15:35:26.294+09:00 INFO 75898 --- [gateway] [ctor-http-nio-4] c.e.m.filter.LoggingGlobalFilter : PreFilter ===> requestId: f0abf148-3
2024-02-06T15:35:26.294+09:00 INFO 75898 --- [gateway] [ctor-http-nio-4] c.e.m.filter.LoggingGlobalFilter : Request ===> for requestId: f0abf148-3, method: GET, url: http://localhost:8200/first-service/hello
2024-02-06T15:35:26.294+09:00 INFO 75898 --- [gateway] [ctor-http-nio-4] c.e.m.filter.LoggingGlobalFilter : Route ===> for requestId: f0abf148-3, routeId: first-service, routeUri: lb://first-service
2024-02-06T15:35:26.304+09:00 INFO 75898 --- [gateway] [ctor-http-nio-4] c.e.m.filter.LoggingGlobalFilter : Response Body ===> for requestId: f0abf148-3, responseBody: {"message":"Hello from first service"}
2024-02-06T15:35:26.304+09:00 INFO 75898 --- [gateway] [ctor-http-nio-4] c.e.m.filter.LoggingGlobalFilter : PostFilter ===> for requestId: f0abf148-3, responseCode: 200 OK
'Spring' 카테고리의 다른 글
Spring Cloud Gateway Filter에서 응답 보내기 (0) | 2024.02.08 |
---|---|
Spring Boot spring-security-oauth2-jose 사용해 JWKS URI로 JWT 검증하기 (0) | 2024.02.08 |
Java Spring JWT 생성 및 검증 로직 구현 (0) | 2024.01.11 |
Java Spring에서 JWKS(JSON Web Key Set) API 구현 (1) | 2024.01.11 |
Spring JPA @Id 사용시 GenerationType (2) | 2023.12.05 |
댓글