Zuul整合Spring

Zuul整合Spring

我们搭建一个简单的服务来演示下如何在Spring中继承Zuul使用。首先看下简单的架构图:

建立一个Zuul-Spring项目,包含下面4个子项目:

  • zuul-eureka-server:Eureka服务器,应用端口为8761
  • zuul-book-service:书本模块,服务提供者,提供REST /book/{bookId}接口,用于查找图书,最后返回Book的JSON字符串,应用端口为9000
  • zuul-sale-service:销售模块,服务调用者,对外发布销售服务,/sale-book/{bookId},在zuul-book-service来查找Book,应用端口为9100
  • zuul-gateway:网关服务,提供路由转发功能,将请求转发到销售服务上

zuul-eureka-server

其pom依赖为:

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
</dependencies>

配置文件为:

1
2
3
4
5
6
7
8
9
server:
port: 8761
eureka:
client:
registerWithEureka: false
fetchRegistry: false
server:
enable-self-preservation: false

启动类为:

1
2
3
4
5
6
7
@SpringBootApplication
@EnableEurekaServer
public class ServerApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ServerApplication.class).run(args);
}
}

zuul-book-service

服务提供方,其pom文件为:

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>

RestController为:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class BookController {

@RequestMapping(value = "/book/{bookId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public Book findBook(@PathVariable Integer bookId) {
Book book = new Book();
book.setId(bookId);
book.setAuthor("sadjaskl");
book.setName("XXXX");
return book;
}
}

Book类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Book {

private Integer id;

private String name;

private String author;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getAuthor() {
return author;
}

public void setAuthor(String author) {
this.author = author;
}

}

启动类:

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
@EnableEurekaClient
public class BookApplication {

public static void main(String[] args) {
new SpringApplicationBuilder(BookApplication.class).properties(
"server.port=9000").run(args);
}
}

zuul-sale-service

pom:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</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-feign</artifactId>
</dependency>
</dependencies>

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 9100
spring:
application:
name: zuul-sale-service
eureka:
instance:
hostname: localhost
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/

RestController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class SaleController {

@Autowired
private BookService bookService;

@RequestMapping(value = "/sale-book/{bookId}", method = RequestMethod.GET)
public String saleBook(@PathVariable Integer bookId) {
Book book = bookService.getBook(bookId);
System.out.println("销售模块处理销售,要销售的图书id:" + book.getId() + ", 书名:" + book.getName());
return "Success";
}

}

Service:

1
2
3
4
5
@FeignClient("zuul-book-service")
public interface BookService {
@RequestMapping(method = RequestMethod.GET, value = "/book/{bookId}")
Book getBook(@PathVariable("bookId") Integer id);
}

Book类即为zuul-book-service中的Book类,这里不再赘述

启动类:

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class SaleApplication {

public static void main(String[] args) {
SpringApplication.run(SaleApplication.class, args);
}
}

zuul-gateway

pom:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</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-config</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
</dependencies>

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
application:
name: zuul-gateway
eureka:
instance:
hostname: localhost
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/

zuul:
routes:
sale:
path: /sale/**
serviceId: zuul-sale-service

启动类:

1
2
3
4
5
6
7
@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(GatewayApplication.class).properties("server.port=8080").run(args);
}
}

按照如下顺序启动各个服务后:

  1. zuul-eureka-server
  2. zuul-book-service
  3. zuul-sale-service
  4. zuul-gateway

调用接口http://localhost:8080/sale/sale-book/1,就能看到调用结果了

接着就能在zuul-sale-service的日志能找到如下日志:

路由配置

简单路由

Spring Cloud在Zuul的routing阶段实现了几个过滤器,这些过滤器决定如何进行路由工作。其中,最基本的就是SimpleHostRoutingFilter,该过滤器运行后,会将HTTP请求全部转发到“源服务”。

以下为简单路由的配置,同时使用了path和url:

1
2
3
4
5
zuul:
routes:
routeTest:
path: /routeTest/163
url: http://www.163.com

以上的配置访问http://localhost:8080/routeTest/163,将会跳转到163网站。

为了配置渐变,可以省略path,默认情况下使用routeId作为path:

1
2
3
4
zuul:
routes:
route163:
url: http://www.163.com

访问http://localhost:8080/route163,同样会路由到163网站。实际上,要出发简单路由,配置的url的值需要以http或者https字符串开头。以下的配置不能出发简单路由:

1
2
3
4
zuul:
routes:
route163:
url: www.163.com

简单路由的过滤器SimpleHostRoutingFilter使用HttpClient进行转发,该过滤器会将HttPServletRequest的相关数据(HTTP方法、参数、请求头等)转换为HttpClient的请求实例(HttpRequest),再使用CloseableHttpClient进行转发。

再此过程中,为了保证转发的性能,使用了HttpClient的连接池功能。设计连接池,就需要对其进行配置。在使用简单路由时,可以配置一下两项,修改HttpClient连接池的属性:

  • zuul.host.maxTotalConnections:目标主机的最大连接数,默认值为200.配置该项,相当于调用了PoolingHttpClientConnectionManager的setMaxTotal方法
  • zuul.host.maxPerRouteConnections:每个主机的初始连接数,默认值为20.配置该项,相当于调用了PoolingHttpClientConnectionManager的setDefaultMaxPerRoute方法。

跳转路由

当外部方位网关的A地址时,会跳转到B地址,处理跳转路由的过滤器为SendForwardFilter。

在zuul-gateway中添加如下配置:

1
2
3
4
5
6
7
8
zuul:
routes:
sale:
path: /sale/**
serviceId: zuul-sale-service
helloRoute:
path: /test/**
url: forward:/source/hello

在zuul-gateway中添加如下REST Controller:

1
2
3
4
5
6
7
8
@RestController
public class SourceController {

@RequestMapping(value = "/source/hello/{name}", method = RequestMethod.GET)
public String hello(@PathVariable("name") String name) {
return "Hello " + name;
}
}

它做的就是将/test/xx 等接口,转移到/source/hello接口上。

输入地址http://localhost:8080/test/qq访问,就能看到以下结果:

跳转路由实现较为简单,实际上调用了RequestDispatcher的forward方法进行跳转。

Ribbon路由

当网关作为Eureka客户端注册到Eureka服务器时,可以配置通过serviceID将请求转发到集群的服务中。使用以下配置,可以执行Ribbon路由过滤器:

1
2
3
4
5
zuul:
routes:
sale:
path: /sale/**
serviceId: zuul-sale-service

与简单路由类似,serviceID也可以被省略。当省略时,将会使用routeId作为serviceId,下面的配置,等同于上面的配置:

1
2
3
4
zuul:
routes:
zuul-sale-service:
path: /sale/**

需要注意的是,如果提供的url配置项不是简单路由格式(不以http或者https开头),也不是跳转路由格式(forward开头),那么将会执行Ribbon路由过滤器,将url看做一个serviceId。下面的配置,也等同于前面的配置:

1
2
3
4
5
zuul:
routes:
sale:
path: /sale/**
url: zuul-sale-service

自定义路由规则

如果上面的路由配置无法满足实际需求,可以考虑使用自定义的路由规则。实现方式较为简单,在配置类中创建一个PatternServiceRouteMapper即可。

1
2
3
4
5
6
7
8
@Configuration
public class MyConfig {

public PatternServiceRouteMapper patternServiceRouteMapper() {
return new PatternServiceRouteMapper("(zuul)-(?<module.+>)-(service)", "${module}/**");
}
}

创建了PatternServiceRouteMapper实例,构造器的第一个参数为serviceId的正则表达式,第二个参数为路由的path。访问module/**的请求,将会被路由到zuul-module-service的微服务。

更进一步,以上的路由规则,如果想让一个或多个服务不被路由,可以使用zuul.ignoredServices属性。如果向排除zuul-sale-service、zuul-book-service这两个模块,可以配置zuul.ignoredServices: zuul-sale-service,zuul-book-service

忽略路由

除了上面提到的zuul.ignoredServices配置可以忽略路由外,还可以使用zuul.ignoredPatterns来设置不进行路由的URL。

1
2
3
4
5
6
zuul:
ignoredPatterns: /sale/noRoute
routes:
sale:
path: /sale/**
serviceId: zuul-sale-service

访问/sale路径的请求都会被路由到zuul-sale-service进行处理,但/sale/noRoute除外。

Zuul的其他配置

请求头配置

在集群的服务间共享请求头并没有什么问题,但是如果请求会被转发到其他系统,那么对于敏感的请求头信息,就需要进行处理。在默认情况下,HTTP请求头的Cookie、Set-Cookie、Authorization属性不会传递到“源服务”,可以使用sensitiveHeader属性来配置敏感请求头,下面的配置对全局生效:

1
2
zuul:
sensitiveHeaders: accept-language, cookie

以下的配置片段,仅对一个路由生效:

1
2
3
4
5
6
zuul:
routes:
sale:
path: /sale/**
serviceId: zuul-sale-service
sensitiveHeaders: cookie

除了使用sensitiveHeader属性外,还可以使用ignoredHeaders属性来配置全局忽略的请求头。使用该配置项后,请求与响应中所配置的头信息均被忽略。

1
2
zuul:
ignoredHeaders: accept-language

路由端点

在网关项目中提供了一个/routes服务,可以让我们查看路由映射信息。如果想开启该服务,需要满足一下条件:

  1. 网关项目中引入了Spring Boot Actuator
  2. 项目中使用了@EnableZuulProxy注解

一般情况下Actuator开启了端点的安全认证,即使符合以上两个条件,也无法访问routes服务。要解决该问题,可以在配置文件中将management.security.enabled属性设置为false关闭安全认证。

1
2
3
4

management:
security:
enabled: false

Zuul和Hystrix

当我们对网关进行配置让其调用集群的服务时,将会执行Ribbon路由过滤器。该过滤器在进行转发时会封装为一个Hystrix命令予以执行。换言之,它具有容错的功能。如果“源服务”出现问题,那么所执行的Hystrix命令将会触发回退。下面将会测试Zuul中的回退。

为zuul-sale-service的控制器添加一个超市方法

1
2
3
4
5
@RequestMapping(value = "/errorTest", method = RequestMethod.GET)
public String errorTest() throws Exception {
Thread.sleep(3000);
return "errorTest";
}

在zuul-gateway中建立一个网关处理类,处理回退逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class MyFallbackProvider implements ZuulFallbackProvider {
// 返回路由名称
public String getRoute() {
return "zuul-sale-service";
}

// 回退触发时,返回默认的响应
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}

public int getRawStatusCode() throws IOException {
return 200;
}

public String getStatusText() throws IOException {
return "OK";
}

public void close() {
}

public InputStream getBody() throws IOException {
return new ByteArrayInputStream("fallback".getBytes());
}

public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
return headers;
}
};
}
}

回退处理类需要实现ZuulFallbackProvider接口,实现的getRoute方法返回路由的名称,该方法将与配置中的路由进行对应,本例配置的路由如下:

1
2
3
4
5
zuul:
routes:
sale:
path: /sale/**
serviceId: zuul-sale-service

简单说就是,zuul-sale-service路由出现问题导致触发回退时,由MyFallbackProvider处理。MyFallbackProvider类实现的fallbackResponse方法要返回一个CLientHttpResponse实例。

为了让Spring容器知道MyFallbackProvider,在配置类中新建MyFallbackProvider的Bean。

1
2
3
4
5
6
7
8
@Configuration
public class FallbackConfig {
@Bean
public ZuulFallbackProvider saleFallbackProvider() {
return new MyFallbackProvider();
}
}

启动服务后,访问如下地址:http://localhost:8080/sale/errorTest,就能看到浏览器返回“fallback”字符,可见回退被触发。

进阶

过滤器优先级

Spring Cloud为HTTP请求的各个阶层提供了多个过滤器,这些过滤器的执行顺序由它们各自提供的一个int值决定,提供的值越小,优先级越高。

自定义过滤器

新建过滤器类,继承ZuulFilter。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyFilter extends ZuulFilter {
public String filterType() {
// 过滤器类型为routing过滤器
return FilterConstants.ROUTE_TYPE;
}

public int filterOrder() {
// 定义执行器优先级,在Route里面最先执行
return 1;
}

// 是否应该执行这个过滤器
public boolean shouldFilter() {
return true;
}

public Object run() {
System.out.println("执行MyFilter过滤器");
return null;
}
}

为了让Spring容器知道过滤器的存在,需要对该类进行配置

1
2
3
4
5
6
7
8
9
@Configuration
public class FilterConfig {

@Bean
public MyFilter myFilter() {
return new MyFilter();
}

}

重启网关服务,随便访问一个地址,就能看到过滤器被执行了

动态加载过滤器

相对于集群的其他节点,网关更需要长期、稳定地提供服务。如果需要增加过滤器,重启网关的代价太大,为了解决该问题,Zuul提供了过滤器的动态加载功能。可以使用Groovy来编写过滤器,然后添加到加载目录,让Zuul去动态加载。

先增加Groovy的依赖:

1
2
3
4
5
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.12</version>
</dependency>

在zuul-gateway的启动类中,调用Zuul的API来实现动态加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@PostConstruct
public void zuulInit() {
FilterLoader.getInstance().setCompiler(new GroovyCompiler());
// 读取配置,获取脚本根目录
String scriptRoot = System.getProperty("zuul.filter.root", "groovy/filters");
// 获取刷新间隔
String refreshInterval = System.getProperty("zuul.filter.refreshInterval", "5");
if (0 < scriptRoot.length()) {
scriptRoot = scriptRoot + File.separator;
}

try {
FilterFileManager.setFilenameFilter(new GroovyFileFilter());
FilterFileManager.init(Integer.parseInt(refreshInterval), scriptRoot + "pre", scriptRoot + "route", scriptRoot + "post");
} catch (Exception e) {
throw new RuntimeException(e);
}
}

新建一个Groovy脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class DynamicFilter extends ZuulFilter {
public boolean shouldFilter() {
return true;
}

public Object run() {
System.out.println("====== 这是一个动态加载的过滤器:DynamicFilter.groovy");
return null;
}

public String filterType() {
return FilterConstants.ROUTE_TYPE;
}

public int filterOrder() {
return 3;
}
}

重启zuul-gateway,往groovy/filters/route目录下丢这个groovy文件,最多5秒之后,zuul-gateway就能检测到这个拦截器,并自动加载。

禁用过滤器

可以通过如下配置,禁用特定的过滤器:

1
2
3
4
zuul:
SendForwardFilter:
route:
disable: true

请求上下文

HTTP请求的信息全部封装在一个RequestContext对象中,该对象继承ConcurrentHashMap。可将RequestContext看做一个Map,RequestContext维护着当前线程的全部请求变量,例如请求的URI、serviceId、主机等信息。

借下面这个例子,说明RequestContext的使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class RestTemplateFilter extends ZuulFilter {

private RestTemplate restTemplate;

public RestTemplateFilter(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}

public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// 获取请求uri
String uri = request.getRequestURI();
// 为了不影响其他路由,uri中含有 rest-tpl-sale 才执行本路由器
if(uri.indexOf("rest-tpl-sale") != -1) {
return true;
} else {
return false;
}
}

public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
// 获取需要调用的服务id
String serviceId = (String)ctx.get("serviceId");
// 获取请求的uri
String uri = (String)ctx.get("requestURI");
// 组合成url给RestTemplate调用
String url = "http://" + serviceId + uri;
System.out.println("执行RestTemplateFilter, 调用的url:" + url);
// 调用并获取结果
String result = this.restTemplate.getForObject(url, String.class);
// 设置路由状态,表示已经进行路由
ctx.setResponseBody(result);
// 设置响应标识
ctx.sendZuulResponse();
return null;
}

@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}

@Override
public int filterOrder() {
// TODO Auto-generated method stub
return 2;
}
}

RestTemplateFilter的主要功能时使用RestTemplate来调用集群服务。过滤器中的shouldFilter方法从RequestContext中获取HttpServletRequest,再得到请求的uri,如果uri含有rest-tpl-sale字符串,才执行本过滤器。

RestTemplateFilter的执行方法中,从RequestContext中获取了serviceId以及请求的uri,再组合成一个url给RestTemplate执行,执行返回的结果被设置到RequestContext中。

需要注意的是,最后调用了RequestContext的sendZuulResponse方法来设置响应标识。

调用了该方法后,Spring Cloud自带的Ribbon路由过滤器、简单过滤器将不会执行。

在FilterConfig中新增以下代码片段:

1
2
3
4
5
6
7
8
9
10
@Bean
public RestTemplateFilter restTemplateFilter(RestTemplate restTemplate) {
return new RestTemplateFilter(restTemplate);
}

@LoadBalanced
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}

然后在zuul-gateway中的配置文件中,增加以下配置:

1
2
3
restTestRoute:
path: /rest-tpl-sale/**
serviceId: zuul-sale-service

以上配置片段,设置路由的path为/rest-tpl-sale,当访问该地址时,将会执行前面的RestTemplateFilter。

访问以下地址:http://localhost:8080/rest-tpl-sale/sale-book/1就能得到如下结果

控制台上也能看到

@EnableZuulServer注解

@EnableZuulServer注解,也可以开启Zuul功能,但是使用它之后,SimpleHostRoutingFilter、RibbonRoutingFilter等过滤器将不会开启。

error过滤器

各阶段的过滤器执行时,抛出的异常会被捕获,然后调用RequestContext的SetThrowable方法设置异常。error阶段的SendErrorFilter过滤器会判断RequestContext中是否存在异常(getThrowable是否为null),如果存在,才会执行SendErrorFilter过滤器。

SendErrorFilter过滤器在执行时,会将异常信息设置到HttpServletRequest中,再调用RequestDispatcher的forward方法,默认跳转到/error页面。

参考资料

疯狂Spring Cloud微服务架构实战

0%