简介
微服务具有系统小(一个程序员可以独立完成开发),可以独立部署,能快速进行迭代等优点。因为系统切分的小,必然也就意味着会有更多的系统需要进行维护。在实际应用中,相关的系统一般部署在同一个机房,内部之间通过Eureka的服务发现机制与Ribbon客户端负载便可以很好的实现系统间的调用。而外部的应用如何来访问公司内部各种各样的微服务呢?在微服务架构中,后端服务往往不直接开放给调用端,而是通过一个API网关根据请求的url,路由到相应的服务。当添加API网关后,在第三方调用端和服务提供方之间就创建了一面墙,这面墙直接与调用方通信进行权限控制,后将请求均衡分发给后台服务端。(功能上应该和Nginx差不多,Zuul基于Eureka的服务发现功能动态实现路由的功能)
简单实例
Zuul属于Netflix下的开源项目,我们在项目中可以单独使用,官方地址:https://github.com/Netflix/zuul。由于博文属于Spring Cloud系列,我们的简单实例还是在Spring Cloud下面来实现一下Zuul版的Hello world。
- 还是万年不变的导入依赖的包,pom.xml文件的配置如下:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Edgware.SR4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
</dependencies>
- 编写启动类App.java,这里用到了一个新的注解EnableZuulProxy。代码如下:
package com.ivan.zuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
/**
*
* 功能描述:
*
* @version 2.0.0
* @author zhiminchen
*/
@SpringBootApplication
@EnableZuulProxy
public class App
{
public static void main( String[] args )
{
SpringApplication.run(App.class, args);
}
}
- 编写配置文件,代码如下
server.port=10000
zuul.routes.provider.path=/provider/**
zuul.routes.provider.url=http://localhost:8000/
上面配置zuul.routes 的 provider可以是任何值,其中的path指的是访问Zuul这个系统时的路径,而下面的url指的就是在访问上面的path的时候会路由的路径,是不是很简单。Zuul对访问路径做了一层代理,我们可以基于Zuul做数据的裁剪以及聚合功能。权限过滤也可以放在这一层。
Zuul与Eureka整合,实现服务自动发现与负载均衡
- 首先还是在pom.xml里加入Eureka的依赖。代码如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
- 修改application.properties文件
server.port=10000
spring.application.name=zuul
eureka.instance.hostname=localhost
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
ribbon.ReadTimeout=5000
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
zuul.routes.provider.path=/provider/**
zuul.routes.provider.serviceId=PROVIDER
可以看出上面的属性path对应了相应的serviceId。而这个serviceId就是我们微服务配置的spring.application.name属性。上面的配置自动集成了Ribbon的负载均衡。
Zuul源码解读
参考文章:https://blog.csdn.net/forezp/article/details/76211680
![5796101-56a0969b19dc9050](https://i-blog.csdnimg.cn/blog_migrate/72d1fd66ca4d0df3eae42de3168c079c.webp?x-image-process=image/format,png)
在zuul中, 整个请求的过程是这样的,首先将请求给zuulservlet处理,zuulservlet中有一个zuulRunner对象,该对象中初始化了RequestContext:作为存储整个请求的一些数据,并被所有的zuulfilter共享。zuulRunner中还有 FilterProcessor,FilterProcessor作为执行所有的zuulfilter的管理器。FilterProcessor从filterloader 中获取zuulfilter,而zuulfilter是被filterFileManager所加载,并支持groovy热加载,采用了轮询的方式热加载。有了这些filter之后,zuulservelet首先执行的Pre类型的过滤器,再执行route类型的过滤器,最后执行的是post 类型的过滤器,如果在执行这些过滤器有错误的时候则会执行error类型的过滤器。执行完这些过滤器,最终将请求的结果返回给客户端。Zuul过滤器的运行机制如下图:
![5796101-5688c9e5af1906b5.png](https://i-blog.csdnimg.cn/blog_migrate/91d06f0b4f9b13d5c14368900b549de2.webp?x-image-process=image/format,png)
上面的解释来源于网络,我们还以从EnableZuulProxy这个注解看看他帮我们做了些什么事情吧。EnableZuulProxy 的代码如下:
package org.springframework.cloud.netflix.zuul;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.context.annotation.Import;
@EnableCircuitBreaker
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}
可以看出这里还是通过Spring 的Import机制加载了ZuulProxyMarkerConfiguration这个类。现在看看ZuulProxyMarkerConfiguration这个类都做了些什么。源代码如下:
package org.springframework.cloud.netflix.zuul;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ZuulProxyMarkerConfiguration {
@Bean
public Marker zuulProxyMarkerBean() {
return new Marker();
}
class Marker {
}
}
可以看到这个类首先是由Configuration注解类修饰,这样这个类里申明的Bean会被加载到Spring IOC容器里,从这个类里看,并没有加载我们希望看到的zuulservlet对象,而且这个Marker类没有任何代码,只是做了一下申明。在相同的包下,我们找到了ZuulProxyAutoConfiguration这个类,以下是这个类的截图:
![5796101-287a6b18dd94d3dd.png](https://i-blog.csdnimg.cn/blog_migrate/4c6310b89ef11009413f3ee88170cc2b.webp?x-image-process=image/format,png)
这个类继承ZuulServerAutoConfiguration,在这个类里加载了zuulServlet,代码如下:
@Bean
@ConditionalOnMissingBean(name = "zuulServlet")
public ServletRegistrationBean zuulServlet() {
ServletRegistrationBean servlet = new ServletRegistrationBean(new ZuulServlet(),
this.zuulProperties.getServletPattern());
// The whole point of exposing this servlet is to provide a route that doesn't
// buffer requests.
servlet.addInitParameter("buffer-requests", "false");
return servlet;
}
debug看下这个ZuulServlet对应的url是什么。效果如下:
![5796101-b5f3edfc2c47468c.png](https://i-blog.csdnimg.cn/blog_migrate/990d35106460bde9a5cf2a33e80633f9.webp?x-image-process=image/format,png)
这里配置的url为/zuul/*,但很明显我们在测试的时候并没有用在访问url上面加上zuul。那这个zuulServlet是如何做到拦截的呢。我们找到了ZuulController类,这个类持有zullServlet的代理。而zullController的handlerMapping类拦截的是/,代码截图如下:
![5796101-35f0ce16a46400d2.png](https://i-blog.csdnimg.cn/blog_migrate/7e7832a3ff5a25aae711c885d64eeecc.webp?x-image-process=image/format,png)
请求URL映射就分析到这里了,下面我们来看看 zullServlet里是如何处理请求的。
service方法如下:
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
// Marks this request as having passed through the "Zuul engine", as opposed to servlets
// explicitly bound in web.xml, for which requests will not have the same data attached
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
preRoute();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
postRoute();
} catch (ZuulException e) {
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
这里的处理逻辑结合下面的图是不是很清晰了。
![5796101-5688c9e5af1906b5.png](https://i-blog.csdnimg.cn/blog_migrate/91d06f0b4f9b13d5c14368900b549de2.webp?x-image-process=image/format,png)
我们捡最重要的route()方法看看是如何处理逻辑的吧。代码如下:
/**
* executes "post" ZuulFilters
*
* @throws ZuulException
*/
void postRoute() throws ZuulException {
zuulRunner.postRoute();
}
/**
* executes "route" filters
*
* @throws ZuulException
*/
void route() throws ZuulException {
zuulRunner.route();
}
/**
* executes "pre" filters
*
* @throws ZuulException
*/
void preRoute() throws ZuulException {
zuulRunner.preRoute();
}
最终处理的还是zuulRunner对象。而在zuulRunner里的处理逻辑就更简单了,代码如下:
public void route() throws ZuulException {
FilterProcessor.getInstance().route();
}
直接交给了FilterProcessor处理,FilterProcessor的代码如下,我只截取部分代友,并做相应的注释:
public class FilterProcessor {
//zuulRunner的route方法会调到这个方法里
public void route() throws ZuulException {
try {
//传入的route代表的是一种filter类型,目前有"pre", "route", "post" 三种类型
runFilters("route");
} catch (ZuulException e) {
throw e;
} catch (Throwable e) {
throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_ROUTE_FILTER_" + e.getClass().getName());
}
}
/**
这个方法里会根据传入的filter类型,执行相应的filter逻辑
**/
public Object runFilters(String sType) throws Throwable {
if (RequestContext.getCurrentContext().debugRouting()) {
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
//FilterLoader 里持用所有的filter对象
List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
if (list != null) {
for (int i = 0; i < list.size(); i++) {
ZuulFilter zuulFilter = list.get(i);
//执行具体的filter
Object result = processZuulFilter(zuulFilter);
if (result != null && result instanceof Boolean) {
bResult |= ((Boolean) result);
}
}
}
return bResult;
}
}
/**
这个方法会处理具体的ZuulFilter
**/
public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
//处理的上下文
RequestContext ctx = RequestContext.getCurrentContext();
boolean bDebug = ctx.debugRouting();
final String metricPrefix = "zuul.filter-";
long execTime = 0;
String filterName = "";
try {
long ltime = System.currentTimeMillis();
filterName = filter.getClass().getSimpleName();
RequestContext copy = null;
Object o = null;
Throwable t = null;
if (bDebug) {
Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);
copy = ctx.copy();
}
//在这里会执行run方法啦
ZuulFilterResult result = filter.runFilter();
ExecutionStatus s = result.getStatus();
execTime = System.currentTimeMillis() - ltime;
switch (s) {
case FAILED:
t = result.getException();
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
break;
case SUCCESS:
o = result.getResult();
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
if (bDebug) {
Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
Debug.compareContextState(filterName, copy);
}
break;
default:
break;
}
if (t != null) throw t;
usageNotifier.notify(filter, s);
return o;
} catch (Throwable e) {
if (bDebug) {
Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage());
}
usageNotifier.notify(filter, ExecutionStatus.FAILED);
if (e instanceof ZuulException) {
throw (ZuulException) e;
} else {
ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
throw ex;
}
}
}
代码分析到这里,整个流程也就走的差不多了,但还是有个疑问,我们自定义的ZuulFilter系统是如何发现的呢?FilterLoader又是如何根据类型取到ZuulFilter?带着这样的疑问我们来看一下FilterLoader类,关键代码有做相应的注释,代码如下:
public class FilterLoader {
final static FilterLoader INSTANCE = new FilterLoader();
private static final Logger LOG = LoggerFactory.getLogger(FilterLoader.class);
private final ConcurrentHashMap<String, Long> filterClassLastModified = new ConcurrentHashMap<String, Long>();
private final ConcurrentHashMap<String, String> filterClassCode = new ConcurrentHashMap<String, String>();
private final ConcurrentHashMap<String, String> filterCheck = new ConcurrentHashMap<String, String>();
private final ConcurrentHashMap<String, List<ZuulFilter>> hashFiltersByType = new ConcurrentHashMap<String, List<ZuulFilter>>();
//最终所有的filter都是注册到这个类上的,类的名字已经可以看出它的作用了
private FilterRegistry filterRegistry = FilterRegistry.instance();
static DynamicCodeCompiler COMPILER;
static FilterFactory FILTER_FACTORY = new DefaultFilterFactory();
// overidden by tests
public void setFilterRegistry(FilterRegistry r) {
this.filterRegistry = r;
}
/**
* @return Singleton FilterLoader
*/
public static FilterLoader getInstance() {
return INSTANCE;
}
}
public List<ZuulFilter> getFiltersByType(String filterType) {
//hashFiltersByType 相当于FilterLoader内部的缓存
List<ZuulFilter> list = hashFiltersByType.get(filterType);
if (list != null) return list;
list = new ArrayList<ZuulFilter>();
//从这里可以看出 filterRegistry 一定集成了一个集合对象
Collection<ZuulFilter> filters = filterRegistry.getAllFilters();
for (Iterator<ZuulFilter> iterator = filters.iterator(); iterator.hasNext(); ) {
ZuulFilter filter = iterator.next();
if (filter.filterType().equals(filterType)) {
list.add(filter);
}
}
Collections.sort(list); // sort by priority
hashFiltersByType.putIfAbsent(filterType, list);
return list;
}
FilterRegistry 类的代码就很简单啦,内部持有一个ConcurrentHashMap。代码如下:
public class FilterRegistry {
private static final FilterRegistry INSTANCE = new FilterRegistry();
public static final FilterRegistry instance() {
return INSTANCE;
}
private final ConcurrentHashMap<String, ZuulFilter> filters = new ConcurrentHashMap<String, ZuulFilter>();
private FilterRegistry() {
}
public ZuulFilter remove(String key) {
return this.filters.remove(key);
}
public ZuulFilter get(String key) {
return this.filters.get(key);
}
public void put(String key, ZuulFilter filter) {
this.filters.putIfAbsent(key, filter);
}
public int size() {
return this.filters.size();
}
public Collection<ZuulFilter> getAllFilters() {
return this.filters.values();
}
}
到现在我们已经知道如何根据类型取ZuulFilter对象了,但FilterRegistry 里的ZuulFilter是如何put进去的,FilterRegistry 类的ConcurrentHashMap里的key又是什么呢?必竟我们上面在写实例的时候并没有调用这个方法。带着这样的问题,我在ZuulServerAutoConfiguration类发现初使化的地方,下图是debug状态。
![5796101-8b834f21eab30dac.png](https://i-blog.csdnimg.cn/blog_migrate/233bc9e1bf610f8d64c31c1990406f3d.webp?x-image-process=image/format,png)
到这里我们就分析完了Zuul的关键代码。下面自定义一个ZuulFilter看看是否能起作用
自定义ZuulFilter
有了上面的源码解析,要自定义ZuulFilter就很容易了,直接上代码吧:
package com.ivan.zuul.filter;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.exception.ZuulException;
@Component
public class ZuulFiterTest extends ZuulFilter{
//返回为true才会执行
public boolean shouldFilter() {
return true;
}
public Object run() throws ZuulException {
System.out.println("执行了里面的逻辑啦");
return "true";
}
//ZuulFilter的类型
@Override
public String filterType() {
return FilterConstants.ROUTE_TYPE;
}
/**
* 优先级
*/
@Override
public int filterOrder() {
return 0;
}
}
效果如下图:
![5796101-873c53a748c7288b.png](https://i-blog.csdnimg.cn/blog_migrate/769d843f6b807b8872ad1e6c3acb9254.webp?x-image-process=image/format,png)
可以看到我们自定义的Filter已经起作用了,对于特殊的路由规则,我们可以定义自已的ZuulFilter。比如可以实现基于Zuul做数据的裁剪以及聚合功能。