目录导航
前言
前面的章节我们讲了Spring Cloud 服务调用
本节,继续微服务专题的内容分享,共计16小节,分别是:
- 微服务专题01-Spring Application
- 微服务专题02-Spring Web MVC 视图技术
- 微服务专题03-REST
- 微服务专题04-Spring WebFlux 原理
- 微服务专题05-Spring WebFlux 运用
- 微服务专题06-云原生应用(Cloud Native Applications)
- 微服务专题07-Spring Cloud 配置管理
- 微服务专题08-Spring Cloud 服务发现
- 微服务专题09-Spring Cloud 负载均衡
- 微服务专题10-Spring Cloud 服务熔断
- 微服务专题11-Spring Cloud 服务调用
- 微服务专题12-Spring Cloud Gateway
- 微服务专题13-Spring Cloud Stream (上)
- 微服务专题14-Spring Cloud Bus
- 微服务专题15-Spring Cloud Stream 实现
- 微服务专题16-Spring Cloud 整体回顾
本节内容重点为:
- 核心概念:介绍服务网关使用场景、服务能力、依赖关系、架构以及类型
- Ribbon 整合
- Hystrix 整合
Spring Cloud Gateway 简介
SpringCloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
Spring Cloud Gateway 类似于 Spring WebFlux。WebFlux的目的在于去 Servlet 化(Java EE Web 技术中心),WebFlux其技术核心为:Reactor + Netty + Lambda。
实际上,目前函数式编程、网络编程、Reactive这三点技术栈已成为主流:
-
函数式编程:Java Lambda、Koltin、Scala、Groovy。
-
网络编程:Old Java BIO、Java 1.4 NIO( Reactor 模式)、Java 1.7 NIO2 和 AIO、Netty。
-
Reactive:编程模型(非阻塞 + 异步) + 对象设计模式(观察者模式)。
透过这三个技术点我们也能看到典型的技术代表:
- 单机版(函数式、并发编程)
- Reactor
- RxJava
- Java 9 Flow API
- 网络版(函数式、并发编程、网络编程)
- Netty + Reactor :WebFlux、Spring Cloud Gateway
- Vert.x (Netty)
Spring Cloud Gateway 的特性
取代 Zuul 1.x(基于 Servlet)
SpringCloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul 2.0之前的非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
既然 Spring Cloud Gateway 的出现是为了取代Zuul 1.x,而Zuul 1.x则是基于Servlet实现,在Servlet之前则是 Resin Servlet 容器,据说可以与 Nginx 匹敌,继 Resin之后,则是 Tomcat Servlet 容器的时代,Tomcat里有一个核心概念是连接器,通常分为三种:
- Java Blocking Connector
- Java Non Blocking Connector
- APR/native Connector
实际上Servlet容器还有很多,比如
- JBoss
- Weblogic
- Netflix Zuul :缺点是实现 API 不是非常友好
服务发现
Spring Cloud Gateway 的目标,不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
Spring Cloud Zuul
Zuul是Netflix开源的微服务网关,可以和Eureka、Ribbon、Hystrix等组件配合使用,Spring Cloud对Zuul进行了整合与增强,Zuul默认使用的HTTP客户端是Apache HTTPClient,也可以使用RestClient或okhttp3.OkHttpClient。
Spring Cloud Zuul 简单流程
1、 @Enable
模块装配
-
@EnableZuulProxy
-
配合注解:
@Import
2、 依赖服务发现
- 我是谁
- 目的服务在哪里
3、 依赖服务路由
- URI 映射到目的服务
4、 依赖服务熔断(可选)
举例说明
假设路由的 URI :
/gateway/spring-cloud-server-application/say
,其中 Servlet Path是/gateway
,而spring-cloud-server-application
是服务的应用名称,/say
是spring-cloud-server-application
的服务 URI。怎么实现网关呢?
新建spring-cloud-servlet-gateway项目。
1、配置依赖
这里忽略,详情见本文末的完整演示代码github链接
2、配置文件
# 当前应用名称
spring.application.name = spring-cloud-servlet-gateway
# Servlet Gateway 服务端口
server.port = 20000
3、编写服务网关的路由规则测试类:
/**
* 服务网关的路由规则
* /{service-name}/{service-uri}
* /gateway/rest-api/hello-world-> http://127.0.0.1:8080/hello-world
*/
@WebServlet(name = "gateway", urlPatterns = "/gateway/*")
public class GatewayServlet extends HttpServlet {
@Autowired
private DiscoveryClient discoveryClient;
private ServiceInstance randomChoose(String serviceName) {
// 获取服务实例列表(服务IP、端口、是否为HTTPS)
List<ServiceInstance> serviceInstances = discoveryClient.getInstances(serviceName);
// 获得服务实例总数
int size = serviceInstances.size();
// 随机获取数组下标
int index = new Random().nextInt(size);
return serviceInstances.get(index);
}
@Override
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// ${service-name}/${service-uri}
String pathInfo = request.getPathInfo();
String[] parts = StringUtils.split(pathInfo.substring(1), "/");
// 获取服务名称
String serviceName = parts[0];
// 获取服务 URI
String serviceURI = "/" + parts[1];
// 随机选择一台服务实例
ServiceInstance serviceInstance = randomChoose(serviceName);
// 构建目标服务 URL -> scheme://ip:port/serviceURI
String targetURL = buildTargetURL(serviceInstance, serviceURI, request);
// 创建转发客户端
RestTemplate restTemplate = new RestTemplate();
// 构造 Request 实体
RequestEntity<byte[]> requestEntity = null;
try {
requestEntity = createRequestEntity(request, targetURL);
ResponseEntity<byte[]> responseEntity = restTemplate.exchange(requestEntity, byte[].class);
writeHeaders(responseEntity, response);
writeBody(responseEntity, response);
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
private String buildTargetURL(ServiceInstance serviceInstance, String serviceURI, HttpServletRequest request) {
StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append(serviceInstance.isSecure() ? "https://" : "http://")
.append(serviceInstance.getHost()).append(":").append(serviceInstance.getPort())
.append(serviceURI);
String queryString = request.getQueryString();
if (StringUtils.hasText(queryString)) {
urlBuilder.append("?").append(queryString);
}
return urlBuilder.toString();
}
private RequestEntity<byte[]> createRequestEntity(HttpServletRequest request, String url) throws URISyntaxException, IOException {
// 获取当前请求方法
String method = request.getMethod();
// 装换 HttpMethod
HttpMethod httpMethod = HttpMethod.resolve(method);
byte[] body = createRequestBody(request);
MultiValueMap<String, String> headers = createRequestHeaders(request);
RequestEntity<byte[]> requestEntity = new RequestEntity<byte[]>(body, headers, httpMethod, new URI(url));
return requestEntity;
}
private MultiValueMap<String, String> createRequestHeaders(HttpServletRequest request) {
HttpHeaders headers = new HttpHeaders();
List<String> headerNames = Collections.list(request.getHeaderNames());
for (String headerName : headerNames) {
List<String> headerValues = Collections.list(request.getHeaders(headerName));
for (String headerValue : headerValues) {
headers.add(headerName, headerValue);
}
}
return headers;
}
private byte[] createRequestBody(HttpServletRequest request) throws IOException {
InputStream inputStream = request.getInputStream();
return StreamUtils.copyToByteArray(inputStream);
}
/**
* 输出 Body 部分
*
* @param responseEntity
* @param response
* @throws IOException
*/
private void writeBody(ResponseEntity<byte[]> responseEntity, HttpServletResponse response) throws IOException {
if (responseEntity.hasBody()) {
byte[] body = responseEntity.getBody();
// 输出二进值
ServletOutputStream outputStream = response.getOutputStream();
// 输出 ServletOutputStream
outputStream.write(body);
outputStream.flush();
}
}
private void writeHeaders(ResponseEntity<byte[]> responseEntity, HttpServletResponse response) {
// 获取相应头
HttpHeaders httpHeaders = responseEntity.getHeaders();
// 输出转发 Response 头
for (Map.Entry<String, List<String>> entry : httpHeaders.entrySet()) {
String headerName = entry.getKey();
List<String> headerValues = entry.getValue();
for (String headerValue : headerValues) {
response.addHeader(headerName, headerValue);
}
}
}
}
4、启动zk服务
我的zk服务地址,localhost:2181
5、分别启动客户端与服务端和gateway服务
访问地址:
http://localhost:20000/gateway/spring-cloud-server-application/say?message=world
6、结果输出
整合负载均衡(Ribbon)
实现 ILoadBalancer
实现 IRule
1、通过继承BaseLoadBalancer实现负载均衡:
public class ZookeeperLoadBalancer extends BaseLoadBalancer {
@Value("${spring.application.name}")
private String currentApplicationName = "spring-cloud-servlet-gateway";
private final DiscoveryClient discoveryClient;
private Map<Stri ng, BaseLoadBalancer> loadBalancerMap = new ConcurrentHashMap<>();
public ZookeeperLoadBalancer(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
//
updateServers();
}
@Override
public Server chooseServer(Object key) {
if (key instanceof String) {
String serviceName = String.valueOf(key);
BaseLoadBalancer baseLoadBalancer = loadBalancerMap.get(serviceName);
return baseLoadBalancer.chooseServer(serviceName);
}
return super.chooseServer(key);
}
/**
* 更新所有服务器
*/
@Scheduled(fixedRate = 5000)
public void updateServers() {
discoveryClient.getServices().stream().forEach(serviceName -> {
BaseLoadBalancer loadBalancer = new BaseLoadBalancer();
loadBalancerMap.put(serviceName, loadBalancer);
List<ServiceInstance> serviceInstances = discoveryClient.getInstances(serviceName);
serviceInstances.forEach(serviceInstance -> {
Server server = new Server(serviceInstance.isSecure() ? "https://" : "http://",
serviceInstance.getHost(), serviceInstance.getPort());
loadBalancer.addServer(server);
});
});
}
}
2、基于负载均衡策略的网关实现:
/**
* 服务网关的路由规则
* /{service-name}/{service-uri}
* /gateway/rest-api/hello-world-> http://127.0.0.1:8080/hello-world
*/
@WebServlet(name = "ribbonGateway", urlPatterns = "/ribbon/gateway/*")
public class RibbonGatewayServlet extends HttpServlet {
@Autowired
private ZookeeperLoadBalancer zookeeperLoadBalancer;
private Server chooseServer(String serviceName) {
Server server = zookeeperLoadBalancer.chooseServer(serviceName);
return server;
}
@Override
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// ${service-name}/${service-uri}
String pathInfo = request.getPathInfo();
String[] parts = StringUtils.split(pathInfo.substring(1), "/");
// 获取服务名称
String serviceName = parts[0];
// 获取服务 URI
String serviceURI = "/" + parts[1];
// 随机选择一台服务实例
Server server = chooseServer(serviceName);
// 构建目标服务 URL -> scheme://ip:port/serviceURI
String targetURL = buildTargetURL(server, serviceURI, request);
// 创建转发客户端
RestTemplate restTemplate = new RestTemplate();
// 构造 Request 实体
RequestEntity<byte[]> requestEntity = null;
try {
requestEntity = createRequestEntity(request, targetURL);
ResponseEntity<byte[]> responseEntity = restTemplate.exchange(requestEntity, byte[].class);
writeHeaders(responseEntity, response);
writeBody(responseEntity, response);
} catch (URISyntaxException e) {
e.printStackTrace();
}
}
private String buildTargetURL(Server server, String serviceURI, HttpServletRequest request) {
StringBuilder urlBuilder = new StringBuilder();
urlBuilder.append(server.getScheme())
.append(server.getHost()).append(":").append(server.getPort())
.append(serviceURI);
String queryString = request.getQueryString();
if (StringUtils.hasText(queryString)) {
urlBuilder.append("?").append(queryString);
}
return urlBuilder.toString();
}
private RequestEntity<byte[]> createRequestEntity(HttpServletRequest request, String url) throws URISyntaxException, IOException {
// 获取当前请求方法
String method = request.getMethod();
// 装换 HttpMethod
HttpMethod httpMethod = HttpMethod.resolve(method);
byte[] body = createRequestBody(request);
MultiValueMap<String, String> headers = createRequestHeaders(request);
RequestEntity<byte[]> requestEntity = new RequestEntity<byte[]>(body, headers, httpMethod, new URI(url));
return requestEntity;
}
private MultiValueMap<String, String> createRequestHeaders(HttpServletRequest request) {
HttpHeaders headers = new HttpHeaders();
List<String> headerNames = Collections.list(request.getHeaderNames());
for (String headerName : headerNames) {
List<String> headerValues = Collections.list(request.getHeaders(headerName));
for (String headerValue : headerValues) {
headers.add(headerName, headerValue);
}
}
return headers;
}
private byte[] createRequestBody(HttpServletRequest request) throws IOException {
InputStream inputStream = request.getInputStream();
return StreamUtils.copyToByteArray(inputStream);
}
/**
* 输出 Body 部分
*
* @param responseEntity
* @param response
* @throws IOException
*/
private void writeBody(ResponseEntity<byte[]> responseEntity, HttpServletResponse response) throws IOException {
if (responseEntity.hasBody()) {
byte[] body = responseEntity.getBody();
// 输出二进值
ServletOutputStream outputStream = response.getOutputStream();
// 输出 ServletOutputStream
outputStream.write(body);
outputStream.flush();
}
}
private void writeHeaders(ResponseEntity<byte[]> responseEntity, HttpServletResponse response) {
// 获取相应头
HttpHeaders httpHeaders = responseEntity.getHeaders();
// 输出转发 Response 头
for (Map.Entry<String, List<String>> entry : httpHeaders.entrySet()) {
String headerName = entry.getKey();
List<String> headerValues = entry.getValue();
for (String headerValue : headerValues) {
response.addHeader(headerName, headerValue);
}
}
}
}
3、访问地址:
http://localhost:20000/ribbon/gateway/spring-cloud-client-application/rest/say?message=world
4、结果输出
后记
本节代码地址:Spring Cloud Gateway
更多架构知识,欢迎关注本套Java系列文章:Java架构师成长之路