1、API 网关
API 网关是对外服务的一个入口,其隐藏了内部框架的实现,是微服务架构中必不可少的一个组件。API 网关可以为我们管理大量的 API 接口,还可以对接客户、适配协议、进行安全认证、转发路由、限制流量、监控日志、防止爬虫、进行灰度发布等。
随着业务的发展,服务越来越多,前端用户如何调用微服务就成了一个难题。比如用户评估一个小区,评估完成之后需要展示小区详情、房价走势、成交数据、挂牌数据等,这些信息都在不同的服务器中,前端徐彤想要实现这么一个功能就需要和众多的服务进行交互,调用他们提供的接口,这样性能肯定是低的。而且前端系统的逻辑更复杂了,它需要知道所有提供信息的微服务。这个时候 API 网关的作用就体现出来了,通过 API 聚合内部服务,提供统一对外的 API 接口给前端系统,屏蔽内部实现细节。
2、Zuul 简介
Zuul 是 Netflix OSS 中的一员,是一个基于 JVM 路由和服务端的负载均衡器。提供路由、监控、弹性、安全等方面的服务框架。Zuul 能够与 Eureka、Ribbon、Hystrix 等组件配合使用。
Zuul 的核心是过滤器,通过这些过滤器我们可以扩展出很多功能,比如:
- 动态路由:动态地将客户端的请求路由到后端不同的服务器,做一些逻辑处理,比如聚合多个服务的数据返回。
- 请求监控:可以对整个系统的请求进行监控,记录详细的请求响应日志,可以实时统计出当前系统的访问量以及监控状态。
- 认证鉴权:对每一个访问的请求做认证,拒绝非法请求,保护好后端的服务。
- 压力测试:压力测试是一项很重要的工作,像一些电商公司需要模拟更多真实的用户并发量来保证重大活动时系统的稳定。通过 Zuul 可以动态地将请求转发到后端服务的集群中,还可以识别测试流量和真实流量,从而做一些特殊处理。
- 灰度发布:灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。
3、 使用 Zuul 构建微服务网关
在实际开发中通常使用 Zuul 来代理请求转发到内部的服务上去,统一为外部提供服务。内部服务的数量会很多,而且可以随时拓展,我们不可能增加一个服务就改一次路由配置,所以也得通过结合 Eureka 来实现动态的路由转发功能。
3.1 创建项目
- 创建一个 Maven 项目 spring-cloud-zuul,pom.xml 如下:
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.pky</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../spring-cloud-dependencies/pom.xml</relativePath>
</parent>
<artifactId>spring-cloud-zuul</artifactId>
<packaging>jar</packaging>
<name>spring-cloud-zuul</name>
<dependencies>
<!-- Spring Boot Begin -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot End -->
<!-- Spring Cloud Begin -->
<!-- 服务注册与发现 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<!-- API 网关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- Spring Cloud End -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.pky.spring.cloud.zuul.ZuulApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
主要是增加了 Zuul 的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
- 创建启动类 ZuulApplication,添加 @EnableZuulProxy 来开启路由代理功能。
package com.pky.spring.cloud.zuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy // 开启路由代理功能
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class, args);
}
}
@ EnableZuulProxy 已经自带了 @EnableDiscoveryClient。
- application.yml
server:
port: 9801
servlet:
context-path: /api/
spring:
application:
name: zuul
# 注册中心
eureka:
client:
service-url:
defaultZone: http://pky:123@master:9501/eureka/,\
http://pky:123@slaves:9502/eureka/
# 网关
zuul:
routes:
api-provider:
path: /provider/**
serviceId: admin-provider
api-feign:
path: /consumer/**
serviceId: admin-feign
通过 zuul.routes 来配置路由转发,api-provider 和 api-feign 是自定义的名字。
路由说明如下:
- 以 /provider/ 开头的请求都转发给 admin-provider 服务
- 以 /consumer/ 开头的请求都转发给 admin-feign 服务
3.2 测试 Zuul
启动服务后,浏览器分别输入 http://localhost:9801/api/a/admin 和 http://localhost:9801/api/b/feign ,则都会返回 “hello, I’m service admin from port :9601” 的结果。至此说明 Zuul 的路由功能配置成功。
访问规则是“API 网关地址 + 访问的服务名称 + 接口 URI”。
3.3 Zuul 路由配置
当 Zuul 集成 Eureka 之后,其实就是可以为 Eureka 中所有的服务进行路由操作了,默认的转发规则是“API 网关地址 + 访问的服务名称 + 接口 UR”。在给服务指定名称的时候,应尽量短一点,这样的话我们就可以用默认的路由规则进行请求,不需要为每个服务都顶一个路由规则,这样就算新增了服务,API 网关也不用修改和重启了。
默认规则举例:
- API 网关地址:http://localhost:9801/api
- 用户服务名称:user-service
- 用户登录接口:/user/login
那么通过 Zuul 访问接口的规则就是 http://localhost:9801/api/user-service/user/login
注意:上述 application.yml 中 path:/provider/** 需要两个星号,表示可以转发任一层级的 URL,比如 “/ provider/email/history”。如果只配置一个星,那么就只能转发一级,比如 “/ provider/email”。
3.4 过滤器类型
Zuul 中的过滤器跟我们之前使用的 javax.servlet.Filter 不一样,javax.servlet.Filter 只有一种类型,可以通过配置 urlPatterns 来拦截对应的请求。
Zuul 中的过滤器总共有 4 中类型,每种类型都有对应的使用场景。
- pre:可以在请求被路与路由之前调用。适用于身份认证的场景,认证通过后再继续执行下面的流程。
- route:在路由请求时被调用。适用于灰度发布场景,在将要路由的时候可以做一些自定义的逻辑。
- post:在route 和 error 过滤之后被调用。这种过滤器将请求路哟到达具体的服务之后执行。适用于需要添加响应头,记录响应日志等以你雇佣场景。
- error:处理请求时发生错误时被调用。在执行过程中发送错误时会进入 error 过滤器,可以用来统一记录错误信息。
3.5 请求生命周期
可以通过下图看出整个过滤器的执行生命周期,此图来自 Zuul GitHub wiki 主页,地址为: https://github.com/Netflix/zuul/wiki/How-it-Works
通过上面的图可以清楚地知道整个执行的顺序,请求发过来首先到 pre 过滤器,再到 routing 过滤器,最后到 post 过滤器,任何一个过滤器有异常都会进入 error 过滤器。
&emps; 通过 com.netflix.zuul.http.ZuulServlet 也可以看出完整执行顺序,ZuulServlet 类似 Spring MVC 中的 DispatcherServlet,所有的 Request 都要进过 ZuulServlet 的处理,如下代码所示:
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
try {
preRouting();
} catch (ZuulException e) {
error(e);
postRouting();
return;
}
// Only forward onto to the chain if a zuul response is not being sent
if (!RequestContext.getCurrentContext().sendZuulResponse()) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
try {
routing();
} catch (ZuulException e) {
error(e);
postRouting();
return;
}
try {
postRouting();
} catch (ZuulException e) {
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_FROM_FILTER_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
3.6 使用过滤器
1)我们创建一个 pre 过滤器,来实现登陆 token 的过滤操作,如下所示:
package com.pky.spring.cloud.zuul.filter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
/**
* 登录过滤器
*/
@Component
public class LoginFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(LoginFilter.class);
/**
* 配置过滤类型,有四种不同生命周期的过滤器类型
* 1. pre:路由之前
* 2. routing:路由之时
* 3. post:路由之后
* 4. error:发生错误调用
* @return
*/
@Override
public String filterType() {
return PRE_TYPE;
}
/**
* 配置过滤的顺序
* @return
*/
@Override
public int filterOrder() {
return 1;
}
/**
* 配置是否需要过滤:true/需要,false/不需要
* @return
*/
@Override
public boolean shouldFilter() {
//此方法可以根据请求的url进行判断是否需要拦截
return true;
}
/**
* 过滤器的具体业务代码
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
// RequestContext 用于记录Request的context。前面也分析了,由于Servlet是单例多线程的,而Request由唯一 worker线程处理,这里的RequestContext使用`ThreadLocal`实现,其本身简单wrap了`ConcurrentHashMap`
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
//避免中文乱码
currentContext.addZuulResponseHeader("Content-type", "text/json;charset=UTF-8");
currentContext.getResponse().setCharacterEncoding("UTF-8");
//打印日志
logger.info("{} >>> {}", request.getMethod(), request.getRequestURL().toString());
String token = request.getParameter("token");
if (token == null) {
logger.warn("Token is empty");
//设置为false则不往下走(不调用api接口)
currentContext.setSendZuulResponse(false);
//响应一个状态码:401
currentContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
try {
currentContext.getResponse().getWriter().write("Token is empty");
} catch (IOException e) {
}
} else {
logger.info("OK");
}
return null;
}
}
filterType:返回一个字符串代表过滤器的类型,在 Zuul 中定义了四种不同生命周期的过滤器类型
- pre:路由之前
- routing:路由之时
- post: 路由之后
- error:发生错误调用
filterOrder:过滤的顺序
shouldFilter:是否需要过滤,这里是 true,需要过滤
run:过滤器的具体业务代码
2)测试过滤器
重启项目,浏览器访问 http://localhost:9801/api/consumer/feign ,则显示 “Token is empty”
浏览器访问 http://localhost:9801/api/consumer/feign?token=123 ,则显示 “hello, I’m service admin from port :9601”
3.7 过滤器中传递数据
项目中往往会存在很多的过滤器,执行的顺序是根据 filterOrder 决定的,name肯定有一些过滤器是在后面执行的,如果你有这样的一个需求:第一个过滤器需要告诉第二个过滤器一些信息,这个时候就涉及过滤器中怎么去传递数据给后面的过滤器。
在上述重写的 run 方法中,我们获取了一个 RequestContext
RequestContext currentContext = RequestContext.getCurrentContext();
currentContext .set("msg", "user");
后面的过滤就可以通过 RequestContext 的 get 方法获取数据:
RequestContext currentContext = RequestContext.getCurrentContext();
currentContext .get("msg");
3.8 过滤器中异常处理
对于异常来说,无论在那个地方都需要处理。锅炉器中的异常主要发生在 run 方法中,可以用 try catch 来处理。Zuul 中也为我们提供了一个异常处理的过滤器,当过滤器在执行过程中发生异常,若没有被捕获到,就会进入 error 过滤器中。
我们可以定义一个 error 过滤器来记录异常信息,相关代码如下: