前言
zuul 是netflix 提供的一個開源組件,致力于在云平臺上提供動態路由,監控,彈性,安全等邊緣服務的框架。也有很多公司使用它來作為網關的重要組成部分,碰巧今年公司的架構組決定自研一個網關產品,集動態路由,動態權限,限流配額等功能為一體,為其他部門的項目提供統一的外網調用管理,最終形成產品(這方面阿里其實已經有成熟的網關產品了,但是不太適用于個性化的配置,也沒有集成權限和限流降級)。
不過這里并不想介紹整個網關的架構,而是想著重于討論其中的一個關鍵點,并且也是經常在交流群中聽人說起的:動態路由怎么做?
再闡釋什么是動態路由之前,需要介紹一下架構的設計。
傳統互聯網架構圖
上圖是沒有網關參與的一個最典型的互聯網架構(本文中統一使用book代表應用實例,即真正提供服務的一個業務系統)
加入eureka的架構圖
book注冊到eureka注冊中心中,zuul本身也連接著同一個eureka,可以拉取book眾多實例的列表。服務中心的注冊發現一直是值得推崇的一種方式,但是不適用與網關產品。因為我們的網關是面向眾多的其他部門的已有或是異構架構的系統,不應該強求其他系統都使用eureka,這樣是有侵入性的設計。
最終架構圖
要強調的一點是,gateway最終也會部署多個實例,達到分布式的效果,在架構圖中沒有畫出,請大家自行腦補。
本博客的示例使用最后一章架構圖為例,帶來動態路由的實現方式,會有具體的代碼。
動態路由
動態路由需要達到可持久化配置,動態刷新的效果。如架構圖所示,不僅要能滿足從spring的配置文件properties加載路由信息,還需要從數據庫加載我們的配置。另外一點是,路由信息在容器啟動時就已經加載進入了內存,我們希望配置完成后,實施發布,動態刷新內存中的路由信息,達到不停機維護路由信息的效果。
zuul–helloworlddemo
項目結構
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
|
<groupid>com.sinosoft</groupid> <artifactid>zuul-gateway-demo</artifactid> <packaging>pom</packaging> <version> 1.0 </version> <parent> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-parent</artifactid> <version> 1.5 . 2 .release</version> </parent> <modules> <module>gateway</module> <module>book</module> </modules> <dependencymanagement> <dependencies> <dependency> <groupid>org.springframework.cloud</groupid> <artifactid>spring-cloud-dependencies</artifactid> <version>camden.sr6</version> <type>pom</type> <scope> import </scope> </dependency> </dependencies> </dependencymanagement> |
tip:springboot-1.5.2對應的springcloud的版本需要使用camden.sr6,一開始想專門寫這個demo時,只替換了springboot的版本1.4.0->1.5.2,結果啟動就報錯了,最后發現是版本不兼容的鍋。
gateway項目:
啟動類:gatewayapplication.java
1
2
3
4
5
6
7
8
9
|
@enablezuulproxy @springbootapplication public class gatewayapplication { public static void main(string[] args) { springapplication.run(gatewayapplication. class , args); } } |
配置:application.properties
1
2
3
4
5
6
7
|
#配置在配置文件中的路由信息 zuul.routes.books.url=http: //localhost:8090 zuul.routes.books.path=/books/** #不使用注冊中心,會帶來侵入性 ribbon.eureka.enabled= false #網關端口 server.port= 8080 |
book項目:
啟動類:bookapplication.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@restcontroller @springbootapplication public class bookapplication { @requestmapping (value = "/available" ) public string available() { system.out.println( "spring in action" ); return "spring in action" ; } @requestmapping (value = "/checked-out" ) public string checkedout() { return "spring boot in action" ; } public static void main(string[] args) { springapplication.run(bookapplication. class , args); } } |
配置類:application.properties
1
|
server.port= 8090 |
測試訪問:http://localhost:8080/books/available
上述demo是一個簡單的靜態路由,簡單看下源碼,zuul是怎么做到轉發,路由的。
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
|
@configuration @enableconfigurationproperties ({ zuulproperties. class }) @conditionalonclass (zuulservlet. class ) @import (serverpropertiesautoconfiguration. class ) public class zuulconfiguration { @autowired //zuul的配置文件,對應了application.properties中的配置信息 protected zuulproperties zuulproperties; @autowired protected serverproperties server; @autowired (required = false ) private errorcontroller errorcontroller; @bean public hasfeatures zuulfeature() { return hasfeatures.namedfeature( "zuul (simple)" , zuulconfiguration. class ); } //核心類,路由定位器,最最重要 @bean @conditionalonmissingbean (routelocator. class ) public routelocator routelocator() { //默認配置的實現是simpleroutelocator.class return new simpleroutelocator( this .server.getservletprefix(), this .zuulproperties); } //zuul的控制器,負責處理鏈路調用 @bean public zuulcontroller zuulcontroller() { return new zuulcontroller(); } //mvc handlermapping that maps incoming request paths to remote services. @bean public zuulhandlermapping zuulhandlermapping(routelocator routes) { zuulhandlermapping mapping = new zuulhandlermapping(routes, zuulcontroller()); mapping.seterrorcontroller( this .errorcontroller); return mapping; } //注冊了一個路由刷新監聽器,默認實現是zuulrefreshlistener.class,這個是我們動態路由的關鍵 @bean public applicationlistener<applicationevent> zuulrefreshrouteslistener() { return new zuulrefreshlistener(); } @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; } // pre filters @bean public servletdetectionfilter servletdetectionfilter() { return new servletdetectionfilter(); } @bean public formbodywrapperfilter formbodywrapperfilter() { return new formbodywrapperfilter(); } @bean public debugfilter debugfilter() { return new debugfilter(); } @bean public servlet30wrapperfilter servlet30wrapperfilter() { return new servlet30wrapperfilter(); } // post filters @bean public sendresponsefilter sendresponsefilter() { return new sendresponsefilter(); } @bean public senderrorfilter senderrorfilter() { return new senderrorfilter(); } @bean public sendforwardfilter sendforwardfilter() { return new sendforwardfilter(); } @configuration protected static class zuulfilterconfiguration { @autowired private map<string, zuulfilter> filters; @bean public zuulfilterinitializer zuulfilterinitializer() { return new zuulfilterinitializer( this .filters); } } //上面提到的路由刷新監聽器 private static class zuulrefreshlistener implements applicationlistener<applicationevent> { @autowired private zuulhandlermapping zuulhandlermapping; private heartbeatmonitor heartbeatmonitor = new heartbeatmonitor(); @override public void onapplicationevent(applicationevent event) { if (event instanceof contextrefreshedevent || event instanceof refreshscoperefreshedevent || event instanceof routesrefreshedevent) { //設置為臟,下一次匹配到路徑時,如果發現為臟,則會去刷新路由信息 this .zuulhandlermapping.setdirty( true ); } else if (event instanceof heartbeatevent) { if ( this .heartbeatmonitor.update(((heartbeatevent) event).getvalue())) { this .zuulhandlermapping.setdirty( true ); } } } } } |
我們要解決動態路由的難題,第一步就得理解路由定位器的作用。
很失望,因為從接口關系來看,spring考慮到了路由刷新的需求,但是默認實現的simpleroutelocator沒有實現refreshableroutelocator接口,看來我們只能借鑒discoveryclientroutelocator去改造simpleroutelocator使其具備刷新能力。
1
2
3
|
public interface refreshableroutelocator extends routelocator { void refresh(); } |
discoveryclientroutelocator比simpleroutelocator多了兩個功能,第一是從discoveryclient(如eureka)發現路由信息,之前的架構圖已經給大家解釋清楚了,我們不想使用eureka這種侵入式的網關模塊,所以忽略它,第二是實現了refreshableroutelocator接口,能夠實現動態刷新。
對simpleroutelocator.class的源碼加一些注釋,方便大家閱讀:
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
|
public class simpleroutelocator implements routelocator { //配置文件中的路由信息配置 private zuulproperties properties; //路徑正則配置器,即作用于path:/books/** private pathmatcher pathmatcher = new antpathmatcher(); private string dispatcherservletpath = "/" ; private string zuulservletpath; private atomicreference<map<string, zuulroute>> routes = new atomicreference<>(); public simpleroutelocator(string servletpath, zuulproperties properties) { this .properties = properties; if (servletpath != null && stringutils.hastext(servletpath)) { this .dispatcherservletpath = servletpath; } this .zuulservletpath = properties.getservletpath(); } //路由定位器和其他組件的交互,是最終把定位的routes以list的方式提供出去,核心實現 @override public list<route> getroutes() { if ( this .routes.get() == null ) { this .routes.set(locateroutes()); } list<route> values = new arraylist<>(); for (string url : this .routes.get().keyset()) { zuulroute route = this .routes.get().get(url); string path = route.getpath(); values.add(getroute(route, path)); } return values; } @override public collection<string> getignoredpaths() { return this .properties.getignoredpatterns(); } //這個方法在網關產品中也很重要,可以根據實際路徑匹配到route來進行業務邏輯的操作,進行一些加工 @override public route getmatchingroute( final string path) { if (log.isdebugenabled()) { log.debug( "finding route for path: " + path); } if ( this .routes.get() == null ) { this .routes.set(locateroutes()); } if (log.isdebugenabled()) { log.debug( "servletpath=" + this .dispatcherservletpath); log.debug( "zuulservletpath=" + this .zuulservletpath); log.debug( "requestutils.isdispatcherservletrequest()=" + requestutils.isdispatcherservletrequest()); log.debug( "requestutils.iszuulservletrequest()=" + requestutils.iszuulservletrequest()); } string adjustedpath = adjustpath(path); zuulroute route = null ; if (!matchesignoredpatterns(adjustedpath)) { for (entry<string, zuulroute> entry : this .routes.get().entryset()) { string pattern = entry.getkey(); log.debug( "matching pattern:" + pattern); if ( this .pathmatcher.match(pattern, adjustedpath)) { route = entry.getvalue(); break ; } } } if (log.isdebugenabled()) { log.debug( "route matched=" + route); } return getroute(route, adjustedpath); } private route getroute(zuulroute route, string path) { if (route == null ) { return null ; } string targetpath = path; string prefix = this .properties.getprefix(); if (path.startswith(prefix) && this .properties.isstripprefix()) { targetpath = path.substring(prefix.length()); } 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(); } return new route(route.getid(), targetpath, route.getlocation(), prefix, retryable, route.iscustomsensitiveheaders() ? route.getsensitiveheaders() : null ); } //注意這個類并沒有實現refresh接口,但是卻提供了一個protected級別的方法,旨在讓子類不需要重復維護一個private atomicreference<map<string, zuulroute>> routes = new atomicreference<>();也可以達到刷新的效果 protected void dorefresh() { this .routes.set(locateroutes()); } //具體就是在這兒定位路由信息的,我們之后從數據庫加載路由信息,主要也是從這兒改寫 /** * compute a map of path pattern to route. the default is just a static map from the * { @link zuulproperties}, but subclasses can add dynamic calculations. */ protected map<string, zuulroute> locateroutes() { linkedhashmap<string, zuulroute> routesmap = new linkedhashmap<string, zuulroute>(); for (zuulroute route : this .properties.getroutes().values()) { routesmap.put(route.getpath(), route); } return routesmap; } protected boolean matchesignoredpatterns(string path) { for (string pattern : this .properties.getignoredpatterns()) { log.debug( "matching ignored pattern:" + pattern); if ( this .pathmatcher.match(pattern, path)) { log.debug( "path " + path + " matches ignored pattern " + pattern); return true ; } } return false ; } private string adjustpath( final string path) { string adjustedpath = path; if (requestutils.isdispatcherservletrequest() && stringutils.hastext( this .dispatcherservletpath)) { if (! this .dispatcherservletpath.equals( "/" )) { adjustedpath = path.substring( this .dispatcherservletpath.length()); log.debug( "stripped dispatcherservletpath" ); } } else if (requestutils.iszuulservletrequest()) { if (stringutils.hastext( this .zuulservletpath) && ! this .zuulservletpath.equals( "/" )) { adjustedpath = path.substring( this .zuulservletpath.length()); log.debug( "stripped zuulservletpath" ); } } else { // do nothing } log.debug( "adjustedpath=" + path); return adjustedpath; } } |
重寫過后的自定義路由定位器如下:
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
|
public class customroutelocator extends simpleroutelocator implements refreshableroutelocator{ public final static logger logger = loggerfactory.getlogger(customroutelocator. class ); private jdbctemplate jdbctemplate; private zuulproperties properties; public void setjdbctemplate(jdbctemplate jdbctemplate){ this .jdbctemplate = jdbctemplate; } public customroutelocator(string servletpath, zuulproperties properties) { super (servletpath, properties); this .properties = properties; logger.info( "servletpath:{}" ,servletpath); } //父類已經提供了這個方法,這里寫出來只是為了說明這一個方法很重要!!! // @override // protected void dorefresh() { // super.dorefresh(); // } @override public void refresh() { dorefresh(); } @override protected map<string, zuulroute> locateroutes() { linkedhashmap<string, zuulroute> routesmap = new linkedhashmap<string, zuulroute>(); //從application.properties中加載路由信息 routesmap.putall( super .locateroutes()); //從db中加載路由信息 routesmap.putall(locateroutesfromdb()); //優化一下配置 linkedhashmap<string, zuulroute> values = new linkedhashmap<>(); for (map.entry<string, zuulroute> entry : routesmap.entryset()) { string path = entry.getkey(); // prepend with slash if not already present. if (!path.startswith( "/" )) { path = "/" + path; } if (stringutils.hastext( this .properties.getprefix())) { path = this .properties.getprefix() + path; if (!path.startswith( "/" )) { path = "/" + path; } } values.put(path, entry.getvalue()); } return values; } private map<string, zuulroute> locateroutesfromdb(){ map<string, zuulroute> routes = new linkedhashmap<>(); list<zuulroutevo> results = jdbctemplate.query( "select * from gateway_api_define where enabled = true " , new beanpropertyrowmapper<>(zuulroutevo. class )); for (zuulroutevo result : results) { if (org.apache.commons.lang3.stringutils.isblank(result.getpath()) || org.apache.commons.lang3.stringutils.isblank(result.geturl()) ){ continue ; } zuulroute zuulroute = new zuulroute(); try { org.springframework.beans.beanutils.copyproperties(result,zuulroute); } catch (exception e) { logger.error( "=============load zuul route info from db with error==============" ,e); } routes.put(zuulroute.getpath(),zuulroute); } return routes; } public static class zuulroutevo { /** * the id of the route (the same as its map key by default). */ private string id; /** * the path (pattern) for the route, e.g. /foo/**. */ private string path; /** * the service id (if any) to map to this route. you can specify a physical url or * a service, but not both. */ private string serviceid; /** * a full physical url to map to the route. an alternative is to use a service id * and service discovery to find the physical address. */ private string url; /** * flag to determine whether the prefix for this route (the path, minus pattern * patcher) should be stripped before forwarding. */ private boolean stripprefix = true ; /** * flag to indicate that this route should be retryable (if supported). generally * retry requires a service id and ribbon. */ private boolean retryable; private boolean enabled; public string getid() { return id; } public void setid(string id) { this .id = id; } public string getpath() { return path; } public void setpath(string path) { this .path = path; } public string getserviceid() { return serviceid; } public void setserviceid(string serviceid) { this .serviceid = serviceid; } public string geturl() { return url; } public void seturl(string url) { this .url = url; } public boolean isstripprefix() { return stripprefix; } public void setstripprefix( boolean stripprefix) { this .stripprefix = stripprefix; } public boolean getretryable() { return retryable; } public void setretryable( boolean retryable) { this .retryable = retryable; } public boolean getenabled() { return enabled; } public void setenabled( boolean enabled) { this .enabled = enabled; } } } |
配置這個自定義的路由定位器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@configuration public class customzuulconfig { @autowired zuulproperties zuulproperties; @autowired serverproperties server; @autowired jdbctemplate jdbctemplate; @bean public customroutelocator routelocator() { customroutelocator routelocator = new customroutelocator( this .server.getservletprefix(), this .zuulproperties); routelocator.setjdbctemplate(jdbctemplate); return routelocator; } } |
現在容器啟動時,就可以從數據庫和配置文件中一起加載路由信息了,離動態路由還差最后一步,就是實時刷新,前面已經說過了,默認的zuulconfigure已經配置了事件監聽器,我們只需要發送一個事件就可以實現刷新了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class refreshrouteservice { @autowired applicationeventpublisher publisher; @autowired routelocator routelocator; public void refreshroute() { routesrefreshedevent routesrefreshedevent = new routesrefreshedevent(routelocator); publisher.publishevent(routesrefreshedevent); } } |
具體的刷新流程其實就是從數據庫重新加載了一遍,有人可能會問,為什么不自己是手動重新加載locator.dorefresh?非要用事件去刷新。這牽扯到內部的zuul內部組件的工作流程,不僅僅是locator本身的一個變量,具體想要了解的還得去看源碼。
到這兒我們就實現了動態路由了,所以的實例代碼和建表語句我會放到github上,下載的時候記得給我star qaq ?。?!
鏈接:https://github.com/lexburner/zuul-gateway-demo
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://blog.csdn.net/u013815546/article/details/68944039