作者:青木,工程师,DevOps践行者,微服务化,容器化业务实践者。
https://github.com/spring-cloud/spring-cloud-gateway是Spring Cloud官方推出的一个网关项目,主要是基于reactor-netty实现。网关在微服务系统主要充当了一个入口”门”的作用,所有的IN/OUT都需要经过这一道门,才能访问到微服务池子中的功能api。
这样的设计方便了我们对业务功能的保护api资源的保护,我们可以在这里灵活的控制对外开放的API集合,而这些API集合就构成了我们的"系统"。
我们还可以方便的在这里完成鉴权,如果是非法用户之间在这里干掉,从而避免了对业务的调用。
还可以对参数进行转换,比如从调用者带过来的是一个JWT我们可以解析这个Token,得到诸如currentUserId,租户id,username,等等一些列参数后,再往后传递到具体的微服务上。
这里只是一些常用功能的列举,本质上来说呢,他就是一组Filters,我们可以通过扩展它的Filter,快速完成业务所需要的功能效果。
首先肯定是需要导入gateway
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
我们还需要服务发现eureka,这里直接排除掉jersey的相关依赖,能少点jar就少点吧。
<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相关的东西,排除掉好了。
<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的依赖
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.1.0</version>
</dependency>
需要分布式追踪功能加入zipkin和sleuth的依赖。
<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 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>
为什么要有异常处理?
需要将异常信息处理成我们所需要的格式。
统一处理自定义filter抛出的异常。
在不处理异常信息时,gateway默认返回的信息格式如下
{
"timestamp":"2019-08-21T06:46:19.819+0000"
,"path":"/demo1-service/hello"
,"status":504
,"error":"Gateway Timeout"
,"message":"Response took longer than timeout: PT20S"
}
这显然不太和我们一般定义的返回结构不同,一般常见的返回结构如下:
{
"code": 0
,"data"": {}
,"message": ""
}
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")))));
}
}
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;
}
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%。
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
spring:
cloud:
gateway:
discovery:
locator:
lowerCaseServiceId: true
enabled: true
配置gateway的http client的相关参数
spring:
cloud:
gateway:
httpclient:
pool:
max-connections: ${MAX_CONNECTIONS:300}
connect-timeout: ${CONNECT_TIMEOUT:10000}
response-timeout: ${RESPONSE_TIMEOUT:5s}
配置全局默认filters,这里我们可以激活retry filter。
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 如下
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: "*"
启动类如下
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-serivce和demo2-service,通过如下url即可访问到。
访问demo1服务的hello接口
$ 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中看到,如下图:
gateway1
访问demo2服务的hello接口
$ 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
这样我们就完成了网关的动态映射。
END