前言

[https://github.com/spring-cloud/spring-cloud-gateway](Spring Cloud Gateway)是Spring Cloud官方推出的一个网关项目,主要是基于reactor-netty实现。网关在微服务系统主要充当了一个入口"门"的作用,所有的IN/OUT都需要经过这一道门,才能访问到微服务池子中的功能api。 这样的设计方便了我们对业务功能的保护api资源的保护,我们可以在这里灵活的控制对外开放的API集合,而这些API集合就构成了我们的"系统"

我们还可以方便的在这里完成鉴权,如果是非法用户之间在这里干掉,从而避免了对业务的调用。

还可以对参数进行转换,比如从调用者带过来的是一个JWT我们可以解析这个Token,得到诸如currentUserId租户idusername,等等一些列参数后,再往后传递到具体的微服务上。

这里只是一些常用功能的列举,本质上来说呢,他就是一组Filters,我们可以通过扩展它的Filter,快速完成业务所需要的功能效果。

依赖

首先肯定是需要导入gateway

1
2
3
4
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

我们还需要服务发现eureka,这里直接排除掉jersey的相关依赖,能少点jar就少点吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    <exclusions>
        <exclusion>
            <groupId>com.sun.jersey</groupId>
            <artifactId>jersey-client</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.sun.jersey</groupId>
            <artifactId>jersey-core</artifactId>
        </exclusion>
        <exclusion>
            <groupId>com.sun.jersey.contribs</groupId>
            <artifactId>jersey-apache-client4</artifactId>
        </exclusion>
    </exclusions>
</dependency>

网关也是需要对服务进行LoadBalance,这里导入ribbon的依赖。这里他也依赖了jersey相关的东西,排除掉好了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
    <exclusions>
        <exclusion>
            <artifactId>jersey-client</artifactId>
            <groupId>com.sun.jersey</groupId>
        </exclusion>
        <exclusion>
            <artifactId>jersey-apache-client4</artifactId>
            <groupId>com.sun.jersey.contribs</groupId>
        </exclusion>
    </exclusions>
</dependency>

我们可以启用ribbon中的okhttpclent,需要加入okhttp的依赖

1
2
3
4
5
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.1.0</version>
</dependency>

需要分布式追踪功能加入zipkin和sleuth的依赖。

1
2
3
4
5
6
7
8
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

pom.xml

完整的pom长这样

  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
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>io.qingmu</groupId>
    <artifactId>demo-gateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo-gateway</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>com.sun.jersey</groupId>
                    <artifactId>jersey-client</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.sun.jersey</groupId>
                    <artifactId>jersey-core</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>com.sun.jersey.contribs</groupId>
                    <artifactId>jersey-apache-client4</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>jersey-client</artifactId>
                    <groupId>com.sun.jersey</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>jersey-apache-client4</artifactId>
                    <groupId>com.sun.jersey.contribs</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.4</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>com.spotify</groupId>
                <artifactId>docker-maven-plugin</artifactId>
                <configuration>
                    <imageName>
                        freemanliu/demo-gatway:v1.0.0
                    </imageName>
                    <registryUrl></registryUrl>
                    <workdir>/work</workdir>
                    <rm>true</rm>
                    <env>
                        <TZ>Asia/Shanghai</TZ>
                        <JAVA_OPTS>
                            -XX:+UnlockExperimentalVMOptions \
                            -XX:+UseCGroupMemoryLimitForHeap \
                            -XX:MaxRAMFraction=2 \
                            -XX:CICompilerCount=8 \
                            -XX:ActiveProcessorCount=8 \
                            -XX:+UseG1GC \
                            -XX:+AggressiveOpts \
                            -XX:+UseFastAccessorMethods \
                            -XX:+UseStringDeduplication \
                            -XX:+UseCompressedOops \
                            -XX:+OptimizeStringConcat
                        </JAVA_OPTS>
                    </env>
                    <baseImage>freemanliu/openjre:8.212</baseImage>
                    <cmd>
                        /sbin/tini java ${JAVA_OPTS} -jar ${project.build.finalName}.jar
                    </cmd>
                    <!--是否推送image-->
                    <pushImage>true</pushImage>
                    <resources>
                        <resource>
                            <directory>${project.build.directory}</directory>
                            <include>${project.build.finalName}.jar</include>
                        </resource>
                    </resources>
                    <serverId>docker-hub</serverId>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>build</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

异常处理器

为什么要有异常处理?

  1. 需要将异常信息处理成我们所需要的格式。
  2. 统一处理自定义filter抛出的异常。

在不处理异常信息时,gateway默认返回的信息格式如下

1
2
3
4
5
6
7
{
"timestamp":"2019-08-21T06:46:19.819+0000"
,"path":"/demo1-service/hello"
,"status":504
,"error":"Gateway Timeout"
,"message":"Response took longer than timeout: PT20S"
}

这显然不太和我们一般定义的返回结构不同,一般常见的返回结构如下:

1
2
3
4
5
{
"code": 0
,"data"": {}
,"message": ""
}

CustomExceptionHandler.java

 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

package io.qingmu.demogateway.advice;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.validation.ValidationException;
import java.nio.charset.Charset;

@Setter
@Slf4j
public class CustomExceptionHandler implements ErrorWebExceptionHandler {


    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        // 按照异常类型进行处理
        final ServerHttpRequest request = exchange.getRequest();
        HttpStatus httpStatus;
        String body;
        int code = 500;
        if (ex instanceof ResponseStatusException) {
            ResponseStatusException responseStatusException = (ResponseStatusException) ex;
            httpStatus = responseStatusException.getStatus();
            if (httpStatus == HttpStatus.NOT_FOUND) {
                body = "服务接口未找到-404,path:" + request.getPath().value();
            } else
                body = responseStatusException.getMessage();
        } else if (ex instanceof CustomException) {
            body = ((CustomException) ex).getMessage();
            code = ((CustomException) ex).getCode();
        } else if (ex instanceof WebClientResponseException) {
            final Response result = JsonUtils.fromJson(((WebClientResponseException) ex).getResponseBodyAsString(), Response.class);
            body = result.getMessage();
            code = result.getCode();
        } else if (ex instanceof ValidationException) {
            body = ex.getMessage();
            code = 400;
        } else {
            log.error(ex.getMessage(), ex);
            body = "服务器繁忙-请稍后重试。";
        }
        log.error("[全局异常处理]异常请求路径:{},记录异常信息:{}", request.getPath(), ex.getMessage());
        final ServerHttpResponse response = exchange.getResponse();
        if (response.isCommitted()) {
            return Mono.error(ex);
        }
        response.getHeaders()
                .setContentType(MediaType
                        .APPLICATION_JSON_UTF8);
        response.setStatusCode(HttpStatus
                .INTERNAL_SERVER_ERROR);
        return response
                .writeWith(Mono
                        .just(response
                                .bufferFactory()
                                .wrap(JsonUtils
                                        .toJson(Response
                                                .builder()
                                                .code(code)
                                                .message(body)
                                                .build())
                                        .getBytes(Charset
                                                .forName("UTF-8")))));
    }
}

Response.java

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

package io.qingmu.demogateway.advice;

import lombok.*;
import java.io.Serializable;

@Getter
@Setter
@NoArgsConstructor
@Builder
@AllArgsConstructor
public class Response<T> implements Serializable {

    protected int code;

    protected String message;

    protected T data;
}

DemoGatewayApplication

 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
package io.qingmu.demogateway;

import io.qingmu.demogateway.advice.CustomExceptionHandler;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

@SpringBootApplication
@EnableDiscoveryClient
public class DemoGatewayApplication {

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

    @Primary
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public ErrorWebExceptionHandler errorWebExceptionHandler() {
        return new CustomExceptionHandler();
    }

}

静态路由

服务发现动态路由

配置zipkin的采集率,1.0 表示100%,0.1表示采集10%。

1
2
3
4
5
6
spring:
  zipkin:
    base-url: ${ZIPKIN:http://10.96.0.13:9411/}
  sleuth:
    sampler:
      probability: ${SAMPLER_PROBABILITY:1.0}

启用服务发现,自动配置路由。 比如我们有服务user-service中有api资源获取单个用户信息接口 /get 我们直接访问user-service的http接口为: http://ip:port/get 通过网关服务发现后的访问http接口为: http://gatewayip:gatwayport/user-service/get

1
2
3
4
5
6
7
spring:
  cloud:
    gateway:
      discovery:
        locator:
          lowerCaseServiceId: true
          enabled: true

配置gateway的http client的相关参数

1
2
3
4
5
6
7
8
spring:
  cloud:
    gateway:
      httpclient:
        pool:
          max-connections: ${MAX_CONNECTIONS:300}
        connect-timeout: ${CONNECT_TIMEOUT:10000}
        response-timeout: ${RESPONSE_TIMEOUT:5s}

配置全局默认filters,这里我们可以激活retry filter。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
spring:
  cloud:
    gateway:
      default-filters:
        - StripPrefix=1
        - name: Retry
          args:
            retries: 3
            series:
            - SERVER_ERROR
            - CLIENT_ERROR
            statuses:
            - INTERNAL_SERVER_ERROR
            methods:
            - GET
            - POST
            exceptions:
            - java.io.IOException
            - java.util.concurrent.TimeoutException

完成的 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
server:
  port: 8084
spring:
  zipkin:
    base-url: ${ZIPKIN:http://10.96.0.13:9411/}
  sleuth:
    sampler:
      probability: ${SAMPLER_PROBABILITY:1.0}
  cloud:
    gateway:
      discovery:
        locator:
          lowerCaseServiceId: true
          enabled: true
      httpclient:
        pool:
          max-connections: ${MAX_CONNECTIONS:300}
        connect-timeout: ${CONNECT_TIMEOUT:10000}
        response-timeout: ${RESPONSE_TIMEOUT:5s}
      metrics:
        enabled: true
      default-filters:
        - StripPrefix=1
        - name: Retry
          args:
            retries: 3
            series:
            - SERVER_ERROR
            - CLIENT_ERROR
            statuses:
            - INTERNAL_SERVER_ERROR
            methods:
            - GET
            - POST
            exceptions:
            - java.io.IOException
            - java.util.concurrent.TimeoutException
  application:
    name: demo-gateway
ribbon:
  okhttp:
    enabled: true
logging:
  logPath: /var/log/${spring.application.name}
  level:
    com.netflix.discovery.shared.resolver.aws: ERROR
management:
  endpoints:
    web:
      exposure:
        include: "*"

DemoGatewayApplication

启动类如下

 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
package io.qingmu.demogateway;

import io.qingmu.demogateway.advice.CustomExceptionHandler;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

@SpringBootApplication
@EnableDiscoveryClient
public class DemoGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoGatewayApplication.class, args);
    }
    @Primary
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public ErrorWebExceptionHandler errorWebExceptionHandler() {
        return new CustomExceptionHandler();
    }
}

启动我们网关服务,等待他启动完成,我们就可以通过网关来统一业务服务的访问。 启动我们的之前的业务服务demo1-serivcedemo2-service,通过如下url即可访问到。

访问demo1服务的hello接口

1
2
3
4
5
6
7
8

$ curl -i http://127.0.0.1:8084/demo1-service/hello
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 5
Date: Wed, 21 Aug 2019 10:41:45 GMT

hello

此时的调用链条,我们可以从zipkin中看到,如下图:

访问demo2服务的hello接口

1
2
3
4
5
6
7
8

$ curl -i http://127.0.0.1:8084/demo2-service/world
HTTP/1.1 200 OK
Content-Type: text/plain;charset=UTF-8
Content-Length: 5
Date: Wed, 21 Aug 2019 10:47:05 GMT

world

这样我们就完成了网关的动态映射。