0%

Feign基本使用

1、功能概述

img

2、自定义错误处理

Feign(默认配置)对于所有错误情况只抛出FeignException异常,但是如果你想处理一个特殊的异常,可以通过在Feign.builder.errorDecoder()实现feign.codec.ErrorDecoder接口来达到目的。

例子:

自定义错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StashErrorDecoder implements ErrorDecoder {

@Override
public Exception decode(String methodKey, Response response) {
if (response.status() >= 400 && response.status() <= 499) {
return new StashClientException(
response.status(),
response.reason()
);
}
if (response.status() >= 500 && response.status() <= 599) {
return new StashServerException(
response.status(),
response.reason()
);
}
return errorStatus(methodKey, response);
}
}

应用错误处理

1
2
3
return Feign.builder()
.errorDecoder(new StashErrorDecoder())
.target(StashApi.class, url);

3、基本用法

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);

@RequestLine("POST /repos/{owner}/{repo}/issues")
void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);

}

public static class Contributor {
String login;
int contributions;
}

public static class Issue {
String title;
String body;
List<String> assignees;
int milestone;
List<String> labels;
}

public class MyApp {
public static void main(String... args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");

// Fetch and print a list of the contributors to this library.
List<Contributor> contributors = github.contributors("OpenFeign", "feign");
for (Contributor contributor : contributors) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
}
}

3.1、接口注解

Feign的Contract定义的注解规定了底层客户端和接口之间如何工作。

Feign的默认Contract定义了以下注解:

注解 接口对象 用法
@RequestLine Method 为请求定义 HttpMethod 和 UriTemplate。表达式,用大括号 {expression} 包裹的值使用其相应的 @Param 注释参数解析。
@Param Parameter 定义一个模板变量,其值将用于解析相应的模板表达式,按名称提供作为注释值。 如果缺少值,它将尝试从字节码方法参数名称中获取名称(如果代码是使用 -parameters 标志编译的)。
@Headers Method, Type 定义一个 HeaderTemplate; UriTemplate 的变体。使用@Param 注释值来解析相应的表达式。在 Type 上使用时,模板将应用于每个请求。当在method上使用时,模板将仅适用于带注释的方法。
@QueryMap Parameter 定义名称-值对的 Map 或 POJO,以扩展为查询字符串。
@HeaderMap Parameter 定义一个名称-值对的 Map,以扩展到 Http Headers。
@Body Method 定义一个模板,类似于 UriTemplate 和 HeaderTemplate,它使用@Param 注释值来解析相应的表达式。

3.2、模板和表达式

Feign 表达式表示由 URI Template - RFC 6570 定义的简单字符串表达式(Level 1)。表达式使用其相应的 Param 注释方法参数进行扩展。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface GitHub {

@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repository);

class Contributor {
String login;
int contributions;
}
}

public class MyApp {
public static void main(String[] args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");

/* The owner and repository parameters will be used to expand the owner and repo expressions
* defined in the RequestLine.
*
* the resulting uri will be https://api.github.com/repos/OpenFeign/feign/contributors
*/
github.contributors("OpenFeign", "feign");
}
}

表达式必须用大括号 {} 括起来,并且可以包含正则表达式模式,用冒号 : 分隔以限制解析的值。示例owner必须按字母顺序排列。 {owner:[a-zA-Z]*}

3.3、请求参数扩展

RequestLine 和 QueryMap 模板遵循 URI Template - RFC 6570 Level 1 模板规范,其中指定了以下内容:

  • 未解析的表达式被省略。
  • 所有文字和变量值都是 pct 编码的,如果尚未编码或标记为通过 @Param 注释编码。

3.4、未定义与空值

未定义的表达式是表达式的值为显式 null 或未提供值的表达式。根据 URI Template - RFC 6570,可以为表达式提供空值。 Feign 解析表达式时,它首先确定该值是否已定义,如果已定义,则查询参数将保留。如果表达式未定义,则删除查询参数。请参阅下面的完整细分。

空字符串

1
2
3
4
5
public void test() {
Map<String, Object> parameters = new LinkedHashMap<>();
parameters.put("param", "");
this.demoClient.test(parameters);
}

结果

1
http://localhost:8080/test?param=

丢失

1
2
3
4
public void test() {
Map<String, Object> parameters = new LinkedHashMap<>();
this.demoClient.test(parameters);
}

结果

1
http://localhost:8080/test

未定义

1
2
3
4
5
public void test() {
Map<String, Object> parameters = new LinkedHashMap<>();
parameters.put("param", null);
this.demoClient.test(parameters);
}

结果

1
http://localhost:8080/test

3.5、自定义扩展

@Param 注释有一个可选的属性扩展器,允许完全控制单个参数的扩展。 expander 属性必须引用一个实现 Expander 接口的类:

1
2
3
public interface Expander {
String expand(Object value);
}

3.6、请求标头扩展

Headers 和 HeaderMap 模板遵循与请求参数扩展相同的规则,但有以下变化:

  • 未解析的表达式被省略。如果结果是一个空的标头值,则整个标头将被删除。
  • 不执行 pct 编码。

关于@Param 参数及其名称的说明:

所有具有相同名称的表达式,无论它们在 @RequestLine、@QueryMap、@BodyTemplate 或 @Headers 上的位置如何,都将解析为相同的值。在以下示例中,contentType 的值将用于解析标头和路径表达式:

1
2
3
4
5
public interface ContentService {
@RequestLine("GET /api/documents/{contentType}")
@Headers("Accept: {contentType}")
String getDocumentByType(@Param("contentType") String type);
}

3.7、请求正文扩展

正文模板遵循与请求参数扩展相同的规则,但有以下更改:

  • 未解析的表达式被省略。
  • 扩展的值在放置到请求正文之前不会通过编码器传递。
  • 必须指定 Content-Type 标头。

3.8、定制

Feign 有几个方面可以定制。 对于简单的情况,您可以使用 Feign.builder() 来构建带有自定义组件的 API 接口。 对于请求设置,您可以使用 target() 上的 options(Request.Options options) 来设置 connectTimeout、connectTimeoutUnit、readTimeout、readTimeoutUnit、followRedirects。 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Bank {
@RequestLine("POST /account/{id}")
Account getAccountInfo(@Param("id") String id);
}

public class BankService {
public static void main(String[] args) {
Bank bank = Feign.builder()
.decoder(new AccountDecoder())
.options(new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true))
.target(Bank.class, "https://api.examplebank.com");
}
}

3.9、多个接口

Feign 可以产生多个 api 接口。这些被定义为 Target(默认 HardCodedTarget),允许在执行之前动态发现和修饰请求。 例如,以下模式可能会使用来自身份服务的当前 url 和身份验证令牌来修饰每个请求。

1
2
3
4
5
6
7
8
9
10
public class CloudService {
public static void main(String[] args) {
CloudDNS cloudDNS = Feign.builder()
.target(new CloudIdentityTarget<CloudDNS>(user, apiKey));
}

class CloudIdentityTarget extends Target<CloudDNS> {
/* implementation of a Target */
}
}

3.10、集成

3.10.1、Gson

Gson 包含一个编码器和解码器,您可以将其与 JSON API 一起使用。 像这样将 GsonEncoder 和/或 GsonDecoder 添加到 Feign.Builder 中:

1
2
3
4
5
6
7
8
9
public class Example {
public static void main(String[] args) {
GsonCodec codec = new GsonCodec();
GitHub github = Feign.builder()
.encoder(new GsonEncoder())
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
}
}

3.10.2、Jackson

Jackson 包含一个编码器和解码器,您可以将其与 JSON API 一起使用。 将 JacksonEncoder 和/或 JacksonDecoder 添加到您的 Feign.Builder 中,如下所示:

1
2
3
4
5
6
7
8
public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(GitHub.class, "https://api.github.com");
}
}

3.10.3、Sax

SaxDecoder 允许您以与普通 JVM 和 Android 环境兼容的方式解码 XML。 以下是如何配置 Sax 响应解析的示例:

1
2
3
4
5
6
7
8
9
public class Example {
public static void main(String[] args) {
Api api = Feign.builder()
.decoder(SAXDecoder.builder()
.registerContentHandler(UserIdHandler.class)
.build())
.target(Api.class, "https://apihost");
}
}

3.10.4、JAXB

JAXB 包括可与 XML API 一起使用的编码器和解码器。 将 JAXBEncoder 和/或 JAXBDecoder 添加到您的 Feign.Builder 中,如下所示:

1
2
3
4
5
6
7
8
public class Example {
public static void main(String[] args) {
Api api = Feign.builder()
.encoder(new JAXBEncoder())
.decoder(new JAXBDecoder())
.target(Api.class, "https://apihost");
}
}

3.10.5、JAX-RS

JAXRSContract 覆盖注解处理以使用 JAX-RS 规范提供的标准处理。这是目前针对 1.1 规范的目标。 这是上面重写的示例以使用 JAX-RS:

1
2
3
4
5
6
7
8
9
10
11
12
interface GitHub {
@GET @Path("/repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@PathParam("owner") String owner, @PathParam("repo") String repo);
}

public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.contract(new JAXRSContract())
.target(GitHub.class, "https://api.github.com");
}
}

3.10.6、OkHttp

OkHttpClient 将 Feign 的 http 请求定向到 OkHttp,从而实现 SPDY 和更好的网络控制。 要将 OkHttp 与 Feign 一起使用,请将 OkHttp 模块添加到您的类路径中。然后,配置 Feign 以使用 OkHttpClient:

1
2
3
4
5
6
7
public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.client(new OkHttpClient())
.target(GitHub.class, "https://api.github.com");
}
}

3.10.7、Ribbon

RibbonClient 覆盖了 Feign 客户端的 URL 解析,增加了 Ribbon 提供的智能路由和弹性能力。 集成要求您将功能区客户端名称作为 url 的主机部分传递,例如 myAppProd。

1
2
3
4
5
6
7
public class Example {
public static void main(String[] args) {
MyService api = Feign.builder()
.client(RibbonClient.create())
.target(MyService.class, "https://myAppProd");
}
}

3.10.8、Java 11 Http2

Http2Client 将 Feign 的 http 请求定向到实现 HTTP/2 的 Java11 New HTTP/2 Client。 要将新 HTTP/2 客户端与 Feign 一起使用,请使用 Java SDK 11。然后,将 Feign 配置为使用 Http2Client:

1
2
3
GitHub github = Feign.builder()
.client(new Http2Client())
.target(GitHub.class, "https://api.github.com");

3.10.9、Hystrix

HystrixFeign 配置了 Hystrix 提供的断路器支持。 要将 Hystrix 与 Feign 一起使用,请将 Hystrix 模块添加到您的类路径中。然后使用 HystrixFeign 构建器:

1
2
3
4
5
public class Example {
public static void main(String[] args) {
MyService api = HystrixFeign.builder().target(MyService.class, "https://myAppProd");
}
}

3.10.10、SOAP

SOAP 包括可以与 XML API 一起使用的编码器和解码器。 该模块添加了对通过 JAXB 和 SOAPMessage 编码和解码 SOAP Body 对象的支持。它还通过将它们包装到原始 javax.xml.ws.soap.SOAPFaultException 中来提供 SOAPFault 解码功能,因此您只需捕获 SOAPFaultException 即可处理 SOAPFault。 像这样将 SOAPEncoder 和/或 SOAPDecoder 添加到你的 Feign.Builder 中:

1
2
3
4
5
6
7
8
9
public class Example {
public static void main(String[] args) {
Api api = Feign.builder()
.encoder(new SOAPEncoder(jaxbFactory))
.decoder(new SOAPDecoder(jaxbFactory))
.errorDecoder(new SOAPErrorDecoder())
.target(MyApi.class, "http://api");
}
}

3.10.11、SLF4J

SLF4JModule 允许将 Feign 的日志记录定向到 SLF4J,允许您轻松使用您选择的日志记录后端(Logback、Log4J 等) 要将 SLF4J 与 Feign 一起使用,请将 SLF4J 模块和您选择的 SLF4J 绑定添加到您的类路径中。然后,配置 Feign 以使用 Slf4jLogger:

1
2
3
4
5
6
7
8
public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.logger(new Slf4jLogger())
.logLevel(Level.FULL)
.target(GitHub.class, "https://api.github.com");
}
}

3.11、解码器

Feign.builder() 允许您指定其他配置,例如如何解码响应。 如果接口中的任何方法返回 Response、String、byte[] 或 void 之外的类型,则需要配置非默认解码器。 下面是如何配置 JSON 解码(使用 feign-gson 扩展):

1
2
3
4
5
6
7
public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
}
}

如果您需要在将响应提供给解码器之前对其进行预处理,则可以使用 mapAndDecode 构建器方法。一个示例用例是处理仅提供 jsonp 的 API,您可能需要在将其发送到您选择的 Json 解码器之前解开 jsonp:

1
2
3
4
5
6
7
public class Example {
public static void main(String[] args) {
JsonpApi jsonpApi = Feign.builder()
.mapAndDecode((response, type) -> jsopUnwrap(response, type), new GsonDecoder())
.target(JsonpApi.class, "https://some-jsonp-api.com");
}
}

3.12、编码器

将请求正文发送到服务器的最简单方法是定义一个 POST 方法,该方法有一个 String 或 byte[] 参数,上面没有任何注释。您可能需要添加一个 Content-Type 标头。

1
2
3
4
5
6
7
8
9
10
11
interface LoginClient {
@RequestLine("POST /")
@Headers("Content-Type: application/json")
void login(String content);
}

public class Example {
public static void main(String[] args) {
client.login("{\"user_name\": \"denominator\", \"password\": \"secret\"}");
}
}

通过配置编码器,您可以发送类型安全的请求正文。这是使用 feign-gson 扩展的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static class Credentials {
final String user_name;
final String password;

Credentials(String user_name, String password) {
this.user_name = user_name;
this.password = password;
}
}

interface LoginClient {
@RequestLine("POST /")
void login(Credentials creds);
}

public class Example {
public static void main(String[] args) {
LoginClient client = Feign.builder()
.encoder(new GsonEncoder())
.target(LoginClient.class, "https://foo.com");

client.login(new Credentials("denominator", "secret"));
}
}

3.13、@Body 模板

@Body 注释指示使用@Param 注释的参数扩展的模板。您可能需要添加一个 Content-Type 标头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface LoginClient {

@RequestLine("POST /")
@Headers("Content-Type: application/xml")
@Body("<login \"user_name\"=\"{user_name}\" \"password\"=\"{password}\"/>")
void xml(@Param("user_name") String user, @Param("password") String password);

@RequestLine("POST /")
@Headers("Content-Type: application/json")
// json curly braces must be escaped!
@Body("%7B\"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
void json(@Param("user_name") String user, @Param("password") String password);
}

public class Example {
public static void main(String[] args) {
client.xml("denominator", "secret"); // <login "user_name"="denominator" "password"="secret"/>
client.json("denominator", "secret"); // {"user_name": "denominator", "password": "secret"}
}
}

3.14、请求头

Feign 支持请求的设置标头作为 api 的一部分或作为客户端的一部分,具体取决于用例。

3.14.1、使用 apis 设置标题

在特定接口或调用应始终设置某些标头值的情况下,将标头定义为 api 的一部分是有意义的。 可以使用 @Headers 注释在 api 接口或方法上设置静态标头。

可以使用 @Headers 注释在 api 接口或方法上设置静态标头。

1
2
3
4
5
6
@Headers("Accept: application/json")
interface BaseApi<V> {
@Headers("Content-Type: application/json")
@RequestLine("PUT /api/{key}")
void put(@Param("key") String key, V value);
}

方法可以使用@Headers 中的变量扩展为静态标题指定动态内容。

1
2
3
4
5
public interface Api {
@RequestLine("POST /")
@Headers("X-Ping: {token}")
void post(@Param("token") String token);
}

如果标头字段键和值都是动态的,并且可能键的范围无法提前知道,并且可能因同一 api/客户端中的不同方法调用而异(例如自定义元数据标头字段,例如“x-amz- meta-“ 或 “x-goog-meta-“),可以使用 HeaderMap 对 Map 参数进行注释,以构造使用地图内容作为其标头参数的查询。

1
2
3
4
public interface Api {
@RequestLine("POST /")
void post(@HeaderMap Map<String, Object> headerMap);
}

这些方法将标头条目指定为 api 的一部分,并且在构建 Feign 客户端时不需要任何自定义。

3.14.2、为每个目标设置请求头

要为 Target 上的每个请求方法自定义请求头,可以使用 RequestInterceptor。 RequestInterceptors 可以跨 Target 实例共享,并且应该是线程安全的。 RequestInterceptors 应用于 Target 上的所有请求方法。 如果您需要按方法自定义,则需要自定义 Target,因为 RequestInterceptor 无权访问当前方法元数据。 有关使用 RequestInterceptor 设置标头的示例,请参阅请求拦截器部分。 请求头可以设置为自定义目标的一部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static class DynamicAuthTokenTarget<T> implements Target<T> {
public DynamicAuthTokenTarget(Class<T> clazz,
UrlAndTokenProvider provider,
ThreadLocal<String> requestIdProvider);

@Override
public Request apply(RequestTemplate input) {
TokenIdAndPublicURL urlAndToken = provider.get();
if (input.url().indexOf("http") != 0) {
input.insert(0, urlAndToken.publicURL);
}
input.header("X-Auth-Token", urlAndToken.tokenId);
input.header("X-Request-ID", requestIdProvider.get());

return input.request();
}
}

public class Example {
public static void main(String[] args) {
Bank bank = Feign.builder()
.target(new DynamicAuthTokenTarget(Bank.class, provider, requestIdProvider));
}
}

这些方法取决于在构建 Feign 客户端时在 Feign 客户端上设置的自定义 RequestInterceptor 或 Target,并且可以用作在每个客户端的基础上在所有 api 调用上设置标头的方法。这对于执行诸如在每个客户端的所有 api 请求的标头中设置身份验证令牌等操作非常有用。当在调用 api 调用的线程上进行 api 调用时运行这些方法,这允许在调用时以特定于上下文的方式动态设置标头——例如,线程本地存储可用于根据调用线程设置不同的标头值,这对于诸如为请求设置线程特定的跟踪标识符之类的事情很有用。

4、高级用法

4.1、基础Apis

在许多情况下,服务的 api 遵循相同的约定。 Feign 通过单继承接口支持这种模式。 考虑这个例子:

1
2
3
4
5
6
7
interface BaseAPI {
@RequestLine("GET /health")
String health();

@RequestLine("GET /all")
List<Entity> all();
}

您可以定义和定位特定的 api,继承基本方法。

1
2
3
4
interface CustomAPI extends BaseAPI {
@RequestLine("GET /custom")
String custom();
}

在许多情况下,资源表示也是一致的。因此,基本 api 接口支持类型参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Headers("Accept: application/json")
interface BaseApi<V> {

@RequestLine("GET /api/{key}")
V get(@Param("key") String key);

@RequestLine("GET /api")
List<V> list();

@Headers("Content-Type: application/json")
@RequestLine("PUT /api/{key}")
void put(@Param("key") String key, V value);
}

interface FooApi extends BaseApi<Foo> { }

interface BarApi extends BaseApi<Bar> { }

4.2、日志记录

您可以通过设置 Logger 来记录进出目标的 http 消息。这是最简单的方法:

1
2
3
4
5
6
7
8
9
public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.logger(new Logger.JavaLogger("GitHub.Logger").appendToFile("logs/http.log"))
.logLevel(Logger.Level.FULL)
.target(GitHub.class, "https://api.github.com");
}
}

关于 JavaLogger 的注意事项:避免使用默认的 JavaLogger() 构造函数 - 它被标记为已弃用,很快将被删除。

4.3、请求拦截器

当您需要更改所有请求时,无论其目标是什么,您都需要配置一个 RequestInterceptor。例如,如果您充当中介,您可能想要传播 X-Forwarded-For 标头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static class ForwardedForInterceptor implements RequestInterceptor {
@Override public void apply(RequestTemplate template) {
template.header("X-Forwarded-For", "origin.host.com");
}
}

public class Example {
public static void main(String[] args) {
Bank bank = Feign.builder()
.decoder(accountDecoder)
.requestInterceptor(new ForwardedForInterceptor())
.target(Bank.class, "https://api.examplebank.com");
}
}

拦截器的另一个常见示例是身份验证,例如使用内置的 BasicAuthRequestInterceptor。

1
2
3
4
5
6
7
8
public class Example {
public static void main(String[] args) {
Bank bank = Feign.builder()
.decoder(accountDecoder)
.requestInterceptor(new BasicAuthRequestInterceptor(username, password))
.target(Bank.class, "https://api.examplebank.com");
}
}

4.4、自定义@Param 扩展

使用 Param 注释的参数基于它们的 toString 展开。通过指定自定义 Param.Expander,用户可以控制此行为,例如格式化日期。

1
2
3
public interface Api {
@RequestLine("GET /?since={date}") Result list(@Param(value = "date", expander = DateToMillis.class) Date date);
}

4.5、动态查询参数

可以使用 QueryMap 对 Map 参数进行注释,以构造使用地图内容作为其查询参数的查询。

1
2
3
4
public interface Api {
@RequestLine("GET /find")
V find(@QueryMap Map<String, Object> queryMap);
}

这也可用于使用 QueryMapEncoder 从 POJO 对象生成查询参数。

1
2
3
4
public interface Api {
@RequestLine("GET /find")
V find(@QueryMap CustomPojo customPojo);
}

以这种方式使用时,在不指定自定义 QueryMapEncoder 的情况下,将使用成员变量名称作为查询参数名称生成查询映射。下面的 POJO 将生成“/find?name={name}&number={number}”的查询参数(不保证包含的查询参数的顺序,和往常一样,如果任何值为空,它将被排除在外)。

1
2
3
4
5
6
7
8
9
public class CustomPojo {
private final String name;
private final int number;

public CustomPojo (String name, int number) {
this.name = name;
this.number = number;
}
}

要设置自定义 QueryMapEncoder:

1
2
3
4
5
6
7
public class Example {
public static void main(String[] args) {
MyApi myApi = Feign.builder()
.queryMapEncoder(new MyCustomQueryMapEncoder())
.target(MyApi.class, "https://api.hostname.com");
}
}

使用@QueryMap 注释对象时,默认编码器使用反射来检查提供的对象字段以将对象值扩展为查询字符串。如果您希望使用 Java Beans API 中定义的 getter 和 setter 方法构建查询字符串,请使用 BeanQueryMapEncoder

1
2
3
4
5
6
7
public class Example {
public static void main(String[] args) {
MyApi myApi = Feign.builder()
.queryMapEncoder(new BeanQueryMapEncoder())
.target(MyApi.class, "https://api.hostname.com");
}
}

4.6、错误处理

如果您需要更多控制处理意外响应,Feign 实例可以通过构建器注册自定义 ErrorDecoder。

1
2
3
4
5
6
7
public class Example {
public static void main(String[] args) {
MyApi myApi = Feign.builder()
.errorDecoder(new MyErrorDecoder())
.target(MyApi.class, "https://api.hostname.com");
}
}

导致 HTTP 状态不在 2xx 范围内的所有响应都将触发 ErrorDecoder 的 decode 方法,允许您处理响应、将失败包装到自定义异常中或执行任何其他处理。如果您想再次重试请求,请抛出 RetryableException。这将调用注册的重试器。

4.7、重试

默认情况下,Feign 会自动重试 IOExceptions,不管 HTTP 方法如何,将它们视为与网络相关的瞬态异常,以及从 ErrorDecoder 抛出的任何 RetryableException。要自定义此行为,请通过构建器注册自定义 Retryer 实例。

1
2
3
4
5
6
7
public class Example {
public static void main(String[] args) {
MyApi myApi = Feign.builder()
.retryer(new MyRetryer())
.target(MyApi.class, "https://api.hostname.com");
}
}

重试器负责通过从方法 continueOrPropagate(RetryableException e); 返回 true 或 false 来确定是否应该进行重试;将为每个客户端执行创建一个 Retryer 实例,允许您根据需要维护每个请求之间的状态。 如果确定重试不成功,则会抛出最后一个 RetryException。要抛出导致重试失败的原始原因,请使用 exceptionPropagationPolicy() 选项构建您的 Feign 客户端。

4.8、指标

默认情况下,feign 不会收集任何指标。 但是,可以向任何 feign 客户端添加指标收集功能。 Metric Capabilities 提供了一流的 Metrics API,用户可以利用它来深入了解请求/响应生命周期。

Dropwizard Metrics 4

1
2
3
4
5
6
7
8
9
10
public class MyApp {
public static void main(String[] args) {
GitHub github = Feign.builder()
.addCapability(new Metrics4Capability())
.target(GitHub.class, "https://api.github.com");

github.contributors("OpenFeign", "feign");
// metrics will be available from this point onwards
}
}

Dropwizard Metrics 5

1
2
3
4
5
6
7
8
9
10
public class MyApp {
public static void main(String[] args) {
GitHub github = Feign.builder()
.addCapability(new Metrics5Capability())
.target(GitHub.class, "https://api.github.com");

github.contributors("OpenFeign", "feign");
// metrics will be available from this point onwards
}
}

Micrometer

1
2
3
4
5
6
7
8
9
10
public class MyApp {
public static void main(String[] args) {
GitHub github = Feign.builder()
.addCapability(new MicrometerCapability())
.target(GitHub.class, "https://api.github.com");

github.contributors("OpenFeign", "feign");
// metrics will be available from this point onwards
}
}

4.9、静态和默认方法

Feign 所针对的接口可能具有静态或默认方法(如果使用 Java 8+)。这些允许 Feign 客户端包含未由底层 API 明确定义的逻辑。例如,静态方法可以轻松指定常见的客户端构建配置;默认方法可用于组合查询或定义默认参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);

@RequestLine("GET /users/{username}/repos?sort={sort}")
List<Repo> repos(@Param("username") String owner, @Param("sort") String sort);

default List<Repo> repos(String owner) {
return repos(owner, "full_name");
}

/**
* Lists all contributors for all repos owned by a user.
*/
default List<Contributor> contributors(String user) {
MergingContributorList contributors = new MergingContributorList();
for(Repo repo : this.repos(owner)) {
contributors.addAll(this.contributors(user, repo.getName()));
}
return contributors.mergeResult();
}

static GitHub connect() {
return Feign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
}
}

4.10、通过 CompletableFuture 异步执行

Feign 10.8 引入了一个新的构建器 AsyncFeign,它允许方法返回 CompletableFuture 实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
CompletableFuture<List<Contributor>> contributors(@Param("owner") String owner, @Param("repo") String repo);
}

public class MyApp {
public static void main(String... args) {
GitHub github = AsyncFeign.asyncBuilder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");

// Fetch and print a list of the contributors to this library.
CompletableFuture<List<Contributor>> contributors = github.contributors("OpenFeign", "feign");
for (Contributor contributor : contributors.get(1, TimeUnit.SECONDS)) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
}
}

初始实现包括 2 个异步客户端:

  • AsyncClient.Default
  • AsyncApacheHttp5Client

feign-form基本使用

此模块添加了对编码 application/x-www-form-urlencoded 和 multipart/form-data 表单的支持。

1、添加依赖

包含对您的应用程序的依赖项:

1.1、Maven:

1
2
3
4
5
6
7
8
9
<dependencies>
...
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.8.0</version>
</dependency>
...
</dependencies>

1.2、Gradle:

1
compile 'io.github.openfeign.form:feign-form:3.8.0'

2、要求

feign-form 扩展依赖于 OpenFeign 及其具体版本:

  • 3.5.0 之前的所有 feign-form 版本都适用于 OpenFeign 9.* 版本;
  • 从 feign-form 的 3.5.0 版开始,该模块适用于 OpenFeign 10.1.0 及更高版本。

重要提示:没有向后兼容性,也没有任何保证 3.5.0 之后的 feign-form 版本与 10.* 之前的 OpenFeign 一起使用。 OpenFeign 在第 10 个版本中被重构,所以最好的方法 - 使用最新的 OpenFeign 和 feign-form 版本。

注意:

  • spring-cloud-openfeign 在 v2.0.3.RELEASE 之前使用 OpenFeign 9.*,之后使用 10.*。反正这个依赖已经有合适的feign-form版本了,看依赖pom,不需要单独指定;
  • spring-cloud-starter-feign 是一个已弃用的依赖项,它始终使用 OpenFeign 的 9.* 版本。

3、用法

像这样将 FormEncoder 添加到 Feign.Builder 中:

1
2
3
SomeApi github = Feign.builder()
.encoder(new FormEncoder())
.target(SomeApi.class, "http://api.some.org");

此外,您可以像这样装饰现有的编码器,例如 JsonEncoder:

1
2
3
SomeApi github = Feign.builder()
.encoder(new FormEncoder(new JacksonEncoder()))
.target(SomeApi.class, "http://api.some.org");

并一起使用它们:

1
2
3
4
5
6
7
8
9
10
interface SomeApi {

@RequestLine("POST /json")
@Headers("Content-Type: application/json")
void json (Dto dto);

@RequestLine("POST /form")
@Headers("Content-Type: application/x-www-form-urlencoded")
void from (@Param("field1") String field1, @Param("field2") String[] values);
}

您可以通过 Content-Type 标头指定两种类型的编码形式。

application/x-www-form-urlencoded

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface SomeApi {

@RequestLine("POST /authorization")
@Headers("Content-Type: application/x-www-form-urlencoded")
void authorization (@Param("email") String email, @Param("password") String password);

// Group all parameters within a POJO
@RequestLine("POST /user")
@Headers("Content-Type: application/x-www-form-urlencoded")
void addUser (User user);

class User {

Integer id;

String name;
}
}

multipart/form-data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
interface SomeApi {

// File parameter
@RequestLine("POST /send_photo")
@Headers("Content-Type: multipart/form-data")
void sendPhoto (@Param("is_public") Boolean isPublic, @Param("photo") File photo);

// byte[] parameter
@RequestLine("POST /send_photo")
@Headers("Content-Type: multipart/form-data")
void sendPhoto (@Param("is_public") Boolean isPublic, @Param("photo") byte[] photo);

// FormData parameter
@RequestLine("POST /send_photo")
@Headers("Content-Type: multipart/form-data")
void sendPhoto (@Param("is_public") Boolean isPublic, @Param("photo") FormData photo);

// Group all parameters within a POJO
@RequestLine("POST /send_photo")
@Headers("Content-Type: multipart/form-data")
void sendPhoto (MyPojo pojo);

class MyPojo {

@FormProperty("is_public")
Boolean isPublic;

File photo;
}
}

在上面的示例中,sendPhoto 方法使用 photo 参数使用三种不同的受支持类型。

  • File 将使用 File 的扩展名来检测 Content-Type;
  • byte[] 将使用 application/octet-stream 作为 Content-Type;
  • FormData 将使用 FormData 的 Content-Type 和 fileName;
  • 用于分组参数(包括上述类型)的客户端自定义 POJO。

FormData 是一个自定义对象,它包装了一个 byte[] 并定义了一个 Content-Type 和 fileName,如下所示:

1
2
FormData formData = new FormData("image/png", "filename.png", myDataAsByteArray);
someApi.sendPhoto(true, formData);

4、Spring MultipartFile 和 Spring Cloud Netflix @FeignClient 支持

您还可以将表单编码器与 Spring MultipartFile 和 @FeignClient 一起使用。 将依赖项包含到项目的 pom.xml 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>3.8.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>3.8.0</version>
</dependency>
</dependencies>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@FeignClient(
name = "file-upload-service",
configuration = FileUploadServiceClient.MultipartSupportConfig.class
)
public interface FileUploadServiceClient extends IFileUploadServiceClient {

public class MultipartSupportConfig {

@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;

@Bean
public Encoder feignFormEncoder () {
return new SpringFormEncoder(new SpringEncoder(messageConverters));
}
}
}

或者,如果您不需要 Spring 的标准编码器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@FeignClient(
name = "file-upload-service",
configuration = FileUploadServiceClient.MultipartSupportConfig.class
)
public interface FileUploadServiceClient extends IFileUploadServiceClient {

public class MultipartSupportConfig {

@Bean
public Encoder feignFormEncoder () {
return new SpringFormEncoder();
}
}
}

感谢 tf-haotri-pham 的特性,它利用了 Apache commons-fileupload 库,处理多部分响应的解析。正文数据部分作为字节数组保存在内存中。 要使用此功能,请在解码器的消息转换器列表中包含 SpringManyMultipartFilesReader,并让 Feign 客户端返回一个 MultipartFile 数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@FeignClient(
name = "${feign.name}",
url = "${feign.url}"
configuration = DownloadClient.ClientConfiguration.class
)
public interface DownloadClient {

@RequestMapping("/multipart/download/{fileId}")
MultipartFile[] download(@PathVariable("fileId") String fileId);

class ClientConfiguration {

@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;

@Bean
public Decoder feignDecoder () {
List<HttpMessageConverter<?>> springConverters =
messageConverters.getObject().getConverters();

List<HttpMessageConverter<?>> decoderConverters =
new ArrayList<HttpMessageConverter<?>>(springConverters.size() + 1);

decoderConverters.addAll(springConverters);
decoderConverters.add(new SpringManyMultipartFilesReader(4096));

HttpMessageConverters httpMessageConverters = new HttpMessageConverters(decoderConverters);

return new SpringDecoder(new ObjectFactory<HttpMessageConverters>() {

@Override
public HttpMessageConverters getObject() {
return httpMessageConverters;
}
});
}
}
}

SpringCloud编程模型

云原生应用

Cloud Native 是一种应用程序开发风格,它鼓励在持续交付和价值驱动的开发领域轻松采用最佳实践。一个相关的学科是构建 12 要素应用程序,其中开发实践与交付和运营目标保持一致 —— 例如,通过使用声明式编程以及管理和监控。 Spring Cloud 以多种特定方式促进了这些开发风格。起点是一组特性,分布式系统中的所有组件都需要轻松访问这些特性。

其中许多功能都包含在 Spring Boot 中,Spring Cloud 在其上构建。 Spring Cloud 提供了更多功能作为两个库:Spring Cloud Context 和 Spring Cloud Commons。 Spring Cloud Context 为 Spring Cloud 应用程序的 ApplicationContext 提供实用程序和特殊服务(引导上下文、加密、刷新范围和环境端点)。 Spring Cloud Commons 是在不同的 Spring Cloud 实现(例如 Spring Cloud Netflix 和 Spring Cloud Consul)中使用的一组抽象和通用类。

如果由于“非法密钥大小”而出现异常并且您使用 Sun 的 JDK,则需要安装 Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files。有关更多信息,请参阅以下链接:

对于您使用的任何 JRE/JDK x64/x86 版本,将文件解压缩到 JDK/jre/lib/security 文件夹中。

1. Spring Cloud Context:应用程序上下文服务

Spring Boot 对如何使用 Spring 构建应用程序有自己的看法。例如,它具有常见配置文件的常规位置,并具有用于常见管理和监控任务的端点。 Spring Cloud 在此基础上构建,并添加了一些系统中许多组件会使用或偶尔需要的功能。

1.1. Bootstrap 应用程序上下文

Spring Cloud 应用程序通过创建“引导程序”上下文来运行,该上下文是主应用程序的父上下文。此上下文负责从外部源加载配置属性并解密本地外部配置文件中的属性。这两个上下文共享一个环境,它是任何 Spring 应用程序的外部属性的来源。默认情况下,引导属性(不是 bootstrap.properties,而是在引导阶段加载的属性)以高优先级添加,因此它们不能被本地配置覆盖。

引导上下文使用与主应用程序上下文不同的约定来定位外部配置。您可以使用 bootstrap.yml 代替 application.yml(或 .properties),将引导程序和主上下文的外部配置很好地分开。以下清单显示了一个示例:

示例 1. bootstrap.yml

1
2
3
4
5
6
spring:
application:
name: foo
cloud:
config:
uri: ${SPRING_CONFIG_URI:http://localhost:8888}

如果您的应用程序需要来自服务器的任何特定于应用程序的配置,最好设置 spring.application.name(在 bootstrap.yml 或 application.yml 中)。要将属性 spring.application.name 用作应用程序的上下文 ID,您必须在 bootstrap.[properties|yml]中设置它。 。

如果要检索特定的配置文件配置,还应该在 bootstrap.[properties|yml] 中设置 spring.profiles.active。

您可以通过设置 spring.cloud.bootstrap.enabled=false (例如,在系统属性中)来完全禁用引导过程。

1.2.应用程序上下文层次结构

如果您从 SpringApplication 或 SpringApplicationBuilder 构建应用程序上下文,则 Bootstrap 上下文将作为父级添加到该上下文。 Spring 的一个特性是子上下文从其父上下文继承属性源和配置文件,因此“主”应用程序上下文包含额外的属性源,与在没有 Spring Cloud Config 的情况下构建相同的上下文相比。额外的财产来源是:

  • “bootstrap”:如果在 bootstrap 上下文中找到任何 PropertySourceLocators 并且它们具有非空属性,则会出现一个具有高优先级的可选 CompositePropertySource。一个例子是来自 Spring Cloud Config Server 的属性。有关如何自定义此属性源的内容,请参阅“自定义 Bootstrap 属性源”。
  • “applicationConfig: [classpath:bootstrap.yml]”(以及相关文件,如果 Spring 配置文件处于活动状态):如果您有 bootstrap.yml(或 .properties),这些属性用于配置引导程序上下文。然后当它的父级被设置时,它们被添加到子上下文中。它们的优先级低于 application.yml(或 .properties)以及作为创建 Spring Boot 应用程序过程的正常部分添加到子项的任何其他属性源。有关如何自定义这些属性源的内容,请参阅“更改 Bootstrap 属性的位置”。

由于属性源的排序规则,“引导”条目优先。但是,请注意这些不包含来自 bootstrap.yml 的任何数据,它具有非常低的优先级但可用于设置默认值。

您可以通过设置您创建的任何 ApplicationContext 的父上下文来扩展上下文层次结构 — 例如,通过使用它自己的接口或使用 SpringApplicationBuilder 便捷方法(parent()、child() 和sibling())。引导上下文是您自己创建的最高级祖先的父级。层次结构中的每个上下文都有自己的“引导程序”(可能是空的)属性源,以避免无意中将值从父级提升到其后代。如果有配置服务器,层次结构中的每个上下文也可以(原则上)具有不同的 spring.application.name,因此,具有不同的远程属性源。普通 Spring 应用程序上下文行为规则适用于属性解析:来自子上下文的属性覆盖父上下文中的属性,按名称以及属性源名称。 (如果子级具有与父级同名的属性源,则父级的值不包含在子级中)。

请注意, SpringApplicationBuilder 允许您在整个层次结构中共享环境,但这不是默认设置。因此,兄弟上下文(特别是)不需要具有相同的配置文件或属性源,即使它们可能与其父级共享共同的值。

1.3.更改 Bootstrap 属性的位置

可以通过设置 spring.cloud.bootstrap.name(默认:bootstrap)、spring.cloud.bootstrap.location(默认:空)或 spring.cloud.bootstrap.additional-location 来指定 bootstrap.yml(或 .properties)位置(默认:空) — 例如,在系统属性中。

这些属性的行为类似于具有相同名称的 spring.config.* 变体。使用 spring.cloud.bootstrap.location 替换默认位置,仅使用指定的位置。要将位置添加到默认列表中,可以使用 spring.cloud.bootstrap.additional-location。事实上,它们用于通过在 Environment 中设置这些属性来设置 bootstrap ApplicationContext。如果有一个活动配置文件(来自 spring.profiles.active 或通过您正在构建的上下文中的环境 API),该配置文件中的属性也会被加载,就像在常规 Spring Boot 应用程序中一样 — 例如,来自 bootstrap -development.properties 用于开发配置文件。

1.4.覆盖远程属性的值

由引导上下文添加到应用程序的属性源通常是“远程的”(例如,来自 Spring Cloud Config Server)。默认情况下,它们不能在本地被覆盖。如果你想让你的应用程序用他们自己的系统属性或配置文件覆盖远程属性,远程属性源必须通过设置 spring.cloud.config.allowOverride=true 来授予它权限(在本地设置它不起作用) .一旦设置了该标志,两个更细粒度的设置将控制与系统属性和应用程序本地配置相关的远程属性的位置:

  • spring.cloud.config.overrideNone=true:从任何本地属性源覆盖。
  • spring.cloud.config.overrideSystemProperties=false:只有系统属性、命令行参数和环境变量(而不是本地配置文件)应该覆盖远程设置。

1.5.自定义引导配置

通过在名为 org.springframework.cloud.bootstrap.BootstrapConfiguration 的键下向 /META-INF/spring.factories 添加条目,可以将引导上下文设置为执行您喜欢的任何操作。这包含用于创建上下文的 Spring @Configuration 类的逗号分隔列表。您希望在主应用程序上下文中可用于自动装配的任何 bean 都可以在此处创建。 ApplicationContextInitializer 类型的@Beans 有一个特殊的契约。如果要控制启动顺序,可以用@Order 注解标记类(默认顺序是最后)。

添加自定义 BootstrapConfiguration 时,请注意您添加的类不会错误地@ComponentScanned 到您的“主”应用程序上下文中,在那里可能不需要它们。为引导配置类使用单独的包名称,并确保该名称尚未被 @ComponentScan 或 @SpringBootApplication 注释的配置类覆盖。

bootstrap 过程通过将初始化器注入主 SpringApplication 实例(这是正常的 Spring Boot 启动序列,无论它作为独立应用程序运行还是部署在应用程序服务器中)结束。首先,从 spring.factories 中的类创建引导上下文。然后,ApplicationContextInitializer 类型的所有@Beans 在启动之前被添加到主 SpringApplication 中。

1.6.自定义 Bootstrap 属性源

bootstrap 进程添加的外部配置的默认属性源是 Spring Cloud Config Server,但您可以通过将 PropertySourceLocator 类型的 bean 添加到 bootstrap 上下文(通过 spring.factories)来添加其他源。例如,您可以插入来自不同服务器或数据库的其他属性。

例如,请考虑以下自定义定位器:

1
2
3
4
5
6
7
8
9
10
@Configuration
public class CustomPropertySourceLocator implements PropertySourceLocator {

@Override
public PropertySource<?> locate(Environment environment) {
return new MapPropertySource("customProperty",
Collections.<String, Object>singletonMap("property.from.sample.custom.source", "worked as intended"));
}

}

传入的 Environment 是即将创建的 ApplicationContext 的环境 — 换句话说,我们为其提供附加属性源的环境。它已经拥有普通的 Spring Boot 提供的属性源,因此您可以使用它们来定位特定于此环境的属性源(例如,通过在 spring.application.name 上键入它,就像在默认的 Spring Cloud Config Server 中所做的那样属性源定位器)。

如果您在其中创建一个包含此类的 jar,然后添加包含以下设置的 META-INF/spring.factories,则 customProperty PropertySource 将出现在其类路径中包含该 jar 的任何应用程序中:

1
org.springframework.cloud.bootstrap.BootstrapConfiguration=sample.custom.CustomPropertySourceLocator

1.7.日志配置

如果你使用 Spring Boot 来配置日志设置,你应该把这个配置放在 bootstrap.[yml|properties] 如果您希望它适用于所有事件。

为了让 Spring Cloud 正确初始化日志配置,您不能使用自定义前缀。例如,在初始化日志系统时,Spring Cloud 无法识别使用 custom.loggin.logpath。

1.8.环境变化

应用程序侦听 EnvironmentChangeEvent 并以几种标准方式对更改做出反应(额外的 ApplicationListeners 可以以正常方式添加为 @Beans)。当观察到 EnvironmentChangeEvent 时,它具有已更改的键值列表,应用程序使用这些值:

  • 在上下文中重新绑定任何 @ConfigurationProperties bean。
  • 为 logging.level.* 中的任何属性设置记录器级别。

请注意,默认情况下,Spring Cloud Config Client 不会轮询环境中的更改。通常,我们不建议使用这种方法来检测更改(尽管您可以使用 @Scheduled 注释进行设置)。如果您有横向扩展的客户端应用程序,最好将 EnvironmentChangeEvent 广播到所有实例,而不是让它们轮询更改(例如,通过使用 Spring Cloud Bus)。

EnvironmentChangeEvent 涵盖了一大类刷新用例,只要您可以实际更改 Environment 并发布事件即可。请注意,这些 API 是公共的并且是核心 Spring 的一部分)。您可以通过访问 /configprops 端点(标准 Spring Boot Actuator 功能)来验证更改是否绑定到 @ConfigurationProperties bean。例如,DataSource 可以在运行时更改其 maxPoolSize(Spring Boot 创建的默认 DataSource 是一个 @ConfigurationProperties bean)并动态增加容量。重新绑定 @ConfigurationProperties 不涵盖另一大类用例,在这些用例中,您需要对刷新进行更多控制,并且需要对整个 ApplicationContext 进行原子性更改。为了解决这些问题,我们有@RefreshScope。

1.9.刷新范围

当配置发生变化时,标记为@RefreshScope 的 Spring @Bean 会得到特殊处理。此功能解决了有状态 bean 的问题,这些 bean 仅在初始化时注入其配置。例如,如果在通过环境更改数据库 URL 时 DataSource 具有打开的连接,您可能希望这些连接的持有者能够完成他们正在做的事情。然后,下一次从池中借用连接时,它会获得一个带有新 URL 的连接。

有时,甚至可能必须在某些只能初始化一次的 bean 上应用 @RefreshScope 注释。如果 bean 是“不可变的”,则必须使用 @RefreshScope 注释 bean 或在属性键下指定类名:spring.cloud.refresh.extra-refreshable。

如果你有一个作为 HikariDataSource 的 DataSource bean,它不能被刷新。这是 spring.cloud.refresh.never-refreshable 的默认值。如果需要刷新,请选择不同的 DataSource 实现。

刷新作用域 bean 是在使用时(即调用方法时)进行初始化的惰性代理,作用域充当已初始化值的缓存。要强制 bean 在下一次方法调用时重新初始化,您必须使其缓存条目无效。

RefreshScope 是上下文中的一个 bean,并且有一个公共 refreshAll() 方法通过清除目标缓存来刷新范围内的所有 bean。 /refresh 端点公开了此功能(通过 HTTP 或 JMX)。要按名称刷新单个 bean,还有一个 refresh(String) 方法。

要公开 /refresh 端点,您需要向应用程序添加以下配置:

1
2
3
4
5
management:
endpoints:
web:
exposure:
include: refresh

@RefreshScope 在 @Configuration 类上工作(技术上),但它可能会导致令人惊讶的行为。例如,这并不意味着该类中定义的所有@Beans 本身都在@RefreshScope 中。具体来说,任何依赖于这些 bean 的东西都不能依赖于在启动刷新时更新它们,除非它本身在 @RefreshScope 中。在这种情况下,它会在刷新时重建,并重新注入其依赖项。那时,它们从刷新的@Configuration 重新初始化)。

1.10.加密和解密

Spring Cloud 有一个 Environment 预处理器,用于在本地解密属性值。它遵循与 Spring Cloud Config Server 相同的规则,并通过 encrypt.* 具有相同的外部配置。因此,您可以使用 {cipher}* 形式的加密值,并且只要存在有效密钥,它们就会在主应用程序上下文获得环境设置之前被解密。要在应用程序中使用加密功能,您需要在类路径中包含 Spring Security RSA(Maven 坐标:org.springframework.security:spring-security-rsa),并且您还需要在 JVM 中使用完整的 JCE 扩展.

如果由于“非法密钥大小”而出现异常并且您使用 Sun 的 JDK,则需要安装 Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files。有关更多信息,请参阅以下链接:

对于您使用的任何 JRE/JDK x64/x86 版本,将文件解压缩到 JDK/jre/lib/security 文件夹中。

1.11.端点

对于 Spring Boot Actuator 应用程序,可以使用一些额外的管理端点。您可以使用:

  • POST 到 /actuator/env 以更新环境并重新绑定 @ConfigurationProperties 和日志级别。要启用此端点,您必须设置 management.endpoint.env.post.enabled=true。
  • /actuator/refresh 重新加载引导上下文并刷新@RefreshScope bean。
  • /actuator/restart 关闭 ApplicationContext 并重新启动它(默认情况下禁用)。
  • /actuator/pause 和 /actuator/resume 用于调用生命周期方法(ApplicationContext 上的 stop() 和 start())。

如果您禁用 /actuator/restart 端点,那么 /actuator/pause 和 /actuator/resume 端点也将被禁用,因为它们只是 /actuator/restart 的一个特例。

2. Spring Cloud Commons:通用抽象

服务发现、负载平衡和断路器等模式适合一个公共抽象层,所有 Spring Cloud 客户端都可以使用该抽象层,独立于实现(例如,使用 Eureka 或 Consul 进行发现)。

2.1. @EnableDiscoveryClient 注解

Spring Cloud Commons 提供了 @EnableDiscoveryClient 注解。这会寻找带有 META-INF/spring.factories 的 DiscoveryClient 和 ReactiveDiscoveryClient 接口的实现。发现客户端的实现在 org.springframework.cloud.client.discovery.EnableDiscoveryClient 键下的 spring.factories 中添加了一个配置类。 DiscoveryClient 实现的示例包括 Spring Cloud Netflix Eureka、Spring Cloud Consul Discovery 和 Spring Cloud Zookeeper Discovery。

默认情况下,Spring Cloud 将提供阻塞和反应式服务发现客户端。您可以通过设置 spring.cloud.discovery.blocking.enabled=false 或 spring.cloud.discovery.reactive.enabled=false 轻松禁用阻塞和/或反应客户端。要完全禁用服务发现,您只需设置 spring.cloud.discovery.enabled=false。

默认情况下, DiscoveryClient 的实现会自动向远程发现服务器注册本地 Spring Boot 服务器。可以通过在 @EnableDiscoveryClient 中设置 autoRegister=false 来禁用此行为。

不再需要@EnableDiscoveryClient。您可以在类路径上放置一个 DiscoveryClient 实现,以使 Spring Boot 应用程序向服务发现服务器注册。

2.1.1.健康指标

Commons 自动配置以下 Spring Boot 健康指标。

发现客户端健康指标DiscoveryClientHealthIndicator

此运行状况指标基于当前注册的 DiscoveryClient 实现。

  • 要完全禁用,请设置 spring.cloud.discovery.client.health-indicator.enabled=false。
  • 要禁用描述字段,请设置 spring.cloud.discovery.client.health-indicator.include-description=false。否则,它可能会冒泡作为汇总的 HealthIndicator 的描述。
  • 要禁用服务检索,请设置 spring.cloud.discovery.client.health-indicator.use-services-query=false。默认情况下,指标调用客户端的 getServices 方法。在具有许多注册服务的部署中,在每次检查期间检索所有服务的成本可能太高。这将跳过服务检索,而是使用客户端的探测方法。
DiscoveryCompositeHealthContributor

此复合健康指标基于所有已注册的 DiscoveryHealthIndicator bean。要禁用,请设置 spring.cloud.discovery.client.composite-indicator.enabled=false。

2.1.2.Ordering DiscoveryClient 实例

DiscoveryClient 接口扩展了 Ordered。这在使用多个发现客户端时很有用,因为它允许您定义返回的发现客户端的顺序,类似于如何对 Spring 应用程序加载的 bean 进行排序。默认情况下,任何 DiscoveryClient 的顺序设置为 0。如果您想为自定义 DiscoveryClient 实现设置不同的顺序,您只需覆盖 getOrder() 方法,以便它返回适合您设置的值。除此之外,您可以使用属性来设置 Spring Cloud 提供的 DiscoveryClient 实现的顺序,其中包括 ConsulDiscoveryClient、EurekaDiscoveryClient 和 ZookeeperDiscoveryClient。为此,您只需将 spring.cloud.{clientIdentifier}.discovery.order (或 Eureka 的 eureka.client.order)属性设置为所需的值。

2.1.3. SimpleDiscoveryClient

如果类路径中没有 Service-Registry-backed DiscoveryClient,将使用 SimpleDiscoveryClient 实例,它使用属性来获取有关服务和实例的信息。

有关可用实例的信息应通过以下格式的属性传递给: spring.cloud.discovery.client.simple.instances.service1[0].uri=http://s11:8080,其中 spring.cloud.discovery .client.simple.instances 是公共前缀,然后service1代表所涉及的服务的ID,而[0]代表实例的索引号(如示例中可见,索引从0开始),然后是uri 的值是实例可用的实际 URI。

2.2. ServiceRegistry

Commons 现在提供了一个 ServiceRegistry 接口,该接口提供 register(Registration) 和 deregister(Registration) 等方法,让您可以提供自定义注册服务。注册是一个标记界面。

以下示例显示了正在使用的 ServiceRegistry:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@EnableDiscoveryClient(autoRegister=false)
public class MyConfiguration {
private ServiceRegistry registry;

public MyConfiguration(ServiceRegistry registry) {
this.registry = registry;
}

// called through some external process, such as an event or a custom actuator endpoint
public void register() {
Registration registration = constructRegistration();
this.registry.register(registration);
}
}

每个 ServiceRegistry 实现都有自己的 Registry 实现。

  • ZookeeperRegistration 与 ZookeeperServiceRegistry 一起使用
  • EurekaRegistration 与 EurekaServiceRegistry 一起使用
  • ConsulRegistration 与 ConsulServiceRegistry 一起使用

如果您使用 ServiceRegistry 接口,您将需要为您正在使用的 ServiceRegistry 实现传递正确的 Registry 实现。

2.2.1. ServiceRegistry Auto-Registration

默认情况下,ServiceRegistry 实现会自动注册正在运行的服务。要禁用该行为,您可以设置: * @EnableDiscoveryClient(autoRegister=false) 以永久禁用自动注册。 * spring.cloud.service-registry.auto-registration.enabled=false 通过配置禁用行为。

ServiceRegistry Auto-Registration Events

服务自动注册时将触发两个事件。第一个事件称为 InstancePreRegisteredEvent,在注册服务之前触发。第二个事件称为 InstanceRegisteredEvent,在注册服务后触发。您可以注册一个 ApplicationListener(s) 来监听和响应这些事件。

如果 spring.cloud.service-registry.auto-registration.enabled 属性设置为 false,则不会触发这些事件。

2.2.2. Service Registry Actuator Endpoint

Spring Cloud Commons 提供了一个 /service-registry 执行器端点。此端点依赖于 Spring 应用程序上下文中的注册 bean。使用 GET 调用 /service-registry 会返回注册的状态。对具有 JSON 正文的同一端点使用 POST 会将当前注册的状态更改为新值。 JSON 正文必须包含具有首选值的状态字段。请参阅更新状态和状态返回值时用于允许值的 ServiceRegistry 实现的文档。例如,Eureka 支持的状态是 UP、DOWN、OUT_OF_SERVICE 和 UNKNOWN。

2.3. Spring RestTemplate 作为负载均衡器客户端

您可以配置 RestTemplate 以使用负载平衡器客户端。要创建负载平衡的 RestTemplate,请创建 RestTemplate @Bean 并使用 @LoadBalanced 限定符,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class MyConfiguration {

@LoadBalanced
@Bean
RestTemplate restTemplate() {
return new RestTemplate();
}
}

public class MyClass {
@Autowired
private RestTemplate restTemplate;

public String doOtherStuff() {
String results = restTemplate.getForObject("http://stores/stores", String.class);
return results;
}
}

RestTemplate bean 不再通过自动配置创建。个人应用程序必须创建它。

URI 需要使用虚拟主机名(即服务名,而不是主机名)。 BlockingLoadBalancerClient 用于创建完整的物理地址。

要使用负载平衡的 RestTemplate,您需要在类路径中有一个负载平衡器实现。将 Spring Cloud LoadBalancer starter 添加到您的项目中以便使用它。

2.4. Spring WebClient 作为负载均衡器客户端

您可以将 WebClient 配置为自动使用负载平衡器客户端。要创建负载均衡的 WebClient,请创建一个 WebClient.Builder @Bean 并使用 @LoadBalanced 限定符,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class MyConfiguration {

@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
}

public class MyClass {
@Autowired
private WebClient.Builder webClientBuilder;

public Mono<String> doOtherStuff() {
return webClientBuilder.build().get().uri("http://stores/stores")
.retrieve().bodyToMono(String.class);
}
}

URI 需要使用虚拟主机名(即服务名,而不是主机名)。 Spring Cloud LoadBalancer 用于创建完整的物理地址。

如果你想使用@LoadBalanced WebClient.Builder,你需要在类路径中有一个负载均衡器实现。我们建议您将 Spring Cloud LoadBalancer starter 添加到您的项目中。然后,在下面使用 ReactiveLoadBalancer。

2.4.1.重试失败的请求

负载平衡的 RestTemplate 可以配置为重试失败的请求。默认情况下,此逻辑被禁用。对于非响应式版本(使用 RestTemplate),您可以通过将 Spring Retry 添加到应用程序的类路径来启用它。对于响应式版本(使用 WebTestClient),您需要设置 `spring.cloud.loadbalancer.retry.enabled=true。

如果您想在类路径上使用 Spring Retry 或 Reactive Retry 禁用重试逻辑,您可以设置 spring.cloud.loadbalancer.retry.enabled=false。

对于非反应式实现,如果您想在重试中实现 BackOffPolicy,则需要创建一个 LoadBalancedRetryFactory 类型的 bean 并覆盖 createBackOffPolicy() 方法。

对于反应式实现,您只需要通过将 spring.cloud.loadbalancer.retry.backoff.enabled 设置为 false 来启用它。

您可以设置:

  • spring.cloud.loadbalancer.retry.maxRetriesOnSameServiceInstance - 指示应在同一个 ServiceInstance 上重试请求的次数(为每个选定的实例单独计数)
  • spring.cloud.loadbalancer.retry.maxRetriesOnNextServiceInstance - 指示应重试新选择的 ServiceInstance 请求的次数
  • spring.cloud.loadbalancer.retry.retryableStatusCodes - 始终重试失败请求的状态代码。

对于反应式实现,您还可以设置: - spring.cloud.loadbalancer.retry.backoff.minBackoff - 设置最小退避持续时间(默认为 5 毫秒) - spring.cloud.loadbalancer.retry.backoff.maxBackoff - 设置最大退避持续时间(默认情况下,最大长值毫秒) - spring.cloud.loadbalancer.retry.backoff.jitter - 设置用于计算每次调用的实际退避持续时间的抖动(默认情况下,0.5)。

对于反应式实现,您还可以实现自己的 LoadBalancerRetryPolicy 以更详细地控制负载平衡的调用重试。

对于负载平衡重试,默认情况下,我们使用 RetryAwareServiceInstanceListSupplier 包装 ServiceInstanceListSupplier bean,以从先前选择的实例中选择一个不同的实例(如果可用)。您可以通过将 spring.cloud.loadbalancer.retry.avoidPreviousInstance 的值设置为 false 来禁用此行为。

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class MyConfiguration {
@Bean
LoadBalancedRetryFactory retryFactory() {
return new LoadBalancedRetryFactory() {
@Override
public BackOffPolicy createBackOffPolicy(String service) {
return new ExponentialBackOffPolicy();
}
};
}
}

如果您想将一个或多个 RetryListener 实现添加到您的重试功能中,您需要创建一个 LoadBalancedRetryListenerFactory 类型的 bean 并返回您想用于给定服务的 RetryListener 数组,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration
public class MyConfiguration {
@Bean
LoadBalancedRetryListenerFactory retryListenerFactory() {
return new LoadBalancedRetryListenerFactory() {
@Override
public RetryListener[] createRetryListeners(String service) {
return new RetryListener[]{new RetryListener() {
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
//TODO Do you business...
return true;
}

@Override
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
//TODO Do you business...
}

@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
//TODO Do you business...
}
}};
}
};
}
}

2.5.多个 RestTemplate 对象

如果您想要一个非负载均衡的 RestTemplate,请创建一个 RestTemplate bean 并注入它。要访问负载平衡的 RestTemplate,请在创建 @Bean 时使用 @LoadBalanced 限定符,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Configuration
public class MyConfiguration {

@LoadBalanced
@Bean
WebClient.Builder loadBalanced() {
return WebClient.builder();
}

@Primary
@Bean
WebClient.Builder webClient() {
return WebClient.builder();
}
}

public class MyClass {
@Autowired
private WebClient.Builder webClientBuilder;

@Autowired
@LoadBalanced
private WebClient.Builder loadBalanced;

public Mono<String> doOtherStuff() {
return loadBalanced.build().get().uri("http://stores/stores")
.retrieve().bodyToMono(String.class);
}

public Mono<String> doStuff() {
return webClientBuilder.build().get().uri("http://example.com")
.retrieve().bodyToMono(String.class);
}
}

2.7. Spring WebFlux WebClient 作为负载均衡器客户端

Spring WebFlux 可以使用反应式和非反应式 WebClient 配置,如主题所述:

  • 带有 ReactorLoadBalancerExchangeFilterFunction 的 Spring WebFlux WebClient
  • [负载平衡器交换过滤器功能负载平衡器交换过滤器功能]
2.7.1.带有 ReactorLoadBalancerExchangeFilterFunction 的 Spring WebFlux WebClient

您可以将 WebClient 配置为使用 ReactiveLoadBalancer。如果您将 Spring Cloud LoadBalancer starter 添加到您的项目中并且如果 spring-webflux 在类路径上,则 ReactorLoadBalancerExchangeFilterFunction 是自动配置的。以下示例显示如何配置 WebClient 以使用反应式负载均衡器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyClass {
@Autowired
private ReactorLoadBalancerExchangeFilterFunction lbFunction;

public Mono<String> doOtherStuff() {
return WebClient.builder().baseUrl("http://stores")
.filter(lbFunction)
.build()
.get()
.uri("/stores")
.retrieve()
.bodyToMono(String.class);
}
}

URI 需要使用虚拟主机名(即服务名,而不是主机名)。 ReactorLoadBalancer 用于创建完整的物理地址。

2.7.2.带有非反应式负载均衡器客户端的 Spring WebFlux WebClient

如果 spring-webflux 在类路径上,LoadBalancerExchangeFilterFunction 是自动配置的。但是请注意,这在后台使用了一个非反应式客户端。以下示例显示如何配置 WebClient 以使用负载均衡器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyClass {
@Autowired
private LoadBalancerExchangeFilterFunction lbFunction;

public Mono<String> doOtherStuff() {
return WebClient.builder().baseUrl("http://stores")
.filter(lbFunction)
.build()
.get()
.uri("/stores")
.retrieve()
.bodyToMono(String.class);
}
}

URI 需要使用虚拟主机名(即服务名,而不是主机名)。 LoadBalancerClient 用于创建完整的物理地址。

警告:此方法现已弃用。我们建议您使用带有反应式负载均衡器的 WebFlux。

2.8.忽略网络接口

有时,忽略某些命名的网络接口很有用,以便它们可以从服务发现注册中排除(例如,在 Docker 容器中运行时)。可以设置正则表达式列表以导致所需的网络接口被忽略。以下配置忽略了 docker0 接口和所有以 veth 开头的接口:

示例 2. application.yml

1
2
3
4
5
6
spring:
cloud:
inetutils:
ignoredInterfaces:
- docker0
- veth.*

您还可以通过使用正则表达式列表强制仅使用指定的网络地址,如以下示例所示:

示例 3. bootstrap.yml

1
2
3
4
5
6
spring:
cloud:
inetutils:
preferredNetworks:
- 192.168
- 10.0

您还可以强制仅使用站点本地地址,如以下示例所示:

示例 4. application.yml

1
2
3
4
spring:
cloud:
inetutils:
useOnlySiteLocalInterfaces: true

有关什么构成站点本地地址的更多详细信息,请参阅 Inet4Address.html.isSiteLocalAddress()。

2.9. HTTP 客户端工厂

Spring Cloud Commons 提供了用于创建 Apache HTTP 客户端 (ApacheHttpClientFactory) 和 OK HTTP 客户端 (OkHttpClientFactory) 的 bean。只有当 OK HTTP jar 位于类路径上时,才会创建 OkHttpClientFactory bean。此外,Spring Cloud Commons 提供了用于创建两个客户端使用的连接管理器的 bean:ApacheHttpClientConnectionManagerFactory 用于 Apache HTTP 客户端,OkHttpClientConnectionPoolFactory 用于 OK HTTP 客户端。如果您想自定义如何在下游项目中创建 HTTP 客户端,您可以提供您自己的这些 bean 的实现。此外,如果您提供类型为 HttpClientBuilder 或 OkHttpClient.Builder 的 bean,则默认工厂使用这些构建器作为返回到下游项目的构建器的基础。您还可以通过将 spring.cloud.httpclientfactories.apache.enabled 或 spring.cloud.httpclientfactories.ok.enabled 设置为 false 来禁用这些 bean 的创建。

2.10.启用的功能

Spring Cloud Commons 提供了一个 /features 执行器端点。此端点返回类路径上可用的功能以及它们是否已启用。返回的信息包括功能类型、名称、版本和供应商。

2.10.1.特征类型

有两种类型的“特征”:抽象的和命名的。

抽象特性是定义接口或抽象类以及创建实现的特性,例如 DiscoveryClient、LoadBalancerClient 或 LockService。抽象类或接口用于在上下文中查找该类型的 bean。显示的版本是 bean.getClass().getPackage().getImplementationVersion()。

命名特性是没有它们实现的特定类的特性。这些功能包括“断路器”、“API 网关”、“Spring Cloud Bus”等。这些功能需要一个名称和一个 bean 类型。

2.10.2.声明功能

任何模块都可以声明任意数量的 HasFeature bean,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Bean
public HasFeatures commonsFeatures() {
return HasFeatures.abstractFeatures(DiscoveryClient.class, LoadBalancerClient.class);
}

@Bean
public HasFeatures consulFeatures() {
return HasFeatures.namedFeatures(
new NamedFeature("Spring Cloud Bus", ConsulBusAutoConfiguration.class),
new NamedFeature("Circuit Breaker", HystrixCommandAspect.class));
}

@Bean
HasFeatures localFeatures() {
return HasFeatures.builder()
.abstractFeature(Something.class)
.namedFeature(new NamedFeature("Some Other Feature", Someother.class))
.abstractFeature(Somethingelse.class)
.build();
}

这些 bean 中的每一个都应该放在一个适当保护的 @Configuration 中。

2.11. Spring Cloud 兼容性验证

由于部分用户在设置 Spring Cloud 应用程序时遇到问题,我们决定添加兼容性验证机制。如果您当前的设置与 Spring Cloud 要求不兼容,它将中断,并附上一份报告,显示究竟出了什么问题。

目前我们验证将哪个版本的 Spring Boot 添加到您的类路径中。

报告示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
***************************
APPLICATION FAILED TO START
***************************

Description:

Your project setup is incompatible with our requirements due to following reasons:

- Spring Boot [2.1.0.RELEASE] is not compatible with this Spring Cloud release train


Action:

Consider applying the following actions:

- Change Spring Boot version to one of the following versions [1.2.x, 1.3.x] .
You can find the latest Spring Boot versions here [https://spring.io/projects/spring-boot#learn].
If you want to learn more about the Spring Cloud Release train compatibility, you can visit this page [https://spring.io/projects/spring-cloud#overview] and check the [Release Trains] section.

要禁用此功能,请将 spring.cloud.compatibility-verifier.enabled 设置为 false。如果要覆盖兼容的 Spring Boot 版本,只需使用逗号分隔的兼容 Spring Boot 版本列表设置 spring.cloud.compatibility-verifier.compatible-boot-versions 属性。

3. Spring Cloud 负载均衡器

Spring Cloud 提供了自己的客户端负载均衡器抽象和实现。对于负载均衡机制,添加了 ReactiveLoadBalancer 接口,并为其提供了基于 Round-Robin 和 Random 的实现。为了让实例从反应式 ServiceInstanceListSupplier 中选择。目前,我们支持 ServiceInstanceListSupplier 的基于服务发现的实现,该实现使用类路径中可用的发现客户端从服务发现中检索可用实例。

可以通过将 spring.cloud.loadbalancer.enabled 的值设置为 false 来禁用 Spring Cloud LoadBalancer。

3.1.在负载平衡算法之间切换

默认使用的 ReactiveLoadBalancer 实现是 RoundRobinLoadBalancer。要为选定的服务或所有服务切换到不同的实现,您可以使用自定义 LoadBalancer 配置机制。

例如,可以通过@LoadBalancerClient 注解传递以下配置以切换到使用 RandomLoadBalancer:

1
2
3
4
5
6
7
8
9
10
11
public class CustomLoadBalancerConfiguration {

@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(loadBalancerClientFactory
.getLazyProvider(name, ServiceInstanceListSupplier.class),
name);
}
}

您作为 @LoadBalancerClient 或 @LoadBalancerClients 配置参数传递的类不应使用 @Configuration 进行注释或不在组件扫描范围内。

3.2. Spring Cloud LoadBalancer 集成

为了方便使用 Spring Cloud LoadBalancer,我们提供了可与 WebClient 一起使用的 ReactorLoadBalancerExchangeFilterFunction 和与 RestTemplate 一起使用的 BlockingLoadBalancerClient。您可以在以下部分中查看更多信息和用法示例:

  • Spring RestTemplate 作为负载均衡器客户端
  • Spring WebClient 作为负载均衡器客户端
  • 带有 ReactorLoadBalancerExchangeFilterFunction 的 Spring WebFlux WebClient

3.3. Spring Cloud LoadBalancer 缓存

除了在每次必须选择实例时通过 DiscoveryClient 检索实例的基本 ServiceInstanceListSupplier 实现之外,我们还提供了两个缓存实现。

3.3.1.Caffeine支持的 LoadBalancer 缓存实现

如果类路径中有 com.github.ben-manes.caffeine:caffeine,则将使用基于咖啡因的实现。有关如何配置它的信息,请参阅 LoadBalancerCacheConfiguration 部分。

如果您使用的是 Caffeine,您还可以通过在 spring.cloud.loadbalancer.cache.caffeine.spec 属性中传递您自己的 Caffeine Specification 来覆盖 LoadBalancer 的默认 Caffeine 缓存设置。

警告:传递您自己的 Caffeine 规范将覆盖任何其他 LoadBalancerCache 设置,包括常规 LoadBalancer 缓存配置字段,例如 ttl 和容量。

3.3.2.默认 LoadBalancer 缓存实现

如果类路径中没有 Caffeine,则将使用 spring-cloud-starter-loadbalancer 自动附带的 DefaultLoadBalancerCache。有关如何配置它的信息,请参阅 LoadBalancerCacheConfiguration 部分。

要使用 Caffeine 而不是默认缓存,请将 com.github.ben-manes.caffeine:caffeine 依赖项添加到类路径。

3.3.3.负载均衡器缓存配置

您可以设置自己的 ttl 值(写入后条目应过期的时间),表示为 Duration,方法是将符合 Spring Boot String 的 String 传递到 Duration 转换器语法。作为 spring.cloud.loadbalancer.cache.ttl 属性的值。您还可以通过设置 spring.cloud.loadbalancer.cache.capacity 属性的值来设置自己的 LoadBalancer 缓存初始容量。

默认设置包括 ttl 设置为 35 秒,默认 initialCapacity 为 256。

您还可以通过将 spring.cloud.loadbalancer.cache.enabled 的值设置为 false 来完全禁用 loadBalancer 缓存。

尽管基本的非缓存实现对于原型设计和测试很有用,但它的效率远低于缓存版本,因此我们建议始终在生产中使用缓存版本。如果缓存已由 DiscoveryClient 实现完成,例如 EurekaDiscoveryClient,则应禁用负载平衡器缓存以防止双重缓存。

3.4.基于区域的负载平衡

为了启用基于区域的负载平衡,我们提供了 ZonePreferenceServiceInstanceListSupplier。我们使用 DiscoveryClient 特定的区域配置(例如,eureka.instance.metadata-map.zone)来选择客户端尝试过滤可用服务实例的区域。

您还可以通过设置 spring.cloud.loadbalancer.zone 属性的值来覆盖特定于 DiscoveryClient 的区域设置。

目前,只有 Eureka Discovery Client 被检测来设置 LoadBalancer 区域。对于其他发现客户端,设置 spring.cloud.loadbalancer.zone 属性。更多仪器即将推出。

为了确定检索到的 ServiceInstance 的区域,我们检查其元数据映射中“区域”键下的值。

ZonePreferenceServiceInstanceListSupplier 过滤检索到的实例并只返回同一区域内的实例。如果该区域为空或同一区域内没有实例,则返回所有检索到的实例。

为了使用基于区域的负载平衡方法,您必须在自定义配置中实例化 ZonePreferenceServiceInstanceListSupplier bean。

我们使用委托来处理 ServiceInstanceListSupplier bean。我们建议在 ZonePreferenceServiceInstanceListSupplier 的构造函数中传递一个 DiscoveryClientServiceInstanceListSupplier 委托,然后用 CachingServiceInstanceListSupplier 包装后者以利用 LoadBalancer 缓存机制。

您可以使用此示例配置进行设置:

1
2
3
4
5
6
7
8
9
10
11
12
public class CustomLoadBalancerConfiguration {

@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withZonePreference()
.withCaching()
.build(context);
}
}

3.5. LoadBalancer 的实例健康检查

可以为 LoadBalancer 启用计划的 HealthCheck。为此提供了 HealthCheckServiceInstanceListSupplier。它会定期验证委托 ServiceInstanceListSupplier 提供的实例是否仍然存在并且只返回健康的实例,除非没有 - 然后它返回所有检索到的实例。

这种机制在使用 SimpleDiscoveryClient 时特别有用。对于由实际 Service Registry 支持的客户端,没有必要使用,因为我们在查询外部 ServiceDiscovery 后已经获得了健康的实例。

还建议将此供应商用于每个服务具有少量实例的设置,以避免在失败的实例上重试调用。

如果使用任何服务发现支持的供应商,通常不需要添加此健康检查机制,因为我们直接从服务注册处检索实例的健康状态。

HealthCheckServiceInstanceListSupplier 依赖于由委托通量提供的更新实例。在极少数情况下,您想使用不刷新实例的委托,即使实例列表可能发生变化(例如我们提供的 DiscoveryClientServiceInstanceListSupplier),您可以设置 spring.cloud.loadbalancer.health-check.refetch -instances 为 true 以使 HealthCheckServiceInstanceListSupplier 刷新实例列表。然后,您还可以通过修改 spring.cloud.loadbalancer.health-check.refetch-instances-interval 的值来调整刷新间隔,并通过设置 spring.cloud.loadbalancer.health-check.repeat- 选择禁用额外的健康检查重复health-check 为 false,因为每个实例重新获取也会触发健康检查。

HealthCheckServiceInstanceListSupplier 使用以 spring.cloud.loadbalancer.health-check 为前缀的属性。您可以为调度程序设置 initialDelay 和间隔。您可以通过设置 spring.cloud.loadbalancer.health-check.path.default 属性的值来设置健康检查 URL 的默认路径。您还可以通过设置 spring.cloud.loadbalancer.health-check.path.[SERVICE_ID] 属性的值,将 [SERVICE_ID] 替换为您的服务的正确 ID,为任何给定服务设置特定值。如果未设置路径,则默认使用 /actuator/health。

如果您依赖默认路径 (/actuator/health),请确保将 spring-boot-starter-actuator 添加到协作者的依赖项中,除非您计划自行添加此类端点。

为了使用健康检查调度程序方法,您必须在自定义配置中实例化 HealthCheckServiceInstanceListSupplier bean。

我们使用委托来处理 ServiceInstanceListSupplier bean。我们建议在 HealthCheckServiceInstanceListSupplier 的构造函数中传递一个 DiscoveryClientServiceInstanceListSupplier 委托。

您可以使用此示例配置进行设置:

1
2
3
4
5
6
7
8
9
10
11
public class CustomLoadBalancerConfiguration {

@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withHealthChecks()
.build(context);
}
}

对于非反应式堆栈,使用 withBlockingHealthChecks() 创建此供应商。您还可以传递您自己的 WebClient 或 RestTemplate 实例以用于检查。

HealthCheckServiceInstanceListSupplier 有自己的基于 Reactor Flux replay() 的缓存机制。因此,如果正在使用它,您可能希望跳过使用 CachingServiceInstanceListSupplier 包装该供应商。

3.6. LoadBalancer 的相同实例首选项

您可以设置 LoadBalancer,使其更喜欢先前选择的实例(如果该实例可用)。

为此,您需要使用 SameInstancePreferenceServiceInstanceListSupplier。您可以通过将 spring.cloud.loadbalancer.configurations 的值设置为 same-instance-preference 或提供您自己的 ServiceInstanceListSupplier bean — 来配置它,例如:

1
2
3
4
5
6
7
8
9
10
11
public class CustomLoadBalancerConfiguration {

@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withSameInstancePreference()
.build(context);
}
}

这也是 Zookeeper StickyRule 的替代品。

3.7. LoadBalancer 的基于请求的粘性会话

您可以设置 LoadBalancer,使其更喜欢在请求 cookie 中提供 instanceId 的实例。如果请求通过 ClientRequestContext 或 ServerHttpRequestContext 传递给 LoadBalancer,我们当前支持此功能,SC LoadBalancer 交换过滤器功能和过滤器使用它们。

为此,您需要使用 RequestBasedStickySessionServiceInstanceListSupplier。您可以通过将 spring.cloud.loadbalancer.configurations 的值设置为 request-based-sticky-session 或通过提供您自己的 ServiceInstanceListSupplier bean — 来配置它,例如:

1
2
3
4
5
6
7
8
9
10
11
public class CustomLoadBalancerConfiguration {

@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withRequestBasedStickySession()
.build(context);
}
}

对于该功能,在转发请求之前更新选定的服务实例(如果原始请求 cookie 不可用,则该实例可能与原始请求 cookie 中的服务实例不同)很有用。为此,请将 spring.cloud.loadbalancer.sticky-session.add-service-instance-cookie 的值设置为 true。

默认情况下,cookie 的名称是 sc-lb-instance-id。您可以通过更改 spring.cloud.loadbalancer.instance-id-cookie-name 属性的值来修改它。

WebClient 支持的负载平衡当前支持此功能。

3.8. Spring Cloud LoadBalancer 提示

Spring Cloud LoadBalancer 允许您设置传递给 Request 对象内的 LoadBalancer 的字符串提示,稍后可以在可以处理它们的 ReactiveLoadBalancer 实现中使用。

您可以通过设置 spring.cloud.loadbalancer.hint.default 属性的值来为所有服务设置默认提示。您还可以通过设置 spring.cloud.loadbalancer.hint.[SERVICE_ID] 属性的值,将 [SERVICE_ID] 替换为您的服务的正确 ID,为任何给定服务设置特定值。如果用户未设置提示,则使用默认值。

3.9.基于提示的负载平衡

我们还提供了一个 HintBasedServiceInstanceListSupplier,它是一个 ServiceInstanceListSupplier 实现,用于基于提示的实例选择。

HintBasedServiceInstanceListSupplier 检查提示请求标头(默认标头名称为 X-SC-LB-Hint,但您可以通过更改 spring.cloud.loadbalancer.hint-header-name 属性的值来修改它),如果是找到一个提示请求头,使用头中传递的提示值过滤服务实例。

如果没有添加提示头,HintBasedServiceInstanceListSupplier 使用属性中的提示值来过滤服务实例。

如果头或属性没有设置提示,则返回委托提供的所有服务实例。

在过滤时,HintBasedServiceInstanceListSupplier 查找在其 metadataMap 中的提示键下设置了匹配值的服务实例。如果没有找到匹配的实例,则返回委托提供的所有实例。

您可以使用以下示例配置进行设置:

1
2
3
4
5
6
7
8
9
10
11
12
public class CustomLoadBalancerConfiguration {

@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withDiscoveryClient()
.withHints()
.withCaching()
.build(context);
}
}

3.10.转换负载均衡的 HTTP 请求

您可以使用选定的 ServiceInstance 来转换负载均衡的 HTTP 请求。

对于 RestTemplate,需要实现和定义 LoadBalancerRequestTransformer 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Bean
public LoadBalancerRequestTransformer transformer() {
return new LoadBalancerRequestTransformer() {
@Override
public HttpRequest transformRequest(HttpRequest request, ServiceInstance instance) {
return new HttpRequestWrapper(request) {
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.putAll(super.getHeaders());
headers.add("X-InstanceId", instance.getInstanceId());
return headers;
}
};
}
};
}

对于WebClient,需要实现和定义LoadBalancerClientRequestTransformer如下:

1
2
3
4
5
6
7
8
9
10
11
@Bean
public LoadBalancerClientRequestTransformer transformer() {
return new LoadBalancerClientRequestTransformer() {
@Override
public ClientRequest transformRequest(ClientRequest request, ServiceInstance instance) {
return ClientRequest.from(request)
.header("X-InstanceId", instance.getInstanceId())
.build();
}
};
}

如果定义了多个转换器,它们将按照定义 Bean 的顺序应用。或者,您可以使用 LoadBalancerRequestTransformer.DEFAULT_ORDER 或 LoadBalancerClientRequestTransformer.DEFAULT_ORDER 来指定顺序。

3.11. Spring Cloud LoadBalancer 启动器

我们还提供了一个启动器,允许您在 Spring Boot 应用程序中轻松添加 Spring Cloud LoadBalancer。为了使用它,只需将 org.springframework.cloud:spring-cloud-starter-loadbalancer 添加到构建文件中的 Spring Cloud 依赖项中。

Spring Cloud LoadBalancer starter 包括 Spring Boot Caching 和 Evictor。

3.12.传递你自己的 Spring Cloud LoadBalancer 配置

也可以使用@LoadBalancerClient注解传递自己的负载均衡客户端配置,传递负载均衡客户端的名称和配置类,如下:

1
2
3
4
5
6
7
8
9
10
@Configuration
@LoadBalancerClient(value = "stores", configuration = CustomLoadBalancerConfiguration.class)
public class MyConfiguration {

@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
}

提示

为了更轻松地处理您自己的 LoadBalancer 配置,我们在 ServiceInstanceListSupplier 类中添加了 builder() 方法。

提示

您还可以使用我们的替代预定义配置代替默认配置,方法是将 spring.cloud.loadbalancer.configurations 属性的值设置为 zone-preference 以使用 ZonePreferenceServiceInstanceListSupplier 与缓存或健康检查以使用 HealthCheckServiceInstanceListSupplier 与缓存。

您可以使用此功能来实例化 ServiceInstanceListSupplier 或 ReactorLoadBalancer 的不同实现,它们可以由您编写,也可以由我们作为替代方案提供(例如 ZonePreferenceServiceInstanceListSupplier)以覆盖默认设置。

您可以在此处查看自定义配置示例。

注释值参数(存储在上面的示例中)指定了我们应该使用给定的自定义配置向其发送请求的服务的服务 ID。

您还可以通过 @LoadBalancerClients 注释传递多个配置(用于多个负载均衡器客户端),如以下示例所示:

1
2
3
4
5
6
7
8
9
10
@Configuration
@LoadBalancerClients({@LoadBalancerClient(value = "stores", configuration = StoresLoadBalancerClientConfiguration.class), @LoadBalancerClient(value = "customers", configuration = CustomersLoadBalancerClientConfiguration.class)})
public class MyConfiguration {

@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
}

您作为 @LoadBalancerClient 或 @LoadBalancerClients 配置参数传递的类不应使用 @Configuration 进行注释或不在组件扫描范围内。

3.13. Spring Cloud LoadBalancer 生命周期

使用自定义 LoadBalancer 配置注册可能有用的一种 bean 是 LoadBalancerLifecycle。

LoadBalancerLifecycle bean 提供回调方法,名为 onStart(Request request)、onStartRequest(Request request, Response lbResponse) 和 onComplete(CompletionContext<RES, T, RC> completionContext),您应该实现这些方法指定在负载平衡之前和之后应该执行的操作。

onStart(Request request) 将 Request 对象作为参数。它包含用于选择适当实例的数据,包括下游客户端请求和提示。 onStartRequest 还接受 Request 对象和 Response 对象作为参数。另一方面,为 onComplete(CompletionContext<RES, T, RC> completionContext) 方法提供了一个 CompletionContext 对象。它包含 LoadBalancer 响应,包括选定的服务实例、针对该服务实例执行的请求的状态和(如果可用)返回到下游客户端的响应,以及(如果发生异常)相应的 Throwable。

supports(Class requestContextClass, Class responseClass, Class serverTypeClass) 方法可用于确定相关处理器是否处理所提供类型的对象。如果没有被用户覆盖,则返回 true。

上述方法调用中,RC表示RequestContext类型,RES表示客户端响应类型,T表示返回服务器类型。

3.14. Spring Cloud LoadBalancer 统计

我们提供了一个名为 MicrometerStatsLoadBalancerLifecycle 的 LoadBalancerLifecycle bean,它使用 Micrometer 为负载平衡调用提供统计信息。

为了将此 bean 添加到您的应用程序上下文中,请将 spring.cloud.loadbalancer.stats.micrometer.enabled 的值设置为 true 并使用 MeterRegistry(例如,通过将 Spring Boot Actuator 添加到您的项目中)。

MicrometerStatsLoadBalancerLifecycle 在 MeterRegistry 中注册以下仪表:

  • loadbalancer.requests.active:允许您监控任何服务实例的当前活动请求数量的量表(服务实例数据可通过标签获得);
  • loadbalancer.requests.success:一个计时器,用于测量已结束将响应传递给底层客户端的任何负载平衡请求的执行时间;
  • loadbalancer.requests.failed:一个计时器,用于测量任何以异常结束的负载平衡请求的执行时间;
  • loadbalancer.requests.discard:一个计数器,用于测量被丢弃的负载平衡请求的数量,即负载均衡器尚未检索到运行请求的服务实例的请求。

只要可用,有关服务实例、请求数据和响应数据的附加信息就会通过标签添加到指标中。

对于某些实现,例如 BlockingLoadBalancerClient,请求和响应数据可能不可用,因为我们从参数建立泛型类型并且可能无法确定类型并读取数据。

当为给定仪表添加至少一个记录时,仪表将在注册表中注册。

您可以通过添加 MeterFilters 进一步配置这些指标的行为(例如,添加发布百分位数和直方图)

4. Spring Cloud 断路器

4.1.介绍

Spring Cloud 断路器提供了跨不同断路器实现的抽象。它提供了在您的应用程序中使用的一致 API,让您(开发人员)可以选择最适合您的应用程序需求的断路器实现。

4.1.1.支持的实现

Spring Cloud 支持以下断路器实现:

4.2.核心概念

要在您的代码中创建断路器,您可以使用 CircuitBreakerFactory API。当您在类路径中包含 Spring Cloud Circuit Breaker starter 时,会自动为您创建一个实现此 API 的 bean。以下示例显示了如何使用此 API 的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public static class DemoControllerService {
private RestTemplate rest;
private CircuitBreakerFactory cbFactory;

public DemoControllerService(RestTemplate rest, CircuitBreakerFactory cbFactory) {
this.rest = rest;
this.cbFactory = cbFactory;
}

public String slow() {
return cbFactory.create("slow").run(() -> rest.getForObject("/slow", String.class), throwable -> "fallback");
}

}

CircuitBreakerFactory.create API 创建了一个名为 CircuitBreaker 的类的实例。 run 方法接受一个供应商和一个函数。供应商是您要包装在断路器中的代码。该功能是在断路器跳闸时运行的回退。该函数传递了引发回退的 Throwable。如果您不想提供后备,您可以选择排除后备。

4.2.1.反应式代码中的断路器

如果 Project Reactor 在类路径上,您还可以将 ReactiveCircuitBreakerFactory 用于您的反应式代码。以下示例显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Service
public static class DemoControllerService {
private ReactiveCircuitBreakerFactory cbFactory;
private WebClient webClient;


public DemoControllerService(WebClient webClient, ReactiveCircuitBreakerFactory cbFactory) {
this.webClient = webClient;
this.cbFactory = cbFactory;
}

public Mono<String> slow() {
return webClient.get().uri("/slow").retrieve().bodyToMono(String.class).transform(
it -> cbFactory.create("slow").run(it, throwable -> return Mono.just("fallback")));
}
}

ReactiveCircuitBreakerFactory.create API 创建了一个名为 ReactiveCircuitBreaker 的类的实例。 run 方法采用 Mono 或 Flux 并将其包装在断路器中。您可以选择配置回退函数,如果断路器跳闸并传递导致故障的 Throwable,则将调用该函数。

4.3.配置

您可以通过创建自定义程序类型的 bean 来配置断路器。定制器接口有一个方法(称为定制),可以让对象进行定制。

有关如何自定义给定实现的详细信息,请参阅以下文档:

每次调用 CircuitBreaker#run 时,某些 CircuitBreaker 实现(例如 Resilience4JCircuitBreaker)都会调用自定义方法。它可能效率低下。在这种情况下,您可以使用 CircuitBreaker#once 方法。在多次调用自定义没有意义的情况下很有用,例如,在使用 Resilience4j 的事件的情况下。

下面的例子展示了每个 io.github.resilience4j.circuitbreaker.CircuitBreaker 消费事件的方式。

1
2
3
4
Customizer.once(circuitBreaker -> {
circuitBreaker.getEventPublisher()
.onStateTransition(event -> log.info("{}: {}", event.getCircuitBreakerName(), event.getStateTransition()));
}, CircuitBreaker::getName)

5. CachedRandomPropertySource

Spring Cloud Context 提供了一个 PropertySource,它根据一个键缓存随机值。在缓存功能之外,它的工作方式与 Spring Boot 的 RandomValuePropertySource 相同。如果您想要一个即使在 Spring 应用程序上下文重新启动后也保持一致的随机值,则此随机值可能很有用。属性值采用 cachedrandom.[yourkey].[type] 的形式,其中 yourkey 是缓存中的键。类型值可以是 Spring Boot 的 RandomValuePropertySource 支持的任何类型。

1
myrandom=${cachedrandom.appname.value}

6. Security

6.1. Single Sign On

所有 OAuth2 SSO 和资源服务器功能都在 1.3 版中移至 Spring Boot。您可以在 Spring Boot 用户指南中找到文档。

6.1.1.客户端令牌中继relay

如果您的应用是面向 OAuth2 客户端的用户(即已声明 @EnableOAuth2Sso 或 @EnableOAuth2Client),则它在 Spring Boot 的请求范围内具有 OAuth2ClientContext。您可以从此上下文创建自己的 OAuth2RestTemplate 和自动装配的 OAuth2ProtectedResourceDetails,然后上下文将始终向下游转发访问令牌,并在访问令牌过期时自动刷新访问令牌。 (这些是 Spring Security 和 Spring Boot 的特性。)

6.1.2.资源服务器令牌中继relay

如果您的应用程序具有 @EnableResourceServer,您可能希望将传入的令牌向下游中继到其他服务。如果您使用 RestTemplate 来联系下游服务,那么这只是如何使用正确的上下文创建模板的问题。

如果您的服务使用 UserInfoTokenServices 来验证传入的令牌(即它使用 security.oauth2.user-info-uri 配置),那么您可以简单地使用自动装配的 OAuth2ClientContext 创建一个 OAuth2RestTemplate(它将在命中之前由身份验证过程填充后端代码)。等效地(使用 Spring Boot 1.4),您可以注入 UserInfoRestTemplateFactory 并在您的配置中获取其 OAuth2RestTemplate。例如:

MyConfiguration.java

1
2
3
4
@Bean
public OAuth2RestTemplate restTemplate(UserInfoRestTemplateFactory factory) {
return factory.getUserInfoRestTemplate();
}

这个 rest 模板将具有与身份验证过滤器使用的相同的 OAuth2ClientContext(请求范围),因此您可以使用它来发送具有相同访问令牌的请求。

如果您的应用程序没有使用 UserInfoTokenServices 但仍然是客户端(即它声明了 @EnableOAuth2Client 或 @EnableOAuth2Sso),那么使用 Spring Security Cloud,用户从 @Autowired OAuth2Context 创建的任何 OAuth2RestOperations 也将转发令牌。这个特性默认实现为一个MVC处理程序拦截器,所以它只适用于Spring MVC。如果您不使用 MVC,您可以使用自定义过滤器或 AOP 拦截器包装 AccessTokenContextRelay 来提供相同的功能。

这是一个基本示例,展示了使用在别处创建的自动装配的休息模板(“foo.com”是一个资源服务器,接受与周围应用程序相同的令牌):

MyController.java

1
2
3
4
5
6
7
8
9
@Autowired
private OAuth2RestOperations restTemplate;

@RequestMapping("/relay")
public String relay() {
ResponseEntity<String> response =
restTemplate.getForEntity("https://foo.com/bar", String.class);
return "Success! (" + response.getBody() + ")";
}

如果您不想转发令牌(这是一个有效的选择,因为您可能想扮演自己的角色,而不是向您发送令牌的客户端),那么您只需要创建自己的 OAuth2Context 而不是自动装配默认一个。

如果可用,Feign 客户端还将选择使用 OAuth2ClientContext 的拦截器,因此他们还应该在 RestTemplate 所在的任何地方进行令牌中继。

7. 配置属性

附录 A:常见的应用程序属性

可以在 application.properties 文件、application.yml 文件或命令行开关中指定各种属性。本附录提供了常见 Spring Cloud Commons 属性的列表以及对使用它们的底层类的引用。

属性贡献可以来自类路径上的其他 jar 文件,因此您不应认为这是一个详尽的列表。此外,您可以定义自己的属性。

名称 默认值 描述
spring.cloud.compatibility-verifier.compatible-boot-versions Spring Boot 依赖项的默认接受版本。如果不想指定具体值,可以为补丁版本设置 {@code x}。示例:{@code 3.4.x}
spring.cloud.compatibility-verifier.enabled false 启用创建 Spring Cloud 兼容性验证。
spring.cloud.config.allow-override true 指示可以使用 {@link #isOverrideSystemProperties() systemPropertiesOverride} 的标志。设置为 false 以防止用户意外更改默认值。默认为真。
spring.cloud.config.override-none false 标记以指示当 {@link #setAllowOverride(boolean) allowOverride} 为 true 时,外部属性应具有最低优先级并且不应覆盖任何现有属性源(包括本地配置文件)。默认为假。
spring.cloud.config.override-system-properties true 标志以指示外部属性应覆盖系统属性。默认为真。
spring.cloud.decrypt-environment-post-processor.enabled true 启用 DecryptEnvironmentPostProcessor。
spring.cloud.discovery.client.composite-indicator.enabled true 启用发现客户端复合健康指标。
spring.cloud.discovery.client.health-indicator.enabled true
spring.cloud.discovery.client.health-indicator.include-description false
spring.cloud.discovery.client.health-indicator.use-services-query true 指标是否应使用 {@link DiscoveryClient#getServices} 来检查其健康状况。当设置为 {@code false} 时,指示器会使用较轻的 {@link DiscoveryClient#probe()}。这在返回的服务数量使操作不必要地繁重的大型部署中很有帮助。
spring.cloud.discovery.client.simple.instances
spring.cloud.discovery.client.simple.order
spring.cloud.discovery.enabled true 启用发现客户端健康指标。
spring.cloud.features.enabled true 启用功能端点。
spring.cloud.httpclientfactories.apache.enabled true 允许创建 Apache Http 客户端工厂 bean。
spring.cloud.httpclientfactories.ok.enabled true 启用 OK Http Client 工厂 bean 的创建。
spring.cloud.hypermedia.refresh.fixed-delay 5000
spring.cloud.hypermedia.refresh.initial-delay 10000
spring.cloud.inetutils.default-hostname localhost 默认主机名。发生错误时使用。
spring.cloud.inetutils.default-ip-address 127.0.0.1 默认 IP 地址。发生错误时使用。
spring.cloud.inetutils.ignored-interfaces 将被忽略的网络接口的 Java 正则表达式列表。
spring.cloud.inetutils.preferred-networks 首选网络地址的 Java 正则表达式列表。
spring.cloud.inetutils.timeout-seconds 1 计算主机名的超时时间,以秒为单位。
spring.cloud.inetutils.use-only-site-local-interfaces false 是否仅使用具有站点本地地址的接口。有关更多详细信息,请参阅 {@link InetAddress#isSiteLocalAddress()}。
spring.cloud.loadbalancer.cache.caffeine.spec 用于创建缓存的规范。有关规范格式的更多详细信息,请参阅 CaffeineSpec。
spring.cloud.loadbalancer.cache.capacity 256 初始缓存容量表示为 int。
spring.cloud.loadbalancer.cache.enabled true 启用 Spring Cloud LoadBalancer 缓存机制
spring.cloud.loadbalancer.cache.ttl 35s 生存时间 - 从写入记录开始计算的时间,之后缓存条目过期,表示为 {@link Duration}。属性 {@link String} 必须符合 Spring Boot StringToDurationConverter 中指定的适当语法。 @see StringToDurationConverter.java
spring.cloud.loadbalancer.configurations default 启用预定义的 LoadBalancer 配置。
spring.cloud.loadbalancer.enabled true 启用 Spring Cloud LoadBalancer。
spring.cloud.loadbalancer.health-check.initial-delay 0 HealthCheck 调度程序的初始延迟值。
spring.cloud.loadbalancer.health-check.interval 25s 重新运行 HealthCheck 调度程序的时间间隔。
spring.cloud.loadbalancer.health-check.path
spring.cloud.loadbalancer.health-check.refetch-instances false 指示是否应由 HealthCheckServiceInstanceListSupplier 重新获取实例。如果实例可以更新并且底层委托不提供持续的流量,则可以使用此方法。
spring.cloud.loadbalancer.health-check.refetch-instances-interval 25s 重新获取可用服务实例的时间间隔。
spring.cloud.loadbalancer.health-check.repeat-health-check true 指示是否应继续重复运行状况检查。如果定期重新获取实例,将其设置为 false 可能会很有用,因为每次重新获取也会触发健康检查。
spring.cloud.loadbalancer.hint 允许设置传递给 LoadBalancer 请求的 hint 值,随后可以在 {@link ReactiveLoadBalancer} 实现中使用。
spring.cloud.loadbalancer.hint-header-name X-SC-LB-Hint 允许设置用于传递基于提示的服务实例过滤提示的标头名称。
spring.cloud.loadbalancer.retry.avoid-previous-instance true 如果 Spring-Retry 在类路径中,则启用使用 RetryAwareServiceInstanceListSupplier 包装 ServiceInstanceListSupplier bean。
spring.cloud.loadbalancer.retry.backoff.enabled false 指示是否应应用反应器重试退避。
spring.cloud.loadbalancer.retry.backoff.jitter 0.5 用于设置 {@link RetryBackoffSpec#jitter}。
spring.cloud.loadbalancer.retry.backoff.max-backoff 用于设置 {@link RetryBackoffSpec#maxBackoff}。
spring.cloud.loadbalancer.retry.backoff.min-backoff 5ms 用于设置 {@link RetryBackoffSpec#minBackoff}。
spring.cloud.loadbalancer.retry.enabled true 启用 LoadBalancer 重试。
spring.cloud.loadbalancer.retry.max-retries-on-next-service-instance 1 要在下一个 ServiceInstance 上执行的重试次数。在每次重试调用之前选择一个 ServiceInstance
spring.cloud.loadbalancer.retry.max-retries-on-same-service-instance 0 要在同一 ServiceInstance 上执行的重试次数。
spring.cloud.loadbalancer.retry.retry-on-all-operations false 表示应尝试对除 {@link HttpMethod#GET} 以外的操作进行重试。
spring.cloud.loadbalancer.retry.retryable-status-codes 应触发重试的状态代码 {@link Set}。
spring.cloud.loadbalancer.service-discovery.timeout 调用服务发现的超时时间的字符串表示形式。
spring.cloud.loadbalancer.sticky-session.add-service-instance-cookie false 指示 SC LoadBalancer 是否应添加带有新选择实例的 cookie。
spring.cloud.loadbalancer.sticky-session.instance-id-cookie-name sc-lb-instance-id 保存首选实例 ID 的 cookie 的名称。
spring.cloud.loadbalancer.zone Spring Cloud LoadBalancer 区域。
spring.cloud.refresh.additional-property-sources-to-retain 刷新期间要保留的其他属性源。通常只保留系统属性源。此属性还允许保留属性源,例如由 EnvironmentPostProcessors 创建的属性源。
spring.cloud.refresh.enabled true 启用刷新范围和相关功能的自动配置。
spring.cloud.refresh.extra-refreshable true 用于将处理后处理到刷新范围的 bean 的其他类名。
spring.cloud.refresh.never-refreshable true 逗号分隔的 bean 类名列表,永远不会被刷新或反弹。
spring.cloud.service-registry.auto-registration.enabled true 是否开启服务自动注册。默认为真。
spring.cloud.service-registry.auto-registration.fail-fast false 如果没有 AutoServiceRegistration,是否启动失败。默认为假。
spring.cloud.service-registry.auto-registration.register-management true 是否将管理注册为服务。默认为真。
spring.cloud.util.enabled true 启用创建 Spring Cloud 实用程序 bean。

Spring Cloud OpenFeign基本使用

该项目通过自动配置和绑定到 Spring Environment 和其他 Spring 编程模型,为 Spring Boot 应用程序提供 OpenFeign 集成。

1、声明式 REST 客户端:Feign

Feign 是一个声明式 Web 服务客户端。它使编写 Web 服务客户端变得更容易。要使用 Feign 创建一个接口并对其进行注释。它具有可插入的注释支持,包括 Feign 注释和 JAX-RS 注释。 Feign 还支持可插拔的编码器和解码器。 Spring Cloud 添加了对 Spring MVC 注释的支持,并支持使用 Spring Web 中默认使用的相同 HttpMessageConverters。 Spring Cloud 集成了 Eureka、Spring Cloud CircuitBreaker 和 Spring Cloud LoadBalancer,在使用 Feign 时提供负载均衡的 http 客户端。

1.1、如何集成 Feign

要将 Feign 包含在您的项目中,请使用带有group org.springframework.cloud 和artifact ID spring-cloud-starter-openfeign 的 starter。有关使用当前 Spring Cloud Release Train 设置构建系统的详细信息,请参阅 Spring Cloud 项目页面。

示例:

1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableFeignClients
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}

StoreClient.java

1
2
3
4
5
6
7
8
9
10
11
@FeignClient("stores")
public interface StoreClient {
@RequestMapping(method = RequestMethod.GET, value = "/stores")
List<Store> getStores();

@RequestMapping(method = RequestMethod.GET, value = "/stores")
Page<Store> getStores(Pageable pageable);

@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
Store update(@PathVariable("storeId") Long storeId, Store store);
}

在@FeignClient 注释中,String 值(上面的“stores”)是任意客户端名称,用于创建 Spring Cloud LoadBalancer 客户端。您还可以使用 url 属性(绝对值或仅主机名)指定 URL。应用程序上下文中 bean 的名称是接口的完全限定名称。要指定您自己的别名值,您可以使用 @FeignClient 注释的限定符值。

上面的负载平衡器客户端将想要发现“stores”服务的物理地址。如果您的应用程序是 Eureka 客户端,那么它将解析 Eureka 服务注册表中的服务。如果不想使用 Eureka,可以使用 SimpleDiscoveryClient 在外部配置中配置服务器列表。

要在 @Configuration-annotated-classes 上使用 @EnableFeignClients 注释,请确保指定客户端所在的位置,例如:@EnableFeignClients(basePackages = “com.example.clients”) 或明确列出它们:@EnableFeignClients(clients = InventoryServiceFeignClient .class)

1.2.覆盖 Feign 默认值

Spring Cloud 的 Feign 支持的一个核心概念是命名客户端。每个 feign 客户端都是一个组件集合的一部分,这些组件一起工作以根据需要联系远程服务器,并且集合有一个名称,您可以使用 @FeignClient 注释作为应用程序开发人员为其指定。 Spring Cloud 使用 FeignClientsConfiguration 为每个命名的客户端按需创建一个新的集成作为 ApplicationContext。这包含(除其他外)一个 feign.Decoder、一个 feign.Encoder 和一个 feign.Contract。可以使用 @FeignClient 注释的 contextId 属性来覆盖该集合的名称。

Spring Cloud 允许您通过使用 @FeignClient 声明额外的配置(在 FeignClientsConfiguration 之上)来完全控制 feign 客户端。例子:

1
2
3
4
@FeignClient(name = "stores", configuration = FooConfiguration.class)
public interface StoreClient {
//..
}

在这种情况下,客户端由 FeignClientsConfiguration 中已有的组件和 FooConfiguration 中的任何组件组成(后者将覆盖前者)。

FooConfiguration 不需要用@Configuration 注释。但是,如果是,那么请注意将其从任何包含此配置的@ComponentScan 中排除,因为在指定时它将成为 feign.Decoder、feign.Encoder、feign.Contract 等的默认源。这可以通过将它放在与任何 @ComponentScan 或 @SpringBootApplication 分开的、不重叠的包中来避免,或者可以在 @ComponentScan 中明确排除它。

除了更改 ApplicationContext 集合的名称之外,使用 @FeignClient 注释的 contextId 属性,它将覆盖客户端名称的别名,并将用作为该客户端创建的配置 bean 名称的一部分。

name 和 url 属性支持占位符。

1
2
3
4
@FeignClient(name = "${feign.name}", url = "${feign.url}")
public interface StoreClient {
//..
}

Spring Cloud OpenFeign 默认为 feign 提供了以下 bean(BeanType beanName: ClassName):

  • DecoderfeignDecoder: ResponseEntityDecoder( 包装了一个SpringDecoder)
  • Encoder feignEncoder: SpringEncoder
  • Logger feignLogger: Slf4jLogger
  • MicrometerCapabilitymicrometerCapability:如果feign-micrometer在类路径上并且MeterRegistry可用
  • Contract feignContract: SpringMvcContract
  • Feign.Builder feignBuilder: FeignCircuitBreaker.Builder
  • ClientfeignClient:如果 Spring Cloud LoadBalancer 在类路径上,FeignBlockingLoadBalancerClient则使用。如果它们都不在类路径上,则使用默认的 feign 客户端。

spring-cloud-starter-openfeign 支持 spring-cloud-starter-loadbalancer。但是,作为一个可选的依赖项,如果您想使用它,您需要确保将其添加到您的项目中。

OkHttpClient 和 ApacheHttpClient 以及 ApacheHC5 feign 客户端可以通过将 feign.okhttp.enabled 或 feign.httpclient.enabled 或 feign.httpclient.hc5.enabled 分别设置为 true 并将它们放在类路径上来使用。您可以通过在使用 Apache 时提供 org.apache.http.impl.client.CloseableHttpClient 或 okhttp3.OkHttpClient 在使用 OK HTTP 或 org.apache.hc.client5.http.impl.classic 时提供 bean 来自定义使用的 HTTP 客户端。使用 Apache HC5 时的 CloseableHttpClient。

Spring Cloud OpenFeign 默认没有为 feign 提供以下 bean,但仍然会从应用程序上下文中查找这些类型的 bean 来创建 feign 客户端:

  • Logger.Level
  • Retryer
  • ErrorDecoder
  • Request.Options
  • Collection<RequestInterceptor>
  • SetterFactory
  • QueryMapEncoder
  • Capability (MicrometerCapability is provided by default)

默认情况下会创建 Retryer.NEVER_RETRY 类型为 Retryer 的 bean,这将禁用重试。请注意,这种重试行为与 Feign 默认行为不同,它会自动重试 IOExceptions,将它们视为与网络相关的瞬态异常,以及从 ErrorDecoder 抛出的任何 RetryableException。

创建其中一种类型的 bean 并将其放置在 @FeignClient 配置中(例如上面的 FooConfiguration)允许您覆盖所描述的每个 bean。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class FooConfiguration {
@Bean
public Contract feignContract() {
return new feign.Contract.Default();
}

@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("user", "password");
}
}

这将 SpringMvcContract 替换为 feign.Contract.Default 并将 RequestInterceptor 添加到 RequestInterceptor 的集合中。

@FeignClient 也可以使用配置属性进行配置。

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
feign:
client:
config:
feignName:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
retryer: com.example.SimpleRetryer
defaultQueryParameters:
query: queryValue
defaultRequestHeaders:
header: headerValue
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
decode404: false
encoder: com.example.SimpleEncoder
decoder: com.example.SimpleDecoder
contract: com.example.SimpleContract
capabilities:
- com.example.FooCapability
- com.example.BarCapability
metrics.enabled: false

可以以与上述类似的方式在 @EnableFeignClients 属性 defaultConfiguration 中指定默认配置。不同之处在于此配置将适用于所有 feign 客户端。

如果您更喜欢使用配置属性来配置所有 @FeignClient,您可以使用默认的 feign 名称创建配置属性。

您可以使用 feign.client.config.feignName.defaultQueryParameters 和 feign.client.config.feignName.defaultRequestHeaders 来指定将与名为 feignName 的客户端的每个请求一起发送的查询参数和标头。

application.yml

1
2
3
4
5
6
7
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic

如果我们同时创建@Configuration bean 和配置属性,配置属性将获胜。它将覆盖@Configuration 值。但是如果你想把优先级改成@Configuration,你可以把feign.client.default-to-properties改成false。

如果我们想创建多个具有相同名称或 url 的 feign 客户端,以便它们指向同一服务器但每个具有不同的自定义配置,那么我们必须使用 @FeignClient 的 contextId 属性以避免这些配置的名称冲突Bean。

1
2
3
4
@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
public interface FooClient {
//..
}
1
2
3
4
@FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class)
public interface BarClient {
//..
}

也可以将 FeignClient 配置为不从父上下文继承 bean。您可以通过覆盖 FeignClientConfigurer bean 中的 inheritParentConfiguration() 以返回 false 来实现此目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class CustomConfiguration{

@Bean
public FeignClientConfigurer feignClientConfigurer() {
return new FeignClientConfigurer() {

@Override
public boolean inheritParentConfiguration() {
return false;
}
};

}
}

默认情况下,Feign 客户端不编码斜杠/字符。您可以通过将 feign.client.decodeSlash 的值设置为 false 来更改此行为。

1.2.1. SpringEncoder 配置

在我们提供的 SpringEncoder 中,我们为二进制内容类型设置空字符集,为所有其他类型设置 UTF-8。

您可以修改此行为以通过将 feign.encoder.charset-from-content-type 的值设置为 true 来从 Content-Type 标头字符集派生字符集。

1.3.超时处理

我们可以在默认客户端和命名客户端上配置超时。 OpenFeign 使用两个超时参数:

  • connectTimeout 防止由于服务器处理时间长而阻塞调用者。
  • readTimeout 从连接建立时开始应用,在返回响应时间过长时触发。

如果服务器未运行或不可用,则数据包会导致连接被拒绝。通信以错误消息或回退结束。如果它设置得非常低,这可能会在 connectTimeout 之前发生。执行查找和接收此类数据包所花费的时间会导致此延迟的很大一部分。它可能会根据涉及 DNS 查找的远程主机进行更改。

1.4.手动创建 Feign 客户端

在某些情况下,可能需要以使用上述方法无法实现的方式自定义您的 Feign Client。在这种情况下,您可以使用 Feign Builder API 创建客户端。下面是一个示例,它创建了两个具有相同接口的 Feign 客户端,但为每个客户端配置了一个单独的请求拦截器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Import(FeignClientsConfiguration.class)
class FooController {

private FooClient fooClient;

private FooClient adminClient;

@Autowired
public FooController(Client client, Encoder encoder, Decoder decoder, Contract contract, MicrometerCapability micrometerCapability) {
this.fooClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.addCapability(micrometerCapability)
.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
.target(FooClient.class, "https://PROD-SVC");

this.adminClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.addCapability(micrometerCapability)
.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
.target(FooClient.class, "https://PROD-SVC");
}
}

上面例子中的 FeignClientsConfiguration.class 是 Spring Cloud OpenFeign 提供的默认配置。

PROD-SVC 是客户端将向其发出请求的服务的名称。

Feign Contract 对象定义了接口上哪些注解和值是有效的。自动装配的 Contract bean 提供对 SpringMVC 注释的支持,而不是默认的 Feign 原生注释。

您还可以使用 Builder将 FeignClient 配置为不从父上下文继承 bean。您可以通过在 Builder 上覆盖调用inheritParentContext(false) 来做到这一点。

1.5. Feign Spring Cloud 断路器支持

如果 Spring Cloud CircuitBreaker 在类路径上并且 feign.circuitbreaker.enabled=true,Feign 将使用断路器包装所有方法。 要在每个客户端的基础上禁用 Spring Cloud CircuitBreaker 支持,请创建一个具有“prototype”范围的 vanilla Feign.Builder,例如:

1
2
3
4
5
6
7
8
@Configuration
public class FooConfiguration {
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder() {
return Feign.builder();
}
}

断路器名称遵循此模式 #()。当调用带有 FooClient 接口的 @FeignClient 并且被调用的没有参数的接口方法是 bar 时,断路器名称将是 FooClient#bar()。

从 2020.0.2 开始,断路器名称模式已从 _ 更改。使用 2020.0.4 中引入的 CircuitBreakerNameResolver,断路器名称可以保留旧模式。

提供 CircuitBreakerNameResolver 的 bean,您可以更改断路器名称模式。

1
2
3
4
5
6
7
@Configuration
public class FooConfiguration {
@Bean
public CircuitBreakerNameResolver circuitBreakerNameResolver() {
return (String feignClientName, Target<?> target, Method method) -> feignClientName + "_" + method.getName();
}
}

要启用 Spring Cloud CircuitBreaker 组,请将 feign.circuitbreaker.group.enabled 属性设置为 true(默认为 false)。

1.6. Feign Spring Cloud 断路器Fallbacks

Spring Cloud CircuitBreaker 支持fallback的概念:当电路打开或出现错误时执行的默认代码路径。要为给定的 @FeignClient 启用回退,请将回退属性设置为实现回退的类名。您还需要将您的实现声明为 Spring bean。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@FeignClient(name = "test", url = "http://localhost:${server.port}/", fallback = Fallback.class)
protected interface TestClient {

@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello getHello();

@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
String getException();

}

@Component
static class Fallback implements TestClient {

@Override
public Hello getHello() {
throw new NoFallbackAvailableException("Boom!", new RuntimeException());
}

@Override
public String getException() {
return "Fixed response";
}

}

If one needs access to the cause that made the fallback trigger, one can use the fallbackFactory attribute inside @FeignClient.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@FeignClient(name = "testClientWithFactory", url = "http://localhost:${server.port}/",
fallbackFactory = TestFallbackFactory.class)
protected interface TestClientWithFactory {

@RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello getHello();

@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
String getException();

}

@Component
static class TestFallbackFactory implements FallbackFactory<FallbackWithFactory> {

@Override
public FallbackWithFactory create(Throwable cause) {
return new FallbackWithFactory();
}

}

static class FallbackWithFactory implements TestClientWithFactory {

@Override
public Hello getHello() {
throw new NoFallbackAvailableException("Boom!", new RuntimeException());
}

@Override
public String getException() {
return "Fixed response";
}

}

1.7. Feign 和@Primary

将 Feign 与 Spring Cloud CircuitBreaker fallback一起使用时,ApplicationContext 中有多个相同类型的 bean。这将导致 @Autowired 无法工作,因为没有一个 bean,或者一个标记为主要的 bean。为了解决这个问题,Spring Cloud OpenFeign 将所有 Feign 实例标记为 @Primary,因此 Spring Framework 将知道要注入哪个 bean。在某些情况下,这可能是不可取的。要关闭此行为,请将 @FeignClient 的主要属性设置为 false。

1
2
3
4
@FeignClient(name = "hello", primary = false)
public interface HelloClient {
// methods here
}

1.8. Feign 继承支持

Feign 通过单继承接口支持样板 API。这允许将常见操作分组到方便的基本接口中。

UserService.java

1
2
3
4
5
public interface UserService {

@RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
User getUser(@PathVariable("id") long id);
}

UserResource.java

1
2
3
4
@RestController
public class UserResource implements UserService {

}

UserClient.java

1
2
3
4
5
6
package project.user;

@FeignClient("users")
public interface UserClient extends UserService {

}

通常不建议在服务器和客户端之间共享一个接口。它引入了紧耦合,也不是所有维护的 Spring MVC 版本都支持(某些版本没有继承方法参数映射)。

1.9. Feign 请求/响应压缩

您可以考虑为您的 Feign 请求启用请求或响应 GZIP 压缩。您可以通过启用以下属性之一来执行此操作:

1
2
feign.compression.request.enabled=true
feign.compression.response.enabled=true

Feign 请求压缩为您提供类似于您为 Web 服务器设置的设置:

1
2
3
feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048

这些属性允许您选择压缩媒体类型和最小请求阈值长度。

对于除了 OkHttpClient 之外的 http 客户端,可以启用默认的 gzip 解码器来解码 UTF-8 编码的 gzip 响应:

1
2
feign.compression.response.enabled=true
feign.compression.response.useGzipDecoder=true

1.10.Feign logging

为每个创建的 Feign 客户端创建一个记录器。默认情况下,记录器的名称是用于创建 Feign 客户端的接口的完整类名。 Feign logging 只响应 DEBUG 级别。

application.yml

1
logging.level.project.user.UserClient: DEBUG

您可以为每个客户端配置的 Logger.Level 对象告诉 Feign 要记录多少。选择是:

  • NONE, 无日志记录(默认)。
  • BASIC, 仅记录请求方法和 URL 以及响应状态代码和执行时间。
  • HEADERS, 记录基本信息以及请求和响应标头。
  • FULL, 记录请求和响应的标头、正文和元数据。

例如,以下内容会将 Logger.Level 设置为 FULL:

1
2
3
4
5
6
7
@Configuration
public class FooConfiguration {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}

1.11. Feign Capability支持

Feign 功能公开了核心 Feign 组件,以便可以修改这些组件。例如,功能可以获取客户端,对其进行装饰,并将装饰后的实例返回给 Feign。对指标库的支持是一个很好的现实例子。请参阅 Feign 指标。

创建一个或多个 Capability bean 并将它们放置在 @FeignClient 配置中,让您可以注册它们并修改相关客户端的行为。

1
2
3
4
5
6
7
@Configuration
public class FooConfiguration {
@Bean
Capability customCapability() {
return new CustomCapability();
}
}

1.12. Feign 指标

如果以下所有条件都为真,则会创建并注册 MicrometerCapability bean,以便您的 Feign 客户端将指标发布到 Micrometer:

  • feign-micrometer 在classpath上
  • A MeterRegistry bean可用
  • feign 指标属性设置为 true(默认情况下)
    • feign.metrics.enabled=true (作用所有客户端)
    • feign.client.config.feignName.metrics.enabled=true (作用单个客户端)

如果您的应用程序已经使用 Micrometer,那么启用指标就像将 feign-micrometer 放到您的类路径中一样简单。

您还可以通过以下任一方式禁用该功能:

  • 从您的类路径中排除 feign-micrometer
  • 将 feign 指标属性之一设置为 false
    • feign.metrics.enabled=false
    • feign.client.config.feignName.metrics.enabled=false

feign.metrics.enabled=false 禁用对所有 Feign 客户端的度量支持,而不管客户端级别标志的值:feign.client.config.feignName.metrics.enabled。如果要为每个客户端启用或禁用 merics,请不要设置 feign.metrics.enabled 并使用 feign.client.config.feignName.metrics.enabled。

您还可以通过注册自己的 bean 来自定义 MicrometerCapability:

1
2
3
4
5
6
7
@Configuration
public class FooConfiguration {
@Bean
public MicrometerCapability micrometerCapability(MeterRegistry meterRegistry) {
return new MicrometerCapability(meterRegistry);
}
}

1.13. Feign @QueryMap 支持

OpenFeign @QueryMap 注释支持将 POJO 用作 GET 参数映射。不幸的是,默认的 OpenFeign QueryMap 注释与 Spring 不兼容,因为它缺少 value 属性。

Spring Cloud OpenFeign 提供了一个等效的 @SpringQueryMap 注解,用于将 POJO 或 Map 参数注解为查询参数映射。

例如:

Params 类定义了参数 param1 和 param2:

1
2
3
4
5
6
7
// Params.java
public class Params {
private String param1;
private String param2;

// [Getters and setters omitted for brevity]
}

以下 feign 客户端通过使用 @SpringQueryMap 注解来使用 Params 类:

1
2
3
4
5
6
@FeignClient("demo")
public interface DemoTemplate {

@GetMapping(path = "/demo")
String demoEndpoint(@SpringQueryMap Params params);
}

如果您需要对生成的查询参数映射进行更多控制,则可以实现自定义 QueryMapEncoder bean。

1.14. HATEOAS 支持

Spring 提供了一些 API 来创建遵循 HATEOAS 原则、Spring Hateoas 和 Spring Data REST 的 REST 表示。

如果您的项目使用 org.springframework.boot:spring-boot-starter-hateoas starter 或 org.springframework.boot:spring-boot-starter-data-rest starter,则默认启用 Feign HATEOAS 支持。

启用 HATEOAS 支持后,允许 Feign 客户端序列化和反序列化 HATEOAS 表示模型:EntityModel、CollectionModel 和 PagedModel。

1
2
3
4
5
6
@FeignClient("demo")
public interface DemoTemplate {

@GetMapping(path = "/stores")
CollectionModel<Store> getStores();
}

1.15. Spring @MatrixVariable 支持

Spring Cloud OpenFeign 提供对 Spring @MatrixVariable 注解的支持。

如果映射作为方法参数传递,@MatrixVariable 路径段是通过使用 = 连接映射中的键值对来创建的。

如果传递了不同的对象,则@MatrixVariable 批注(如果已定义)中提供的名称或带批注的变量名称使用 = 与提供的方法参数连接。

即使在服务器端,Spring 不要求用户将路径段占位符命名为与matrix variable名称相同的名称,因为它在客户端过于模糊,Spring Cloud OpenFeign 要求您添加一个路径段占位符与@MatrixVariable 注释(如果已定义)中提供的名称或带注释的变量名称匹配的名称。

例如:

1
2
@GetMapping("/objects/links/{matrixVars}")
Map<String, List<String>> getObjects(@MatrixVariable Map<String, List<String>> matrixVars);

请注意,变量名称和路径段占位符都称为 matrixVars。

1
2
3
4
5
6
@FeignClient("demo")
public interface DemoTemplate {

@GetMapping(path = "/stores")
CollectionModel<Store> getStores();
}

1.16. Feign CollectionFormat 支持

我们通过提供 @CollectionFormat 注释来支持 feign.CollectionFormat。您可以通过传递所需的 feign.CollectionFormat 作为注释值来注释 Feign 客户端方法。

在以下示例中,使用 CSV 格式而不是默认的 EXPLODED 来处理方法。

1
2
3
4
5
6
7
8
@FeignClient(name = "demo")
protected interface PageableFeignClient {

@CollectionFormat(feign.CollectionFormat.CSV)
@GetMapping(path = "/page")
ResponseEntity performRequest(Pageable page);

}

在发送 Pageable 作为查询参数时设置 CSV 格式,以便正确编码。

1.17.Reactive支持

由于 OpenFeign 项目目前不支持响应式客户端,例如 Spring WebClient,Spring Cloud OpenFeign 也不支持。我们将在核心项目中尽快添加对它的支持。

在完成之前,我们建议使用 feign-reactive 来支持 Spring WebClient。

1.17.1.早期初始化错误

根据您使用 Feign 客户端的方式,您可能会在启动应用程序时看到初始化错误。要解决此问题,您可以在自动装配客户端时使用 ObjectProvider。

1
2
@Autowired
ObjectProvider<TestFeginClient> testFeginClient;

1.18.Spring Data支持

您可以考虑启用 Jackson Modules 以支持 org.springframework.data.domain.Page 和 org.springframework.data.domain.Sort 解码。

1
feign.autoconfiguration.jackson.enabled=true

1.19. Spring @RefreshScope 支持

如果启用了 Feign 客户端刷新,则使用 feign.Request.Options 作为刷新范围的 bean 创建每个 feign 客户端。这意味着可以通过 POST /actuator/refresh 针对任何 Feign 客户端实例刷新诸如 connectTimeout 和 readTimeout 之类的属性。

默认情况下,Feign 客户端中的刷新行为是禁用的。使用以下属性启用刷新行为:

1
feign.client.refresh-enabled=true

不要用@RefreshScope 注释来注释@FeignClient 接口。

2、配置属性

可以在 application.properties 文件、application.yml 文件或命令行开关中指定各种属性。本附录提供了常见 Spring Cloud OpenFeign 属性的列表以及对使用它们的底层类的引用。

属性贡献可以来自类路径上的其他 jar 文件,因此您不应认为这是一个详尽的列表。此外,您可以定义自己的属性。

名称 默认值 描述
feign.autoconfiguration.jackson.enabled false 如果为 true,将为 Jackson 页面解码提供 PageJacksonModule 和 SortJacksonModule bean。
feign.circuitbreaker.enabled false 如果为 true,则 OpenFeign 客户端将使用 Spring Cloud CircuitBreaker 断路器包装。
feign.circuitbreaker.group.enabled false 如果为 true,则 OpenFeign 客户端将使用带有组的 Spring Cloud CircuitBreaker 断路器包装。
feign.client.config
feign.client.decode-slash true Feign 客户端默认不编码斜杠/字符。要更改此行为,请将 decodeSlash 设置为 false。
feign.client.default-config default
feign.client.default-to-properties true
feign.client.refresh-enabled false 为 Feign 启用选项值刷新功能。
feign.compression.request.enabled false 使 Feign 发送的请求能够被压缩。
feign.compression.request.mime-types [text/xml, application/xml, application/json] 支持的 MIME 类型列表。
feign.compression.request.min-request-size 2048 最小阈值内容大小。
feign.compression.response.enabled false 使来自 Feign 的响应能够被压缩。
feign.compression.response.useGzipDecoder false 启用要使用的默认 gzip 解码器。
feign.encoder.charset-from-content-type false 指示字符集是否应从 {@code Content-Type} 标头派生。
feign.httpclient.connection-timeout 2000
feign.httpclient.connection-timer-repeat 3000
feign.httpclient.disable-ssl-validation false
feign.httpclient.enabled true 允许 Feign 使用 Apache HTTP 客户端。
feign.httpclient.follow-redirects true
feign.httpclient.hc5.enabled false 允许 Feign 使用 Apache HTTP Client 5。
feign.httpclient.hc5.pool-concurrency-policy 池并发策略。
feign.httpclient.hc5.pool-reuse-policy 池连接重用策略。
feign.httpclient.hc5.socket-timeout 5 Socket超时的默认值。
feign.httpclient.hc5.socket-timeout-unit Socket超时单位的默认值。
feign.httpclient.max-connections 200
feign.httpclient.max-connections-per-route 50
feign.httpclient.time-to-live 900
feign.httpclient.time-to-live-unit
feign.metrics.enabled true 为 Feign 启用指标功能。
feign.okhttp.enabled false 允许 Feign 使用 OK HTTP Client。

STOMP协议

STOMP 协议规范,版本 1.2

概述

背景

STOMP 源于需要从 Ruby、Python 和 Perl 等脚本语言连接到企业消息代理。在这样的环境中,通常执行逻辑上简单的操作,例如“可靠地发送单个消息并断开连接”或“使用给定目的地上的所有消息”。

它是其他开放消息传递协议(如 AMQP)和 JMS 代理(如 OpenWire)中使用的特定于实现的线路协议的替代方案。它通过覆盖一小部分常用消息传递操作而不是提供全面的消息传递 API 来区分自己。

最近,STOMP 已经成熟为一种协议,该协议可以在其现在提供的线级功能方面超越这些简单的用例,但仍保持其简单性和互操作性的核心设计原则。

协议概述

STOMP 是一种基于帧的协议,其帧以 HTTP 为模型。一个帧由一个命令、一组可选标题和一个可选正文组成。 STOMP 是基于文本的,但也允许传输二进制消息。 STOMP 的默认编码是 UTF-8,但它支持消息正文的替代编码规范。

STOMP 服务器被建模为一组可以发送消息的目的地。 STOMP 协议将目的地视为不透明字符串,它们的语法是特定于服务器实现的。此外,STOMP 没有定义目的地的交付语义应该是什么。目的地的传递或“消息交换”语义可能因服务器而异,甚至因目的地而异。这允许服务器在他们可以用 STOMP 支持的语义上进行创造性的工作。

STOMP 客户端是一个用户代理,可以在两种(可能同时)模式下运行:

  • 作为生产者,通过 SEND 帧将消息发送到服务器上的目的地
  • 作为消费者,为给定的目的地发送一个订阅帧,并从服务器接收消息作为 MESSAGE 帧。

协议的变化

STOMP 1.2 主要向后兼容 STOMP 1.1。只有两个不兼容的更改:

  • 现在可以使用回车加换行而不是仅换行来结束帧行
  • 消息确认已被简化,现在使用专用标头

除此之外,STOMP 1.2 没有引入任何新特性,而是着重于阐明规范的一些领域,例如:

  • 重复的帧头条目
  • 使用 content-length 和 content-type 标头
  • 需要服务器支持 STOMP 框架
  • 持续连接
  • 订阅和transaction标识符的范围和唯一性
  • RECEIPT 帧相对于先前帧的意义

设计理念

推动 STOMP 设计的主要理念是简单性和互操作性。

STOMP 被设计为一种轻量级协议,易于在客户端和服务器端以多种语言实现。这尤其意味着,对服务器架构的约束并不多,而且许多特性(例如目标命名和可靠性语义)是特定于实现的。

在本规范中,我们将注意 STOMP 1.2 未明确定义的服务器功能。您应该查阅 STOMP 服务器的文档以了解这些功能的实现特定细节。

一致性

本文档中的关键词“MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、“MAY”和“OPTIONAL”是按照 RFC 2119 中的描述进行解释。

实现可能会对不受约束的输入施加特定于实现的限制,例如以防止拒绝服务攻击、防止内存不足或解决特定于平台的限制。

本规范定义的一致性类是 STOMP 客户端和 STOMP 服务器。

STOMP帧

STOMP 是一种基于帧的协议,它在下面假定了一个可靠的 2-way 流网络协议(例如 TCP)。客户端和服务器将使用通过流发送的 STOMP 帧进行通信。框架的结构如下所示:

1
2
3
4
5
COMMAND
header1:value1
header2:value2

Body^

该帧以一个以行尾 (EOL) 结尾的命令字符串开始,它由一个可选的回车(octet 13)和一个必需的换行符(octet 10)组成。命令后面是 : 格式的零个或多个标头条目。每个报头条目都由一个 EOL 终止。空行(即额外的 EOL)表示标题的结尾和正文的开头。正文之后是 NULL 八位字节。本文档中的示例将使用 ASCII 中的 ^@、control-@ 来表示 NULL 八位字节。 NULL 八位字节可以有选择地跟随多个 EOL。有关如何解析 STOMP 帧的更多详细信息,请参阅本文档的Augmented BNF 部分。

本文档中引用的所有命令和标题名称均区分大小写。

值编码

命令和标头以 UTF-8 编码。除 CONNECT 和 CONNECTED 帧之外的所有帧也将转义在生成的 UTF-8 编码标头中找到的任何回车、换行或冒号。

需要转义以允许标题键和值包含那些作为值分隔八位字节的帧标题。 CONNECT 和 CONNECTED 帧不会转义回车、换行或冒号八位字节,以保持与 STOMP 1.0 的向后兼容。

C 样式字符串文字转义用于对在 UTF-8 编码标头中找到的任何回车、换行或冒号进行编码。解码帧头时,必须应用以下转换:

  • \r (octet 92 and 114) 翻译成回车 (octet 13)
  • \n (octet 92 and 110) 转换为换行 (octet 10)
  • \c (octet 92 and 99) 翻译成 : (octet 58)
  • \\ (octet 92 and 92) 翻译成 \ (octet 92)

未定义的转义序列如 \t(octet 92 and 116)必须被视为致命的协议错误。相反,在编码帧头时,必须应用逆向转换。

STOMP 1.0 规范包括许多在标头中填充的示例帧,并且实现了许多服务器和客户端来修剪或填充标头值。如果应用程序想要发送不应被修剪的标头,这会导致问题。在 STOMP 1.2 中,客户端和服务器绝不能用空格修剪或填充标头。

Body

只有 SEND、MESSAGE 和 ERROR 帧可能有正文。所有其他帧不得有主体。

标准头

对于大多数帧,可以使用某些标题并且具有特殊含义。

Header content-length

所有的帧都可以包含一个内容长度的头部。此标头是消息正文长度的八位字节计数。如果包含内容长度标头,则必须读取此八位字节数,无论正文中是否存在 NULL 八位字节。该帧仍需要以 NULL 八位字节结束。

如果存在帧体,则 SEND、MESSAGE 和 ERROR 帧应该包含内容长度标头以简化帧解析。如果帧体包含 NULL 八位字节,则帧必须包含内容长度头。

Header content-type

如果存在帧体,则 SEND、MESSAGE 和 ERROR 帧应该包含一个内容类型的头,以帮助帧的接收者解释其体。如果设置了 content-type 标头,它的值必须是描述正文格式的 MIME 类型。否则,接收者应该将主体视为二进制 blob。

以 text/ 开头的 MIME 类型的隐含文本编码是 UTF-8。如果您使用具有不同编码的基于文本的 MIME 类型,那么您应该将 ;charset= 附加到 MIME 类型。例如,如果您以 UTF-16 编码发送 HTML 正文,则应使用 text/html;charset=utf-16 。 ;charset= 也应该附加到任何可以解释为文本的非文本/ MIME 类型。一个很好的例子是 UTF-8 编码的 XML。它的内容类型应该设置为 application/xml;charset=utf-8

所有 STOMP 客户端和服务器必须支持 UTF-8 编码和解码。因此,为了在异构计算环境中实现最大的互操作性,建议使用 UTF-8 对基于文本的内容进行编码。

Header receipt

除了 CONNECT 之外的任何客户端框架都可以指定具有任意值的接收标头。这将导致服务器使用 RECEIPT 帧确认客户端帧的处理(有关更多详细信息,请参阅 RECEIPT 帧)。

1
2
3
4
5
SEND
destination:/queue/a
receipt:message-12345

hello queue a^@

重复的头条目

由于消息系统可以按照存储和转发拓扑进行组织,类似于 SMTP,因此消息在到达消费者之前可能会遍历多个消息服务器。 STOMP 服务器可以通过在消息中添加标头或在消息中就地修改标头来“更新”标头值。

如果客户端或服务器收到重复的帧头条目,则只有第一个头条目应该用作头条目的值。后续值仅用于维护标头状态更改的历史记录,可以忽略。

例如,如果客户端收到:

1
2
3
4
5
MESSAGE
foo:World
foo:Hello

^@

foo 头的值就是 World。

大小限制

为了防止恶意客户端利用服务器中的内存分配,服务器可以设置最大限制:

  • 单个帧中允许的帧头数
  • Header行的最大长度
  • 帧体的最大尺寸

如果超过这些限制,服务器应该向客户端发送一个 ERROR 帧,然后关闭连接。

连接延迟

STOMP 服务器必须能够支持快速连接和断开连接的客户端。

这意味着服务器可能只允许关闭的连接在连接重置之前短暂停留。

因此,在套接字重置之前,客户端可能不会收到服务器发送的最后一帧(例如,错误帧或响应断开帧的接收帧)。

连接

STOMP 客户端通过发送 CONNECT 帧来启动与服务器的流或 TCP 连接:

1
2
3
4
5
CONNECT
accept-version:1.2
host:stomp.github.org

^@

如果服务器接受连接尝试,它将以 CONNECTED 帧响应:

1
2
3
4
CONNECTED
version:1.2

^@

服务器可以拒绝任何连接尝试。服务器应该用一个 ERROR 帧来响应,解释连接被拒绝的原因,然后关闭连接。

CONNECT或STOMP帧

STOMP 服务器必须以与 CONNECT 帧相同的方式处理 STOMP 帧。 STOMP 1.2 客户端应该继续使用 CONNECT 命令来保持与 STOMP 1.0 服务器的向后兼容。

使用 STOMP 帧而不是 CONNECT 帧的客户端将只能连接到 STOMP 1.2 服务器(以及一些 STOMP 1.1 服务器),但优点是协议嗅探器/鉴别器将能够将 STOMP 连接与HTTP 连接。

STOMP 1.2 客户端必须设置以下标头:

  • accept-version : 客户端支持的 STOMP 协议版本。有关更多详细信息,请参阅协议协商
  • host : 客户端希望连接的虚拟主机的名称。建议客户端将其设置为建立套接字所针对的主机名,或他们选择的任何名称。如果此标头与已知虚拟主机不匹配,则支持虚拟主机的服务器可以选择默认虚拟主机或拒绝连接。

STOMP 1.2 客户端可以设置以下标头:

  • login : 用于针对安全 STOMP 服务器进行身份验证的用户标识符。
  • passcode : 用于对安全 STOMP 服务器进行身份验证的密码。
  • heart-beat : 心跳设置。

连接帧

STOMP 1.2 服务器必须设置以下标头:

  • version : 会话将使用的 STOMP 协议的版本。有关更多详细信息,请参阅协议协商。

STOMP 1.2 服务器可以设置以下标头:

  • heart-beat : 心跳设置。

  • session : 唯一标识会话的会话标识符。

  • server : 包含有关 STOMP 服务器的信息的字段。该字段必须包含一个服务器名称字段,并且可以跟随着由空格八位字节分隔的可选注释字段。

    server-name 字段由一个名称标记和一个可选的版本号标记组成。

    server = name ["/" version] *(comment)

    例子:

    server:Apache/1.3.9

协议交互

从 STOMP 1.1 开始,CONNECT 帧必须包含 accept-version 标头。它应该设置为客户端支持的递增 STOMP 协议版本的逗号分隔列表。如果缺少accept-version 头,则表示客户端仅支持1.0 版协议。

将用于会话其余部分的协议将是客户端和服务器共有的最高协议版本。

例如,如果客户端发送:

1
2
3
4
5
CONNECT
accept-version:1.0,1.1,2.0
host:stomp.github.org

^@

服务器将使用与客户端相同的协议的最高版本进行响应:

1
2
3
4
CONNECTED
version:1.1

^@

如果客户端和服务器不共享任何公共协议版本,则服务器必须以类似于以下的 ERROR 帧响应,然后关闭连接:

1
2
3
4
5
ERROR
version:1.2,2.1
content-type:text/plain

Supported protocol versions are 1.2 2.1^@

心跳

可以选择使用心跳来测试底层 TCP 连接的健康状况,并确保远程端处于活动状态并正常运行。

为了启用心跳,每一方都必须声明它可以做什么以及希望另一方做什么。这发生在 STOMP 会话的最开始时,通过向 CONNECT 和 CONNECTED 帧添加心跳标头。

使用时,心跳头必须包含两个用逗号分隔的正整数。

第一个数字代表帧的发送者可以做什么(传出心跳):

  • 0 表示不能发送心跳

  • 否则它是它可以保证的心跳之间的最小毫秒数 第二个数字代表帧的发送者想要得到什么(传入的心跳):

    • 0 表示它不想接收心跳

    • 否则它是心跳之间所需的毫秒数

心跳报头是可选的。丢失的心跳报头必须以与“heart-beat:0,0”报头相同的方式处理,即:一方不能发送也不想接收心跳。

心跳报头提供了足够的信息,以便每一方都可以查明是否可以使用心跳、在哪个方向以及以哪个频率使用。

更正式地说,初始帧如下所示:

1
2
3
4
5
CONNECT
heart-beat:<cx>,<cy>

CONNECTED
heart-beat:<sx>,<sy>

对于从客户端到服务器的心跳:

  • 如果 为 0(客户端无法发送心跳)或 为 0(服务器不想接收心跳),则不会有
  • 否则,每 MAX(,) 毫秒就会有一次心跳

在另一个方向上, 的用法相同。

关于心跳本身,通过网络连接接收到的任何新数据都表明远程端处于活动状态。在给定的方向上,如果每 毫秒需要一次心跳:

  • 发送方必须至少每 毫秒通过网络连接发送新数据
  • 如果发送方没有真正的 STOMP 帧要发送,它必须发送一个行尾(EOL)
  • 如果在至少 毫秒的时间窗口内,接收器没有收到任何新数据,它可以认为连接已死
  • 由于时序不准确,接收器应该容忍并考虑误差容限

客户端帧

客户端可以发送不在这个列表中的帧,但是对于这样的帧,STOMP 1.2 服务器可以响应一个 ERROR 帧,然后关闭连接。

SEND

SEND 帧将消息发送到消息系统中的目的地。它有一个 REQUIRED 标头,即目的地,指示将消息发送到何处。 SEND 帧的主体是要发送的消息。例如:

1
2
3
4
5
6
SEND
destination:/queue/a
content-type:text/plain

hello queue a
^@

这会向名为 /queue/a 的目的地发送一条消息。请注意,STOMP 将此目的地视为不透明字符串,并且目的地名称不假定传递语义。您应该查阅 STOMP 服务器的文档以了解如何构造一个目标名称,该名称为您提供应用程序所需的传递语义。

消息的可靠性语义也是特定于服务器的,将取决于正在使用的目标值和其他消息头,例如事务头或其他服务器特定的消息头。

SEND 支持允许事务发送的事务标头。

如果正文存在,SEND 帧应该包括一个content-length 标头和一个content-type 标头。

应用程序可以向 SEND 帧添加任意用户定义的标头。用户定义的标头通常用于允许消费者使用订阅帧上的选择器根据应用程序定义的标头过滤消息。用户定义的头必须在 MESSAGE 帧中传递。

如果服务器由于任何原因无法成功处理 SEND 帧,则服务器必须向客户端发送一个 ERROR 帧,然后关闭连接。

SUBSCRIBE

SUBSCRIBE 帧用于注册以侦听给定的目的地。与 SEND 帧一样,SUBSCRIBE 帧需要一个目的地标头,指示客户端想要订阅的目的地。在订阅目标上收到的任何消息将作为 MESSAGE 帧从服务器传递到客户端。 ack 头控制消息确认模式。

例子:

1
2
3
4
5
6
SUBSCRIBE
id:0
destination:/queue/foo
ack:client

^@

如果服务器无法成功创建订阅,则服务器必须向客户端发送一个 ERROR 帧,然后关闭连接。

STOMP 服务器可以支持额外的特定于服务器的标头来定制订阅的交付语义。有关详细信息,请参阅服务器的文档。

SUBSCRIBE id Header

由于单个连接可以与服务器有多个开放订阅,因此帧中必须包含一个 id 标头以唯一标识订阅。 id 头允许客户端和服务器将后续的 MESSAGE 或 UNSUBSCRIBE 帧与原始订阅相关联。

在同一个连接中,不同的订阅必须使用不同的订阅标识符。

SUBSCRIBE ack Header

ack 标头的有效值为 auto、client 或 client-individual。如果未设置标头,则默认为自动。

当 ack 模式为 auto 时,客户端不需要为它收到的消息发送服务器 ACK 帧。服务器将在将消息发送给客户端后立即假定客户端已收到该消息。这种确认模式可能会导致传输到客户端的消息被丢弃。

当 ack 模式为客户端时,客户端必须为它处理的消息发送服务器 ACK 帧。如果在客户端发送消息的 ACK 帧之前连接失败,服务器将假定消息尚未处理,并可以将消息重新传递给另一个客户端。客户端发送的 ACK 帧将被视为累积确认。这意味着确认对 ACK 帧中指定的消息以及在 ACK 消息之前发送到订阅的所有消息进行操作。

如果客户端没有处理一些消息,它应该发送 NACK 帧来告诉服务器它没有使用这些消息。

当 ack 模式为 client-individual 时,除了客户端发送的 ACK 或 NACK 帧不累积之外,确认的操作与客户端确认模式相同。这意味着后续消息的 ACK 或 NACK 帧不得导致先前消息得到确认。

UNSUBSCRIBE

UNSUBSCRIBE 帧用于删除现有订阅。一旦订阅被删除,STOMP 连接将不再接收来自该订阅的消息。

由于单个连接可以与服务器有多个开放订阅,因此帧中必须包含一个 id 标头以唯一标识要删除的订阅。此标头必须与现有订阅的订阅标识符匹配。

例子:

1
2
3
4
UNSUBSCRIBE
id:0

^@

ACK

ACK 用于使用客户端或客户端个人确认来确认来自订阅的消息的消耗。在通过 ACK 确认消息之前,不会认为从此类订阅接收到的任何消息已被消费。

ACK 帧必须包含一个与被确认的 MESSAGE 的 ack 头匹配的 id 头。可选地,可以指定一个事务头,表明消息确认应该是命名事务的一部分。

1
2
3
4
5
ACK
id:12345
transaction:tx1

^@

NACK

NACK 是 ACK 的反义词。它用于告诉服务器客户端没有消费该消息。然后,服务器可以将消息发送到不同的客户端、丢弃它或将其放入死信队列。确切的行为是特定于服务器的。

NACK 采用与 ACK 相同的标头:id(必需)和交易(可选)。

NACK 应用于单个消息(如果订阅的 ack 模式是客户端个人)或应用于之前发送但尚未确认或 NACK 的所有消息(如果订阅的确认模式是客户端)。

BEGIN

BEGIN 用于启动事务。这种情况下的交易适用于发送和确认 - 在交易期间发送或确认的任何消息都将根据交易进行原子处理。

1
2
3
4
BEGIN
transaction:tx1

^@

事务标头是必需的,事务标识符将用于 SEND、COMMIT、ABORT、ACK 和 NACK 帧以将它们绑定到命名事务。在同一个连接中,不同的事务必须使用不同的事务标识符。

如果客户端发送 DISCONNECT 帧或 TCP 连接因任何原因失败,则任何尚未提交的已启动事务都将隐式中止。

COMMIT

COMMIT 用于提交正在进行的事务。

1
2
3
4
COMMIT
transaction:tx1

^@

事务头是必需的,并且必须指定要提交的事务的标识符。

ABORT

ABORT 用于回滚正在进行的事务。

1
2
3
4
ABORT
transaction:tx1

^@

事务头是必需的,并且必须指定要中止的事务的标识符。

DISCONNECT

客户端可以随时通过关闭套接字与服务器断开连接,但不能保证先前发送的帧已被服务器接收。要进行正常关闭,客户端确保服务器已收到所有先前的帧,客户端应该:

  1. 发送带有接收标头集的 DISCONNECT 帧。例子:

    1
    2
    3
    DISCONNECT
    receipt:77
    ^@
  2. 等待 RECEIPT 帧对 DISCONNECT 的响应。例子:

    1
    2
    3
    RECEIPT
    receipt-id:77
    ^@
  3. 关闭套接字。

请注意,如果服务器过快地关闭套接字的末尾,客户端可能永远不会收到预期的 RECEIPT 帧。有关更多信息,请参阅连接延迟部分。

发送 DISCONNECT 帧后,客户端不得再发送任何帧。

服务端帧

服务器有时会向客户端发送帧(除了初始的 CONNECTED 帧)。这些帧可能是以下之一:

MESSAGE

MESSAGE 帧用于将消息从订阅传送到客户端。

MESSAGE 帧必须包含一个目的地标头,指示消息被发送到的目的地。如果消息是使用 STOMP 发送的,则此目标标头应该与相应 SEND 帧中使用的标头相同。

MESSAGE 帧还必须包含一个 message-id 标头,该标头具有该消息的唯一标识符和一个与接收消息的订阅标识符匹配的订阅标头。

如果从需要显式确认的订阅接收消息(客户端或客户端个人模式),则 MESSAGE 帧还必须包含具有任意值的 ack 标头。该报头将用于将消息与后续 ACK 或 NACK 帧相关联。

帧体包含消息的内容:

1
2
3
4
5
6
7
MESSAGE
subscription:0
message-id:007
destination:/queue/a
content-type:text/plain

hello queue a^@

如果正文存在,MESSAGE 帧应该包括一个content-length标头和一个content-type标头。

MESSAGE 帧还将包括所有用户定义的头,这些头在消息发送到目的地时出现,除了可能被添加到帧中的服务器特定头。查阅您的服务器的文档以找出它添加到消息中的特定于服务器的标头。

RECEIPT

一旦服务器成功处理了请求接收的客户端帧,就会从服务器向客户端发送一个 RECEIPT 帧。一个 RECEIPT 帧必须包含首部receipt-id,其中的值是作为接收的帧中的接收首部的值。

1
2
3
4
RECEIPT
receipt-id:message-12345

^@

RECEIPT 帧是对相应客户端帧已被服务器处理的确认。由于 STOMP 是基于流的,因此接收也是服务器已接收到所有先前帧的累积确认。然而,这些先前的帧可能还没有被完全处理。如果客户端断开连接,先前收到的帧应该继续由服务器处理。

ERROR

如果出现问题,服务器可能会发送 ERROR 帧。在这种情况下,它必须在发送 ERROR 帧后立即关闭连接。请参阅有关连接延迟的下一节。

ERROR 帧应该包含一个带有错误简短描述的消息头,并且主体可以包含更详细的信息(或者可以为空)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ERROR
receipt-id:message-12345
content-type:text/plain
content-length:170
message:malformed frame received

The message:
-----
MESSAGE
destined:/queue/a
receipt:message-12345

Hello queue a!
-----
Did not contain a destination header, which is REQUIRED
for message propagation.
^@

如果错误与客户端发送的特定帧有关,则服务器应该添加额外的标头以帮助识别导致错误的原始帧。例如,如果帧包含接收头,则错误帧应该设置接收标识头以匹配与错误相关的帧的接收头的值。

如果正文存在,则错误帧应该包括内容长度标头和内容类型标头。

Frames and Headers

除了上面描述的标准头(内容长度、内容类型和接收)之外,这里是本规范中定义的每个帧必须或可以使用的所有头:

1
CONNECT` or `STOMP
  • REQUIRED: accept-version, host
  • OPTIONAL: login, passcode, heart-beat
1
CONNECTED
  • REQUIRED: version
  • OPTIONAL: session, server, heart-beat
1
SEND
  • REQUIRED: destination
  • OPTIONAL: transaction
1
SUBSCRIBE
  • REQUIRED: destination, id
  • OPTIONAL: ack
1
UNSUBSCRIBE
  • REQUIRED: id
  • OPTIONAL: none
1
ACK` or `NACK
  • REQUIRED: id
  • OPTIONAL: transaction
1
BEGIN` or `COMMIT` or `ABORT
  • REQUIRED: transaction
  • OPTIONAL: none
1
DISCONNECT
  • REQUIRED: none
  • OPTIONAL: receipt
1
MESSAGE
  • REQUIRED: destination, message-id, subscription
  • OPTIONAL: ack
1
RECEIPT
  • REQUIRED: receipt-id
  • OPTIONAL: none
1
ERROR
  • REQUIRED: none
  • OPTIONAL: message

此外,SEND 和 MESSAGE 帧可以包含任意用户定义的头部,这些头部应该被认为是承载消息的一部分。此外,ERROR 帧应该包括额外的头以帮助识别导致错误的原始帧。

最后,STOMP 服务器可以使用额外的标头来访问诸如持久性或过期等功能。有关详细信息,请参阅服务器的文档。

Augmented BNF

可以使用 HTTP/1.1 RFC 2616 中使用的 Backus-Naur Form (BNF) 语法更正式地描述 STOMP 会话。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
NULL                = <US-ASCII null (octet 0)>
LF = <US-ASCII line feed (aka newline) (octet 10)>
CR = <US-ASCII carriage return (octet 13)>
EOL = [CR] LF
OCTET = <any 8-bit sequence of data>

frame-stream = 1*frame

frame = command EOL
*( header EOL )
EOL
*OCTET
NULL
*( EOL )

command = client-command | server-command

client-command = "SEND"
| "SUBSCRIBE"
| "UNSUBSCRIBE"
| "BEGIN"
| "COMMIT"
| "ABORT"
| "ACK"
| "NACK"
| "DISCONNECT"
| "CONNECT"
| "STOMP"

server-command = "CONNECTED"
| "MESSAGE"
| "RECEIPT"
| "ERROR"

header = header-name ":" header-value
header-name = 1*<any OCTET except CR or LF or ":">
header-value = *<any OCTET except CR or LF or ":">

WebSocket基本使用

WebSockets

文档涵盖了对 Servlet 堆栈的支持、包含原始 WebSocket 交互的 WebSocket 消息传递、通过 SockJS 的 WebSocket 模拟以及通过作为 WebSocket 子协议的 STOMP 的发布订阅消息传递。

WebSocket 简介

WebSocket 协议 RFC 6455 提供了一种标准化方法,可通过单个 TCP 连接在客户端和服务器之间建立全双工、双向通信通道。它是与 HTTP 不同的 TCP 协议,但旨在通过 HTTP 工作,使用端口 80 和 443,并允许重新使用现有的防火墙规则。

WebSocket 交互以 HTTP 请求开始,该请求使用 HTTP Upgrade header进行升级,或者在本例中切换到 WebSocket 协议。以下示例显示了这样的交互:

1
2
3
4
5
6
7
8
GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080

与通常的 200 状态代码不同,具有 WebSocket 支持的服务器返回类似于以下内容的输出:

1
2
3
4
5
HTTP/1.1 101 Switching Protocols 
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp

成功握手后,HTTP 升级请求底层的 TCP 套接字保持打开状态,客户端和服务器都可以继续发送和接收消息。

对 WebSockets 工作原理的完整介绍超出了本文档的范围。请参阅 RFC 6455、HTML5 的 WebSocket 章节或 Web 上的许多介绍和教程中的任何一个。

请注意,如果 WebSocket 服务器在 Web 服务器(例如 nginx)后面运行,您可能需要将其配置为将 WebSocket 升级请求传递到 WebSocket 服务器。同样,如果应用程序在云环境中运行,请查看与 WebSocket 支持相关的云提供商的说明。

HTTP 与 WebSocket

尽管 WebSocket 被设计为与 HTTP 兼容并从 HTTP 请求开始,但重要的是要了解这两种协议会导致非常不同的架构和应用程序编程模型。

在 HTTP 和 REST 中,一个应用程序被建模为多个 URL。为了与应用程序交互,客户端访问这些 URL,请求-响应样式。服务器根据 HTTP URL、方法和标头将请求路由到适当的处理程序。

相比之下,在 WebSockets 中,通常只有一个 URL 用于初始连接。随后,所有应用程序消息都在同一个 TCP 连接上流动。这指向一个完全不同的异步、事件驱动、消息传递架构。

WebSocket 也是一种低级传输协议,与 HTTP 不同,它不对消息内容规定任何语义。这意味着除非客户端和服务器就消息语义达成一致,否则无法路由或处理消息。

WebSocket 客户端和服务器可以通过 HTTP 握手请求上的 Sec-WebSocket-Protocol 标头协商使用更高级别的消息传递协议(例如,STOMP)。如果没有,他们需要提出自己的约定。

何时使用 WebSocket

WebSockets 可以使网页具有动态性和交互性。但是,在许多情况下,Ajax 和 HTTP 流或长轮询的组合可以提供简单有效的解决方案。

例如,新闻、邮件和社交提要需要动态更新,但每隔几分钟更新一次可能完全没问题。另一方面,协作、游戏和金融应用程序需要更接近实时。

延迟本身并不是决定性因素。如果消息量相对较低(例如监控网络故障),HTTP 流或轮询可以提供有效的解决方案。低延迟、高频率和高容量的组合是使用 WebSocket 的最佳案例。

还请记住,在 Internet 上,不受您控制的限制性代理可能会阻止 WebSocket 交互,因为它们未配置为传递 Upgrade 标头,或者因为它们关闭了看似空闲的长期连接。这意味着将 WebSocket 用于防火墙内的内部应用程序是一个比面向公众的应用程序更直接的决定。

WebSocket API

Spring Framework 提供了一个 WebSocket API,您可以使用它来编写处理 WebSocket 消息的客户端和服务器端应用程序。

WebSocketHandler

创建 WebSocket 服务器就像实现 WebSocketHandler 一样简单,或者更有可能扩展 TextWebSocketHandler 或 BinaryWebSocketHandler。以下示例使用 TextWebSocketHandler:

1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;

public class MyHandler extends TextWebSocketHandler {

@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) {
// ...
}

}

有专用的 WebSocket Java 配置和 XML 命名空间支持,用于将前面的 WebSocket 处理程序映射到特定 URL,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler");
}

@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}

}

以下示例显示了与前面示例等效的 XML 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
</websocket:handlers>

<bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

前面的示例用于 Spring MVC 应用程序,应包含在 DispatcherServlet 的配置中。但是,Spring 的 WebSocket 支持不依赖于 Spring MVC。在 WebSocketHttpRequestHandler 的帮助下,将 WebSocketHandler 集成到其他 HTTP 服务环境中相对简单。

直接与间接使用 WebSocketHandler API 时,例如通过 STOMP 消息传递,应用程序必须同步消息的发送,因为底层标准 WebSocket 会话 (JSR-356) 不允许并发发送。一种选择是使用 ConcurrentWebSocketSessionDecorator 包装 WebSocketSession。

WebSocket 握手

自定义初始 HTTP WebSocket 握手请求的最简单方法是通过 HandshakeInterceptor,它公开握手“之前”和“之后”的方法。您可以使用此类拦截器来阻止握手或使任何属性可用于 WebSocketSession。以下示例使用内置拦截器将 HTTP 会话属性传递给 WebSocket 会话:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyHandler(), "/myHandler")
.addInterceptors(new HttpSessionHandshakeInterceptor());
}

}

以下示例显示了与前面示例等效的 XML 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:handshake-interceptors>
<bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
</websocket:handshake-interceptors>
</websocket:handlers>

<bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

一个更高级的选项是扩展 DefaultHandshakeHandler,它执行 WebSocket 握手的步骤,包括验证客户端来源、协商子协议和其他细节。如果应用程序需要配置自定义 RequestUpgradeStrategy 以适应尚不支持的 WebSocket 服务器引擎和版本,则它也可能需要使用此选项(有关此主题的更多信息,请参阅部署)。 Java 配置和 XML 命名空间都可以配置自定义 HandshakeHandler。

Spring 提供了一个 WebSocketHandlerDecorator 基类,您可以使用它来装饰具有附加行为的 WebSocketHandler。使用 WebSocket Java 配置或 XML 命名空间时,默认提供并添加日志记录和异常处理实现。 ExceptionWebSocketHandlerDecorator 捕获任何 WebSocketHandler 方法产生的所有未捕获的异常,并关闭状态为 1011 的 WebSocket 会话,这表示服务器错误。

Deployment

Spring WebSocket API 很容易集成到 Spring MVC 应用程序中,其中 DispatcherServlet 为 HTTP WebSocket 握手和其他 HTTP 请求提供服务。通过调用 WebSocketHttpRequestHandler 也很容易集成到其他 HTTP 处理场景中。这很方便,也很容易理解。但是,对于 JSR-356 运行时需要特殊考虑。

Java WebSocket API (JSR-356) 提供了两种部署机制。第一个涉及启动时的 Servlet 容器类路径扫描(Servlet 3 特性)。另一个是在 Servlet 容器初始化时使用的注册 API。这两种机制都无法使用单个“前端控制器”进行所有 HTTP 处理 — 包括 WebSocket 握手和所有其他 HTTP 请求 — ,例如 Spring MVC 的 DispatcherServlet。

这是 JSR-356 的一个重大限制,即使在 JSR-356 运行时中运行时,Spring 的 WebSocket 支持也可以解决特定于服务器的 RequestUpgradeStrategy 实现。 Tomcat、Jetty、GlassFish、WebLogic、WebSphere 和 Undertow(以及 WildFly)目前存在此类策略。

已经创建了克服 Java WebSocket API 中上述限制的请求,可以在 eclipse-ee4j/websocket-api#211 中遵循该请求。 Tomcat、Undertow 和 WebSphere 提供了它们自己的 API 替代方案,可以做到这一点,Jetty 也可以做到。我们希望更多的服务器能做同样的事情。

第二个考虑因素是,支持 JSR-356 的 Servlet 容器预计会执行 ServletContainerInitializer (SCI) 扫描,这会减慢应用程序启动速度 — 在某些情况下会显着降低。如果在升级到支持 JSR-356 的 Servlet 容器版本后观察到显着影响,应该可以通过使用 web 中的 元素有选择地启用或禁用 web 片段(和 SCI 扫描) .xml,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">

<absolute-ordering/>

</web-app>

然后,您可以按名称有选择地启用 Web 片段,例如 Spring 自己的 SpringServletContainerInitializer,它提供对 Servlet 3 Java 初始化 API 的支持。以下示例显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">

<absolute-ordering>
<name>spring_web</name>
</absolute-ordering>

</web-app>

服务配置

每个底层 WebSocket 引擎都公开控制运行时特性的配置属性,例如消息缓冲区大小、空闲超时等。

对于 Tomcat、WildFly 和 GlassFish,您可以将 ServletServerContainerFactoryBean 添加到您的 WebSocket Java 配置中,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}

}

以下示例显示了与前面示例等效的 XML 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<bean class="org.springframework...ServletServerContainerFactoryBean">
<property name="maxTextMessageBufferSize" value="8192"/>
<property name="maxBinaryMessageBufferSize" value="8192"/>
</bean>

</beans>

对于客户端 WebSocket 配置,您应该使用 WebSocketContainerFactoryBean (XML) 或 ContainerProvider.getWebSocketContainer()(Java 配置)。

对于 Jetty,您需要提供一个预配置的 Jetty WebSocketServerFactory 并通过 WebSocket Java 配置将其插入 Spring 的 DefaultHandshakeHandler。以下示例显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(echoWebSocketHandler(),
"/echo").setHandshakeHandler(handshakeHandler());
}

@Bean
public DefaultHandshakeHandler handshakeHandler() {

WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);

return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}

}

以下示例显示了与前面示例等效的 XML 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:handlers>
<websocket:mapping path="/echo" handler="echoHandler"/>
<websocket:handshake-handler ref="handshakeHandler"/>
</websocket:handlers>

<bean id="handshakeHandler" class="org.springframework...DefaultHandshakeHandler">
<constructor-arg ref="upgradeStrategy"/>
</bean>

<bean id="upgradeStrategy" class="org.springframework...JettyRequestUpgradeStrategy">
<constructor-arg ref="serverFactory"/>
</bean>

<bean id="serverFactory" class="org.eclipse.jetty...WebSocketServerFactory">
<constructor-arg>
<bean class="org.eclipse.jetty...WebSocketPolicy">
<constructor-arg value="SERVER"/>
<property name="inputBufferSize" value="8092"/>
<property name="idleTimeout" value="600000"/>
</bean>
</constructor-arg>
</bean>

</beans>

Allowed Origins

从 Spring Framework 4.1.5 开始,WebSocket 和 SockJS 的默认行为是仅接受同源请求。还可以允许所有或指定的来源列表。此检查主要是为浏览器客户端设计的。没有什么可以阻止其他类型的客户端修改 Origin 标头值(有关更多详细信息,请参阅 RFC 6454:Web Origin 概念)。

三种可能的行为是:

  • 仅允许同源请求(默认):在这种模式下,当启用 SockJS 时,Iframe HTTP 响应头 X-Frame-Options 设置为 SAMEORIGIN,并且禁用 JSONP 传输,因为它不允许检查源要求。因此,启用此模式时不支持 IE6 和 IE7。
  • 允许指定的来源列表:每个允许的来源必须以 http:// 或 https:// 开头。在这种模式下,当 SockJS 启用时,IFrame 传输被禁用。因此,启用此模式时,不支持 IE6 到 IE9。
  • 允许所有来源:要启用此模式,您应该提供 * 作为允许的来源值。在这种模式下,所有传输都可用。

您可以配置 WebSocket 和 SockJS 允许的来源,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com");
}

@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}

}

以下示例显示了与前面示例等效的 XML 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:handlers allowed-origins="https://mydomain.com">
<websocket:mapping path="/myHandler" handler="myHandler" />
</websocket:handlers>

<bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

SockJS Fallback

在公共 Internet 上,不受您控制的限制性代理可能会阻止 WebSocket 交互,因为它们未配置为传递 Upgrade 标头,或者因为它们关闭了看似空闲的长期连接。

该问题的解决方案是 WebSocket 模拟 — 即尝试首先使用 WebSocket,然后再使用基于 HTTP 的技术来模拟 WebSocket 交互并公开相同的应用程序级 API。

在 Servlet 堆栈上,Spring Framework 为 SockJS 协议提供服务器(和客户端)支持。

概述

SockJS 的目标是让应用程序使用 WebSocket API,但在运行时必要时回退到非 WebSocket 替代方案,而无需更改应用程序代码。

SockJS 包括:

  • 以可执行叙述测试的形式定义的 SockJS 协议。
  • SockJS JavaScript 客户端 — 用于浏览器的客户端库。
  • SockJS 服务器实现,包括 Spring Framework spring websocket 模块中的一个。
  • spring-websocket 模块中的 SockJS Java 客户端(自 4.1 版起)。

SockJS 是为在浏览器中使用而设计的。它使用多种技术来支持广泛的浏览器版本。有关 SockJS 传输类型和浏览器的完整列表,请参阅 SockJS 客户端页面。传输分为三大类:WebSocket、HTTP 流和 HTTP 长轮询。

SockJS 客户端首先发送 GET /info 以从服务器获取基本信息。之后,它必须决定使用什么传输。如果可能,使用 WebSocket。如果没有,在大多数浏览器中,至少有一个 HTTP 流选项。如果不是,则使用 HTTP(长)轮询。

所有传输请求都具有以下 URL 结构:

1
https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}

在哪里:

  • {server-id} 用于在集群中路由请求,但不用于其他用途。
  • {session-id} 关联属于 SockJS 会话的 HTTP 请求。
  • {transport} 表示传输类型(例如,websocket、xhr-streaming 等)。

WebSocket 传输只需要一个 HTTP 请求来进行 WebSocket 握手。此后的所有消息都在该socket上交换。

HTTP 传输需要更多请求。例如,Ajax/XHR 流依赖于对服务器到客户端消息的一个长时间运行的请求和对客户端到服务器消息的附加 HTTP POST 请求。长轮询类似,不同之处在于它在每次服务器到客户端发送后结束当前请求。

SockJS 添加了最少的消息框架。例如,服务器最初发送字母 o(“打开”帧),消息作为 [“message1”,”message2”](JSON 编码数组)发送,如果没有消息,则发送字母 h(“心跳”帧)流 25 秒(默认情况下),并使用字母 c(“关闭”帧)关闭会话。

要了解更多信息,请在浏览器中运行示例并观察 HTTP 请求。 SockJS 客户端允许修复传输列表,因此可以一次查看每个传输一个。 SockJS 客户端还提供了一个调试标志,它可以在浏览器控制台中启用有用的消息。在服务器端,您可以为 org.springframework.web.socket 启用 TRACE 日志记录。有关更多详细信息,请参阅 SockJS 协议叙述测试。

启用 SockJS

您可以通过 Java 配置启用 SockJS,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").withSockJS();
}

@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}

}

以下示例显示了与前面示例等效的 XML 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:handlers>
<websocket:mapping path="/myHandler" handler="myHandler"/>
<websocket:sockjs/>
</websocket:handlers>

<bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

前面的示例用于 Spring MVC 应用程序,应包含在 DispatcherServlet 的配置中。但是,Spring 的 WebSocket 和 SockJS 支持不依赖于 Spring MVC。借助 SockJsHttpRequestHandler 集成到其他 HTTP 服务环境中相对简单。

在浏览器端,应用程序可以使用 sockjs-client(版本 1.0.x)。它模拟 W3C WebSocket API 并与服务器通信以选择最佳传输选项,具体取决于它运行的浏览器。请参阅 sockjs-client 页面和浏览器支持的传输类型列表。客户端还提供了几个配置选项 — ,例如,指定要包含哪些传输。

IE 8 and 9

Internet Explorer 8 和 9 仍在使用中。它们是拥有 SockJS 的一个关键原因。本节涵盖有关在这些浏览器中运行的重要注意事项。

SockJS 客户端通过使用 Microsoft 的 XDomainRequest 在 IE 8 和 9 中支持 Ajax/XHR 流。这适用于跨域,但不支持发送 cookie。 Cookie 对于 Java 应用程序通常是必不可少的。但是,由于 SockJS 客户端可以与许多服务器类型(不仅仅是 Java 类型)一起使用,因此它需要知道 cookie 是否重要。如果是这样,SockJS 客户端更喜欢使用 Ajax/XHR 进行流式处理。否则,它依赖于基于 iframe 的技术。

来自 SockJS 客户端的第一个 /info 请求是对可能影响客户端传输选择的信息的请求。这些细节之一是服务器应用程序是否依赖 cookie(例如,出于身份验证目的或使用粘性会话进行集群)。 Spring 的 SockJS 支持包括一个名为 sessionCookieNeeded 的属性。它默认启用,因为大多数 Java 应用程序依赖于 JSESSIONID cookie。如果您的应用程序不需要它,您可以关闭此选项,然后 SockJS 客户端应该在 IE 8 和 9 中选择 xdr-streaming。

如果您确实使用基于 iframe 的传输,请记住,可以通过将 HTTP 响应标头 X-Frame-Options 设置为 DENY、SAMEORIGIN 或 ALLOW-FROM <origin 来指示浏览器阻止在给定页面上使用 IFrame >.这用于防止点击劫持。

Spring Security 3.2+ 支持在每个响应上设置 X-Frame-Options。默认情况下,Spring Security Java 配置将其设置为 DENY。在 3.2 中,Spring Security XML 命名空间默认情况下不会设置该标头,但可以配置为这样做。将来,它可能会默认设置。

有关如何配置 X-Frame-Options 标头设置的详细信息,请参阅 Spring Security 文档的默认安全标头。您还可以查看 SEC-2501 以了解更多背景信息。

如果您的应用程序添加了 X-Frame-Options 响应标头(它应该如此!)并依赖于基于 iframe 的传输,则您需要将标头值设置为 SAMEORIGIN 或 ALLOW-FROM 。 Spring SockJS 支持还需要知道 SockJS 客户端的位置,因为它是从 iframe 加载的。默认情况下,iframe 设置为从 CDN 位置下载 SockJS 客户端。将此选项配置为使用与应用程序同源的 URL 是个好主意。

以下示例显示了如何在 Java 配置中执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS()
.setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");
}

// ...

}

XML 命名空间通过 websocket:sockjs 元素提供了类似的选项。

在初始开发期间,请启用 SockJS 客户端开发模式,以防止浏览器缓存原本会被缓存的 SockJS 请求(如 iframe)。有关如何启用它的详细信息,请参阅 SockJS 客户端页面。

Heartbeats

SockJS 协议要求服务器发送心跳消息以防止代理得出连接挂起的结论。 Spring SockJS 配置有一个名为 heartbeatTime 的属性,您可以使用它来自定义频率。默认情况下,心跳在 25 秒后发送,假设该连接上没有发送其他消息。这个 25 秒的值符合以下 IETF 对公共 Internet 应用程序的建议。

在 WebSocket 和 SockJS 上使用 STOMP 时,如果 STOMP 客户端和服务器协商要交换的心跳,则禁用 SockJS 心跳。

Spring SockJS 支持还允许您配置 TaskScheduler 以安排心跳任务。任务调度程序由线程池支持,默认设置基于可用处理器的数量。您应该考虑根据您的特定需求自定义设置。

客户端断开连接

HTTP 流和 HTTP 长轮询 SockJS 传输要求连接比平时保持打开状态的时间更长。

在 Servlet 容器中,这是通过 Servlet 3 异步支持完成的,该支持允许退出 Servlet 容器线程、处理请求并继续写入来自另一个线程的响应。

一个特定的问题是 Servlet API 不为已消失的客户端提供通知。参见 eclipse-ee4j/servlet-api#44。但是,Servlet 容器在后续尝试写入响应时引发异常。由于 Spring 的 SockJS 服务支持服务器发送的心跳(默认情况下每 25 秒),这意味着通常会在该时间段内检测到客户端断开连接(或更早,如果消息发送更频繁)。

因此,由于客户端断开连接,可能会发生网络 I/O 故障,这可能会用不必要的堆栈跟踪填充日志。 Spring 尽最大努力识别代表客户端断开连接(特定于每个服务器)的此类网络故障,并通过使用专用日志类别 DISCONNECTED_CLIENT_LOG_CATEGORY(在 AbstractSockJsSession 中定义)记录最少的消息。如果您需要查看堆栈跟踪,可以将该日志类别设置为 TRACE。

SockJS 和 CORS

如果您允许跨域请求(请参阅 Allowed Origins),则 SockJS 协议使用 CORS 在 XHR 流和轮询传输中提供跨域支持。因此,除非检测到响应中存在 CORS 标头,否则会自动添加 CORS 标头。因此,如果应用程序已经配置为提供 CORS 支持(例如,通过 Servlet 过滤器),则 Spring 的 SockJsService 会跳过这部分。

也可以通过设置 Spring 的 SockJsService 中的 suppressCors 属性来禁用这些 CORS 标头的添加。

SockJS 需要以下header和值:

  • Access-Control-Allow-Origin: 从 Origin 请求标头的值初始化。
  • Access-Control-Allow-Credentials: 始终设置为真。
  • Access-Control-Request-Headers: 从等效请求标头中的值初始化。
  • Access-Control-Allow-Methods: 传输支持的 HTTP 方法(请参阅 TransportType 枚举)。
  • Access-Control-Max-Age: 设置为 31536000(1 年)。

有关确切的实现,请参阅 AbstractSockJsService 中的 addCorsHeaders 和源代码中的 TransportType 枚举。

或者,如果 CORS 配置允许,请考虑使用 SockJS 端点前缀排除 URL,从而让 Spring 的 SockJsService 处理它。

SockJsClient

Spring 提供了一个 SockJS Java 客户端,无需使用浏览器即可连接到远程 SockJS 端点。当需要在公共网络上的两个服务器之间进行双向通信时(即,网络代理可以阻止使用 WebSocket 协议),这尤其有用。 SockJS Java 客户端对于测试目的也非常有用(例如,模拟大量并发用户)。

SockJS Java 客户端支持 websocket、xhr-streaming 和 xhr-polling 传输。其余的仅在浏览器中使用才有意义。

您可以使用以下命令配置 WebSocketTransport:

  • StandardWebSocketClient 在 JSR-356 运行时中。
  • JettyWebSocketClient 通过使用 Jetty 9+ 原生 WebSocket API。
  • Spring 的 WebSocketClient 的任何实现。

根据定义,XhrTransport 支持 xhr-streaming 和 xhr-polling,因为从客户端的角度来看,除了用于连接到服务器的 URL 之外没有其他区别。目前有两种实现方式:

  • RestTemplateXhrTransport RestTemplateXhrTransport 使用 Spring 的 RestTemplate 来处理 HTTP 请求。
  • JettyXhrTransport 使用 Jetty 的 HttpClient 进行 HTTP 请求。

以下示例显示了如何创建 SockJS 客户端并连接到 SockJS 端点:

1
2
3
4
5
6
List<Transport> transports = new ArrayList<>(2);
transports.add(new WebSocketTransport(new StandardWebSocketClient()));
transports.add(new RestTemplateXhrTransport());

SockJsClient sockJsClient = new SockJsClient(transports);
sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");

SockJS 对消息使用 JSON 格式的数组。默认情况下,使用 Jackson 2 并且需要在类路径上。或者,您可以配置 SockJsMessageCodec 的自定义实现并在 SockJsClient 上进行配置。

要使用 SockJsClient 模拟大量并发用户,您需要配置底层 HTTP 客户端(用于 XHR 传输)以允许足够数量的连接和线程。以下示例显示了如何使用 Jetty 执行此操作:

1
2
3
HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));

以下示例显示了您还应该考虑自定义的服务器端 SockJS 相关属性(有关详细信息,请参阅 javadoc):

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/sockjs").withSockJS()
.setStreamBytesLimit(512 * 1024)
.setHttpMessageCacheSize(1000)
.setDisconnectDelay(30 * 1000);
}

// ...
}
  • 将 streamBytesLimit 属性设置为 512KB(默认为 128KB — 128 * 1024)。
  • 将 httpMessageCacheSize 属性设置为 1,000(默认值为 100)。
  • 将 disconnectDelay 属性设置为 30 属性秒(默认为 5 秒 — 5 * 1000)。

STOMP

WebSocket 协议定义了两种类型的消息(文本和二进制),但它们的内容是未定义的。该协议定义了客户端和服务器协商一个子协议(即更高级别的消息协议)的机制,用于在 WebSocket 之上定义每个可以发送的消息类型,格式是什么,内容是什么。每条消息,等等。子协议的使用是可选的,但无论哪种方式,客户端和服务器都需要就定义消息内容的某些协议达成一致。

概述

STOMP(面向简单文本的消息传递协议)最初是为脚本语言(例如 Ruby、Python 和 Perl)创建的,用于连接到企业消息代理。它旨在解决常用消息传递模式的最小子集。 STOMP 可用于任何可靠的双向流网络协议,例如 TCP 和 WebSocket。尽管 STOMP 是面向文本的协议,但消息有效负载可以是文本或二进制的。

STOMP 是一种基于帧的协议,其帧以 HTTP 为模型。以下清单显示了 STOMP 帧的结构:

1
2
3
4
5
COMMAND
header1:value1
header2:value2

Body^@

客户端可以使用 SEND 或 SUBSCRIBE 命令发送或订阅消息,以及描述消息内容和接收者的目标头。这启用了一个简单的发布-订阅机制,您可以使用该机制通过代理将消息发送到其他连接的客户端,或将消息发送到服务器以请求执行某些工作。

当您使用 Spring 的 STOMP 支持时,Spring WebSocket 应用程序充当客户端的 STOMP 代理。消息被路由到@Controller 消息处理方法或一个简单的内存代理,该代理跟踪订阅并向订阅用户广播消息。您还可以将 Spring 配置为与专用的 STOMP 代理(例如 RabbitMQ、ActiveMQ 等)一起工作,以进行实际的消息广播。在这种情况下,Spring 维护与代理的 TCP 连接,将消息中继到它,并将消息从它向下传递到连接的 WebSocket 客户端。因此,Spring Web 应用程序可以依靠统一的基于 HTTP 的安全性、通用验证和熟悉的编程模型来处理消息。

以下示例显示订阅接收股票报价的客户端,服务器可能会定期发出该报价(例如,通过计划任务通过 SimpMessagingTemplate 向代理发送消息):

1
2
3
4
5
SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*

^@

以下示例显示了发送交易请求的客户端,服务器可以通过 @MessageMapping 方法处理该请求:

1
2
3
4
5
6
SEND
destination:/queue/trade
content-type:application/json
content-length:44

{"action":"BUY","ticker":"MMM","shares",44}^@

执行后,服务器可以向客户端广播交易确认消息和详细信息。

在 STOMP 规范中,目的地的含义故意不透明。它可以是任何字符串,完全由 STOMP 服务器来定义它们支持的目的地的语义和语法。然而,目的地是类似路径的字符串是很常见的,其中 /topic/.. 意味着发布-订阅(一对多)和 /queue/ 意味着点对点(一对一)消息交流。

STOMP 服务器可以使用 MESSAGE 命令向所有订阅者广播消息。以下示例显示服务器向订阅的客户端发送股票报价:

1
2
3
4
5
6
MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM

{"ticker":"MMM","price":129.45}^@

服务器不能发送未经请求的消息。来自服务器的所有消息都必须响应特定的客户端订阅,并且服务器消息的 subscription-id 标头必须与客户端订阅的 id 标头匹配。

前面的概述旨在提供对 STOMP 协议最基本的理解。我们建议全面审查协议规范。

好处

与使用原始 WebSockets 相比,使用 STOMP 作为子协议可以让 Spring Framework 和 Spring Security 提供更丰富的编程模型。对于 HTTP 与原始 TCP 以及它如何让 Spring MVC 和其他 Web 框架提供丰富的功能,可以提出同样的观点。以下是福利清单:

  • 无需发明自定义消息传递协议和消息格式。
  • 可以使用 STOMP 客户端,包括 Spring 框架中的 Java 客户端。
  • 您可以(可选)使用消息代理(例如 RabbitMQ、ActiveMQ 等)来管理订阅和广播消息。
  • 应用程序逻辑可以组织在任意数量的 @Controller 实例中,并且可以根据 STOMP 目标标头将消息路由到它们,而不是使用单个 WebSocketHandler 处理给定连接的原始 WebSocket 消息。
  • 您可以使用 Spring Security 根据 STOMP 目标和消息类型来保护消息。

启用 STOMP

STOMP over WebSocket 支持在 spring-messaging 和 spring-websocket 模块中可用。一旦有了这些依赖项,就可以通过 WebSocket 和 SockJS Fallback 公开 STOMP 端点,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS();
}

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app");
config.enableSimpleBroker("/topic", "/queue");
}
}
  • /portfolio 是 WebSocket(或 SockJS)到的端点的 HTTP URL 客户端需要连接以进行 WebSocket 握手。
  • 目标标头以 /app 开头的 STOMP 消息被路由到 @Controller 类中的 @MessageMapping 方法。
  • 使用内置的消息代理进行订阅和广播以及 将目标标头以 /topic /queue 开头的消息路由到代理。

以下示例显示了与前面示例等效的 XML 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:message-broker application-destination-prefix="/app">
<websocket:stomp-endpoint path="/portfolio">
<websocket:sockjs/>
</websocket:stomp-endpoint>
<websocket:simple-broker prefix="/topic, /queue"/>
</websocket:message-broker>

</beans>

对于内置的简单代理,/topic 和 /queue 前缀没有任何特殊含义。它们只是一种区分发布-订阅与点对点消息传递(即许多订阅者与一个消费者)的约定。当您使用外部代理时,请查看代理的 STOMP 页面以了解其支持的 STOMP 目的地和前缀类型。

要从浏览器连接,对于 SockJS,您可以使用 sockjs-client。对于 STOMP,许多应用程序使用了 jmesnil/stomp-websocket 库(也称为 stomp.js),该库功能完备,已在生产中使用多年但不再维护。目前,JSteunou/webstomp-client 是该库最积极维护和发展的继承者。以下示例代码基于它:

1
2
3
4
5
var socket = new SockJS("/spring-websocket-portfolio/portfolio");
var stompClient = webstomp.over(socket);

stompClient.connect({}, function(frame) {
}

或者,如果您通过 WebSocket(不带 SockJS)连接,则可以使用以下代码:

1
2
3
4
5
var socket = new WebSocket("/spring-websocket-portfolio/portfolio");
var stompClient = Stomp.over(socket);

stompClient.connect({}, function(frame) {
}

请注意,前面示例中的 stompClient 不需要指定登录和密码标头。即使这样做了,它们也会在服务器端被忽略(或者更确切地说,被覆盖)。有关身份验证的更多信息,请参阅连接到代理和身份验证。

WebSocket服务

要配置底层 WebSocket 服务器,服务器配置中的信息适用。但是,对于 Jetty,您需要通过 StompEndpointRegistry 设置 HandshakeHandler 和 WebSocketPolicy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").setHandshakeHandler(handshakeHandler());
}

@Bean
public DefaultHandshakeHandler handshakeHandler() {

WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
policy.setInputBufferSize(8192);
policy.setIdleTimeout(600000);

return new DefaultHandshakeHandler(
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
}
}

消息流

一旦暴露了 STOMP 端点,Spring 应用程序就成为连接客户端的 STOMP 代理。本节介绍服务器端的消息流向。

spring-messaging 模块包含对源自 Spring Integration 的消息传递应用程序的基础支持,后来被提取并合并到 Spring 框架中,以便在许多 Spring 项目和应用程序场景中更广泛地使用。以下列表简要描述了一些可用的消息传递抽象:

Java 配置(即@EnableWebSocketMessageBroker)和XML 命名空间配置(即websocket:message-broker)都使用前面的组件来组装消息工作流。下图显示了启用简单内置消息代理时使用的组件:

message flow simple broker

上图显示了三个消息通道:

  • clientInboundChannel: 用于传递从 WebSocket 客户端接收的消息。
  • clientOutboundChannel: 用于向 WebSocket 客户端发送服务器消息。
  • brokerChannel: 用于从服务器端应用程序代码中向消息代理发送消息。

下图显示了在配置外部代理(例如 RabbitMQ)以管理订阅和广播消息时使用的组件:

message flow broker relay

前面两个图之间的主要区别是使用“代理中继”通过 TCP 将消息向上传递到外部 STOMP 代理,以及将消息从代理向下传递到订阅的客户端。

当从 WebSocket 连接接收到消息时,它们被解码为 STOMP 帧,转换为 Spring Message 表示,并发送到 clientInboundChannel 进行进一步处理。例如,目标标头以 /app 开头的 STOMP 消息可以路由到带注释的控制器中的 @MessageMapping 方法,而 /topic 和 /queue 消息可以直接路由到消息代理。

处理来自客户端的 STOMP 消息的带注释的 @Controller 可以通过 brokerChannel 向消息代理发送消息,并且代理通过 clientOutboundChannel 将消息广播给匹配的订阅者。同一个控制器也可以响应HTTP请求做同样的事情,所以客户端可以执行HTTP POST,然后@PostMapping方法可以向消息代理发送消息广播给订阅的客户端。

我们可以通过一个简单的例子来追踪流程。考虑以下示例,它设置了一个服务器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio");
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic");
}
}

@Controller
public class GreetingController {

@MessageMapping("/greeting")
public String handle(String greeting) {
return "[" + getTimestamp() + ": " + greeting;
}
}

前面的示例支持以下流程:

  1. 客户端连接到 http://localhost:8080/portfolio,一旦建立了 WebSocket 连接,STOMP 帧就开始在其上流动。
  2. 客户端发送一个带有 /topic/greeting 目标头的 SUBSCRIBE 帧。收到并解码后,消息将发送到 clientInboundChannel,然后路由到存储客户端订阅的消息代理。
  3. 客户端向 /app/greeting 发送 SEND 帧。 /app 前缀有助于将其路由到带注释的控制器。去掉 /app 前缀后,目的地的剩余 /greeting 部分映射到 GreetingController 中的 @MessageMapping 方法。
  4. 从 GreetingController 返回的值根据返回值和 /topic/greeting 的默认目标标头(从 /app 替换为 /topic 的输入目标派生)转换为带有有效负载的 Spring 消息。结果消息被发送到 brokerChannel 并由消息代理处理。
  5. 消息代理找到所有匹配的订阅者并通过 clientOutboundChannel 向每个订阅者发送一个 MESSAGE 帧,从那里消息被编码为 STOMP 帧并通过 WebSocket 连接发送。

带注释的Controllers

应用程序可以使用带注释的 @Controller 类来处理来自客户端的消息。此类类可以声明 @MessageMapping、@SubscribeMapping 和 @ExceptionHandler 方法,如以下主题中所述:

@MessageMapping

您可以使用 @MessageMapping 注释根据目的地路由消息的方法。它在方法级别和类型级别都受支持。在类型级别,@MessageMapping 用于表示控制器中所有方法之间的共享映射。

默认情况下,映射值是 Ant 样式的路径模式(例如 /thing*、/thing/**),包括对模板变量的支持(例如 /thing/{id})。可以通过@DestinationVariable 方法参数引用这些值。应用程序还可以切换到以点分隔的目标约定进行映射,如点作为分隔符中所述。

支持的方法参数

下表描述了方法参数:

方法参数 描述
Message 用于访问完整的消息。
MessageHeaders 用于访问消息中的标题。
MessageHeaderAccessor, SimpMessageHeaderAccessor, and StompHeaderAccessor 用于通过类型化访问器方法访问标头。
@Payload 用于访问消息的有效负载,由配置的 MessageConverter 转换(例如,从 JSON)。 不需要此注释的存在,因为默认情况下,如果没有其他参数匹配,则假定它存在。 您可以使用 @javax.validation.Valid 或 Spring 的 @Validated 注释负载参数,以自动验证负载参数。
@Header 用于访问特定标头值 — 以及使用 org.springframework.core.convert.converter.Converter 进行类型转换(如有必要)。
@Headers 用于访问消息中的所有标头。此参数必须可分配给 java.util.Map。
@DestinationVariable 用于访问从消息目标中提取的模板变量。根据需要将值转换为声明的方法参数类型。
java.security.Principal 反映在 WebSocket HTTP 握手时登录的用户。

返回值

默认情况下,@MessageMapping 方法的返回值通过匹配的 MessageConverter 序列化为有效负载,并作为消息发送到 brokerChannel,从那里广播给订阅者。出站消息的目的地与入站消息的目的地相同,但以 /topic 为前缀。

您可以使用 @SendTo 和 @SendToUser 注释来自定义输出消息的目的地。 @SendTo 用于自定义目标目的地或指定多个目的地。 @SendToUser 用于将输出消息定向到与输入消息关联的用户。

您可以在同一方法上同时使用 @SendTo 和 @SendToUser,并且在类级别都支持两者,在这种情况下,它们充当类中方法的默认值。但是,请记住,任何方法级别的 @SendTo 或 @SendToUser 注释都会覆盖类级别的任何此类注释。

消息可以异步处理,@MessageMapping 方法可以返回 ListenableFuture、CompletableFuture 或 CompletionStage。

请注意,@SendTo 和@SendToUser 只是一种便利,相当于使用 SimpMessagingTemplate 发送消息。如有必要,对于更高级的场景,@MessageMapping 方法可以直接使用 SimpMessagingTemplate。这可以代替返回值来完成,或者除了返回值之外。请参阅发送消息。

@SubscribeMapping

@SubscribeMapping 与@MessageMapping 类似,但将映射范围缩小到仅订阅消息。它支持与@MessageMapping 相同的方法参数。但是对于返回值,默认情况下,消息直接发送到客户端(通过 clientOutboundChannel,响应订阅)而不是发送到代理(通过 brokerChannel,作为匹配订阅的广播)。添加 @SendTo 或 @SendToUser 会覆盖此行为并改为发送到代理。

这什么时候有用?假设代理映射到 /topic 和 /queue,而应用程序控制器映射到 /app。在此设置中,代理存储所有用于重复广播的 /topic 和 /queue 订阅,应用程序无需参与。客户端还可以订阅某个 /app 目的地,并且控制器可以返回一个值来响应该订阅,而无需代理再次存储或使用订阅(实际上是一次请求-回复交换)。一个用例是在启动时使用初始数据填充 UI。

这什么时候没用?不要尝试将代理和控制器映射到相同的目标前缀,除非您出于某种原因希望两者独立处理消息,包括订阅。入站消息是并行处理的。不能保证是代理还是控制器首先处理给定的消息。如果目标是在订阅存储并准备好广播时收到通知,如果服务器支持,客户端应该要求收据(简单代理不支持)。例如,使用 Java STOMP 客户端,您可以执行以下操作来添加收据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Autowired
private TaskScheduler messageBrokerTaskScheduler;

// During initialization..
stompClient.setTaskScheduler(this.messageBrokerTaskScheduler);

// When subscribing..
StompHeaders headers = new StompHeaders();
headers.setDestination("/topic/...");
headers.setReceipt("r1");
FrameHandler handler = ...;
stompSession.subscribe(headers, handler).addReceiptTask(() -> {
// Subscription ready...
});

服务器端选项是在 brokerChannel 上注册一个 ExecutorChannelInterceptor 并实现 afterMessageHandled 方法,该方法在处理消息(包括订阅)后调用。

@MessageExceptionHandler

应用程序可以使用@MessageExceptionHandler 方法来处理来自@MessageMapping 方法的异常。如果您想访问异常实例,您可以在注释本身或通过方法参数声明异常。以下示例通过方法参数声明异常:

1
2
3
4
5
6
7
8
9
10
11
@Controller
public class MyController {

// ...

@MessageExceptionHandler
public ApplicationError handleException(MyException exception) {
// ...
return appError;
}
}

@MessageExceptionHandler 方法支持灵活的方法签名,并支持与@MessageMapping 方法相同的方法参数类型和返回值。

通常,@MessageExceptionHandler 方法应用于声明它们的 @Controller 类(或类层次结构)中。如果您希望这些方法在全局范围内(跨控制器)应用,您可以在标有 @ControllerAdvice 的类中声明它们。这与 Spring MVC 中可用的类似支持相当。

发送消息

如果您想从应用程序的任何部分向连接的客户端发送消息怎么办?任何应用程序组件都可以向 brokerChannel 发送消息。最简单的方法是注入一个 SimpMessagingTemplate 并使用它来发送消息。通常,您会按类型注入它,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
public class GreetingController {

private SimpMessagingTemplate template;

@Autowired
public GreetingController(SimpMessagingTemplate template) {
this.template = template;
}

@RequestMapping(path="/greetings", method=POST)
public void greet(String greeting) {
String text = "[" + getTimestamp() + "]:" + greeting;
this.template.convertAndSend("/topic/greetings", text);
}

}

但是,如果存在另一个相同类型的 bean,您也可以通过其名称 (brokerMessagingTemplate) 对其进行限定。

Simple Broker

内置的简单消息代理处理来自客户端的订阅请求,将它们存储在内存中,并将消息广播到具有匹配目的地的连接客户端。代理支持类似路径的目的地,包括订阅 Ant 风格的目的地模式。

应用程序还可以使用点分隔(而不是斜线分隔)目标。将点视为分隔符。

如果配置了任务调度程序,则简单代理支持 STOMP 心跳。为此,您可以声明自己的调度程序或使用自动声明并在内部使用的调度程序。以下示例显示了如何声明您自己的调度程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

private TaskScheduler messageBrokerTaskScheduler;

@Autowired
public void setMessageBrokerTaskScheduler(TaskScheduler taskScheduler) {
this.messageBrokerTaskScheduler = taskScheduler;
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {

registry.enableSimpleBroker("/queue/", "/topic/")
.setHeartbeatValue(new long[] {10000, 20000})
.setTaskScheduler(this.messageBrokerTaskScheduler);

// ...
}
}

External Broker

simple broker 非常适合入门,但仅支持 STOMP 命令的一个子集(它不支持 acks、receipts 和一些其他功能),依赖于简单的消息发送循环,并且不适合集群。作为替代方案,您可以升级您的应用程序以使用功能齐全的消息代理。

请参阅您选择的消息代理(例如 RabbitMQ、ActiveMQ 等)的 STOMP 文档,安装代理,并在启用 STOMP 支持的情况下运行它。然后你可以在 Spring 配置中启用 STOMP 代理中继(而不是简单的代理)。

以下示例配置启用全功能代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS();
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}

}

以下示例显示了与前面示例等效的 XML 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket
https://www.springframework.org/schema/websocket/spring-websocket.xsd">

<websocket:message-broker application-destination-prefix="/app">
<websocket:stomp-endpoint path="/portfolio" />
<websocket:sockjs/>
</websocket:stomp-endpoint>
<websocket:stomp-broker-relay prefix="/topic,/queue" />
</websocket:message-broker>

</beans>

前面配置中的 STOMP 代理中继是一个 Spring MessageHandler,它通过将消息转发到外部消息代理来处理消息。为此,它会与代理建立 TCP 连接,将所有消息转发给它,然后通过客户端的 WebSocket 会话将从代理收到的所有消息转发给客户端。本质上,它充当双向转发消息的“中继”。

将 io.projectreactor.netty:reactor-netty 和 io.netty:netty-all 依赖项添加到您的项目中以进行 TCP 连接管理。

此外,应用程序组件(例如 HTTP 请求处理方法、业务服务等)还可以将消息发送到代理中继,如发送消息中所述,以向订阅的 WebSocket 客户端广播消息。

实际上,代理中继实现了强大且可扩展的消息广播。

Spring Cloud Gateway基本使用

本项目提供了一个构建在 Spring 生态系统之上的 API 网关,包括:Spring 5、Spring Boot 2 和 Project Reactor。 Spring Cloud Gateway 旨在提供一种简单而有效的方式来路由到 API 并为它们提供交叉关注点,例如:安全性、监控/指标和弹性。

1.如何集成Spring Cloud Gateway

要将 Spring Cloud Gateway 包含在您的项目中,请使用具有 org.springframework.cloud 的组 ID 和 spring-cloud-starter-gateway 的工件 ID 的 starter。有关使用当前 Spring Cloud Release Train 设置构建系统的详细信息,请参阅 Spring Cloud 项目页面。

如果包含启动器,但不希望启用网关,请设置 spring.cloud.gateway.enabled=false。

Spring Cloud Gateway 基于 Spring Boot 2.x、Spring WebFlux 和 Project Reactor 构建。因此,当您使用 Spring Cloud Gateway 时,您所知道的许多熟悉的同步库(例如 Spring Data 和 Spring Security)和模式可能不适用。如果您不熟悉这些项目,我们建议您在使用 Spring Cloud Gateway 之前先阅读他们的文档以熟悉一些新概念。

Spring Cloud Gateway 需要 Spring Boot 和 Spring Webflux 提供的 Netty 运行时。它不适用于传统的 Servlet 容器或构建为 WAR 时。

2. 词汇表

  • Route: 网关的基本构建块。它由 ID、目标 URI、谓词集合和过滤器集合定义。如果聚合谓词为真,则匹配路由。
  • Predicate: 这是一个 Java 8 函数谓词。输入类型是 Spring Framework ServerWebExchange。这使您可以匹配来自 HTTP 请求的任何内容,例如标头或参数。
  • Filter: 这些是使用特定工厂构建的 GatewayFilter 实例。在这里,您可以在发送下游请求之前或之后修改请求和响应。

3. 工作原理

下图提供了 Spring Cloud Gateway 工作原理的高级概述:

Spring Cloud Gateway Diagram

客户端向 Spring Cloud Gateway 发出请求。如果网关处理程序映射确定请求与路由匹配,则将其发送到网关 Web 处理程序。此处理程序通过特定于请求的过滤器链运行请求。过滤器被虚线分隔的原因是过滤器可以在发送代理请求之前和之后运行逻辑。执行所有“预”过滤器逻辑。然后进行代理请求。发出代理请求后,将运行“post”过滤器逻辑。

在没有端口的路由中定义的 URI 分别获得 HTTP 和 HTTPS URI 的默认端口值 80 和 443。

4. 配置路由谓词工厂和网关过滤工厂

有两种方法可以配置谓词和过滤器:快捷方式和完全扩展的参数。下面的大多数示例都使用快捷方式。

名称和参数名称将在每个部分的第一句或第二句中作为代码列出。参数通常按快捷方式配置所需的顺序列出。

4.1.快捷方式配置

快捷方式配置由过滤器名称识别,后跟等号 (=),后跟由逗号 (,) 分隔的参数值。

application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- Cookie=mycookie,mycookievalue

前面的示例使用两个参数定义了 Cookie 路由谓词工厂,即 cookie 名称、mycookie 和匹配 mycookievalue 的值。

4.2.完全展开的参数

完全扩展的参数看起来更像是带有名称/值对的标准 yaml 配置。通常,会有一个 name 键和一个 args 键。 args 键是键值对的映射,用于配置谓词或过滤器。

application.yml

1
2
3
4
5
6
7
8
9
10
11
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- name: Cookie
args:
name: mycookie
regexp: mycookievalue

这就是上面显示的 Cookie 谓词的快捷配置的完整配置。

5. 路由谓词工厂

Spring Cloud Gateway 匹配路由作为 Spring WebFlux HandlerMapping 基础结构的一部分。 Spring Cloud Gateway 包含许多内置的路由谓词工厂。所有这些谓词都匹配 HTTP 请求的不同属性。您可以将多个路由谓词工厂与逻辑和语句组合在一起。

5.1.after路由谓词工厂

After 路由谓词工厂接受一个参数,一个日期时间(这是一个 java ZonedDateTime)。此谓词匹配在指定日期时间之后发生的请求。以下示例配置了一个 after 路由谓词:

Example 1. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]

此路由匹配Jan 20, 2017 17:42 Mountain Time (Denver)之后提出的任何请求。

5.2.before路由谓词工厂

Before 路由谓词工厂接受一个参数,一个日期时间(它是一个 java ZonedDateTime)。此谓词匹配在指定日期时间之前发生的请求。以下示例配置了一个 before 路由谓词:

Example 2. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: before_route
uri: https://example.org
predicates:
- Before=2017-01-20T17:42:47.789-07:00[America/Denver]

此路由匹配 Jan 20, 2017 17:42 Mountain Time (Denver)之前提出的任何请求。

5.3.between路由谓词工厂

路由谓词工厂之间有两个参数,datetime1 和 datetime2,它们是 java ZonedDateTime 对象。此谓词匹配发生在 datetime1 之后和 datetime2 之前的请求。 datetime2 参数必须在 datetime1 之后。以下示例配置了一个 between 路由谓词:

Example 3. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: between_route
uri: https://example.org
predicates:
- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]

此路由匹配 2017-01-20T17:42:47.789-07:00[America/Denver]之后和 2017-01-21T17:42:47.789-07:00[America/Denver]之前提出的任何请求。这对于维护窗口可能很有用。

Cookie 路由谓词工厂有两个参数,即 cookie 名称和一个 regexp(这是一个 Java 正则表达式)。此谓词匹配具有给定名称且其值与正则表达式匹配的 cookie。以下示例配置 cookie 路由谓词工厂:

Example 4. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: https://example.org
predicates:
- Cookie=chocolate, ch.p

此路由匹配具有名为 Chocolate 的 cookie 的请求,该 cookie 的值与 ch.p 正则表达式匹配。

5.5.header路由谓词工厂

Header 路由谓词工厂接受两个参数,标题名称和一个 regexp(这是一个 Java 正则表达式)。此谓词与具有给定名称的标头匹配,其值与正则表达式匹配。以下示例配置标头路由谓词:

Example 5. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: header_route
uri: https://example.org
predicates:
- Header=X-Request-Id, \d+

如果请求具有名为 X-Request-Id 的标头,其值与 \d+ 正则表达式匹配(即,它具有一个或多个数字的值),则此路由匹配。

5.6.host路由谓词工厂

Host路由谓词工厂采用一个参数:host名模式列表。该模式是 Ant 风格的模式,带有 .作为分隔符。此谓词匹配与模式匹配的 Host 标头。以下示例配置host路由谓词:

Example 6. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: host_route
uri: https://example.org
predicates:
- Host=**.somehost.org,**.anotherhost.org

还支持 URI 模板变量(例如 {sub}.myhost.org)。

如果请求具有值为 www.somehost.org 或 beta.somehost.org 或 www.anotherhost.org 的 Host 标头,则此路由匹配。

此谓词提取 URI 模板变量(例如,在前面的示例中定义的 sub)作为名称和值的映射,并将其放置在 ServerWebExchange.getAttributes() 中,并使用在 ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE 中定义的键。然后这些值可供 GatewayFilter 工厂使用

5.7.method路由谓词工厂

方法路由谓词工厂采用一个方法参数,它是一个或多个参数:要匹配的 HTTP 方法。以下示例配置方法路由谓词:

Example 7. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: method_route
uri: https://example.org
predicates:
- Method=GET,POST

如果请求方法是 GET 或 POST,则此路由匹配。

5.8.path路由谓词工厂

Path Route Predicate Factory 接受两个参数:一个 Spring PathMatcher 模式列表和一个名为 matchTrailingSlash 的可选标志(默认为 true)。以下示例配置路径路由谓词:

Example 8. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: path_route
uri: https://example.org
predicates:
- Path=/red/{segment},/blue/{segment}

如果请求路径是例如:/red/1 或 /red/1/ 或 /red/blue 或 /blue/green,则此路由匹配。

如果 matchTrailingSlash 设置为 false,则不会匹配请求路径 /red/1/。

此谓词提取 URI 模板变量(例如在前面的示例中定义的段)作为名称和值的映射,并将其放置在 ServerWebExchange.getAttributes() 中,键是在 ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE 中定义的。然后这些值可供 GatewayFilter 工厂使用 可以使用实用方法(称为 get)来更轻松地访问这些变量。以下示例显示了如何使用 get 方法:

1
2
3
Map<String, String> uriVariables = ServerWebExchangeUtils.getPathPredicateVariables(exchange);

String segment = uriVariables.get("segment");

5.9.query路由谓词工厂

Query路由谓词工厂有两个参数:一个必需的参数和一个可选的正则表达式(它是一个 Java 正则表达式)。以下示例配置查询路由谓词:

Example 9. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: query_route
uri: https://example.org
predicates:
- Query=green

如果请求包含绿色查询参数,则前面的路由匹配。

application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: query_route
uri: https://example.org
predicates:
- Query=red, gree.

如果请求包含值与 gree 匹配的红色查询参数,则前面的路由匹配。 regexp,所以 green 和 greet 会匹配。

5.10. RemoteAddr 路由谓词工厂

RemoteAddr 路由谓词工厂采用源列表(最小大小 1),这些源是 CIDR 表示法(IPv4 或 IPv6)字符串,例如 192.168.0.1/16(其中 192.168.0.1 是 IP 地址,16 是子网掩码)。以下示例配置 RemoteAddr 路由谓词:

Example 10. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: remoteaddr_route
uri: https://example.org
predicates:
- RemoteAddr=192.168.1.1/24

如果请求的远程地址是例如 192.168.1.10,则此路由匹配。

5.11.Weight权重路由谓词工厂

Weight 路由谓词工厂采用两个参数:group 和 weight(一个 int)。权重按组计算。以下示例配置权重路由谓词:

Example 11. application.yml

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cloud:
gateway:
routes:
- id: weight_high
uri: https://weighthigh.org
predicates:
- Weight=group1, 8
- id: weight_low
uri: https://weightlow.org
predicates:
- Weight=group1, 2

该路由会将约 80% 的流量转发到 weighthigh.org,将约 20% 的流量转发到 weightlow.org

5.11.1.修改远程地址的解析方式

默认情况下,RemoteAddr 路由谓词工厂使用来自传入请求的远程地址。如果 Spring Cloud Gateway 位于代理层之后,这可能与实际客户端 IP 地址不匹配。

您可以通过设置自定义 RemoteAddressResolver 来自定义解析远程地址的方式。 Spring Cloud Gateway 带有一个基于 X-Forwarded-For 标头 XForwardedRemoteAddressResolver 的非默认远程地址解析器。

XForwardedRemoteAddressResolver 有两个静态构造函数方法,它们采取不同的安全方法:

  • XForwardedRemoteAddressResolver::trustAll 返回一个 RemoteAddressResolver,它总是采用在 X-Forwarded-For 标头中找到的第一个 IP 地址。这种方法容易受到欺骗,因为恶意客户端可以为 X-Forwarded-For 设置一个初始值,该值将被解析器接受。
  • XForwardedRemoteAddressResolver::maxTrustedIndex 采用一个索引,该索引与运行在 Spring Cloud Gateway 前面的受信任基础设施的数量相关。例如,如果 Spring Cloud Gateway 只能通过 HAProxy 访问,则应使用值 1。如果在访问 Spring Cloud Gateway 之前需要两跳可信基础设施,则应使用值 2。

考虑以下标头值:

1
X-Forwarded-For: 0.0.0.1, 0.0.0.2, 0.0.0.3

以下 maxTrustedIndex 值产生以下远程地址:

maxTrustedIndex 结果
[Integer.MIN_VALUE,0] (无效,初始化期间 IllegalArgumentException)
1 0.0.0.3
2 0.0.0.2
3 0.0.0.1
[4, Integer.MAX_VALUE] 0.0.0.1

以下示例显示了如何使用 Java 实现相同的配置:

Example 12. GatewayConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
RemoteAddressResolver resolver = XForwardedRemoteAddressResolver
.maxTrustedIndex(1);

...

.route("direct-route",
r -> r.remoteAddr("10.1.1.1", "10.10.1.1/24")
.uri("https://downstream1")
.route("proxied-route",
r -> r.remoteAddr(resolver, "10.10.1.1", "10.10.1.1/24")
.uri("https://downstream2")
)

6. GatewayFilter 工厂

路由过滤器允许以某种方式修改传入的 HTTP 请求或传出的 HTTP 响应。路由过滤器的范围是特定的路由。 Spring Cloud Gateway 包含许多内置的 GatewayFilter 工厂。

6.1. AddRequestHeader 网关过滤器工厂

AddRequestHeader GatewayFilter 工厂采用名称和值参数。以下示例配置 AddRequestHeader GatewayFilter:

Example 13. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestHeader=X-Request-red, blue

此清单将 X-Request-red:blue 标头添加到所有匹配请求的下游请求标头中。

AddRequestHeader 知道用于匹配路径或主机的 URI 变量。 URI 变量可以在值中使用并在运行时扩展。以下示例配置使用变量的 AddRequestHeader GatewayFilter:

Example 14. application.yml

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
predicates:
- Path=/red/{segment}
filters:
- AddRequestHeader=X-Request-Red, Blue-{segment}

6.2. AddRequestParameter 网关过滤器工厂

AddRequestParameter GatewayFilter Factory 采用名称和值参数。以下示例配置 AddRequestParameter GatewayFilter:

Example 15. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
filters:
- AddRequestParameter=red, blue

这会将 red=blue 添加到所有匹配请求的下游请求的查询字符串中。

AddRequestParameter 知道用于匹配路径或主机的 URI 变量。 URI 变量可以在值中使用并在运行时扩展。以下示例配置使用变量的 AddRequestParameter GatewayFilter:

Example 16. application.yml

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
predicates:
- Host: {segment}.myhost.org
filters:
- AddRequestParameter=foo, bar-{segment}

6.3. AddResponseHeader 网关过滤器工厂

AddResponseHeader GatewayFilter Factory 采用名称和值参数。以下示例配置 AddResponseHeader GatewayFilter:

Example 17. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: add_response_header_route
uri: https://example.org
filters:
- AddResponseHeader=X-Response-Red, Blue

这会将 X-Response-Foo:Bar 标头添加到所有匹配请求的下游响应标头中。

AddResponseHeader 知道用于匹配路径或主机的 URI 变量。 URI 变量可以在值中使用并在运行时扩展。以下示例配置使用变量的 AddResponseHeader GatewayFilter:

Example 18. application.yml

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: add_response_header_route
uri: https://example.org
predicates:
- Host: {segment}.myhost.org
filters:
- AddResponseHeader=foo, bar-{segment}

6.4. DedupeResponseHeader 网关过滤器工厂

DedupeResponseHeader GatewayFilter 工厂采用名称参数和可选的策略参数。 name 可以包含以空格分隔的标题名称列表。以下示例配置 DedupeResponseHeader GatewayFilter:

Example 19. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: dedupe_response_header_route
uri: https://example.org
filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin

在网关 CORS 逻辑和下游逻辑都添加它们的情况下,这会删除 Access-Control-Allow-Credentials 和 Access-Control-Allow-Origin 响应标头的重复值。

DedupeResponseHeader 过滤器还接受一个可选的策略参数。接受的值为 RETAIN_FIRST(默认)、RETAIN_LAST 和 RETAIN_UNIQUE。

6.5. Spring Cloud 断路器网关过滤器工厂

Spring Cloud CircuitBreaker GatewayFilter 工厂使用 Spring Cloud CircuitBreaker API 将网关路由包装在断路器中。 Spring Cloud CircuitBreaker 支持多个可与 Spring Cloud Gateway 一起使用的库。 Spring Cloud 支持开箱即用的 Resilience4J。

要启用 Spring Cloud CircuitBreaker 过滤器,您需要将 spring-cloud-starter-circuitbreaker-reactor-resilience4j 放在类路径上。以下示例配置 Spring Cloud CircuitBreaker GatewayFilter:

Example 20. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: circuitbreaker_route
uri: https://example.org
filters:
- CircuitBreaker=myCircuitBreaker

要配置断路器,请参阅您正在使用的底层断路器实现的配置。

Spring Cloud CircuitBreaker 过滤器还可以接受可选的 fallbackUri 参数。目前,只支持转发:schemed URIs。如果调用回退,则请求将转发到与 URI 匹配的控制器。以下示例配置了这样的回退:

Example 21. application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
gateway:
routes:
- id: circuitbreaker_route
uri: lb://backing-service:8088
predicates:
- Path=/consumingServiceEndpoint
filters:
- name: CircuitBreaker
args:
name: myCircuitBreaker
fallbackUri: forward:/inCaseOfFailureUseThis
- RewritePath=/consumingServiceEndpoint, /backingServiceEndpoint

下面的清单在 Java 中做了同样的事情:

Example 22. Application.java

1
2
3
4
5
6
7
8
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("circuitbreaker_route", r -> r.path("/consumingServiceEndpoint")
.filters(f -> f.circuitBreaker(c -> c.name("myCircuitBreaker").fallbackUri("forward:/inCaseOfFailureUseThis"))
.rewritePath("/consumingServiceEndpoint", "/backingServiceEndpoint")).uri("lb://backing-service:8088")
.build();
}

当调用断路器回退时,此示例转发到 /inCaseofFailureUseThis URI。请注意,此示例还演示了(可选)Spring Cloud LoadBalancer 负载平衡(由目标 URI 上的 lb 前缀定义)。

主要场景是使用 fallbackUri 在网关应用程序中定义内部控制器或处理程序。但是,您也可以将请求重新路由到外部应用程序中的控制器或处理程序,如下所示:

Example 23. application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring:
cloud:
gateway:
routes:
- id: ingredients
uri: lb://ingredients
predicates:
- Path=//ingredients/**
filters:
- name: CircuitBreaker
args:
name: fetchIngredients
fallbackUri: forward:/fallback
- id: ingredients-fallback
uri: http://localhost:9994
predicates:
- Path=/fallback

在此示例中,网关应用程序中没有回退端点或处理程序。但是,在另一个应用程序中有一个,在 localhost:9994 下注册。

如果请求被转发到回退,Spring Cloud CircuitBreaker Gateway 过滤器还提供导致它的 Throwable。它作为 ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR 属性添加到 ServerWebExchange 中,可在网关应用程序中处理回退时使用。

对于外部控制器/处理程序场景,可以添加带有异常详细信息的标头。您可以在 FallbackHeaders GatewayFilter Factory 部分找到有关这样做的更多信息。

6.5.1.根据状态代码使断路器脱扣

在某些情况下,您可能希望根据从其环绕的路由返回的状态代码来触发断路器。断路器配置对象采用状态代码列表,如果返回这些状态代码,将导致断路器跳闸。在设置要使断路器跳闸的状态代码时,您可以使用带有状态代码值的整数或 HttpStatus 枚举的字符串表示形式。

Example 24. application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spring:
cloud:
gateway:
routes:
- id: circuitbreaker_route
uri: lb://backing-service:8088
predicates:
- Path=/consumingServiceEndpoint
filters:
- name: CircuitBreaker
args:
name: myCircuitBreaker
fallbackUri: forward:/inCaseOfFailureUseThis
statusCodes:
- 500
- "NOT_FOUND"

Example 25. Application.java

1
2
3
4
5
6
7
8
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("circuitbreaker_route", r -> r.path("/consumingServiceEndpoint")
.filters(f -> f.circuitBreaker(c -> c.name("myCircuitBreaker").fallbackUri("forward:/inCaseOfFailureUseThis").addStatusCode("INTERNAL_SERVER_ERROR"))
.rewritePath("/consumingServiceEndpoint", "/backingServiceEndpoint")).uri("lb://backing-service:8088")
.build();
}

6.6. FallbackHeaders 网关过滤器工厂

FallbackHeaders 工厂允许您在转发到外部应用程序中的 fallbackUri 的请求的标头中添加 Spring Cloud CircuitBreaker 执行异常详细信息,如下面的场景:

Example 26. application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
cloud:
gateway:
routes:
- id: ingredients
uri: lb://ingredients
predicates:
- Path=//ingredients/**
filters:
- name: CircuitBreaker
args:
name: fetchIngredients
fallbackUri: forward:/fallback
- id: ingredients-fallback
uri: http://localhost:9994
predicates:
- Path=/fallback
filters:
- name: FallbackHeaders
args:
executionExceptionTypeHeaderName: Test-Header

6.6. FallbackHeaders 网关过滤器工厂

FallbackHeaders 工厂允许您在转发到外部应用程序中的 fallbackUri 的请求的标头中添加 Spring Cloud CircuitBreaker 执行异常详细信息,如下面的场景:

Example 26. application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
cloud:
gateway:
routes:
- id: ingredients
uri: lb://ingredients
predicates:
- Path=//ingredients/**
filters:
- name: CircuitBreaker
args:
name: fetchIngredients
fallbackUri: forward:/fallback
- id: ingredients-fallback
uri: http://localhost:9994
predicates:
- Path=/fallback
filters:
- name: FallbackHeaders
args:
executionExceptionTypeHeaderName: Test-Header

在此示例中,在运行断路器时发生执行异常后,请求将转发到在 localhost:9994 上运行的应用程序中的回退端点或处理程序。 FallbackHeaders 过滤器将带有异常类型、消息和(如果可用)根本原因异常类型和消息的标头添加到该请求中。

您可以通过设置以下参数的值(显示为它们的默认值)来覆盖配置中标头的名称:

  • executionExceptionTypeHeaderName ("Execution-Exception-Type")
  • executionExceptionMessageHeaderName ("Execution-Exception-Message")
  • rootCauseExceptionTypeHeaderName ("Root-Cause-Exception-Type")
  • rootCauseExceptionMessageHeaderName ("Root-Cause-Exception-Message")

有关断路器和网关的更多信息,请参阅 Spring Cloud CircuitBreaker Factory 部分。

6.7. MapRequestHeader 网关过滤器工厂

MapRequestHeader GatewayFilter 工厂采用 fromHeader 和 toHeader 参数。它创建一个新的命名标头 (toHeader),并从传入的 http 请求中从现有命名标头 (fromHeader) 中提取该值。如果输入标头不存在,则过滤器没有影响。如果新命名的标头已存在,则其值将使用新值进行扩充。以下示例配置 MapRequestHeader:

Example 27. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: map_request_header_route
uri: https://example.org
filters:
- MapRequestHeader=Blue, X-Request-Red

这会将 X-Request-Red: 标头添加到下游请求中,并使用来自传入 HTTP 请求的 Blue 标头的更新值。

6.8. PrefixPath 网关过滤器工厂

PrefixPath GatewayFilter 工厂采用单个前缀参数。以下示例配置 PrefixPath GatewayFilter:

Example 28. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: https://example.org
filters:
- PrefixPath=/mypath

这会将 /mypath 前缀为所有匹配请求的路径。因此,对 /hello 的请求将被发送到 /mypath/hello。

6.9. PreserveHostHeader 网关过滤器工厂

PreserveHostHeader GatewayFilter 工厂没有参数。此过滤器设置路由过滤器检查的请求属性,以确定是否应发送原始主机标头,而不是由 HTTP 客户端确定的主机标头。以下示例配置 PreserveHostHeader GatewayFilter:

Example 29. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: preserve_host_route
uri: https://example.org
filters:
- PreserveHostHeader

6.10. RequestRateLimiter 网关过滤器工厂

RequestRateLimiter GatewayFilter 工厂使用 RateLimiter 实现来确定是否允许继续处理当前请求。如果不是,则返回 HTTP 429 - Too Many Requests(默认情况下)状态。

此过滤器采用可选的 keyResolver 参数和特定于速率限制器的参数(本节稍后介绍)。

keyResolver 是一个实现 KeyResolver 接口的 bean。在配置中,使用 SpEL 按名称引用 bean。 #{@myKeyResolver} 是一个 SpEL 表达式,它引用名为 myKeyResolver 的 bean。以下清单显示了 KeyResolver 接口:

Example 30. KeyResolver.java

1
2
3
public interface KeyResolver {
Mono<String> resolve(ServerWebExchange exchange);
}

KeyResolver 接口让可插拔策略派生出限制请求的密钥。在未来的里程碑版本中,将有一些 KeyResolver 实现。

KeyResolver 的默认实现是 PrincipalNameKeyResolver,它从 ServerWebExchange 检索 Principal 并调用 Principal.getName()。

默认情况下,如果 KeyResolver 未找到密钥,则拒绝请求。您可以通过设置 spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key(true 或 false)和 spring.cloud.gateway.filter.request-rate-limiter.empty-key 来调整此行为-状态码属性。

RequestRateLimiter 不能使用“快捷方式”表示法进行配置。以下示例无效:

Example 31. application.properties

1
2
# INVALID SHORTCUT CONFIGURATION
spring.cloud.gateway.routes[0].filters[0]=RequestRateLimiter=2, 2, #{@userkeyresolver}

6.10.1. The Redis RateLimiter

Redis 实现基于在 Stripe 完成的工作。它需要使用 spring-boot-starter-data-redis-reactive Spring Boot starter。

使用的算法是令牌桶算法。

redis-rate-limiter.replenishRate 属性是您希望允许用户每秒执行多少请求,而没有任何丢弃的请求。这是令牌桶填充的速率。

redis-rate-limiter.burstCapacity 属性是允许用户在一秒内执行的最大请求数。这是令牌桶可以容纳的令牌数量。将此值设置为零会阻止所有请求。

redis-rate-limiter.requestedTokens 属性是请求花费多少令牌。这是每个请求从存储桶中获取的令牌数量,默认为 1。

稳定速率是通过在replyRate 和burstCapacity 中设置相同的值来实现的。通过将burstCapacity 设置为高于replyRate,可以允许临时突发。在这种情况下,需要允许速率限制器在突发之间有一段时间(根据replyRate),因为两个连续的突发将导致请求丢失(HTTP 429 - Too Many Requests)。以下清单配置了 redis-rate-limiter:

低于 1 请求/秒的速率限制是通过将replyRate 设置为所需的请求数量、将requestedTokens 设置为以秒为单位的时间跨度以及将burstCapacity 设置为replyRate 和requestedTokens 的乘积来实现的,例如设置replyRate=1、requestedTokens=60 和burstCapacity=60 将导致1 请求/分钟的限制。

Example 32. application.yml

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cloud:
gateway:
routes:
- id: requestratelimiter_route
uri: https://example.org
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1

以下示例在 Java 中配置 KeyResolver:

Example 33. Config.java

1
2
3
4
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}

这定义了每个用户 10 的请求速率限制。允许突发 20 个,但在下一秒,只有 10 个请求可用。 KeyResolver 是一个简单的获取用户请求参数的方法(注意,不推荐用于生产)。

您还可以将速率限制器定义为实现 RateLimiter 接口的 bean。在配置中,您可以使用 SpEL 按名称引用 bean。 #{@myRateLimiter} 是一个 SpEL 表达式,它引用名为 myRateLimiter 的 bean。下面的清单定义了一个速率限制器,它使用在前面的清单中定义的 KeyResolver:

Example 34. application.yml

1
2
3
4
5
6
7
8
9
10
11
spring:
cloud:
gateway:
routes:
- id: requestratelimiter_route
uri: https://example.org
filters:
- name: RequestRateLimiter
args:
rate-limiter: "#{@myRateLimiter}"
key-resolver: "#{@userKeyResolver}"

6.11. RedirectTo 网关过滤器工厂

RedirectTo GatewayFilter 工厂接受两个参数,status 和 url。 status 参数应该是一个 300 系列的重定向 HTTP 代码,比如 301。url 参数应该是一个有效的 URL。这是 Location 标头的值。对于相对重定向,您应该使用 uri: no://op 作为路由定义的 uri。以下清单配置了一个 RedirectTo GatewayFilter:

Example 35. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: https://example.org
filters:
- RedirectTo=302, https://acme.org

这将发送带有 Location:https://acme.org 标头的状态 302 以执行重定向。

6.12. RemoveRequestHeader 网关过滤器工厂

RemoveRequestHeader GatewayFilter 工厂采用 name 参数。它是要删除的标题的名称。以下清单配置了 RemoveRequestHeader GatewayFilter:

Example 36. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: removerequestheader_route
uri: https://example.org
filters:
- RemoveRequestHeader=X-Request-Foo

这会在向下游发送之前删除 X-Request-Foo 标头。

6.13. RemoveResponseHeader 网关过滤器工厂

RemoveResponseHeader GatewayFilter 工厂采用 name 参数。它是要删除的标题的名称。以下清单配置了 RemoveResponseHeader GatewayFilter:

Example 37. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: removeresponseheader_route
uri: https://example.org
filters:
- RemoveResponseHeader=X-Response-Foo

这将在响应返回到网关客户端之前从响应中删除 X-Response-Foo 标头。

要删除任何类型的敏感标头,您应该为您可能想要这样做的任何路由配置此过滤器。此外,您可以使用 spring.cloud.gateway.default-filters 配置一次此过滤器,并将其应用于所有路由。

6.14. RemoveRequestParameter 网关过滤器工厂

RemoveRequestParameter GatewayFilter 工厂采用名称参数。它是要删除的查询参数的名称。以下示例配置 RemoveRequestParameter GatewayFilter:

Example 38. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: removerequestparameter_route
uri: https://example.org
filters:
- RemoveRequestParameter=red

这将在向下游发送之前删除red参数。

6.15. RewritePath 网关过滤器工厂

RewritePath GatewayFilter 工厂采用路径正则表达式参数和替换参数。这使用 Java 正则表达式来灵活地重写请求路径。以下清单配置了 RewritePath GatewayFilter:

Example 39. application.yml

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: rewritepath_route
uri: https://example.org
predicates:
- Path=/red/**
filters:
- RewritePath=/red/?(?<segment>.*), /$\{segment}

对于 /red/blue 的请求路径,这会在发出下游请求之前将路径设置为 /blue。请注意,由于 YAML 规范,$ 应替换为 $\。

6.16. RewriteLocationResponseHeader 网关过滤器工厂

RewriteLocationResponseHeader GatewayFilter 工厂修改 Location 响应头的值,通常是为了摆脱后端特定的细节。它采用 stripVersionMode、locationHeaderName、hostValue 和 protocolsRegex 参数。以下清单配置了 RewriteLocationResponseHeader GatewayFilter:

Example 40. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: rewritelocationresponseheader_route
uri: http://example.org
filters:
- RewriteLocationResponseHeader=AS_IN_REQUEST, Location, ,

例如,对于POST api.example.com/some/object/name的请求,object-service.prod.example.net/v2/some/object/id的Location响应头值改写为api.example.com/some/object/id。

stripVersionMode 参数具有以下可能的值:NEVER_STRIP、AS_IN_REQUEST(默认)和 ALWAYS_STRIP。

  • NEVER_STRIP: 即使原始请求路径不包含版本,也不会剥离版本。
  • AS_IN_REQUEST 仅当原始请求路径不包含版本时才会剥离版本。
  • ALWAYS_STRIP 版本总是被剥离,即使原始请求路径包含版本。

hostValue 参数(如果提供)用于替换响应 Location 标头的 host:port 部分。如果未提供,则使用 Host 请求标头的值。

protocolRegex 参数必须是有效的正则表达式字符串,与协议名称匹配。如果不匹配,则过滤器不执行任何操作。默认为 http|https|ftp|ftps。

6.17. RewriteResponseHeader 网关过滤器工厂

RewriteResponseHeader GatewayFilter 工厂采用名称、正则表达式和替换参数。它使用 Java 正则表达式来灵活地重写响应头值。以下示例配置 RewriteResponseHeader GatewayFilter:

Example 41. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: rewriteresponseheader_route
uri: https://example.org
filters:
- RewriteResponseHeader=X-Response-Red, , password=[^&]+, password=***

对于 /42?user=ford&password=omg!what&flag=true 的 header 值,在发出下游请求后设置为 /42?user=ford&password=***&flag=true。由于 YAML 规范,您必须使用 $\ 来表示 $。

6.18. SaveSession 网关过滤器工厂

SaveSession GatewayFilter 工厂在向下游转发调用之前强制执行 WebSession::save 操作。这在将 Spring Session 之类的东西与惰性数据存储一起使用时特别有用,并且您需要确保在进行转发调用之前已保存会话状态。以下示例配置 SaveSession GatewayFilter:

Example 42. application.yml

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: save_session
uri: https://example.org
predicates:
- Path=/foo/**
filters:
- SaveSession

如果您将 Spring Security 与 Spring Session 集成并希望确保安全详细信息已转发到远程进程,那么这很关键。

6.19. SecureHeaders 网关过滤器工厂

根据本博客文章中提出的建议,SecureHeaders GatewayFilter 工厂向响应添加了许多标头。

添加了以下header(显示为默认值):

  • X-Xss-Protection:1 (mode=block)
  • Strict-Transport-Security (max-age=631138519)
  • X-Frame-Options (DENY)
  • X-Content-Type-Options (nosniff)
  • Referrer-Policy (no-referrer)
  • Content-Security-Policy (default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline)'
  • X-Download-Options (noopen)
  • X-Permitted-Cross-Domain-Policies (none)

要更改默认值,请在 spring.cloud.gateway.filter.secure-headers 命名空间中设置适当的属性。以下属性可用:

  • xss-protection-header
  • strict-transport-security
  • x-frame-options
  • x-content-type-options
  • referrer-policy
  • content-security-policy
  • x-download-options
  • x-permitted-cross-domain-policies

要禁用默认值,请使用逗号分隔值设置 spring.cloud.gateway.filter.secure-headers.disable 属性。以下示例显示了如何执行此操作:

1
spring.cloud.gateway.filter.secure-headers.disable=x-frame-options,strict-transport-security

6.20. SetPath 网关过滤器工厂

SetPath GatewayFilter 工厂采用路径模板参数。它提供了一种通过允许路径的模板化段来操作请求路径的简单方法。这使用了 Spring Framework 中的 URI 模板。允许多个匹配段。以下示例配置 SetPath GatewayFilter:

Example 43. application.yml

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: setpath_route
uri: https://example.org
predicates:
- Path=/red/{segment}
filters:
- SetPath=/{segment}

对于 /red/blue 的请求路径,这会在发出下游请求之前将路径设置为 /blue。

6.21. SetRequestHeader 网关过滤器工厂

SetRequestHeader GatewayFilter 工厂采用名称和值参数。以下清单配置了 SetRequestHeader GatewayFilter:

Example 44. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: setrequestheader_route
uri: https://example.org
filters:
- SetRequestHeader=X-Request-Red, Blue

此 GatewayFilter 替换(而不是添加)具有给定名称的所有标头。因此,如果下游服务器以 X-Request-Red:1234 响应,这将替换为 X-Request-Red:Blue,这是下游服务将收到的。

SetRequestHeader 知道用于匹配路径或主机的 URI 变量。 URI 变量可以在值中使用并在运行时扩展。以下示例配置了一个使用变量的 SetRequestHeader GatewayFilter:

Example 45. application.yml

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: setrequestheader_route
uri: https://example.org
predicates:
- Host: {segment}.myhost.org
filters:
- SetRequestHeader=foo, bar-{segment}

6.22. SetResponseHeader 网关过滤器工厂

SetResponseHeader GatewayFilter 工厂采用名称和值参数。以下清单配置了 SetResponseHeader GatewayFilter:

Example 46. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: setresponseheader_route
uri: https://example.org
filters:
- SetResponseHeader=X-Response-Red, Blue

此 GatewayFilter 替换(而不是添加)具有给定名称的所有标头。因此,如果下游服务器以 X-Response-Red:1234 响应,这将替换为 X-Response-Red:Blue,这是网关客户端将收到的。

SetResponseHeader 知道用于匹配路径或主机的 URI 变量。 URI 变量可以在值中使用,并将在运行时扩展。以下示例配置使用变量的 SetResponseHeader GatewayFilter:

Example 47. application.yml

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: setresponseheader_route
uri: https://example.org
predicates:
- Host: {segment}.myhost.org
filters:
- SetResponseHeader=foo, bar-{segment}

6.23. SetStatus 网关过滤器工厂

SetStatus GatewayFilter 工厂采用单个参数 status。它必须是有效的 Spring HttpStatus。它可能是整数值 404 或枚举的字符串表示形式:NOT_FOUND。以下清单配置了 SetStatus GatewayFilter:

Example 48. application.yml

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cloud:
gateway:
routes:
- id: setstatusstring_route
uri: https://example.org
filters:
- SetStatus=BAD_REQUEST
- id: setstatusint_route
uri: https://example.org
filters:
- SetStatus=401

无论哪种情况,响应的 HTTP 状态都设置为 401。

您可以配置 SetStatus GatewayFilter 以在响应的标头中返回来自代理请求的原始 HTTP 状态代码。如果配置了以下属性,则将标头添加到响应中:

Example 49. application.yml

1
2
3
4
5
spring:
cloud:
gateway:
set-status:
original-status-header-name: original-http-status

6.24. StripPrefix 网关过滤器工厂

StripPrefix GatewayFilter 工厂采用一个参数,parts。部分参数指示在将请求发送到下游之前要从请求中剥离的路径中的部分数。以下清单配置了 StripPrefix GatewayFilter:

Example 50. application.yml

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: nameRoot
uri: https://nameservice
predicates:
- Path=/name/**
filters:
- StripPrefix=2

当通过网关向 /name/blue/red 发出请求时,对 nameservice 发出的请求看起来像 nameservice/red。

6.25. Retry网关过滤器工厂

Retry GatewayFilter 工厂支持以下参数:

  • retries: 应该尝试的重试次数。
  • statuses: 应该重试的 HTTP 状态码,用 org.springframework.http.HttpStatus 表示。
  • methods: 应该重试的 HTTP 方法,使用 org.springframework.http.HttpMethod 表示。
  • series: 要重试的一系列状态码,用 org.springframework.http.HttpStatus.Series 表示。
  • exceptions: 应该重试的抛出异常的列表。
  • backoff: 为重试配置的指数backoff。在 firstBackoff * (factor ^ n) 的backoff间隔后执行重试,其中 n 是迭代。如果配置了 maxBackoff,则应用的最大backoff限制为 maxBackoff。如果 basedOnPreviousValue 为真,则使用 prevBackoff * factor计算回退。

如果启用,则为重试过滤器配置以下默认值:

  • retries: 三次
  • series: 5XX系列
  • methods: GET 方法
  • exceptions: IOException 和 TimeoutException
  • backoff: disabled

以下清单配置了重试网关过滤器:

Example 51. application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
cloud:
gateway:
routes:
- id: retry_test
uri: http://localhost:8080/flakey
predicates:
- Host=*.retry.com
filters:
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY
methods: GET,POST
backoff:
firstBackoff: 10ms
maxBackoff: 50ms
factor: 2
basedOnPreviousValue: false

使用带有 forward: 前缀的重试过滤器时,应仔细编写目标端点,以便在出现错误时不会执行任何可能导致响应被发送到客户端并提交的操作。例如,如果目标端点是带注释的控制器,则目标控制器方法不应返回带有错误状态代码的 ResponseEntity。相反,它应该抛出异常或发出错误信号(例如,通过 Mono.error(ex) 返回值),重试过滤器可以配置为通过重试来处理。

将重试过滤器与任何带有正文的 HTTP 方法一起使用时,正文将被缓存并且网关将受到内存限制。正文缓存在由 ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR 定义的请求属性中。对象的类型是 org.springframework.core.io.buffer.DataBuffer。

6.26. RequestSize 网关过滤器工厂

当请求大小大于允许的限制时,RequestSize GatewayFilter 工厂可以限制请求到达下游服务。过滤器采用 maxSize 参数。 maxSize 是一个 `DataSize 类型,因此值可以定义为一个数字,后跟一个可选的 DataUnit 后缀,例如 ‘KB’ 或 ‘MB’。字节的默认值为“B”。它是以字节为单位定义的请求的允许大小限制。以下清单配置了 RequestSize GatewayFilter:

Example 52. application.yml

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cloud:
gateway:
routes:
- id: request_size_route
uri: http://localhost:8080/upload
predicates:
- Path=/upload
filters:
- name: RequestSize
args:
maxSize: 5000000

当请求因大小而被拒绝时,RequestSize GatewayFilter 工厂将响应状态设置为 413 Payload Too Large 并带有一个额外的标头 errorMessage。以下示例显示了这样的错误消息:

1
errorMessage` : `Request size is larger than permissible limit. Request size is 6.0 MB where permissible limit is 5.0 MB

如果未在路由定义中作为过滤器参数提供,则默认请求大小设置为 5 MB。

6.27. SetRequestHostHeader 网关过滤器工厂

在某些情况下,可能需要覆盖主机标头。在这种情况下, SetRequestHostHeader GatewayFilter 工厂可以用指定的值替换现有的主机头。过滤器采用主机参数。以下清单配置了 SetRequestHostHeader GatewayFilter:

Example 53. application.yml

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cloud:
gateway:
routes:
- id: set_request_host_header_route
uri: http://localhost:8080/headers
predicates:
- Path=/headers
filters:
- name: SetRequestHostHeader
args:
host: example.org

SetRequestHostHeader GatewayFilter 工厂用 example.org 替换主机标头的值。

6.28.修改请求正文 GatewayFilter Factory

您可以使用 ModifyRequestBody 过滤器过滤器在请求正文被网关发送到下游之前对其进行修改。

此过滤器只能通过使用 Java DSL 进行配置。

以下清单显示了如何修改请求正文 GatewayFilter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("rewrite_request_obj", r -> r.host("*.rewriterequestobj.org")
.filters(f -> f.prefixPath("/httpbin")
.modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE,
(exchange, s) -> return Mono.just(new Hello(s.toUpperCase())))).uri(uri))
.build();
}

static class Hello {
String message;

public Hello() { }

public Hello(String message) {
this.message = message;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}

如果请求没有正文,则 RewriteFilter 将传递 null。应返回 Mono.empty() 以在请求中分配缺失的主体。

6.29.修改响应体 GatewayFilter 工厂

您可以使用 ModifyResponseBody 过滤器在响应正文发送回客户端之前对其进行修改。

此过滤器只能通过使用 Java DSL 进行配置。

以下清单显示了如何修改响应正文 GatewayFilter:

1
2
3
4
5
6
7
8
9
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("rewrite_response_upper", r -> r.host("*.rewriteresponseupper.org")
.filters(f -> f.prefixPath("/httpbin")
.modifyResponseBody(String.class, String.class,
(exchange, s) -> Mono.just(s.toUpperCase()))).uri(uri))
.build();
}

如果响应没有正文,则 RewriteFilter 将传递 null。应返回 Mono.empty() 以在响应中分配缺失的主体。

6.30.令牌Relay网关过滤器工厂

令牌中继是 OAuth2 消费者充当客户端并将传入令牌转发到传出资源请求的地方。消费者可以是纯客户端(如 SSO 应用程序)或资源服务器。

Spring Cloud Gateway 可以将 OAuth2 访问令牌下游转发到它正在代理的服务。要将此功能添加到网关,您需要像这样添加 TokenRelayGatewayFilterFactory:

App.java

1
2
3
4
5
6
7
8
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("resource", r -> r.path("/resource")
.filters(f -> f.tokenRelay())
.uri("http://localhost:9000"))
.build();
}

或这个

application.yaml

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: resource
uri: http://localhost:9000
predicates:
- Path=/resource
filters:
- TokenRelay=

并且它将(除了登录用户并获取令牌之外)将身份验证令牌下游传递给服务(在本例中为 /resource)。

要为 Spring Cloud Gateway 启用此功能,请添加以下依赖项

  • org.springframework.boot:spring-boot-starter-oauth2-client

它是如何工作的? {githubmaster}/src/main/java/org/springframework/cloud/gateway/security/TokenRelayGatewayFilterFactory.java[filter] 从当前已验证的用户中提取访问令牌,并将其放入下游请求的请求头中。

只有在设置了正确的 spring.security.oauth2.client.* 属性时才会创建 TokenRelayGatewayFilterFactory bean,这将触发 ReactiveClientRegistrationRepository bean 的创建。

TokenRelayGatewayFilterFactory 使用的 ReactiveOAuth2AuthorizedClientService 的默认实现使用内存数据存储。如果您需要更强大的解决方案,您将需要提供自己的实现 ReactiveOAuth2AuthorizedClientService。

6.31.默认过滤器

要添加过滤器并将其应用于所有路由,您可以使用 spring.cloud.gateway.default-filters。此属性采用过滤器列表。以下清单定义了一组默认过滤器:

Example 54. application.yml

1
2
3
4
5
6
spring:
cloud:
gateway:
default-filters:
- AddResponseHeader=X-Response-Default-Red, Default-Blue
- PrefixPath=/httpbin

7. 全局过滤器

GlobalFilter 接口与 GatewayFilter 具有相同的签名。这些是有条件地应用于所有路由的特殊过滤器。

在未来的里程碑版本中,此界面及其用法可能会发生变化。

7.1.组合全局过滤器和网关过滤器排序

当请求与路由匹配时,过滤 Web 处理程序会将 GlobalFilter 的所有实例和 GatewayFilter 的所有特定于路由的实例添加到过滤器链中。这个组合过滤器链由 org.springframework.core.Ordered 接口排序,您可以通过实现 getOrder() 方法设置该接口。

由于 Spring Cloud Gateway 区分过滤器逻辑执行的“pre”和“post”阶段,具有最高优先级的过滤器是“pre”阶段的第一个,“post”阶段的最后一个——阶段。

以下清单配置了过滤器链:

Example 55. ExampleConfiguration.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Bean
public GlobalFilter customFilter() {
return new CustomGlobalFilter();
}

public class CustomGlobalFilter implements GlobalFilter, Ordered {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("custom global filter");
return chain.filter(exchange);
}

@Override
public int getOrder() {
return -1;
}
}

7.2.Forward路由过滤器

ForwardRoutingFilter 在交换属性 ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 中查找 URI。如果 URL 有转发方案(例如 forward:///localendpoint),它会使用 Spring DispatcherHandler 来处理请求。请求 URL 的路径部分被转发 URL 中的路径覆盖。未修改的原始 URL 将附加到 ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR 属性中的列表中。

7.3. ReactiveLoadBalancerClientFilter

ReactiveLoadBalancerClientFilter 在名为 ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 的交换属性中查找 URI。如果 URL 具有 lb 方案(例如 lb://myservice),则它使用 Spring Cloud ReactorLoadBalancer 将名称(在此示例中为 myservice)解析为实际主机和端口,并替换同一属性中的 URI。未修改的原始 URL 将附加到 ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR 属性中的列表中。过滤器还会查看 ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR 属性以查看它是否等于 lb。如果是,则应用相同的规则。以下清单配置了一个 ReactiveLoadBalancerClientFilter:

Example 56. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: myRoute
uri: lb://service
predicates:
- Path=/service/**

默认情况下,当 ReactorLoadBalancer 找不到服务实例时,会返回 503。您可以通过设置 spring.cloud.gateway.loadbalancer.use404=true 将网关配置为返回 404。

从 ReactiveLoadBalancerClientFilter 返回的 ServiceInstance 的 isSecure 值会覆盖向网关发出的请求中指定的方案。例如,如果请求通过 HTTPS 进入网关,但 ServiceInstance 指示它不安全,则通过 HTTP 发出下游请求。相反的情况也可以适用。但是,如果在网关配置中为路由指定了 GATEWAY_SCHEME_PREFIX_ATTR,则前缀将被剥离,并且来自路由 URL 的结果方案将覆盖 ServiceInstance 配置。

7.4. Netty 路由过滤器

如果位于 ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 交换属性中的 URL 具有 http 或 https 方案,则 Netty 路由过滤器运行。它使用 Netty HttpClient 发出下游代理请求。响应放在 ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR 交换属性中,以供稍后过滤器使用。 (还有一个实验性的 WebClientHttpRoutingFilter 执行相同的功能但不需要 Netty。)

7.5. Netty 写响应过滤器

如果 ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR 交换属性中存在 Netty HttpClientResponse,则 NettyWriteResponseFilter 运行。它在所有其他过滤器完成后运行,并将代理响应写回网关客户端响应。 (还有一个实验性的 WebClientWriteResponseFilter 可以执行相同的功能,但不需要 Netty。)

7.6. RouteToRequestUrl 过滤器

如果 ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR 交换属性中存在 Route 对象,则 RouteToRequestUrlFilter 运行。它基于请求 URI 创建一个新的 URI,但使用 Route 对象的 URI 属性进行更新。新 URI 放置在 ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 交换属性中。

如果 URI 具有方案前缀,例如 lb:ws://serviceid,则 lb 方案将从 URI 中剥离并放置在 ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR 中,以便稍后在过滤器链中使用。

7.7. Websocket 路由过滤器

如果位于 ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 交换属性中的 URL 具有 ws 或 wss 方案,则 websocket 路由过滤器运行。它使用 Spring WebSocket 基础结构向下游转发 websocket 请求。

您可以通过在 URI 前加上 lb 来对 websockets 进行负载平衡,例如 lb:ws://serviceid。

如果你使用 SockJS 作为普通 HTTP 的后备,你应该配置一个普通的 HTTP 路由以及 websocket 路由。

以下清单配置了一个 websocket 路由过滤器:

Example 57. application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
gateway:
routes:
# SockJS route
- id: websocket_sockjs_route
uri: http://localhost:3001
predicates:
- Path=/websocket/info/**
# Normal Websocket route
- id: websocket_route
uri: ws://localhost:3001
predicates:
- Path=/websocket/**

7.8.网关指标过滤器

要启用网关指标,请将 spring-boot-starter-actuator 添加为项目依赖项。然后,默认情况下,只要属性 spring.cloud.gateway.metrics.enabled 未设置为 false,网关指标过滤器就会运行。此过滤器添加了一个名为 gateway.requests 的计时器指标,并带有以下标签:

  • routeId: 路由标识。
  • routeUri: API 路由到的 URI。
  • outcome: 结果,由 HttpStatus.Series 分类。
  • status: 返回给客户端的请求的 HTTP 状态。
  • httpStatusCode: 返回给客户端的请求的 HTTP 状态。
  • httpMethod: 用于请求的 HTTP 方法。

然后可以从 /actuator/metrics/gateway.requests 抓取这些指标,并且可以轻松地与 Prometheus 集成以创建 Grafana 仪表板。

要启用 prometheus 端点,请将 micrometer-registry-prometheus 添加为项目依赖项。

7.9.将交换标记为已路由

网关路由 ServerWebExchange 后,它通过将 gatewayAlreadyRouted 添加到交换属性来将该交换标记为“已路由”。一旦请求被标记为路由,其他路由过滤器将不会再次路由该请求,实质上是跳过过滤器。有一些方便的方法可用于将交换标记为已路由或检查交换是否已被路由。

  • ServerWebExchangeUtils.isAlreadyRouted 接受一个 ServerWebExchange 对象并检查它是否已被“路由”。
  • ServerWebExchangeUtils.setAlreadyRouted 接受一个 ServerWebExchange 对象并将其标记为“已路由”。

8. HttpHeadersFilters

HttpHeadersFilters 在向下游发送请求之前应用于请求,例如在 NettyRoutingFilter 中。

8.1.Forwarded Headers过滤器

Forwarded Headers过滤器创建转发头以发送到下游服务。它将当前请求的 Host 标头、方案和端口添加到任何现有的 Forwarded 标头中。

8.2. RemoveHopByHop header过滤器

RemoveHopByHop 标头过滤器从转发的请求中删除标头。删除的默认标头列表来自 IETF。

默认删除的headers是:

  • Connection
  • Keep-Alive
  • Proxy-Authenticate
  • Proxy-Authorization
  • TE
  • Trailer
  • Transfer-Encoding
  • Upgrade

要更改此设置,请将 spring.cloud.gateway.filter.remove-hop-by-hop.headers 属性设置为要删除的标头名称列表。

8.3. XForwarded header过滤器

XForwarded 标头过滤器创建各种 X-Forwarded-* 标头以发送到下游服务。它使用当前请求的主机头、方案、端口和路径来创建各种头。

可以通过以下布尔属性(默认为 true)控制单个header的创建:

  • spring.cloud.gateway.x-forwarded.for-enabled
  • spring.cloud.gateway.x-forwarded.host-enabled
  • spring.cloud.gateway.x-forwarded.port-enabled
  • spring.cloud.gateway.x-forwarded.proto-enabled
  • spring.cloud.gateway.x-forwarded.prefix-enabled

附加多个header可以由以下布尔属性控制(默认为 true):

  • spring.cloud.gateway.x-forwarded.for-append
  • spring.cloud.gateway.x-forwarded.host-append
  • spring.cloud.gateway.x-forwarded.port-append
  • spring.cloud.gateway.x-forwarded.proto-append
  • spring.cloud.gateway.x-forwarded.prefix-append

9. TLS 和 SSL

网关可以通过遵循通常的 Spring 服务器配置来侦听 HTTPS 上的请求。以下示例显示了如何执行此操作:

Example 58. application.yml

1
2
3
4
5
6
7
server:
ssl:
enabled: true
key-alias: scg
key-store-password: scg1234
key-store: classpath:scg-keystore.p12
key-store-type: PKCS12

您可以将网关路由路由到 HTTP 和 HTTPS 后端。如果要路由到 HTTPS 后端,则可以使用以下配置将网关配置为信任所有下游证书:

Example 59. application.yml

1
2
3
4
5
6
spring:
cloud:
gateway:
httpclient:
ssl:
useInsecureTrustManager: true

使用不安全的信任管理器不适合生产。对于生产部署,您可以使用一组可以信任的已知证书配置网关,并使用以下配置:

Example 60. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
httpclient:
ssl:
trustedX509Certificates:
- cert1.pem
- cert2.pem

如果 Spring Cloud Gateway 未提供受信任的证书,则使用默认信任存储(您可以通过设置 javax.net.ssl.trustStore 系统属性来覆盖)。

9.1. TLS 握手

网关维护一个客户端池,用于路由到后端。通过 HTTPS 通信时,客户端会发起 TLS 握手。许多超时与此握手相关联。您可以配置这些超时可以配置(默认显示)如下:

Example 61. application.yml

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
httpclient:
ssl:
handshake-timeout-millis: 10000
close-notify-flush-timeout-millis: 3000
close-notify-read-timeout-millis: 0

10. 配置

Spring Cloud Gateway 的配置由一组 RouteDefinitionLocator 实例驱动。以下清单显示了 RouteDefinitionLocator 接口的定义:

Example 62. RouteDefinitionLocator.java

1
2
3
public interface RouteDefinitionLocator {
Flux<RouteDefinition> getRouteDefinitions();
}

默认情况下,PropertiesRouteDefinitionLocator 使用 Spring Boot 的 @ConfigurationProperties 机制加载属性。

较早的配置示例都使用使用位置参数而不是命名参数的快捷表示法。下面两个例子是等价的:

Example 63. application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
gateway:
routes:
- id: setstatus_route
uri: https://example.org
filters:
- name: SetStatus
args:
status: 401
- id: setstatusshortcut_route
uri: https://example.org
filters:
- SetStatus=401

对于网关的某些用途,属性就足够了,但某些生产用例受益于从外部源(例如数据库)加载配置。未来的里程碑版本将具有基于 Spring Data Repositories 的 RouteDefinitionLocator 实现,例如 Redis、MongoDB 和 Cassandra。