1、Zuul
我们已经知道,在微服务架构中,不同的微服务可以有不同的网络地址,各个微服务之间通过互相调用完成用户请
求,客户端可能通过调用N个微服务的接口完成一个用户请求。比如:用户查看一个商品的信息,它可能包含商品
基本信息、价格信息、评论信息、折扣信息、库存信息等等,而这些信息获取则来源于不同的微服务,诸如产品系
统、价格系统、评论系统、促销系统、库存系统等等,那么要完成用户信息查看则需要调用多个微服务,这样会带
来几个问题:
1、客户端多次请求不同的微服务,增加客户端代码或配置编写的复杂性
2、认证繁杂,访问每个服务都要进行一次认证
3、每个服务都通过http访问,导致http请求增加,效率不高拖慢系统性能
4、多个服务存在跨域请求问题,处理起来比较复杂
我们该如何解决这些问题呢?我们可以尝试想一下,不要让前端直接知道后台诸多微服务的存在,我们的系统本身
就是从业务领域的层次上进行划分,形成多个微服务,这是后台的处理方式。对于前台而言,后台应该仍然类似于
单体应用一样,一次请求即可,于是我们可以在客户端和服务端之间增加一个API网关,所有的外部请求先通过这
个微服务网关。它只需跟网关进行交互,而由网关进行各个微服务的调用。
1、基本使用
1、新建项目,添加pom依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
2、编写配置文件
server:
port: 9001
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
instance:
prefer-ip-address: true
instance-id: gateway-zuul-9001
spring:
application:
name: gateway-zuul-9001
zuul:
routes:
#路径映射,访问9001下的provider/**就会转发给provider-payment-service服务
provider-payment-service: /provider/**
#忽略所有的直接以服务名调用的请求
ignored-services: "*"
#以什么开头,http://localhost:9001/zuul/provider/xxx
prefix: /zuul
#默认路径是/zuul
servlet-path: /
2、网关过滤器
Zuul提供的过滤器是一个父类。父类是ZuulFilter。通过父类中定义的抽象方法filterType,来决定当前的
Filter种类是什么。有前置过滤、路由后过滤、后置过滤、异常过滤。
1、pre:在请求发出之前执行过滤,如果要进行访问,肯定在请求前设置头信息
2、route:在进行路由请求的时候被调用;
3、post:在路由之后发送请求信息的时候被调用;
4、error:出现错误之后进行调用
在父类中提供了4个抽象方法,分别是:filterOrder, shouldFilter, run。其功能分别是:
filterOrder:返回int数据,用于为同filterType的多个过滤器定制执行顺序,返回值越小,执行顺序越优先。
shouldFilter:返回boolean数据,代表当前filter是否生效。
run:具体的过滤执行逻辑。如pre类型的过滤器,可以通过对请求的验证来决定是否将请求路由到服务上; 如post类型的过滤器,可以对服务响应结果做加工处理(如为每个响应增加footer数据)。
1、新建类继承FallbackProvider,实现两个方法,加@Component
2、主要实现fallbackResponse方法
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause)
3、根据不同的异常来返回不同的信息
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
System.out.println("*****fallbackResponse,route:"+route);
if(cause instanceof NullPointerException){
List<Map<String, Object>> result = new ArrayList<>();
Map<String, Object> data = new HashMap<>();
data.put("message", "网关超时,请稍后重试");
result.add(data);
ObjectMapper mapper = new ObjectMapper();
String msg = "";
try {
msg = mapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
msg = "";
}
return this.executeFallback(HttpStatus.GATEWAY_TIMEOUT,
msg, "application", "json", "utf-8");
}else{
return this.fallbackResponse();
}
}
public ClientHttpResponse fallbackResponse() {
System.out.println("ClientHttpResponse fallbackResponse()");
List<Map<String, Object>> result = new ArrayList<>();
Map<String, Object> data = new HashMap<>();
data.put("message", "服务正忙,请稍后重试");
result.add(data);
ObjectMapper mapper = new ObjectMapper();
String msg = "";
try {
msg = mapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
msg = "";
}
return this.executeFallback(HttpStatus.OK, msg,
"application", "json", "utf-8");
}
/**
* 具体处理过程。
* @param status 容错处理后的返回状态,如200正常GET请求结果,201正常POST请求结果,404资源
找不到错误等。
* 使用spring提供的枚举类型对象实现。HttpStatus
* @param contentMsg 自定义的响应内容。就是反馈给客户端的数据。
* @param mediaType 响应类型,是响应的主类型, 如: application、text、media。
* @param subMediaType 响应类型,是响应的子类型, 如: json、stream、html、plain、jpeg、png等。
* @param charsetName 响应结果的字符集。这里只传递字符集名称,如: utf-8、gbk、big5等。
* @return ClientHttpResponse 就是响应的具体内容。
* 相当于一个HttpServletResponse。
*/
private final ClientHttpResponse executeFallback(final HttpStatus status,
String contentMsg, String mediaType, String subMediaType, String charsetName) {
return new ClientHttpResponse() {
/**
* 设置响应的头信息
*/
@Override
public HttpHeaders getHeaders() {
HttpHeaders header = new HttpHeaders();
MediaType mt = new MediaType(mediaType, subMediaType, Charset.forName(charsetName));
header.setContentType(mt);
return header;
}
/**
* 设置响应体
* zuul会将本方法返回的输入流数据读取,并通过HttpServletResponse的输出流输出到客户端。
*/
@Override
public InputStream getBody() throws IOException {
String content = contentMsg;
return new ByteArrayInputStream(content.getBytes());
}
/**
* ClientHttpResponse的fallback的状态码 返回String
*/
@Override
public String getStatusText() throws IOException {
return this.getStatusCode().getReasonPhrase();
}
/**
* ClientHttpResponse的fallback的状态码 返回HttpStatus
*/
@Override
public HttpStatus getStatusCode() throws IOException {
return status;
}
/**
* ClientHttpResponse的fallback的状态码 返回int
*/
@Override
public int getRawStatusCode() throws IOException {
return this.getStatusCode().value();
}
/**
* 回收资源方法
* 用于回收当前fallback逻辑开启的资源对象的。
* 不要关闭getBody方法返回的那个输入流对象。
*/
@Override
public void close() {
}
};
}
2、Gateway
1、基本使用
1、新建项目,添加pom依赖,不能加入web模块
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2、编写配置文件
spring:
application:
name: gateway-gateway-9001
cloud:
gateway:
routes:
- id: payment_get #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: http://localhost:8001 #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** #断言,路径相匹配的进行路由
3、启动Eureka,8001和本项目
4、访问路径http://localhost:xxx/payment/get/xxx即可
2、动态路由
1、将8002也加入服务
2、修改配置文件
spring:
application:
name: gateway-gateway-9001
cloud:
gateway:
#开启动态路由,根据服务名调用
discovery:
locator:
enabled: true
routes:
- id: payment_get #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://provider-payment-service #匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
3、常用的断言
1、Path
符合路径匹配即可通过,- Path=/payment/get/**
2、After
在某一时刻后访问为true,之前为false,- After=2020-06-21T18:25:10.016+08:00[Asia/Shanghai]
3、Before
在某一时刻前访问为true,之后为false,- Before=2020-06-28T18:25:10.016+08:00[Asia/Shanghai]
4、Between
在两个时间之间为true,- Between=2020-06-21T18:25:10.016+08:00[Asia/Shanghai],2020-06-
28T18:25:10.016+08:00[Asia/Shanghai]
5、Cookie
匹配请求中存在Cookie中符合规则的,- Cookie=chocolate, value,存在cookie中chocolate=value,其中value是正则表达式
6、Header
匹配请求中携带符合的请求头的请求,- Header=X-Request-Id, \d+的作用是:匹配请求头中存在符合条件
“key为X-Request-Id,value为数字”的所有请求,\d+是正则表达式
7、Query
匹配请求中携带符合的参数的请求,- Query=username,value的作用是: 匹配所有含有请求参数username且它的值符合正则表达式value的请求,value是正则表达式,可以省略
8、Method
匹配请求中请求方式符合的请求,- Method=GET的作用是:匹配所有是GET请求的请求
9、Host
匹配所有主机符合的请求,- Host=**.somehost.org:例如www.somehost.org,aaa.somehost.org
10、RemoteAddr
匹配所有ip符合的请求,- RemoteAddr=192.168.1.1/24:匹配对应的ip地址
11、如果在一个路由转发的地方设置多个断言,需要所有的断言成功才会进行路由转发
4、过滤器
1、新建类,实现两个接口,在filter里进行鉴权或校验
/**
* Description:
* Author:逗你妹
* Date:2020/6/21
*/
@Component
@Slf4j
public class GatewayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("***********come in MyLogGateWayFilter: "+new Date());
String username = exchange.getRequest().getQueryParams().getFirst("username");
if(StringUtils.isEmpty(username)){
log.info("*******用户名为null,非法用户,o(╥﹏╥)o");
exchange.getResponse().setStatusCode(HttpStatus.METHOD_NOT_ALLOWED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
5、代码路由转发
1、路由转发有两种方式,第一种就是上述演示的配置文件配置,另外一种就是java类的配置
2、新建一个配置类,添加多个RouteLocator实例即可
/**
* Description:
* Author:逗你妹
* Date:2020/6/21
*/
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator routeLocator1(RouteLocatorBuilder routeLocatorBuilder){
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("baidu_guonei",
f -> f.path("/guonei")
.uri("http://news.baidu.com/guonei"));
return routes.build();
}
@Bean
public RouteLocator routeLocator2(RouteLocatorBuilder routeLocatorBuilder){
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("baidu_guoji",
f -> f.path("/guoji")
.uri("http://news.baidu.com/guoji"));
return routes.build();
}
}