前面几篇,简单总结了sping cloud应用中需要用到的几个基础组件,包括服务的注册与发现(eureka),服务的消费(ribbon,fegin),负载均衡,断路器(hystrix),这篇文章简单总结一下另一个比较重要的基础组件,路由网关(zuul),那么路由网关是什么?它能干什么?为什么要用它呢?
先上一张从网上找到的图,这张图的表示的是以微服务为基础的分布式系统的架构图,如果这幅图有点不太明白,先往下看,稍后再回过头来看:
分布式架构的基础是微服务,所有的功能模块都会被抽成一个独立的微服务,所有的外部调用都要先经过一个总的入口,这个总的入口就相当于一道墙,把外部与内部的所有服务接口隔开,所有外部调用都要先经过这个总入口,由这个总的入口来决定,要调用哪个服务,这个总的入口就是路由网关,然后路由网关决定你需要调用哪些服务,内部的服务之间也是可以相互调用,如果有必要很多的服务会集群部署,包括总入口路由网关。有的人就会问了,为什么要在外部与内部所有的服务之间加了这道墙呢?那是因如果让所有外部调用直接去调用自己所需要的服务接口,从安全性上来说,每个服务接口都要加上权限控制,这样一来权限控制的代码不可避免的会污染到服务接口的业务逻辑代码,其次每一个服务都加上权限控制,也造成了代码的重复,所以如果让外部调用直接调用内部服务接口会不可避免出现一系列的问题。
为了解决上面的这些问题,有人就提出一种解决问题的思路,就是在外部与内部的服务之间加上一个的总入口,把权限控制等这样重复性的功能抽离出来,放到这个总的入口出,这个的入口就是这篇文章要介绍的服务的路由网关。
因此对于分布式系统来说,服务网关是不可或缺的部分,它具备了服务路由、负载均衡、权限控制等功能,为内部的所有微服务提供了一个前门保护的作用,spring cloud netflix的zuul就担任了这样一个角色。
下面通过实例了解一下具体是怎么使用的:
新建一个maven项目:serviceGateway
Pom.xml:
xml version="1.0" encoding="UTF-8"?>
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
在启动类ServiceGatewayApplication.java上加注解 @EnableZuulProxy,开启zuul功能
,这里加一个小技巧,@SpringCloudApplication相当于@SpringBootApplication
、@EnableDiscoveryClient
、@EnableCircuitBreaker
三个功能,主要是简化配置。
package com.gaox.serviceGateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ServiceGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceGatewayApplication.class, args);
}
}
application.properties:
server.port=8766
spring.application.name=serviceGateWay
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
zuul.routes.zhangsanHello.path=/zhangsanHello/**
zuul.routes.zhangsanHello.serviceId=zhangsanService
zuul.routes.lisiHello.path=/lisiHello/**
zuul.routes.lisiHello.serviceId=lisiService
通过路由网关的功能,对外提供的服务只需要暴露zuul中配置的调用地址就可以让外部统一调用内部的服务,而不需要了解具体提供服务的主机信息了,zuul提供了两种的映射方式
第一种,也是比较推荐的一种,就是上面的配置,所有的内部服务都在eureka server服务注册中心注册,zuul的路由网关也注册到eureka server,所有服务之间可以彼此发现,这样以来zuul服务也就能在eureka server发现其他服务,我们就可以实现对serviceId的映射。
第二种就是如下配置
server.port=8766
spring.application.name=serviceGateWay
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
zuul.routes.zhangsanHello.path=/zhangsanHello/**
zuul.routes.zhangsanHello.serviceId=zhangsanService
#zuul.routes.lisiHello.path=/lisiHello/**
#zuul.routes.lisiHello.serviceId=lisiService
zuul.routes.lisiHello.path=/lisiHello/**
zuul.routes.lisiHello.url=http://localhost:8765/
所有的符合/lisiHello/**的访问都会映射到http://localhost:8765/ , 也就是说当我们访问http://localhost:8766/lisiHello/hello?name=lisi的时候,zuul会将该请求路由到http://localhost:8765/hello?name=lisi上。
之所以不推荐这一种是因为如果内部的服务大多采用的是集群部署,我们需要知道所有的微服务的主要地址和端口,才能达到所有的映射配置,同时也失去了前端负载均衡的功能,不能根据实时情况合理分配请求到哪些服务。实际上,服务名与服务实例地址的关系在把服务在eureka server中注册后就已经存在了,我们只需要将zuul的服务也注册到eureka server中就可以发现其他服务,也就可以实现对serviceId的映射。
用第一种方法配置后,依次启动serviceCenter、helloService、zhangsanService、lisiService、serviceGateway,然后打开浏览器访问http://localhost:8766/lisiHello/hello?name=lisi
,第一次调用时可能会连接超时,后台报异常,不会没关系,多刷新两遍就会交替出现下面内容:
你好,lisi。我是helloService,端口是8763
你好,lisi。我是helloService,端口是8762
至于第一次为什么会报超时异常,以后再调用就存在异常的原因,一时还这不是很明白,这里推荐一篇文章,也许对我们有帮助:https://juejin.im/entry/5a114ca6f265da4335625a36
上面说了,对一些通用的杼控制可以抽离到路由网关里,这里用到的就是路由网关的过滤器,在完成了服务的路由之后,通过过滤器来实现对服务的安全控制。在服务网关中定义过滤器只需要继承ZuulFilter抽象类并实现其定义的四个抽象方法就可以对请求进行拦截和过滤。
MyFilter.java:
package com.gaox.serviceGateway;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.apache.log4j.Logger;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
/**
* Created by Administrator on 2018/3/8.
*/
public class MyFilter extends ZuulFilter {
private Logger logger=Logger.getLogger(MyFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
RequestContext rt= RequestContext.getCurrentContext();
HttpServletRequest request=rt.getRequest();
Enumeration parameterName= request.getParameterNames();
List list=new ArrayList<>();
while (parameterName.hasMoreElements()){
String name=parameterName.nextElement();
list.add(name);
System.out.println(name+":"+request.getParameter(name));
}
if(list.size()<=0){
rt.setSendZuulResponse(false);
rt.setResponseBody("At least one parameter");
}
return null;
}
}
在启动的时候在容器中实例化该过滤器,就可以起作用了:
@Bean
public MyFilter accessFilter(){
return new MyFilter();
}
filterType:返回一个字符串代表过滤器的类型,在zuul中定义了四种不同生命周期的过滤器类型,具体如下:
pre:可以在请求被路由之前调用
routing:在路由请求时候被调用
post:在routing和error过滤器之后被调用
error:处理请求时发生错误时被调用
filterOrder:通过int值来定义过滤器的执行顺序
shouldFilter:返回一个boolean类型来判断该过滤器是否要执行,所以通过此函数可实现过滤器的开关。这里,我们直接返回true,所以该过滤器总是生效。
run:过滤器的具体逻辑。需要注意,这里我们通过rt.setSendZuulResponse(false)令zuul过滤该请求,不对其进行路由,然后通过rt.setResponseStatusCode(401)设置了其返回的错误码,当然我们也可以进一步优化我们的返回,比如,通过rt.setResponseBody(body)对返回body内容进行编辑等。,或者重新定向到一个错误页面
以上为自己一点一点总结,有兴趣的小伙伴可以加我微信咱们一起讨论研究哦