zuul模块搭建 pom 还是之前的项目继续新增zuul模块。zuul模块继承framework-root ,然后在zuul的pom里配置如上坐标 配置文件 配置文件只需要…
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-zuulartifactId>
dependency>
framework-root
,然后在zuul的pom里配置如上坐标server:
port: 7070
spring:
application:
name: cloud-zuul
#eureka注册中心
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
instance:
prefer-ip-address: true
@EnableZuulProxy
@EnableEurekaClient
就是那么简单,上述步骤只需要三步 ,添加坐标,修改配置,添加启动类。就实现了zuul网关了。
上述zuul启动后http://localhost:7070/cloud-payment-service/payment/get/1
就会被代理到http://localhost:8001/payment/get/1
上。这里可能是8002
http://localhost:7070/cloud-order-service/order/get?id=123
就会被代理到http://localhost/order/get?id=123
上
但是我们并没有像nginx一样配置相关的请求转发呀 。因为zuul网关给我们配置了默认的转发规则。
zuul会为eureka上注册的服务都配置默认拦截。 localhost:7070/[serviceId]/**
会被转发到serviceId的其中一台机器上访问对应服务的 下面的接口。
zuul:
routes:
payment:
path: /cloud-payment-service2/**
serviceId: cloud-payment-service
cloud-zuul
模块新增配置。那么就会实现http://localhost:7070/cloud-payment-service2/payment/get/1
就会被代理到http://localhost:8001/payment/get/1
上。这里可能是8002。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RhyjeS3y-1620569159007)(http://oytmxyuek.bkt.clouddn.com/20210505image-20210423170731140.png)]
通配符 | 说明 |
---|---|
? | 匹配任意单个字符 /a , /b , /c |
* | 匹配任意数量字符 /abc |
** | 匹配任意层级的任意字符 /ab/c |
cloud-payment-service
, cloud-order-service
两个微服务。我们在zuul路由时想把cloud、service都去除。这种情况在配置文件可以实现。但是如果有200个服务,在配置文件中该是不是有点low。PatternServiceRouteMapper
登场了。PatternServiceRouteMapper
登场之前,我觉得我们有必要整理下java.util.regex.Matcher
这个类。因为PatternServiceRouteMapper
看名字我们就知道是通过正则进行接口匹配的。内部就是借助Matcher
来实现的。cloud-payment-service
, cloud-order-service
微服务接口转为类似payment
、 order
的服务名。(\w+)-(\w+)-(\w+)
PatternServiceRouteMapper
愣是没看懂他写的正则。突然感觉自己的正则白学了。PatternServiceRouteMapper
类中举例的格式。其中
这个一开始没看懂。有了这个就会去匹配了。这不就写死了吗。查阅资料才知道这是给group起别名了。?
此时name就是括号所在的group的别名。PatternServiceRouteMapper
PatternServiceRouteMapper
类也很简单。需要两个参数servicePattern
,routePattern
; 前者是正则,后者是整理后的格式。所以我们配置如下 @Bean
public PatternServiceRouteMapper patternServiceRouteMapper() {
return new PatternServiceRouteMapper(
"(?^.+)-(?.+)-(?.+$)" ,
"${name}");
}
假如现在我们要求每个接口参数中必须添加一个token。 为了方便演示我们不对value进行验证。在正规开发中这个value应该也是服务端授予的。
如果有token则放行,否则返回报错
com.netflix.zuul.ZuulFilter
即可。这个类中有四个方法需要我们实现。方法 | 作用 |
---|---|
filterType | 过滤器类型 |
filterOrder | 执行顺序 , 越小越先执行 |
shouldFilter | 是否需要执行 |
run | 具体逻辑 |
关于过滤器类型有pre、route、post、error四种类型。关于他们的执行顺序下面的代码应该解释的很清楚。
pre : 在路由之前执行 , 如果出现异常则会直接执行error和route
route : 在pre之后执行
post : 一切正常情况,会在route路由之后执行
error : 异常执行
在netflix-zuul中默认如下过滤器
在过滤器之间我们可以通过com.netflix.zuul.context.RequestContext
来获取上下文。我们也可以依赖他来进行数据的传递。
@Override
public Object run() throws ZuulException {
System.out.println("我是pre过滤器我被执行啦。。。。。。。。。。。。。。。");
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String token = request.getParameter("token");
if (StringUtils.isEmpty(token)) {
currentContext.setSendZuulResponse(false);
currentContext.setResponseStatusCode(401);
return null;
}
return null;
}
<dependency>
<groupId>org.codehaus.groovygroupId>
<artifactId>groovy-allartifactId>
<version>3.0.0version>
dependency>
FilterFileManager.init
接收两个参数 一个是时间间隔、一个是文件数组 @Bean
public FilterLoader filterLoader() {
FilterLoader instance = FilterLoader.getInstance();
instance.setCompiler(new GroovyCompiler());
try {
FilterFileManager.setFilenameFilter(new GroovyFileFilter());
FilterFileManager.init(5,"D:\\cloud\\filter\\pre");
} catch (Exception e) {
throw new RuntimeException("发生错误啦");
}
return instance;
}
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
public class PreFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
System.out.println("我是被动态加载进来的pre过滤器。。。。。。。。。。。。。。。");
return null;
}
}
D:\cloud\filter\pre
下就可以了 。 然后我们就等待5S在访问路由接口就可以看到我们新加的过滤了。management:
endpoints:
web:
exposure:
include: 'routes,filters'
http://localhost:7070/actuator
那什么是灰度发布呢? 灰度发布就是我们线上资源保留不动。我们只需要将新服务上线。此时原服务和新服务同时运行。这个时候我们将测试流量打向新服务。我们针对这个服务进行测试。测试通过后我们将放少部分流量给新服务试用一段时间然后收集这部分数据使用情况。数据满意后再将全部流量放到新服务上。当然这种发布方式肯定不满足我们传统项目的。灰度发布适用于分布式项目。想要灰度发布我们项目必须支持分布式。比如说我们后台服务的定时任务。多台服务同时在线的话如果不支持分布式那么就会重复执行。
io.jmnarloch.spring.cloud.ribbon.predicate.MetadataAwarePredicate
里实现了通过eureka里metadata属性来进行服务过滤的。 <dependency>
<groupId>io.jmnarlochgroupId>
<artifactId>ribbon-discovery-filter-spring-cloud-starterartifactId>
<version>2.1.0version>
dependency>
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka
instance:
prefer-ip-address: true
metadata-map:
lancher: 1
io.jmnarloch.spring.cloud.ribbon.predicate.MetadataAwarePredicate
我们只需要在zuul过滤器中指定我们需要访问的服务器的metadata属性就可以了 。比如说下面我们通过判断请求中参数是否包含new
参数来判断请求的服务器@Component
public class GreenFiler extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
if (request.getParameter("new") != null) {
// put the serviceId in `RequestContext`
RibbonFilterContextHolder.getCurrentContext().add("lancher", "1");
} else {
RibbonFilterContextHolder.getCurrentContext().add("lancher", "2");
}
return null;
}
}
http://localhost:7070/payment/payment/get/1?token=123&new
首先会验证token通过在根据有new参数被路由到lancher=1 的服务上,即最终访问http://localhost:8001/payment/get/1?token=123&new
上http://localhost:7070/payment/payment/get/1?token=123
会验证token并路由到8002上。Ribbon.Predicate
。想要对灰度发布进行扩展我们就离不开Predicate
//根据输入返回断言 true or false
@GwtCompatible
public interface Predicate<T> {
//针对输入内容进行断言,该方法有且不仅有如下要求: 1、不会造成任何数据污染 2、在T的equals中相等在apply中是相同效果
boolean apply(@Nullable T input);
//返回两个Predicate是否相同。一般情况Predicate实现是不需要重写equals的 。 如果实现可以根据自己需求表明predicate是否相同。什么叫做相同就是两个predicate对象apply的结果相同即为对象相同
@Override
boolean equals(@Nullable Object object);
}
@Test
public void pt() {
List<User> userList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
userList.add(new User(Long.valueOf(i+1), "张三"+(i+1)));
}
Predicate<User> predicate = new Predicate<User>() {
@Override
public boolean apply(User user) {
return user.getId() % 2 == 0;
}
};
ArrayList<User> users = Lists.newArrayList(Iterables.filter(userList, predicate));
System.out.println(users);
}
AbstractServerPredicate
是Predicate
的实现类。这个类也是Ribbon在获取服务列表的关键角色。因为后面都是基于这个类进行功能扩展的。BaseLoadBalancer
中进行负载均衡的。其内部的rule默认是new RoundRobinRule()
,因为我们引入了io-jmnarloch
。先看看内部的类结构io-jmnarloch
内部不是很复杂,至少Ribbon、feign这些比起来他真的是简单到家了。内部一个四个packagepackage | 作用 |
---|---|
api | 提供上下文,供外部使用 |
predicate | 提供获取服务列表过滤器 |
rule | ribbon中的负载均衡策略实现 |
support | 对上述的辅助包 |
RibbonDiscoveryRuleAutoConfiguration
中配置了rule包下定义好的Ribbon的负载均衡类Rule。BaseLoadBalancer
中我们可以看到rule就是我们rule.MetadataAwareRule
这个类。这里和ribbon章节说的好像有出入,我们在ribbon章节说需要自定义rule的时候需要在@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
这种方式。DiscoveryEnabledRule
的时候在注册的时候有 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
表示作用域MetadataAwareRule
结合代码我们可以了解到最终是PredicateBaseRule#choose 在选择服务列表这个predicate 就是我们choose中getPredicate()方法获取的。所以在ribbon进行选择服务之前会通过MetadataAwarePredicate
进行过滤服务。
获取到过滤器对象后,我们就会执行chooseRoundRibbinAfterFiltering
.
MetadataAwarePredicate
最终继承AbstractServerPredicate
。 而AbstractServerPredicate
# chooseRoundRobinAfterFiltering
是依赖getEligibleServers`来获取合适的服务列表的。AbstractServerPredicate
实现了好多chooseXXX的方法。因为ribbon默认是轮询方式所以在BaseLoadBalance中是选择Round对应的方法。这些我们都可以自己去修改方式。这里不赘述Eligible
译为合适的。getEligibleServers 翻译过来是获取合适的服务列表。这就是我们上述通过metadata-map:lancher
配置我们的服务信息。
下面是AbstractServerPredicate
精简后样子。主要就是getEligibleServers
这个方法。
AbstractServerPredicate
结构图中我们可以看到除了DiscoveryEnabledPredicate
这个子类外,还有四个子类。子类 | 作用 |
---|---|
AvailabilityPredicate | 过滤不可用服务器 |
CompositePredicate | 组合模式,保证服务数量一定数量。换句话说就是服务太少则会一个一个fallback知道服务数量达到要求 |
ZoneAffinityPredicate | 选取指定zone区域内的Server |
ZoneAvoidancePredicate | 避免使用符合条件的server . 和ZoneAffinityPredicate功能相反 |
zuul.ignored-patterns : /**/order/**
zuul:
routes:
user:
path: /cloud-user-service/**
url: forward:/zuul
http://localhost:7070/cloud-user-service/getTest?token=123
此时我们访问的接口会最终路由到/zuul/getTest
上。上面是我们zuul路由到payment的请求对象中信息。在请求头中我们添加了zxhtom=helloworld 。并且设置cookie对象 : Cookie_1=value
但是我们在payment中打印下两个值,cookie却没有带过来。
那是因为zuul在进行路由时为什么安全考虑会过滤掉敏感词请求头。 默认的Cookie、Set-Cookie、Authorization三个属性
但是我们为什么对路由分别对待。我们经常
zuul.routes.<router>.customSensitiveHeaders=ture
zuul.router.<router>.sensitiveHeaders=
@Component
public class ServerFallback implements FallbackProvider {
@Override
public String getRoute() {
return "cloud-payment-service";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
if (cause instanceof HystrixTimeoutException) {
return response(HttpStatus.GATEWAY_TIMEOUT);
} else {
return response(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
private ClientHttpResponse response(HttpStatus status) {
return new ClientHttpResponse() {
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("fallback".getBytes());
}
@Override
public HttpStatus getStatusCode() throws IOException {
return status;
}
@Override
public int getRawStatusCode() throws IOException {
return status.value();
}
@Override
public String getStatusText() throws IOException {
return status.getReasonPhrase();
}
@Override
public void close() {
}
};
}
}
cloud-payment-service
转换成了payment , 但是这个时候我们的getRoute里还是需要指定注册在eureka里的服务名及cloud-payment-service
,如果换成了payment
是没有效果的。这里读者自行测试下就理解了。有时间准备研读下 ZuulProxyAutoConfiguration
上述相关源码