springcloud Zuul動態(tài)路由的實現(xiàn)
前言
Zuul 是Netflix 提供的一個開源組件,致力于在云平臺上提供動態(tài)路由,監(jiān)控,彈性,安全等邊緣服務(wù)的框架。也有很多公司使用它來作為網(wǎng)關(guān)的重要組成部分,碰巧今年公司的架構(gòu)組決定自研一個網(wǎng)關(guān)產(chǎn)品,集動態(tài)路由,動態(tài)權(quán)限,限流配額等功能為一體,為其他部門的項目提供統(tǒng)一的外網(wǎng)調(diào)用管理,最終形成產(chǎn)品(這方面阿里其實已經(jīng)有成熟的網(wǎng)關(guān)產(chǎn)品了,但是不太適用于個性化的配置,也沒有集成權(quán)限和限流降級)。
不過這里并不想介紹整個網(wǎng)關(guān)的架構(gòu),而是想著重于討論其中的一個關(guān)鍵點,并且也是經(jīng)常在交流群中聽人說起的:動態(tài)路由怎么做?
再闡釋什么是動態(tài)路由之前,需要介紹一下架構(gòu)的設(shè)計。
傳統(tǒng)互聯(lián)網(wǎng)架構(gòu)圖
上圖是沒有網(wǎng)關(guān)參與的一個最典型的互聯(lián)網(wǎng)架構(gòu)(本文中統(tǒng)一使用book代表應(yīng)用實例,即真正提供服務(wù)的一個業(yè)務(wù)系統(tǒng))
加入eureka的架構(gòu)圖
book注冊到eureka注冊中心中,zuul本身也連接著同一個eureka,可以拉取book眾多實例的列表。服務(wù)中心的注冊發(fā)現(xiàn)一直是值得推崇的一種方式,但是不適用與網(wǎng)關(guān)產(chǎn)品。因為我們的網(wǎng)關(guān)是面向眾多的其他部門的已有或是異構(gòu)架構(gòu)的系統(tǒng),不應(yīng)該強(qiáng)求其他系統(tǒng)都使用eureka,這樣是有侵入性的設(shè)計。
最終架構(gòu)圖
要強(qiáng)調(diào)的一點是,gateway最終也會部署多個實例,達(dá)到分布式的效果,在架構(gòu)圖中沒有畫出,請大家自行腦補(bǔ)。
本博客的示例使用最后一章架構(gòu)圖為例,帶來動態(tài)路由的實現(xiàn)方式,會有具體的代碼。
動態(tài)路由
動態(tài)路由需要達(dá)到可持久化配置,動態(tài)刷新的效果。如架構(gòu)圖所示,不僅要能滿足從spring的配置文件properties加載路由信息,還需要從數(shù)據(jù)庫加載我們的配置。另外一點是,路由信息在容器啟動時就已經(jīng)加載進(jìn)入了內(nèi)存,我們希望配置完成后,實施發(fā)布,動態(tài)刷新內(nèi)存中的路由信息,達(dá)到不停機(jī)維護(hù)路由信息的效果。
zuul–HelloWorldDemo
項目結(jié)構(gòu)
<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對應(yīng)的springcloud的版本需要使用Camden.SR6,一開始想專門寫這個demo時,只替換了springboot的版本1.4.0->1.5.2,結(jié)果啟動就報錯了,最后發(fā)現(xiàn)是版本不兼容的鍋。
gateway項目:
啟動類:GatewayApplication.java
@EnableZuulProxy @SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
配置:application.properties
#配置在配置文件中的路由信息 zuul.routes.books.url=http://localhost:8090 zuul.routes.books.path=/books/** #不使用注冊中心,會帶來侵入性 ribbon.eureka.enabled=false #網(wǎng)關(guān)端口 server.port=8080
book項目:
啟動類:BookApplication.java
@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
server.port=8090
測試訪問:http://localhost:8080/books/available
上述demo是一個簡單的靜態(tài)路由,簡單看下源碼,zuul是怎么做到轉(zhuǎn)發(fā),路由的。
@Configuration @EnableConfigurationProperties({ ZuulProperties.class }) @ConditionalOnClass(ZuulServlet.class) @Import(ServerPropertiesAutoConfiguration.class) public class ZuulConfiguration { @Autowired //zuul的配置文件,對應(yīng)了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() { //默認(rèn)配置的實現(xiàn)是SimpleRouteLocator.class return new SimpleRouteLocator(this.server.getServletPrefix(), this.zuulProperties); } //zuul的控制器,負(fù)責(zé)處理鏈路調(diào)用 @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; } //注冊了一個路由刷新監(jiān)聽器,默認(rèn)實現(xiàn)是ZuulRefreshListener.class,這個是我們動態(tài)路由的關(guān)鍵 @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); } } //上面提到的路由刷新監(jiān)聽器 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) { //設(shè)置為臟,下一次匹配到路徑時,如果發(fā)現(xiàn)為臟,則會去刷新路由信息 this.zuulHandlerMapping.setDirty(true); } else if (event instanceof HeartbeatEvent) { if (this.heartbeatMonitor.update(((HeartbeatEvent) event).getValue())) { this.zuulHandlerMapping.setDirty(true); } } } } }
我們要解決動態(tài)路由的難題,第一步就得理解路由定位器的作用。
很失望,因為從接口關(guān)系來看,spring考慮到了路由刷新的需求,但是默認(rèn)實現(xiàn)的SimpleRouteLocator沒有實現(xiàn)RefreshableRouteLocator接口,看來我們只能借鑒DiscoveryClientRouteLocator去改造SimpleRouteLocator使其具備刷新能力。
public interface RefreshableRouteLocator extends RouteLocator { void refresh(); }
DiscoveryClientRouteLocator比SimpleRouteLocator多了兩個功能,第一是從DiscoveryClient(如Eureka)發(fā)現(xiàn)路由信息,之前的架構(gòu)圖已經(jīng)給大家解釋清楚了,我們不想使用eureka這種侵入式的網(wǎng)關(guān)模塊,所以忽略它,第二是實現(xiàn)了RefreshableRouteLocator接口,能夠?qū)崿F(xiàn)動態(tài)刷新。
對SimpleRouteLocator.class的源碼加一些注釋,方便大家閱讀:
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的方式提供出去,核心實現(xiàn) @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(); } //這個方法在網(wǎng)關(guān)產(chǎn)品中也很重要,可以根據(jù)實際路徑匹配到Route來進(jìn)行業(yè)務(wù)邏輯的操作,進(jìn)行一些加工 @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); } //注意這個類并沒有實現(xiàn)refresh接口,但是卻提供了一個protected級別的方法,旨在讓子類不需要重復(fù)維護(hù)一個private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>();也可以達(dá)到刷新的效果 protected void doRefresh() { this.routes.set(locateRoutes()); } //具體就是在這兒定位路由信息的,我們之后從數(shù)據(jù)庫加載路由信息,主要也是從這兒改寫 /** * 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; } }
重寫過后的自定義路由定位器如下:
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); } //父類已經(jīng)提供了這個方法,這里寫出來只是為了說明這一個方法很重要?。?! // @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()); //優(yōu)化一下配置 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; } } }
配置這個自定義的路由定位器:
@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; } }
現(xiàn)在容器啟動時,就可以從數(shù)據(jù)庫和配置文件中一起加載路由信息了,離動態(tài)路由還差最后一步,就是實時刷新,前面已經(jīng)說過了,默認(rèn)的ZuulConfigure已經(jīng)配置了事件監(jiān)聽器,我們只需要發(fā)送一個事件就可以實現(xiàn)刷新了。
public class RefreshRouteService { @Autowired ApplicationEventPublisher publisher; @Autowired RouteLocator routeLocator; public void refreshRoute() { RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator); publisher.publishEvent(routesRefreshedEvent); } }
具體的刷新流程其實就是從數(shù)據(jù)庫重新加載了一遍,有人可能會問,為什么不自己是手動重新加載Locator.dorefresh?非要用事件去刷新。這牽扯到內(nèi)部的zuul內(nèi)部組件的工作流程,不僅僅是Locator本身的一個變量,具體想要了解的還得去看源碼。
到這兒我們就實現(xiàn)了動態(tài)路由了,所以的實例代碼和建表語句我會放到github上,下載的時候記得給我star QAQ ?。?!
鏈接:https://github.com/lexburner/zuul-gateway-demo
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java swing讀取txt文件實現(xiàn)學(xué)生考試系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Java swing讀取txt文件實現(xiàn)學(xué)生考試系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-06-06java 靜態(tài)工廠代替多參構(gòu)造器的適用情況與優(yōu)劣
這篇文章主要介紹了java 靜態(tài)工廠代替多參構(gòu)造器的優(yōu)劣,幫助大家更好的理解和使用靜態(tài)工廠方法,感興趣的朋友可以了解下2020-12-12完全解析Java編程中finally語句的執(zhí)行原理
這篇文章主要深度介紹了Java編程中finally語句的執(zhí)行原理,細(xì)致講解了finally在異常處理中的流程控制作用,需要的朋友可以參考下2015-11-11Atomikos + MybatisPlus解決多數(shù)據(jù)源事務(wù)一致性問題解決
在實際項目的開發(fā)過程中,我們經(jīng)常會遇到在同一個項目或微服務(wù)中牽涉到使用兩個或多個數(shù)據(jù)源的,本文主要介紹了Atomikos + MybatisPlus解決多數(shù)據(jù)源事務(wù)一致性問題解決,具有一定的參考價值,感興趣的可以了解一下2024-07-07Spring聲明式事務(wù)注解之@EnableTransactionManagement解析
這篇文章主要介紹了Spring聲明式事務(wù)注解之@EnableTransactionManagement解析,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-08-08java 查詢oracle數(shù)據(jù)庫所有表DatabaseMetaData的用法(詳解)
下面小編就為大家?guī)硪黄猨ava 查詢oracle數(shù)據(jù)庫所有表DatabaseMetaData的用法(詳解)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-11-11