为什么需要网关
之前的系列文章中演示了,服务提供方和消费方都注册到注册中心,使得消费方能够直接通过 ServiceId 访问服务方
实际情况是:通常我们的服务方可能都需要做 接口权限校验、限流、软负载均衡 等等
而这类工作,完全可以交给服务方的更上一层:服务网关,来集中处理
这样的目的:保证微服务的无状态性,使其更专注于业务处理
所以说,服务网关是微服务架构中一个很重要的节点,Spring Cloud Netflix 中的 Zuul 就担任了这样的角色
当然了,除了 Zuul 之外,还有很多软件也可以作为 API Gateway 的实现,比如 Nginx Plus、Kong 等等
网关映射
通过服务路由的功能,可以在对外提供服务时,只暴露 Zuul 中配置的调用地址,而调用方就不需要了解后端具体的微服务主机
Zuul 提供了两种映射方式:URL 映射和 ServiceId 映射(后者需要将 Zuul 注册到注册中心,使之能够发现后端的微服务)
ServiceId 映射的好处是:它支持软负载均衡,基于 URL 的方式是不支持的(实际测试也的确如此)
示例代码
示例代码如下(也可以直接从 Github 下载:https://github.com/v5java/demo-cloud-07-zuul)
它是由六个模块组成的 Maven 工程,其中包含兩个服务提供方、两个服务网关、一个注册中心、一个服务消费方
它们的关系是:消费方走软负载均衡调用两个服务网关,服务网关根据路由配置,再一次走软负载均衡调用两个服务提供方
这是公共的 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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xuanyuv.demo</groupId>
<artifactId>demo-cloud-07-zuul</artifactId>
<version>1.1</version>
<packaging>pom</packaging>
<modules>
<module>service-client</module>
<module>service-discovery</module>
<module>service-gateway-01</module>
<module>service-gateway-02</module>
<module>service-server-01</module>
<module>service-server-02</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.5.RELEASE</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Camden.SR6</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
注册中心
这是注册中心的 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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xuanyuv.demo</groupId>
<artifactId>demo-cloud-07-zuul</artifactId>
<version>1.1</version>
</parent>
<artifactId>service-discovery</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
这是注册中心的配置文件 /src/main/resources/application.yml
server:
port: 1100
eureka:
server:
enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
eviction-interval-timer-in-ms: 1000 # 续期时间,即扫描失效服务的间隔时间(缺省为60*1000ms)
client:
# 设置是否从注册中心获取注册信息(缺省true)
# 因为这是一个单点的EurekaServer,不需要同步其它EurekaServer节点的数据,故设为false
fetch-registry: false
# 设置是否将自己作为客户端注册到注册中心(缺省true)
# 这里为不需要(查看@EnableEurekaServer注解的源码,会发现它间接用到了@EnableDiscoveryClient)
register-with-eureka: false
# 在未设置defaultZone的情况下,注册中心在本例中的默认地址就是http://127.0.0.1:1100/eureka/
# 但奇怪的是,启动注册中心时,控制台还是会打印这个地址的节点:http://localhost:8761/eureka/
# 而实际服务端注册时,要使用1100端口的才能注册成功,8761端口的会注册失败并报告异常
serviceUrl:
# 实际测试:若修改尾部的eureka为其它的,比如/myeureka,注册中心启动没问题,但服务端在注册时会失败
# 报告异常:com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server
defaultZone: http://127.0.0.1:${server.port}/eureka/
这是注册中心的 SpringBoot 启动类 ServiceDiscoveryBootStrap.java
package com.xuanyuv.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
//创建服务注册中心
@EnableEurekaServer
@SpringBootApplication
public class ServiceDiscoveryBootStrap {
public static void main(String[] args) {
SpringApplication.run(ServiceDiscoveryBootStrap.class, args);
}
}
服务提供方01
这是第一个服务提供方的 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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xuanyuv.demo</groupId>
<artifactId>demo-cloud-07-zuul</artifactId>
<version>1.1</version>
</parent>
<artifactId>service-server-01</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>
</project>
这是第一个服务提供方的配置文件 /src/main/resources/application.yml
server:
port: 2100
spring:
application:
name: CalculatorServer # 指定发布的微服务名(以后调用时,只需该名称即可访问该服务)
eureka:
instance:
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true # 设置微服务调用地址为IP优先(缺省为false)
lease-renewal-interval-in-seconds: 5 # 心跳时间,即服务续约间隔时间(缺省为30s)
lease-expiration-duration-in-seconds: 15 # 发呆时间,即服务续约到期时间(缺省为90s)
client:
healthcheck:
enabled: true # 开启健康检查(依赖spring-boot-starter-actuator)
serviceUrl:
defaultZone: http://127.0.0.1:1100/eureka/ # 指定服务注册中心的地址
这是第一个服务提供方的 SpringBoot 启动类 ServiceServer01BootStarp.java
package com.xuanyuv.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* 通过 @EnableEurekaClient 注解,为服务提供方赋予注册和发现服务的能力
* --------------------------------------------------------------------------------------------
* 也可以使用org.springframework.cloud.client.discovery.@EnableDiscoveryClient注解
* 详见以下两篇文章的介绍
* http://cloud.spring.io/spring-cloud-static/Camden.SR3/#_registering_with_eureka
* https://spring.io/blog/2015/01/20/microservice-registration-and-discovery-with-spring-cloud-and-netflix-s-eureka
* --------------------------------------------------------------------------------------------
* Created by 玄玉<https://www.xuanyuv.com/> on 2017/1/9 16:00.
*/
@EnableEurekaClient
@SpringBootApplication
public class ServiceServer01BootStarp {
public static void main(String[] args) {
SpringApplication.run(ServiceServer01BootStarp.class, args);
}
}
这是第一个服务提供方暴露的数学运算服务 CalculatorController.java
package com.xuanyuv.demo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 服务提供方暴露的数学运算服务
* Created by 玄玉<https://www.xuanyuv.com/> on 2017/1/9 16:00.
*/
@RestController
public class CalculatorController {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Resource
private DiscoveryClient client;
@RequestMapping("/add")
public int add(int a, int b){
//加运算
int result = a + b;
//输出服务信息
ServiceInstance instance = client.getLocalServiceInstance();
logger.info("uri={},serviceId={},result={}", instance.getUri(), instance.getServiceId(), result);
//返回结果
return result;
}
}
服务提供方02
除了启动端口为2200外,其代码与服务提供方01的完全相同
服务网关01
这是第一个服务网关的 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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xuanyuv.demo</groupId>
<artifactId>demo-cloud-07-zuul</artifactId>
<version>1.1</version>
</parent>
<artifactId>service-gateway-01</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
</dependencies>
</project>
这是第一个服务网关的配置文件 /src/main/resources/application.yml
server:
port: 4100
spring:
application:
name: xuanyu-api-gateway # 指定发布的微服务名(以后调用时,只需该名称即可访问该服务)
zuul:
ignored-services: "*" # 设置忽略的服务,即配置后将不会被路由(但对于明确配置在路由中的,将不会被忽略)
routes:
api-cal-url: # 基于 URL 的映射(这里自定义路由的名字为 api-cal-url,它可任意指定,唯一即可)
path: /cal/** # http://127.0.0.1:4100/cal/add?a=7&b=17会路由至http://127.0.0.1:2100/add?a=7&b=17
url: http://127.0.0.1:2100/
api-add: # 基于 ServiceId 的映射(自定义路由的名字)
path: /caladd/** # http://127.0.0.1:4100/caladd/add?a=6&b=16会路由至CalculatorServer服务的/add?a=6&b=16
serviceId: CalculatorServer
CalculatorServer: # 基于 ServiceId 的映射(路由的名字等于 ServiceId 的情况下,serviceId 属性可以省略)
path: /mycall/** # http://127.0.0.1:4100/mycall/add?a=5&b=15会路由至CalculatorServer服务的 /add?a=5&b=15
#serviceId: CalculatorServer
eureka:
instance:
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true # 设置微服务调用地址为IP优先(缺省为false)
lease-renewal-interval-in-seconds: 5 # 心跳时间,即服务续约间隔时间(缺省为30s)
lease-expiration-duration-in-seconds: 15 # 发呆时间,即服务续约到期时间(缺省为90s)
client:
healthcheck:
enabled: true # 开启健康检查(依赖spring-boot-starter-actuator)
serviceUrl:
defaultZone: http://127.0.0.1:1100/eureka/ # 指定服务注册中心的地址
这是第一个服务网关的 SpringBoot 启动类 ServiceGateway01BootStarp.java
package com.xuanyuv.demo;
import com.netflix.zuul.ZuulFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
//注意不是@EnableZuulServer
@EnableZuulProxy
//注意这里使用了更加简化的@SpringCloudApplication
@SpringCloudApplication
public class ServiceGateway01BootStarp {
/**
* 这里的方法返回值,不能写成com.netflix.zuul.IZuulFilter
* 可以写成com.netflix.zuul.ZuulFilter,或者com.xuanyuv.demo.GatewayFilter
* 虽然语法上允许返回IZuulFilter,但实际测试发现返回IZuulFilter时,网关功能却没有生效
*/
@Bean
public ZuulFilter gatewayFilter() {
return new GatewayFilter();
}
public static void main(String[] args) {
SpringApplication.run(ServiceGateway01BootStarp.class, args);
}
}
这是第一个服务网关中,用于控制接口访问权限的过滤器 GatewayFilter.java
package com.xuanyuv.demo;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
/**
* 利用Zuul的过滤器,可以实现对外服务的安全控制
* -------------------------------------------------------------------------
* 这里实现了在请求被路由之前检查请求中是否有accesstoken参数
* 若有就进行路由,若没有就拒绝访问,返回401 Unauthorized错误
* http://127.0.0.1:4100/mycall/add?a=11&b=22:返回"权限不足"
* http://127.0.0.1:4100/mycall/add?a=11&b=22&accesstoken=token:返回正常
* -------------------------------------------------------------------------
* Created by 玄玉<https://www.xuanyuv.com/> on 2017/1/14 15:05.
*/
public class GatewayFilter extends ZuulFilter {
private Logger log = LoggerFactory.getLogger(getClass());
/**
* 如下所示,Zuul定义了四种不同生命周期的过滤器类型
* pre :可以在请求被路由之前调用
* routing:在路由请求时候被调用
* post :在routing和error过滤器之后被调用
* error :处理请求时发生错误时被调用
*/
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
//通过int值来定义过滤器的执行顺序
return 0;
}
@Override
public boolean shouldFilter() {
//设置该过滤器总是生效,即总是执行拦截请求
return true;
}
/**
* 过滤器的具体逻辑
*/
@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
log.info(String.format("收到 %s 请求 %s", request.getMethod(), request.getRequestURL().toString()));
Object accessToken = request.getParameter("accesstoken");
if(accessToken == null) {
ctx.getResponse().setContentType("text/html;charset=UTF-8");
log.warn("accesstoken为空");
//令zuul过滤该请求,不对其进行路由
ctx.setSendZuulResponse(false);
//设置其返回的错误码和报文体
//这里没有设置应答码为401,是因为401会导致客户端走到它的断路器里面(HystrixCalculatorService)
//所有设置为200,让应答报文体跳过客户端的断路器,返回给前台
ctx.setResponseStatusCode(200);
ctx.setResponseBody("权限不足");
return null;
}
log.info("accesstoken验证通过");
return null;
}
}
服务网关02
除了启动端口为4200外,其代码与服务网关01的完全相同
服务消费方
这是服务消费方的 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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.xuanyuv.demo</groupId>
<artifactId>demo-cloud-07-zuul</artifactId>
<version>1.1</version>
</parent>
<artifactId>service-client</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<!-- spring-cloud-starter-feign的内部已经包含了spring-cloud-starter-ribbon和spring-cloud-starter-hystrix -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
</dependencies>
</project>
这是服务消费方的配置文件 /src/main/resources/application.yml
server:
port: 3100
spring:
application:
name: client-consumer-feign
eureka:
instance:
instance-id: ${spring.application.name}:${server.port}
prefer-ip-address: true
lease-renewal-interval-in-seconds: 5
lease-expiration-duration-in-seconds: 15
client:
healthcheck:
enabled: true
serviceUrl:
defaultZone: http://127.0.0.1:1100/eureka/
这是服务消费方 SpringBoot 启动类 ServiceClientBootStarp.java
package com.xuanyuv.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;
//开启Feign功能(无需显式@EnableCircuitBreaker,其已含此功能)
@EnableFeignClients
@EnableEurekaClient
@SpringBootApplication
public class ServiceClientBootStarp {
public static void main(String[] args) {
SpringApplication.run(ServiceClientBootStarp.class, args);
}
}
这是服务消费方的,包含了断路器配置的,调用服务网关的实现 CalculatorService.java
package com.xuanyuv.demo.feign;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
//绑定該接口到服务网关的xuanyu-api-gateway服务,并通知Feign组件对该接口进行代理(不需要编写接口实现)
@FeignClient(value="xuanyu-api-gateway", fallback=CalculatorService.HystrixCalculatorService.class)
public interface CalculatorService {
////@PathVariable這種也是支持的
//@RequestMapping(value="/mycall/add/{a}", method=RequestMethod.GET)
//int myadd(@PathVariable("a") int a, @RequestParam("b") int b, @RequestParam("accesstoken") String accesstoken);
//通过SpringMVC的注解来配置所綁定的服务下的具体实现
@RequestMapping(value="/mycall/add", method=RequestMethod.GET)
String myadd(@RequestParam("a") int a, @RequestParam("b") int b, @RequestParam("accesstoken") String accesstoken);
/**
* 这里采用和SpringCloud官方文档相同的做法,把fallback类作为内部类放入Feign接口中
* http://cloud.spring.io/spring-cloud-static/Camden.SR6/#spring-cloud-feign-hystrix
* (也可以外面独立定义该类,个人觉得没必要,这种东西写成内部类最合适)
*/
@Component
class HystrixCalculatorService implements CalculatorService {
@Override
public String myadd(@RequestParam("a") int a, @RequestParam("b") int b, @RequestParam("accesstoken") String accesstoken) {
return "负999";
}
}
}
这是服务消费方的调用示例 ConsumerController.java
package com.xuanyuv.demo.feign;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 服务调用方
* Created by 玄玉<https://www.xuanyuv.com/> on 2017/1/10 18:23.
*/
@RestController
@RequestMapping("/demo/feign")
public class ConsumerController {
@Resource
private CalculatorService calculatorService;
@RequestMapping("/toadd")
String toadd(int a, int b, String accesstoken){
return calculatorService.myadd(a, b, accesstoken);
}
}
验证
分别用浏览器多次访问以下地址,然后观察两个服务提供方、两个服务网关的控制台输出即可
http://127.0.0.1:4100/cal/add?a=7&b=17
http://127.0.0.1:4100/cal/add?a=7&b=17&accesstoken=00
http://127.0.0.1:4100/caladd/add?a=6&b=16
http://127.0.0.1:4100/caladd/add?a=6&b=16&accesstoken=00
http://127.0.0.1:4200/mycall/add?a=5&b=15
http://127.0.0.1:4200/mycall/add?a=5&b=15&accesstoken=00
http://127.0.0.1:3100/demo/feign/toadd?a=22&b=56
http://127.0.0.1:3100/demo/feign/toadd?a=22&b=56&accesstoken=00