示例
zuul作为springcloud的网关使用路由功能是,会默认把路由的前缀去掉
比如路由前:http://127.0.0.1:8181/api/demo/list
路由后为: http://192.168.1.100:8080/demo/list
如果我们需要让他不去掉api这个前缀,那么需要在对应路由上加上该属性: strip-prefix: false
源码解析
首先我们从最开始加载zuul说起
@EnableZuulProxy
最开始的配置日入口是这里
1
2
3
4
5
6EnableCircuitBreaker
({ElementType.TYPE})
(RetentionPolicy.RUNTIME)
.class}) ({ZuulProxyMarkerConfiguration
public @interface EnableZuulProxy {
}可以看到这里引用了ZuulProxyMarkerConfiguration
ZuulProxyMarkerConfiguration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ZuulProxyMarkerConfiguration {
public ZuulProxyMarkerConfiguration() {
}
public ZuulProxyMarkerConfiguration.Marker zuulProxyMarkerBean() {
return new ZuulProxyMarkerConfiguration.Marker();
}
class Marker {
Marker() {
}
}
}这里仅仅是加载了ZuulProxyMarkerConfiguration.Marker类型的bean,别无他操作
spring.factories
在Spring Boot中有一种非常解耦的扩展机制:Spring Factories,所以我们找到jar内的MEAT-INF/spring.factories
1
2
3//部分内容
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration这里声明了ZuulProxyAutoConfiguration作为实现类被发现
ZuulProxyAutoConfiguration
1
2
3
4
5
6
7
.class, OkHttpRibbonConfiguration.class, HttpClientRibbonConfiguration.class, HttpClientConfiguration.class}) ({RestClientRibbonConfiguration
//重点在这里 Marker的bean被装载,这个bean也会被装载
@ConditionalOnBean({Marker.class})
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {
.....
}ZuulServerAutoConfiguration
ZuulProxyAutoConfiguration的父类为ZuulServerAutoConfiguration,这里面就加载了我们在最前面提到的ZuulProperties
1
2
3
4
5
6
7
8
9
10
.class}) ({ZuulProperties
@ConditionalOnClass({ZuulServlet.class})
@ConditionalOnBean({Marker.class})
@Import({ServerPropertiesAutoConfiguration.class})
public class ZuulServerAutoConfiguration {
protected ZuulProperties zuulProperties;
....
}ZuulProperties
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"zuul") (
public class ZuulProperties {
public static final List<String> SECURITY_HEADERS = Arrays.asList("Pragma", "Cache-Control", "X-Frame-Options", "X-Content-Type-Options", "X-XSS-Protection", "Expires");
private String prefix = "";
//默认为true
private boolean stripPrefix = true;
private Boolean retryable = false;
//加载了路由规则项,项值为ZuulRoute
private Map<String, ZuulProperties.ZuulRoute> routes = new LinkedHashMap();
private boolean addProxyHeaders = true;
private boolean addHostHeader = false;
private Set<String> ignoredServices = new LinkedHashSet();
private Set<String> ignoredPatterns = new LinkedHashSet();
private Set<String> ignoredHeaders = new LinkedHashSet();
private boolean ignoreSecurityHeaders = true;
private boolean forceOriginalQueryStringEncoding = false;
private String servletPath = "/zuul";
private boolean ignoreLocalService = true;
private ZuulProperties.Host host = new ZuulProperties.Host();
private boolean traceRequestBody = true;
private boolean removeSemicolonContent = true;
private Set<String> sensitiveHeaders = new LinkedHashSet(Arrays.asList("Cookie", "Set-Cookie", "Authorization"));
private boolean sslHostnameValidationEnabled = true;
private ExecutionIsolationStrategy ribbonIsolationStrategy;
private ZuulProperties.HystrixSemaphore semaphore;
private ZuulProperties.HystrixThreadPool threadPool;
public ZuulProperties() {
this.ribbonIsolationStrategy = ExecutionIsolationStrategy.SEMAPHORE;
this.semaphore = new ZuulProperties.HystrixSemaphore();
this.threadPool = new ZuulProperties.HystrixThreadPool();
}
......
}ZuulRoute
1
2
3
4
5
6
7
8
9
10
11
12public static class ZuulRoute {
private String id;
private String path;
private String serviceId;
private String url;
//默认为true
private boolean stripPrefix = true;
private Boolean retryable;
private Set<String> sensitiveHeaders = new LinkedHashSet();
private boolean customSensitiveHeaders = false;
.......
}zuulController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//在org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration中加载了
zuulController
@Bean
public ZuulController zuulController() {
return new ZuulController();
}
--------------------------
public class ZuulController extends ServletWrappingController {
public ZuulController() {
//设置ServletClass为ZuulServlet
this.setServletClass(ZuulServlet.class);
this.setServletName("zuul");
this.setSupportedMethods((String[])null);
}
}ZuulServlet
zuul核心。用来拦截指定url,进行filter拦截处理
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89public class ZuulServlet extends HttpServlet {
private static final long serialVersionUID = -3374242278843351500L;
private ZuulRunner zuulRunner;
public ZuulServlet() {
}
public void init(ServletConfig config) throws ServletException {
super.init(config);
String bufferReqsStr = config.getInitParameter("buffer-requests");
boolean bufferReqs = bufferReqsStr != null && bufferReqsStr.equals("true");
this.zuulRunner = new ZuulRunner(bufferReqs);
}
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
try {
//初始化RequestContext,供后续拦截器使用
this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse);
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
//preRoute
try {
this.preRoute();
} catch (ZuulException var12) {
this.error(var12);
this.postRoute();
return;
}
try {
this.route();
} catch (ZuulException var13) {
this.error(var13);
this.postRoute();
return;
}
try {
this.postRoute();
} catch (ZuulException var11) {
this.error(var11);
}
} catch (Throwable var14) {
this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
void postRoute() throws ZuulException {
this.zuulRunner.postRoute();
}
void route() throws ZuulException {
this.zuulRunner.route();
}
void preRoute() throws ZuulException {
this.zuulRunner.preRoute();
}
void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
this.zuulRunner.init(servletRequest, servletResponse);
}
void error(ZuulException e) {
RequestContext.getCurrentContext().setThrowable(e);
this.zuulRunner.error();
}
.class) (MockitoJUnitRunner
public static class UnitTest {
HttpServletRequest servletRequest;
HttpServletResponseWrapper servletResponse;
FilterProcessor processor;
PrintWriter writer;
public UnitTest() {
}
public void before() {
MockitoAnnotations.initMocks(this);
}
}ZuulFilterConfiguration
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//同样在:org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration
protected static class ZuulFilterConfiguration {
//自动装载所有ZuulFilter实现的bean
private Map<String, ZuulFilter> filters;
protected ZuulFilterConfiguration() {
}
public ZuulFilterInitializer zuulFilterInitializer(CounterFactory counterFactory, TracerFactory tracerFactory) {
FilterLoader filterLoader = FilterLoader.getInstance();
FilterRegistry filterRegistry = FilterRegistry.instance();
//注册至FilterRegistry,在zuulservlet中被用到
return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry);
}
}
-------------------------------------
//com.netflix.zuul.FilterProcessor#runFilters
public Object runFilters(String sType) throws Throwable {
if (RequestContext.getCurrentContext().debugRouting()) {
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
if (list != null) {
for(int i = 0; i < list.size(); ++i) {
ZuulFilter zuulFilter = (ZuulFilter)list.get(i);
Object result = this.processZuulFilter(zuulFilter);
if (result != null && result instanceof Boolean) {
bResult |= (Boolean)result;
}
}
}
return bResult;
}后续就是关于strip-prefix的
PreDecorationFilter
PreDecorationFilter是zuul默认的type为pre的拦截器,主要作用为处理上下文请求,获取route等功能
调用链如下:
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112public class PreDecorationFilter extends ZuulFilter {
private static final Log log = LogFactory.getLog(PreDecorationFilter.class);
/** @deprecated */
public static final int FILTER_ORDER = 5;
private RouteLocator routeLocator;
private String dispatcherServletPath;
private ZuulProperties properties;
private UrlPathHelper urlPathHelper = new UrlPathHelper();
private ProxyRequestHelper proxyRequestHelper;
public PreDecorationFilter(RouteLocator routeLocator, String dispatcherServletPath, ZuulProperties properties, ProxyRequestHelper proxyRequestHelper) {
this.routeLocator = routeLocator;
this.properties = properties;
this.urlPathHelper.setRemoveSemicolonContent(properties.isRemoveSemicolonContent());
this.dispatcherServletPath = dispatcherServletPath;
this.proxyRequestHelper = proxyRequestHelper;
}
public int filterOrder() {
return 5;
}
public String filterType() {
return "pre";
}
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return !ctx.containsKey("forward.to") && !ctx.containsKey("serviceId");
}
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
Route route = this.routeLocator.getMatchingRoute(requestURI);
String location;
String xforwardedfor;
String remoteAddr;
if (route != null) {
location = route.getLocation();
if (location != null) {
ctx.put("requestURI", route.getPath());
ctx.put("proxy", route.getId());
if (!route.isCustomSensitiveHeaders()) {
this.proxyRequestHelper.addIgnoredHeaders((String[])this.properties.getSensitiveHeaders().toArray(new String[0]));
} else {
this.proxyRequestHelper.addIgnoredHeaders((String[])route.getSensitiveHeaders().toArray(new String[0]));
}
if (route.getRetryable() != null) {
ctx.put("retryable", route.getRetryable());
}
if (!location.startsWith("http:") && !location.startsWith("https:")) {
if (location.startsWith("forward:")) {
ctx.set("forward.to", StringUtils.cleanPath(location.substring("forward:".length()) + route.getPath()));
ctx.setRouteHost((URL)null);
return null;
}
ctx.set("serviceId", location);
ctx.setRouteHost((URL)null);
ctx.addOriginResponseHeader("X-Zuul-ServiceId", location);
} else {
ctx.setRouteHost(this.getUrl(location));
ctx.addOriginResponseHeader("X-Zuul-Service", location);
}
if (this.properties.isAddProxyHeaders()) {
this.addProxyHeaders(ctx, route);
xforwardedfor = ctx.getRequest().getHeader("X-Forwarded-For");
remoteAddr = ctx.getRequest().getRemoteAddr();
if (xforwardedfor == null) {
xforwardedfor = remoteAddr;
} else if (!xforwardedfor.contains(remoteAddr)) {
xforwardedfor = xforwardedfor + ", " + remoteAddr;
}
ctx.addZuulRequestHeader("X-Forwarded-For", xforwardedfor);
}
if (this.properties.isAddHostHeader()) {
ctx.addZuulRequestHeader("Host", this.toHostHeader(ctx.getRequest()));
}
}
} else {
log.warn("No route found for uri: " + requestURI);
xforwardedfor = this.dispatcherServletPath;
if (RequestUtils.isZuulServletRequest()) {
log.debug("zuulServletPath=" + this.properties.getServletPath());
location = requestURI.replaceFirst(this.properties.getServletPath(), "");
log.debug("Replaced Zuul servlet path:" + location);
} else {
log.debug("dispatcherServletPath=" + this.dispatcherServletPath);
location = requestURI.replaceFirst(this.dispatcherServletPath, "");
log.debug("Replaced DispatcherServlet servlet path:" + location);
}
if (!location.startsWith("/")) {
location = "/" + location;
}
remoteAddr = xforwardedfor + location;
remoteAddr = remoteAddr.replaceAll("//", "/");
ctx.set("forward.to", remoteAddr);
}
return null;
}
........
}org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator
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
35protected Route getRoute(ZuulRoute route, String path) {
if (route == null) {
return null;
} else {
if (log.isDebugEnabled()) {
log.debug("route matched=" + route);
}
String targetPath = path;
String prefix = this.properties.getPrefix();
if (prefix.endsWith("/")) {
prefix = prefix.substring(0, prefix.length() - 1);
}
if (path.startsWith(prefix + "/") && this.properties.isStripPrefix()) {
targetPath = path.substring(prefix.length());
}
//这里获取了之前设置的StripPrefix,默认true,处理targetPath
if (route.isStripPrefix()) {
int index = route.getPath().indexOf("*") - 1;
if (index > 0) {
String routePrefix = route.getPath().substring(0, index);
targetPath = targetPath.replaceFirst(routePrefix, "");
prefix = prefix + routePrefix;
}
}
Boolean retryable = this.properties.getRetryable();
if (route.getRetryable() != null) {
retryable = route.getRetryable();
}
//封装为新的Route对象供后续filter使用
return new Route(route.getId(), targetPath, route.getLocation(), prefix, retryable, route.isCustomSensitiveHeaders() ? route.getSensitiveHeaders() : null, route.isStripPrefix());
}
}
至此为止,我们已经清晰地明白了StripPrefix是如何起作用的,也大概明白了zuul的运行流程。
后序
zuul核心在于其依赖spring servlet,再加上自定义的拦截器实现请求的转发等
Zuul 中一共有四种不同生命周期的 Filter,分别是:
- pre:在 Zuul 按照规则路由到下级服务之前执行。如果需要对请求进行预处理,比如鉴权、限流等,都应考虑在此类 Filter 实现。
- route:这类 Filter 是 Zuul 路由动作的执行者,是 Apache Http Client 或 Netflix Ribbon 构建和发送原始 HTTP 请求的地方,目前已支持 Okhttp。
- post:这类 Filter 是在源服务返回结果或者异常信息发生后执行的,如果需要对返回信息做一些处理,则在此类 Filter 进行处理。
- error:在整个生命周期内如果发生异常,则会进入 error Filter,可做全局异常处理。
在实际项目中,往往需要自实现以上类型的 Filter 来对请求链路进行处理,根据业务的需求,选取相应生命周期的 Filter 来达成目的。在 Filter 之间,通过 com.netflix.zuul.context.RequestContext 类来进行通信,内部采用 ThreadLocal 保存每个请求的一些信息,包括请求路由、错误信息、HttpServletRequest、HttpServletResponse,这使得一些操作是十分可靠的,它还扩展了 ConcurrentHashMap,目的是为了在处理过程中保存任何形式的信息。