中文 | English
- 本项目旨在手写 SpringCloud Gateway。
- 参考 SpringCloud Gateway 的核心思想,我基本上用自己的方式实现了它所有的功能。
- 可以使用它更快的了解 SpringCloud Gateway 的内部工作原理和二次开发。
- 等等...
- 强大的谓词和过滤器,更容易理解和扩展。
- 路由信息存储可以动态切换到内存或者redis。
- 支持servlet和webflux环境,以及微服务模式和http/https模式。
- 支持自定义配置跨域。
- 等等...
<dependency>
<groupId>com.lzhpo</groupId>
<artifactId>panda-gateway-servlet</artifactId>
<version>${latest-version}</version>
</dependency>
implementation 'com.lzhpo:panda-gateway-servlet:${latest-version}'
<dependency>
<groupId>com.lzhpo</groupId>
<artifactId>panda-gateway-webflux</artifactId>
<version>${latest-version}</version>
</dependency>
implementation 'com.lzhpo:panda-gateway-webflux:${latest-version}'
谓词用来评估当前请求是否符合某个的路由。
名称格式:
[PredicateName]
RoutePredicateFactory
例如:如果我想将请求路径是
/api/service-sample/**
或/api/sample/**
转发到lb://panda-service-sample
。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
predicates:
- name: Path
args:
paths: /api/service-sample/**, /api/sample/**
例如:我想给路由分配权重。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
order: 1
predicates:
- name: Weight
args:
group: service-sample
weight: 8
- id: panda-service-sample-02
uri: lb://panda-service-sample
order: 2
predicates:
- name: Weight
args:
group: service-sample
weight: 2
例如:如果我想将请求参数含有
nickName=Lewis
或age=22
转发到lb://panda-service-sample
。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
predicates:
- name: Parameter
args:
parameters:
nickName: Lewis
age: 20
注意: 值支持正则表达式。
例如:如果我想请求客户端IP为
192.168.200.111
或192.168.200.112
转发到lb://panda-service-sample
。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
predicates:
- name: ClientIp
args:
clientIps: 192.168.200.111, 192.168.200.112
例如:如果我想让请求cookie含有
deviceId=123456
或age=22
转发到lb://panda-service-sample
。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
predicates:
- name: Cookie
args:
cookies:
deviceId: 123456
age: 22
注意: 值支持正则表达式。
例如:如果我想让请求cookie含有
X-B3-TraceId=123456
或X-B3-SpanId=123456
转发到lb://panda-service-sample
。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
predicates:
- name: Header
args:
headers:
X-B3-TraceId: 123456
X-B3-SpanId: 123456
注意: 值支持正则表达式。
例如:如果我想让请求方法是
PUT
或PATCH
转发到lb://panda-service-sample
。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
predicates:
- name: Method
args:
methods: PUT, PATCH
例如:如果我让请求时间是在
2030-06-30T01:29:48.0875598+08:00[Asia/Shanghai]
之后的转发到lb://panda-service-sample
。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
predicates:
- name: After
args:
time: 2030-06-30T01:29:48.0875598+08:00[Asia/Shanghai]
注意: 值的类型是 ZonedDateTime
.
你可以很轻松的获取值的格式:
ZonedDateTime.now().format(DateTimeFormatter.ISO_ZONED_DATE_TIME)
例如:如果我想让请求时间在
2015-06-30T01:29:48.0875598+08:00[Asia/Shanghai]
之前的转发到lb://panda-service-sample
。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
predicates:
- name: Before
args:
time: 2015-06-30T01:29:48.0875598+08:00[Asia/Shanghai]
注意: 值的类型是 ZonedDateTime
.
你可以很轻松的获取值的格式:
ZonedDateTime.now().format(DateTimeFormatter.ISO_ZONED_DATE_TIME)
例如:我想让请求时间在
start time: 2012-06-30T01:29:48.0875598+08:00[Asia/Shanghai] end time: 2018-06-30T01:29:48.0875598+08:00[Asia/Shanghai]或者
start time: 2020-10-01T01:29:48.0875598+08:00[Asia/Shanghai] end time: 2030-10-01T01:29:48.0875598+08:00[Asia/Shanghai]转发到
lb://panda-service-sample
。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
predicates:
- name: Between
args:
times:
- start: 2012-06-30T01:29:48.0875598+08:00[Asia/Shanghai]
end: 2018-06-30T01:29:48.0875598+08:00[Asia/Shanghai]
- start: 2020-10-01T01:29:48.0875598+08:00[Asia/Shanghai]
end: 2030-10-01T01:29:48.0875598+08:00[Asia/Shanghai]
注意:
-
支持多个时间对。
-
值的类型是
ZonedDateTime
.你可以很轻松的获取值的格式:
ZonedDateTime.now().format(DateTimeFormatter.ISO_ZONED_DATE_TIME)
您可以自定义定义路由谓词的关系,通过设置
gateway.routes[x].metadata.predicate-relation
,可以设置为AND
(匹配所有谓词)或OR
(匹配任意一个谓词),不区分大小写,默认是AND
(匹配所有谓词)。
当请求头中的 X-B3-TraceId为123456,并且请求参数中的nickName为Lewis时使用此路由。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
order: 1
metadata:
predicate-relation: and
predicates:
- name: Header
args:
headers:
X-B3-TraceId: 123456
- name: Parameter
args:
parameters:
nickName: Lewis
当请求头中的 X-B3-TraceId为123456,或者请求参数中的nickName为Lewis时使用此路由。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
order: 1
metadata:
predicate-relation: or
predicates:
- name: Header
args:
headers:
X-B3-TraceId: 123456
- name: Parameter
args:
parameters:
nickName: Lewis
我将以After
路由谓词为例来告诉你如何实现它。
@Component
public class AfterRoutePredicateFactory
extends AbstractRoutePredicateFactory<AfterRoutePredicateFactory.Config> {
public AfterRoutePredicateFactory() {
super(Config.class);
}
@Override
public RoutePredicate create(Config config) {
return request -> {
ZonedDateTime nowTime = ZonedDateTime.now();
ZonedDateTime afterTime = config.getTime();
return nowTime.isAfter(afterTime);
};
}
@Data
@Validated
public static class Config {
@NotNull
private ZonedDateTime time;
}
}
@Component
public class AfterRoutePredicateFactory
extends AbstractRoutePredicateFactory<AfterRoutePredicateFactory.Config> {
public AfterRoutePredicateFactory() {
super(Config.class);
}
@Override
public RoutePredicate create(Config config) {
return request -> {
ZonedDateTime nowTime = ZonedDateTime.now();
ZonedDateTime afterTime = config.getTime();
return nowTime.isAfter(afterTime);
};
}
@Data
@Validated
public static class Config {
@NotNull
private ZonedDateTime time;
}
}
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
predicates:
- name: After
args:
time: 2030-06-30T01:29:48.0875598+08:00[Asia/Shanghai]
名称格式:
[FilterName]
RouteFilterFactory路由过滤器只应用当前路由。
例如:如果您想添加请求标头。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
filters:
- name: AddRequestHeader
args:
headers:
name: Lewis
age: 123
eg:如果你想添加请求参数。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
filters:
- name: AddRequestParameter
args:
parameters:
userId: 123
例如:如果你想添加响应头。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
filters:
- name: AddResponseHeader
args:
headers:
name: Jack
age: 20
例如:如果您想删除请求标头。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
filters:
- name: RemoveRequestHeader
args:
headers:
X-B3-TraceId: 123
X-B3-SpanId: 456
Notes: the value support regex expression.
eg:如果你想删除请求参数。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
filters:
- name: RemoveRequestParameter
args:
parameters:
traceId: 123
spanId: 456
Notes: the value support regex expression.
例如:如果你想删除响应头。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
filters:
- name: RemoveResponseHeader
args:
headers:
country: China
city: Guangzhou
Notes: the value support regex expression.
eg:如果你想限制请求的速率。
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
filters:
- name: RateLimiter
args:
includeHeaders: true
replenishRate: 1
burstCapacity: 1
requestedTokens: 1
limitedCode: 429
limitedMessage: "Request too frequent"
keyResolver: "#{@clientIpKeyResolver}"
rateLimiter: "#{@redisRateLimiter}"
我将使用AddResponseHeader
路由过滤器来告诉你如何实现它。
@Component
public class AddResponseHeaderRouteFilterFactory
extends AbstractRouteFilterFactory<AddResponseHeaderRouteFilterFactory.Config>
implements Ordered {
public AddResponseHeaderRouteFilterFactory() {
super(AddResponseHeaderRouteFilterFactory.Config.class);
}
@Override
public RouteFilter create(Config config) {
return (request, response, chain) -> {
Map<String, String> configHeaders = config.getHeaders();
configHeaders.forEach(response::addHeader);
chain.doFilter(request, response);
};
}
@Data
@Validated
public static class Config {
@NotEmpty
private Map<String, String> headers;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
@Component
public class AddResponseHeaderRouteFilterFactory
extends AbstractRouteFilterFactory<AddResponseHeaderRouteFilterFactory.Config>
implements Ordered {
public AddResponseHeaderRouteFilterFactory() {
super(Config.class);
}
@Override
public RouteFilter create(Config config) {
return (exchange, filterChain) -> {
Map<String, String> configHeaders = config.getHeaders();
ServerHttpResponse response = exchange.getResponse();
HttpHeaders respHeaders = response.getHeaders();
configHeaders.forEach(respHeaders::remove);
return filterChain.filter(exchange);
};
}
@Data
@Validated
public static class Config {
@NotEmpty
private Map<String, String> headers;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
gateway:
routes:
- id: panda-service-sample-01
uri: lb://panda-service-sample
filters:
- name: AddResponseHeader
args:
headers:
name: Jack
age: 20
全局过滤器将应用所有路由,并且全局过滤器没有任何名称约束。
@Component
public class ResponseGlobalFilter implements GlobalFilter {
@Override
public void filter(
HttpServletRequest request, HttpServletResponse response, RouteFilterChain chain) {
response.addHeader("country", "China");
response.addHeader("city", "Guangzhou");
chain.doFilter(request, response);
}
}
@Component
public class ResponseGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, DefaultRouteFilterChain filterChain) {
ServerHttpResponse response = exchange.getResponse();
HttpHeaders headers = response.getHeaders();
headers.add("country", "China");
headers.add("city", "Guangzhou");
return filterChain.filter(exchange);
}
}
例如:如果我想允许所有跨域。
gateway:
cross-configurations:
'[/**]':
allowed-headers: "*"
allowed-methods: "*"
allowed-origins: "*"
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;
/**
* Customize error response data.
*
* @see ErrorMvcAutoConfiguration#errorAttributes()
* @author lzhpo
*/
@Component
public class GatewayErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(
WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errors = super.getErrorAttributes(webRequest, options);
Map<String, Object> errorAttributes = new HashMap<>(4);
errorAttributes.put("success", false);
errorAttributes.put("code", errors.getOrDefault("status", 500));
errorAttributes.put("message", getErrorMessage(errors));
errorAttributes.put("data", null);
return errorAttributes;
}
/**
* Get an error message.
*
* @param errors error attributes
* @return error message
*/
private Object getErrorMessage(Map<String, Object> errors) {
return Optional.ofNullable(errors.get("message"))
.orElseGet(() -> errors.getOrDefault("error", "Internal Server Error"));
}
}
返回格式(例子):
{
"code": 504,
"message": "Gateway Timeout",
"data": null,
"success": false
}
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration;
import org.springframework.boot.web.reactive.error.DefaultErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import reactor.core.publisher.Mono;
/**
* Customize error response.
*
* @see DefaultErrorAttributes
* @see ErrorWebFluxAutoConfiguration#errorWebExceptionHandler
* @author lzhpo
*/
@Order(-2)
@Component
public class GatewayErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {
public GatewayErrorWebExceptionHandler(
WebProperties webProperties,
ErrorAttributes errorAttributes,
ServerProperties serverProperties,
ApplicationContext applicationContext,
ObjectProvider<ViewResolver> viewResolvers,
ServerCodecConfigurer serverCodecConfigurer) {
super(
errorAttributes,
webProperties.getResources(),
serverProperties.getError(),
applicationContext);
setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList()));
setMessageWriters(serverCodecConfigurer.getWriters());
setMessageReaders(serverCodecConfigurer.getReaders());
}
@Override
public Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
Map<String, Object> errors =
getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
int status = (int) errors.getOrDefault("status", 500);
Map<String, Object> errorAttributes = new HashMap<>(4);
errorAttributes.put("success", false);
errorAttributes.put("code", status);
errorAttributes.put("message", getErrorMessage(errors));
errorAttributes.put("data", null);
return ServerResponse.status(status)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(errorAttributes));
}
/**
* Get an error message.
*
* @param errors error attributes
* @return error message
*/
private Object getErrorMessage(Map<String, Object> errors) {
return Optional.ofNullable(errors.get("message"))
.orElseGet(() -> errors.getOrDefault("error", "Internal Server Error"));
}
}
返回格式(例子):
{
"code": 504,
"message": "Gateway Timeout",
"data": null,
"success": false
}
使用此方法,webflux环境中自己重写返回的errorAttributes
需要有status
,否则报空指针异常,servlet环境中没有这种情况发生。
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.reactive.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
/**
* Customize error response data.
*
* @author lzhpo
*/
@Component
public class GatewayErrorAttributes extends DefaultErrorAttributes {
/**
* Notes: errorAttributes must containsKey "status", otherwise, will throw NullPointerException
*
* <pre>{@code
* protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
* Map<String, Object> error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
* return ServerResponse.status(getHttpStatus(error)).contentType(MediaType.APPLICATION_JSON)
* .body(BodyInserters.fromValue(error));
* }
*
* protected int getHttpStatus(Map<String, Object> errorAttributes) {
* return (int) errorAttributes.get("status");
* }
* }</pre>
*
* @see DefaultErrorWebExceptionHandler#renderErrorResponse
* @see DefaultErrorWebExceptionHandler#getHttpStatus
* @param request the source request
* @param options options for error attribute contents
* @return error attributes
*/
@Override
public Map<String, Object> getErrorAttributes(
ServerRequest request, ErrorAttributeOptions options) {
Map<String, Object> errors = super.getErrorAttributes(request, options);
Map<String, Object> errorAttributes = new HashMap<>(4);
errorAttributes.put("success", false);
errorAttributes.put("status", errors.getOrDefault("status", 500));
errorAttributes.put("message", getErrorMessage(errors));
errorAttributes.put("data", null);
return errorAttributes;
}
/**
* Get an error message.
*
* @param errors error attributes
* @return error message
*/
private Object getErrorMessage(Map<String, Object> errors) {
return Optional.ofNullable(errors.get("message"))
.orElseGet(() -> errors.getOrDefault("error", "Internal Server Error"));
}
}
详情可见:
// org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler#renderErrorResponse
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
Map<String, Object> error = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return ServerResponse.status(getHttpStatus(error)).contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(error));
}
// org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler#getHttpStatus
protected int getHttpStatus(Map<String, Object> errorAttributes) {
return (int) errorAttributes.get("status");
}
返回格式(例子):
{
"status": 504,
"message": "Gateway Timeout",
"data": null,
"success": false
}
如果我们想对网关做一些事情,我们需要暴露gateway
端点。
management:
endpoints:
web:
exposure:
include: gateway
GET /actuator/gateway/routes
GET /actuator/gateway/routes/${routeId}
GET /actuator/gateway/routes/predicates
GET /actuator/gateway/routes/${routeId}/predicates
GET /actuator/gateway/routes/filters
GET /actuator/gateway/routes/${routeId}/filters
GET /actuator/gateway/routes/global-filters
POST /actuator/gateway/routes/refresh