文章目录
Spring Cloud-Zuul 路由器和过滤器
当我们在使用微服务的时候,完成一个业务可能需要同时调用多个微服务,则需要分别请求多个服务接口,首先客户端需要请求不同的微服务,客户端的复杂性增大认证复杂,每个微服务都需要自己独立的认证方式,某些情况下会存在一些问题,例如跨域问题,每个服务存在的协议可能不同,这里借助一位博友的图片方便各位更好理解zuul入门(1)zuul 的概念和原理
通常情况下假如业务需求,需要分别去调用用户服务,产品服务和订单服务三个独立的微服务,为了这种情况Spring-Cloud提供了zuul服务作为网关层,利用zuul服务我们只需要访问网关层,zuul即会通过特定的路由规则将请求自动转发到指定的微服务上,zuul本身也是一个服务,和其他api服务一样共同注册到eureka上,可以相互发现。
通过zuul我们也可以做很多公共的预处理,例如权限认证、token校验、压力测试等。
1.什么是Zuul
Zuul是Netflix开源的微服务网关,他可以和Eureka,Ribbon,Hystrix等组件配合使用。Zuul组件的核心是一系列的过滤器,这些过滤器可以完成以下功能:
- 身份认证和安全:识别每一个资源的验证要求 审查和监控
- 动态路由:动态将请求路由到不同后端集群
- 压力测试:组建增加指定集群流量,检测性能
- 负载分配:为不同负载类型分配对应容量
- 静态响应处理:边缘位置响应,避免转发到内部集群
- 多区域弹性
Spring Cloud对Zuul进行了整合和增强,Zuul使用的默认是Apache的HTTP Client,也可以使用Rest Client可以设置ribbon.restclient.enabled=true,Zuul 在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web网站后端所有请求的前门。
关于内部过滤器更详细的构造,可以参考下面博友的博客,感谢分享zuul入门(1)zuul 的概念和原理
2.Zuul简单应用
上面对zuul出现的原因和解决的问题进行了介绍,现在利用Zuul实现简单应用进一步去了解
构建项目,引入需要依赖
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>1.4.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>1.4.3.RELEASE</version>
</dependency>
<!--整合达到安全控制-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
application.yml
server:
port: 10900 #程序启动端口,tomcat端口(可自定义)
spring:
application:
name: api-gateway-zuul #应用名称(别名)
eureka:
client:
serviceUrl:
defaultZone: http://user:user@localhost:8888/eureka
instance:
prefer-ip-address: true #显示 ip
security: #配置访问 zuul 的 routes 的时候使用
user:
name: zuul
password: zuul
ZuulApp.java
package com.ithzk.spring.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
/** * Hello world! * */
@SpringBootApplication
@EnableZuulProxy//启用 zuul,自带熔断
public class ZuulApp
{
public static void main( String[] args )
{
SpringApplication.run(ZuulApp.class);
}
}
启动之前的Eureka Server以及provider-user服务,访问localhost:8888 Eureka Server。
可以发现Zuul服务已经被注册上去,通常情况下我们调用提供服务http://localhost:7900/user/55
即可,这里通过Zuul去调用,只需调用http://192.168.126.1:10900/provider-user/user/55
,此处zuul后面跟上服务注册名称以及调用的服务接口和参数即可,这里需要利用我们zuul服务配置的用户名和密码。
可以看出来我们成功通过zuul服务调用到了我们所需的服务得到了想要的结果
3.自定义服务映射地址
上面我们简单去利用zuul完成调用特定服务,但是我们发现当我们服务注册很多并且名称很长没有规律时,可能调用会变得十分复杂,zuul提供了自定义服务映射地址的功能,我们只需修改配置就能实现。
3.1 服务名称映射
application.yml
server:
port: 10900 #程序启动端口,tomcat端口(可自定义)
spring:
application:
name: api-gateway-zuul #应用名称(别名)
eureka:
client:
serviceUrl:
defaultZone: http://user:user@localhost:8888/eureka
instance:
prefer-ip-address: true #显示 ip
security: #配置访问 zuul 的 routes 的时候使用
user:
name: zuul
password: zuul
zuul:
routes: #配置路由映射
provider-user: /route-map1/** #给指定的服务做映射, 当前配置给provider-user添加映射地址为/route-map1
通过
zuul.routes.applicationName
就可以给指定的服务添加地址映射,配置成功后我们调用http://192.168.126.1:10900/route-map1/user/88
可以发现和之前达到同样的效果。我们通过访问http://192.168.126.1:10900/routes可以查看到当前zuul映射的路由地址对应关系。
3.2 path绑定映射
这里再介绍另外一种利用path属性自定义映射地址的方式,同样只需修改配置文件即可
application.yml
server:
port: 10900 #程序启动端口,tomcat端口(可自定义)
spring:
application:
name: api-gateway-zuul #应用名称(别名)
eureka:
client:
serviceUrl:
defaultZone: http://user:user@localhost:8888/eureka
instance:
prefer-ip-address: true #显示 ip
security: #配置访问 zuul 的 routes 的时候使用
user:
name: zuul
password: zuul
zuul:
routes: #配置路由映射
route-map2: #保证唯一即可
path: /route-map2/** #映射的路径
serviceId: provider-user #给特定服务做映射,和之前效果一样
通过这种方式可以和上面达成一样的效果,前者是通过服务名称去映射地址,后者是通过映射地址去绑定服务名称,查看zuul服务路由:
可以看出zuul绑定关系已经修改,通过新的映射地址访问同样可以获得预期效果
3.3 url绑定映射
上面通过serviceId去绑定指定服务名称服务,还可以通过url属性直接绑定对应url
application.yml
server:
port: 10900 #程序启动端口,tomcat端口(可自定义)
spring:
application:
name: api-gateway-zuul #应用名称(别名)
eureka:
client:
serviceUrl:
defaultZone: http://user:user@localhost:8888/eureka
instance:
prefer-ip-address: true #显示 ip
security: #配置访问 zuul 的 routes 的时候使用
user:
name: zuul
password: zuul
zuul:
routes: #配置路由映射
route-map2: #保证唯一即可
path: /route-map2/** #映射的路径
#serviceId: provider-user #给特定服务做映射,和之前效果一样
url: http://localhost:7900/
效果也完全一样,这三种方式都可以指定服务映射地址,需要注意的是这些简单的URL路由不会被执行为HystrixCommand也不能使用Ribbon对多个URL进行负载均衡。
3.4 url绑定映射实现负载均衡
若我们想要达到负载均衡的效果,需要使用serviceId配置的方式再加上listOfServers配置
application.yml
server:
port: 10900 #程序启动端口,tomcat端口(可自定义)
spring:
application:
name: api-gateway-zuul #应用名称(别名)
eureka:
client:
serviceUrl:
defaultZone: http://user:user@localhost:8888/eureka
instance:
prefer-ip-address: true #显示 ip
security: #配置访问 zuul 的 routes 的时候使用
user:
name: zuul
password: zuul
zuul:
routes: #配置路由映射
route-map2: #保证唯一即可
path: /route-map2/** #映射的路径
serviceId: provider-user #给特定服务做映射,和之前效果一样
ribbon:
eureka:
enabled: false #在eureka中禁用 ribbon 的负载均衡
provider-user: #给配置serviceId对应的服务指定ribbon负载均衡,从ListOfServers配置的地址中选择,多个逗号分隔
ribbon:
listOfServers: http://localhost:7900/,http://localhost:7901/
启动两个服务,分别端口为7900、7901,通过zuul调用,和之前配置ribbon负载均衡效果一样,默认轮询
3.5 正则表达式映射
zuul提供了regexmapper和serviceId的规则绑定,若serviceId符合配置的表达式则会被映射到特定路由模式上,主程序里面添加一段官方提供的代码,正则表达式可以自定义,这里使用官方示例。
ZuulApp.java
package com.ithzk.spring.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.netflix.zuul.filters.discovery.PatternServiceRouteMapper;
import org.springframework.context.annotation.Bean;
/** * Hello world! * */
@SpringBootApplication
@EnableZuulProxy//启用 zuul,自带熔断
public class ZuulApp
{
public static void main( String[] args )
{
SpringApplication.run(ZuulApp.class);
}
/** * 解析特定匹配路由 * @return */
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
return new PatternServiceRouteMapper(
"(?<name>^.+)-(?<version>v.+$)",
"${version}/${name}");
}
}
这里我们启动一个提供服务,修改服务名称为如下格式
application.yml
server:
port: 7902 #程序启动后的端口,也就是tomcat的端口,可自定义
spring:
application:
name: provider-user-v1
eureka:
client:
service-url:
defaultZone: http://user:user@localhost:8888/eureka/
启动后访问http://192.168.126.1:10900/routes,发现该服务被解析成了指定模式路由,调用达到效果
3.6 添加前缀映射
zuul还可以为所有映射添加前缀,只需增加配置zuul.prefix,我们还可以通过zuul.stripPrefix=false去关闭添加前缀的行为,但是关闭的前缀只能是zuul.prefix设置的前缀,官方文档写的还是挺详细的。
application.yml
server:
port: 10900 #程序启动端口,tomcat端口(可自定义)
spring:
application:
name: api-gateway-zuul #应用名称(别名)
eureka:
client:
serviceUrl:
defaultZone: http://user:user@localhost:8888/eureka
instance:
prefer-ip-address: true #显示 ip
security: #配置访问 zuul 的 routes 的时候使用
user:
name: zuul
password: zuul
zuul:
prefix: /prefix-name
4.过滤服务代理
Zuul可以自动去发现所有Eureka Server上注册的服务并且去代理,但是有些时候我们并不想让某个或者某些服务被Zuul代理,同样Zuul也提供了配置修改即可实现。
application.yml
server:
port: 10900 #程序启动端口,tomcat端口(可自定义)
spring:
application:
name: api-gateway-zuul #应用名称(别名)
eureka:
client:
serviceUrl:
defaultZone: http://user:user@localhost:8888/eureka
instance:
prefer-ip-address: true #显示 ip
security: #配置访问 zuul 的 routes 的时候使用
user:
name: zuul
password: zuul
zuul:
routes: #配置路由映射
provider-user: /route-map1/** #给指定的服务做映射, 当前配置给provider-user添加映射地址为/route-map1
ignored-services: consumer-order # zuul能发现Eureka上所有注册的服务,然后全部做代理,如果不需要代理其中某些服务,只需添加该配置即可,多个服务以逗号分隔
前后两张图为zuul是否过滤consumer-order服务前后调用接口响应结果记录,可以发现修改配置达到了所需效果,若需要过滤多个服务,则在配置中将多个服务名称用逗号分隔开。
5.通过Zuul上传文件
这个构建一个带有上传功能的项目来接触zuul的上传文件
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</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-netflix-eureka-client</artifactId>
<version>1.4.3.RELEASE</version>
</dependency>
</dependencies>
<!-- 引入spring cloud的依赖 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Edgware.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 添加spring-boot的maven插件 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
application.yml
server:
port: 8555
spring:
application:
name: zuul-file-upload
http:
multipart:
max-file-size: 2000Mb # Max file size,默认1M
max-request-size: 2500Mb # Max request size,默认10M
eureka:
client:
service-url:
defaultZone: http://user:user@localhost:8888/eureka/
instance:
prefer-ip-address: true
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<form method="POST" enctype="multipart/form-data" action="/upload">
File to upload:
<input type="file" name="file">
<input type="submit" value="Upload">
</form>
</body>
</html>
FileUploadController java
package com.ithzk.spring.cloud.controller;
import org.springframework.stereotype.Controller;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
/** * @author hzk * @date 2018/6/28 */
@Controller
public class FileUploadController {
/** * 上传文件 * 测试方法: * 界面化:http://localhost:8050/index.html * 命令:curl -F "file=@文件全名" localhost:8555/upload * 简单实例 * @param file 待上传的文件 * @return 文件在服务器上的绝对路径 * @throws IOException IO异常 */
@RequestMapping(value = "/upload",method = RequestMethod.POST)
@ResponseBody
public String handleFileUpload(@RequestParam(value = "file",required = true) MultipartFile file) throws IOException {
byte[] bytes = file.getBytes();
File saveFile = new File(file.getOriginalFilename());
FileCopyUtils.copy(bytes,saveFile);
return saveFile.getAbsolutePath();
}
}
FileUploadApp .java
package com.ithzk.spring.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/** * @author hzk * @date 2018/6/28 */
@SpringBootApplication
@EnableEurekaClient
public class FileUploadApp {
public static void main(String[] args){
SpringApplication.run(FileUploadApp.class,args);
}
}
我们启动Eureka Server和之前的Zuul,可以发现服务都注册上去了,通过
http://192.168.126.1:8555/,http://192.168.126.1:8555/index.html
都可以访问到上传文件界面。
利用zuul路由我们可以通过http://192.168.126.1:10900/zuul-file-upload
转发到同样的界面,Spring cloud官方文档对于上传大文件建议我们绕过Spring DispatcherServlet,通过访问http://192.168.126.1:10900/zuul/zuul-file-upload
去实现,另外一种方式 我们可以使用命令去上传文件。
通过curl -F "file=@文件全名" localhost:port/zuul/project-name/file
,我们这里执行curl -F "file=@1.mp4" http://192.168.126.1:10900/zuul/zuul-file-upload/update
,返回的信息提示未经过验证,这里是由于我们没有加上zuul身份验证。
在地址前加上身份验证之后可以发现两种方式都能达到文件上传的目的,在我们项目下可以发现上传上来的文件,如果因为网络问题或者文件太大可能出现timeout超时的现象,添加配置可以修改超时时间。
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
ConnectTimeout: 3000
ReadTimeout: 60000
官方文档还介绍到,当我们使用大型文件进行流式传输,需要在请求中使用分块编码
curl -v -H "Transfer-Encoding: chunked" \
-F "file=@mylarge.iso" localhost:9999/zuul/simple/file
6.Zuul回退
我们利用zuul作为路由去转发的时候,有时候比如程序内部错误或者是提供服务出现问题,都会返回给我们返回一个错误提示界面,例如500 404之类的报错页面,但是如果我们想自定义一个更友好化符合实际需求的信息需要怎么做呢?zuul提供了fallback机制,我们可以给断路配置一个自定义回退。
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>1.4.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>1.4.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
application.yml
server:
port: 10900 #程序启动端口,tomcat端口(可自定义)
spring:
application:
name: api-gateway-zuul-fallback #应用名称(别名)
eureka:
client:
serviceUrl:
defaultZone: http://user:user@localhost:8888/eureka
instance:
prefer-ip-address: true #显示 ip
security: #配置访问 zuul 的 routes 的时候使用
user:
name: zuul
password: zuul
ZuulFallBackApp .java
package com.ithzk.spring.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
/** * @author hzk * @date 2018/6/28 */
@SpringBootApplication
@EnableZuulProxy
public class ZuulFallBackApp {
public static void main(String[] args){
SpringApplication.run(ZuulFallBackApp.class,args);
}
}
MyZuulFallBack .java
package com.ithzk.spring.cloud;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
/** * @author hzk * @date 2018/6/28 */
@Component
public class MyZuulFallBack implements ZuulFallbackProvider {//ZuulFallbackProvider过时,推荐使用FallbackProvider
@Override
public String getRoute() {
return "provider-user";
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.BAD_REQUEST;
}
@Override
public int getRawStatusCode() throws IOException {
return HttpStatus.BAD_REQUEST.value();
}
@Override
public String getStatusText() throws IOException {
return HttpStatus.BAD_REQUEST.getReasonPhrase();
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream((" happened error!"+getRoute()).getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
return httpHeaders;
}
};
}
}
启动Eureka Server和provider-user以及该服务,我们通过zuul路由去请求provider-user服务的时候可以发现是正常的,此时我们关闭provider-user制造一个服务错误的现象,再去请求。
这里可以看出来zuul没有出现之前那种默认的错误提示界面而是出现了我们自定义的提示信息,这样zuul的回退就基本达成,但是需要注意的是这里只会针对provider-user这个服务的路由做回退,当我们需要对所有路由做回退的话我们只需将getRoute()这个方法返回设置为*或者null即可。
7.Zuul过滤器
Zuul本身是一系列过滤器的集成,那么他当然也就提供了自定义过滤器的功能,zuul提供了四种过滤器:前置过滤器,路由过滤器,错误过滤器,简单过滤器,实现起来也十分简单,只需要编写一个类去实现zuul提供的接口。
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
<version>1.4.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>1.4.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
application.yml
server:
port: 10900 #程序启动端口,tomcat端口(可自定义)
spring:
application:
name: api-gateway-zuul-filter #应用名称(别名)
eureka:
client:
serviceUrl:
defaultZone: http://user:user@localhost:8888/eureka
instance:
prefer-ip-address: true #显示 ip
security: #配置访问 zuul 的 routes 的时候使用
user:
name: zuul
password: zuul
ZuulFilterApp.java
package com.ithzk.spring.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
/** * Hello world! * */
@SpringBootApplication
@EnableZuulProxy
public class ZuulFilterApp
{
public static void main( String[] args )
{
SpringApplication.run(ZuulFilterApp.class);
}
}
package com.ithzk.spring.cloud;
import com.netflix.zuul.ZuulFilter;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
/** * @author hzk * @date 2018/7/2 */
@Component
public class MyZuulFilter extends ZuulFilter {
/** * 类型包含 pre post route error * pre 代表在路由代理之前执行 * route 代表代理的时候执行 * error 代表出现错的时候执行 * post 代表在route 或者是 error 执行完成后执行 * @return */
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
/** * filter 为链式过滤器,多个filter按顺序执行 * javaee中根据filter先后配置顺序决定 * zuul 根据order决定, 由小到大 * @return */
@Override
public int filterOrder() {
return 1;
}
/** * 是否启用该过滤器 * @return */
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
System.out.println("MyZuulFilter execute!");
return null;
}
}
启动Eureka Server、provider和consumer以及这里的自定义filter的zuul服务,通过zuul访问consumer
http://192.168.126.1:10900/consumer-order/order/11
。
可以看出来过滤器效果实现了,那么我们再把过滤器类型修改为error然后把消费服务关闭。
package com.ithzk.spring.cloud;
import com.netflix.zuul.ZuulFilter;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
/** * @author hzk * @date 2018/7/2 */
@Component
public class MyZuulFilter extends ZuulFilter {
/** * 类型包含 pre post route error * pre 代表在路由代理之前执行 * route 代表代理的时候执行 * error 代表出现错的时候执行 * post 代表在route 或者是 error 执行完成后执行 * @return */
@Override
public String filterType() {
return FilterConstants.ERROR_TYPE;
}
/** * filter 为链式过滤器,多个filter按顺序执行 * javaee中根据filter先后配置顺序决定 * zuul 根据order决定, 由小到大 * @return */
@Override
public int filterOrder() {
return 1;
}
/** * 是否启用该过滤器 * @return */
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
System.out.println("MyZuulFilter error!");
return null;
}
}