https://spring.io/projects/spring-cloud-gateway をさわる。はじめてさわるのでpredicateとかfilterとかをいくつか使ってみる。使い方わからなかったやつはさわってない。個人の日記レベルのさわってみた程度なんで、ちゃんとした情報はリファレンスを参照願います。
やってみる
build.gradle
https://start.spring.io/ でgatewayと入力すればorg.springframework.cloud:spring-cloud-starter-gateway
が入る。以下はそれで生成したもののコピペ。
plugins {
id 'org.springframework.boot' version '2.2.6.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', "Hoxton.SR4")
}
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.boot:spring-boot-devtools'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
gatewayのアプリケーションを作る。以下は2つのrouteを定義している。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class DemoApplication {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_google", r -> r.path("/search")
.uri("https://www.google.co.jp"))
.route("host_yahoo", r -> r.host("localhost:8080").and().path("/hotentry/all")
.uri("https://b.hatena.ne.jp"))
.build();
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
どちらもroute
の引数は識別子なので、適当な一意の値を振る。
- 1つ目は
http://localhost:8080/search
にアクセスするとhttps://www.google.co.jp/search
に飛ぶ。http://localhost:8080/search?q=hoge
とかをブラウザでアクセスするとそれっぽい検索結果が表示される*1。なお、本来の使い方はAPIのプロキシ的な使い方なので、https://www.google.co.jp
に飛ばすのは意味のある例ではないが、適当なAPI用意するの面倒なのでこのようにしている。動作確認できれば良いや、という判断*2。
- 2つ目はホストが
localhost:8080
かつリクエストパスが/hotentry/all
であればhttps://b.hatena.ne.jp
に飛ばす。http://localhost:8080/hotentry/all
にブラウザでアクセスするとそれっぽいホットエントリが表示される。
上記は設定ファイル(application.yaml
とか)で書くこともできる。が、このエントリではそちらはやらない。
predicate
before/after/between
あるZonedDateTime
の、以前・以降・期間、にマッチしたときだけルーティングする。
.route("path_google", r -> r.before(ZonedDateTime.of(2020, 5, 10, 0, 0, 0, 0, ZoneId.systemDefault()))
.uri("https://www.google.co.jp"))
.route("path_google", r -> r.after(ZonedDateTime.of(2020, 5, 10, 0, 0, 0, 0, ZoneId.systemDefault()))
.uri("https://www.google.co.jp"))
.route("path_google", r -> r.between(
ZonedDateTime.of(2020, 5, 1, 0, 0, 0, 0, ZoneId.systemDefault()),
ZonedDateTime.of(2020, 5, 10, 0, 0, 0, 0, ZoneId.systemDefault()))
.uri("https://www.google.co.jp"))
cookieが指定の値を持ってるかどうか。正規表現使用可能なので下はvalue
とかにマッチする。
.route("path_google", r -> r.cookie("hogehoge", "va..e")
.uri("https://www.google.co.jp"))
header
headerがあるか or headerが指定の値を持ってるかどうか、をチェックする。
.route("path_google", r -> r.header("X-Hoge", "va..e")
.uri("https://www.google.co.jp"))
host
hostが指定の値かどうか。正規表現使えるので、ドメイン名で何らかの分岐するときに使う感じか。
method
GET
とかPOST
とかをチェックする。
.route("path_google", r -> r.method(HttpMethod.GET)
.uri("https://www.google.co.jp"))
path
上で触れたので省略。
query
上で触れたので省略。
remoteAddress
CIDRで条件をかける。
.route("path_google", r -> r.remoteAddr("192.168.1.1/24")
.uri("https://www.google.co.jp"))
weight
重み付け。たとえば、以下は8:2
の重み付けでgoogleかtwitterにアクセスが飛ぶ。
.route("path_route_high", r -> r.weight("group1", 8)
.uri("https://www.google.com"))
.route("path_route_low", r -> r.weight("group1", 2)
.uri("https://twitter.com"))
filter
gatewayにリクエストが来たときと、ルーティング先から戻ってきたときに、何らかの処理を挟みたい場合にfilterを使う。
AddRequestHeader
gatewayで何らかのリクエストヘッダーを追加する。
.route("path_google", r -> r.path("/{hoge}")
.filters(f -> f.rewritePath("/.*", "/tools/request_headers.html").addRequestHeader("X-Hoge", "{hoge}"))
.uri("https://uchy.me/"))
動作確認にはHTTPリクエストヘッダー表示ツール https://uchy.me/tools/request_headers.html を使わせて頂いた。http://localhost:8080/sample-value
とかするとX-Hoge sample-value
ヘッダーが送信される。上記のように、pathのURLの変数を{hoge}
にしてaddRequestHeaderでその値を使うことができる。
AddRequestParameter
gatewayで何らかのリクエストパラメータを追加する。
.route("path_google", r -> r.path("/{hoge}")
.filters(f -> f.rewritePath("/.*", "/search").addRequestParameter("q", "{hoge}"))
.uri("https://www.google.com/"))
上はhttp://localhost:8080/search-word
にアクセスするとhttps://www.google.com/search?q=search-word
に行く。
AddResponseHeader
gatewayのクライアントに返すレスポンスにヘッダーを追加する。
.route("path_google", r -> r.path("/search")
.filters(f -> f.addResponseHeader("X-Hoge", "hoge"))
.uri("https://www.google.com/"))
DedupeResponseHeader
試してない。
Hystrix GatewayFilter
リファレンスにこんなことが書いてある。なのでSpring Cloud CircuitBreaker GatewayFilterを使うのが良いと思われる。
Netflix has put Hystrix in maintenance mode. We suggest you use the Spring Cloud CircuitBreaker Gateway Filter with Resilience4J, as support for Hystrix will be removed in a future release.
Spring Cloud CircuitBreaker GatewayFilter
CircuitBreakerの詳細な説明は省略(俺自身ちゃんと理解してないので)して、とりあえず使ってみる。
まずCircuitBreakerの実体としてresilience4jを依存性に追加する。
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j'
とりあえず使ってみる。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class DemoApplication {
@GetMapping("/forward")
public String forward() {
return "forward";
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_google", r -> r.path("/")
.filters(f -> f.circuitBreaker(
c -> c.setName("myCircuitBreaker")
.setFallbackUri("forward:/forward")))
.uri("http://asdasdasdf:8080/"))
.build();
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
上のコードは、http://localhost:8080/
にアクセスすると存在しないhttp://asdasdasdf:8080/
に飛ぼうとするので、fallback-uriのforward:/forward
に飛ぶ。
FallbackHeaders
CircuitBreakerと組み合わせて使うもののようだけど、よくわからない……
MapRequestHeader
あるリクエストヘッダーを別の名前としても送信する。
.route("path_google", r -> r.path("/")
.filters(f -> f.rewritePath("/.*", "/tools/request_headers.html").mapRequestHeader("X-FROM", "X-TO"))
.uri("https://uchy.me/"))
上記のようにすると、リクエストヘッダーX-FROM
にプラスでX-TO
も送信される。
PrefixPath
ルーティング前にリクエストパスにプレフィクスを追加する。
.route("path_google", r -> r.path("/{date}")
.filters(f -> f.prefixPath("/hotentry/all/"))
.uri("https://b.hatena.ne.jp/"))
たとえばhttp://localhost:8080/20200504
とかするとhttps://b.hatena.ne.jp/hotentry/all/20200504
にアクセスが行く。
PreserveHostHeader
試してない。
RequestRateLimiter
なんかむつかしそうなので試してない。
RedirectTo
リダイレクトする。
.route("path_google", r -> r.path("/")
.filters(f -> f.redirect(302, "https://b.hatena.ne.jp/hotentry/all"))
.uri("forward:/forward"))
RemoveRequestHeader
任意のリクエストヘッダーを削除する。
.route("path_google", r -> r.path("/")
.filters(f -> f.removeRequestHeader("X-Hoge"))
.uri("forward:/forward"))
RemoveResponseHeader
なんかエラーになる。追加で設定が必要ぽい?
java.lang.UnsupportedOperationException: null
at org.springframework.http.ReadOnlyHttpHeaders.remove(ReadOnlyHttpHeaders.java:131) ~[spring-web-5.2.5.RELEASE.jar:5.2.5.RELEASE]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
RemoveRequestParameter
リクエストパラメータを削除する。
.route("path_google", r -> r.path("/")
.filters(f -> f.removeRequestParameter("hoge"))
.uri("forward:/forward"))
たとえばhttp://localhost:8080/?hoge=hoge
とかするとhogeパラメータが削除される。
RewritePath
.route("path_google", r -> r.path("/**")
.filters(f -> f.rewritePath("path-is-(?<segment>.*)", "${segment}"))
.uri("https://www.google.com"))
http://localhost:8080/path-is-search
とかするとhttps://www.google.com/search
にアクセスがいく。
Spring Cloud Gatewayとは直接関係ないが(?<name>X)
という記法は https://docs.oracle.com/javase/jp/11/docs/api/java.base/java/util/regex/Pattern.html にある通り「名前付きの前方参照を行う正規表現グループ」というもの。
RewriteLocationResponseHeader
試してない。
RewriteResponseHeader
なんかうまく動いてくれない。
.route("path_google", r -> r.path("/**")
.filters(f -> f.rewriteResponseHeader("X-Hoge", ".*", "foo"))
.uri("http://localhost:8080/"))
SaveSession
Spring SessionとかSpring Securityとかが云々って書いてあるので試してない。
SecureHeaders
試してない。
SetPath
.route("path_google", r -> r.path("/{date}")
.filters(f -> f.setPath("/hotentry/all/{date}"))
.uri("https://b.hatena.ne.jp/"))
http://localhost:8080/20200501
とかするとhttps://b.hatena.ne.jp/hotentry/all/20200501
にアクセスがいく。
SetRequestHeader
addでなくてreplaceする、とリファレンスに書いてあるけどイマイチ違いがわからん。
.route("path_google", r -> r.path("/")
.filters(f -> f.setRequestHeader("X-Hoge", "hogeValue"))
.uri("forward:/forward"))
SetResponseHeader
なんか実行時例外になる。
.route("path_google", r -> r.path("/")
.filters(f -> f.setResponseHeader("X-Hoge-Hoge", "hogeValue"))
.uri("forward:/forward"))
SetStatus
gatewayクライアントに返すときにステータスを変更する。
.route("path_google", r -> r.path("/")
.filters(f -> f.setStatus(999))
.uri("https://b.hatena.ne.jp/"))
上にするとステータスコード999
が返ってくる。
StripPrefix
リクエストのパスを前から指定数分削除する。
.route("path_google", r -> r.path("/**")
.filters(f -> f.stripPrefix(2))
.uri("https://b.hatena.ne.jp/"))
http://localhost:8080/strip1/strip2/hotentry/all
とするとhttps://b.hatena.ne.jp/hotentry/all
に行く。
Retry
リトライ。
.route("path_google", r -> r.path("/**")
.filters(f -> f.retry(3))
.uri("http://localhost:8081/"))
動作確認用に、以下のような適当な5xx
を返すエンドポイントを用意して(ポートは8081)、
@RestController
@SpringBootApplication
public class Main {
@ResponseStatus(value = HttpStatus.BAD_GATEWAY)
@GetMapping("/return-5xx")
public String return5xx(@RequestHeader Map<String, String> map) {
System.out.println("5xx");
return "";
}
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
http://localhost:8080/return-5xx
とすると3回リトライする。
GatewayFilterSpec.retry(int retries)
のjavadocによると、デフォルトでは5xxかつGETのときリトライ、と書いてある。細かく挙動を指定したい場合は別のメソッドを使う。他に指定可能な条件としては、例外とかbackoffとかがある。
RequestSize
リクエストのサイズに制限をかける。
.route("path_google", r -> r.path("/**").and().method(HttpMethod.POST)
.filters(f -> f.setRequestSize(DataSize.ofBytes(1)))
.uri("https://b.hatena.ne.jp/"))
上のようにするとmax1byte制限になるので、適当なサイズのPOSTをすると413Request Entity Too Large
が返される。
ModifyRequestBody
リクエストボディを書き換える。
.route("path_google", r -> r.path("/**").and().method(HttpMethod.POST)
.filters(f -> f.modifyRequestBody(String.class, String.class, (exchange, s) -> Mono.just(s.toUpperCase())))
.uri("http://httpbin.org/"))
上はPOSTの中身を大文字に書き換えている。
ModifyResponseBody
レスポンスボディを書き換える。
.route("path_google", r -> r.path("/**").and().method(HttpMethod.POST)
.filters(f -> f.modifyResponseBody(String.class, String.class, (exchange, s) -> Mono.just(s.toUpperCase())))
.uri("http://httpbin.org/"))
上はgateway先から返ってきたレスポンスボディを大文字に書き換えている。