新聞中心
怎么在springcloud中使用Zuul實(shí)現(xiàn)動態(tài)路由?很多新手對此不是很清楚,為了幫助大家解決這個(gè)難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來學(xué)習(xí)下,希望你能有所收獲。
為察哈爾右翼后等地區(qū)用戶提供了全套網(wǎng)頁設(shè)計(jì)制作服務(wù),及察哈爾右翼后網(wǎng)站建設(shè)行業(yè)解決方案。主營業(yè)務(wù)為網(wǎng)站建設(shè)、成都網(wǎng)站制作、察哈爾右翼后網(wǎng)站設(shè)計(jì),以傳統(tǒng)方式定制建設(shè)網(wǎng)站,并提供域名空間備案等一條龍服務(wù),秉承以專業(yè)、用心的態(tài)度為用戶提供真誠的服務(wù)。我們深信只要達(dá)到每一位用戶的要求,就會得到認(rèn)可,從而選擇與我們長期合作。這樣,我們也可以走得更遠(yuǎn)!
傳統(tǒng)互聯(lián)網(wǎng)架構(gòu)圖
上圖是沒有網(wǎng)關(guān)參與的一個(gè)最典型的互聯(lián)網(wǎng)架構(gòu)(本文中統(tǒng)一使用book代表應(yīng)用實(shí)例,即真正提供服務(wù)的一個(gè)業(yè)務(wù)系統(tǒng))
加入eureka的架構(gòu)圖
book注冊到eureka注冊中心中,zuul本身也連接著同一個(gè)eureka,可以拉取book眾多實(shí)例的列表。服務(wù)中心的注冊發(fā)現(xiàn)一直是值得推崇的一種方式,但是不適用與網(wǎng)關(guān)產(chǎn)品。因?yàn)槲覀兊木W(wǎng)關(guān)是面向眾多的其他部門的已有或是異構(gòu)架構(gòu)的系統(tǒng),不應(yīng)該強(qiáng)求其他系統(tǒng)都使用eureka,這樣是有侵入性的設(shè)計(jì)。
最終架構(gòu)圖
要強(qiáng)調(diào)的一點(diǎn)是,gateway最終也會部署多個(gè)實(shí)例,達(dá)到分布式的效果,在架構(gòu)圖中沒有畫出,請大家自行腦補(bǔ)。
本博客的示例使用最后一章架構(gòu)圖為例,帶來動態(tài)路由的實(shí)現(xiàn)方式,會有具體的代碼。
動態(tài)路由
動態(tài)路由需要達(dá)到可持久化配置,動態(tài)刷新的效果。如架構(gòu)圖所示,不僅要能滿足從spring的配置文件properties加載路由信息,還需要從數(shù)據(jù)庫加載我們的配置。另外一點(diǎn)是,路由信息在容器啟動時(shí)就已經(jīng)加載進(jìn)入了內(nèi)存,我們希望配置完成后,實(shí)施發(fā)布,動態(tài)刷新內(nèi)存中的路由信息,達(dá)到不停機(jī)維護(hù)路由信息的效果。
zuul–HelloWorldDemo
項(xiàng)目結(jié)構(gòu)
com.sinosoft zuul-gateway-demo pom 1.0 org.springframework.boot spring-boot-starter-parent 1.5.2.RELEASE gateway book org.springframework.cloud spring-cloud-dependencies Camden.SR6 pom import
tip:springboot-1.5.2對應(yīng)的springcloud的版本需要使用Camden.SR6,一開始想專門寫這個(gè)demo時(shí),只替換了springboot的版本1.4.0->1.5.2,結(jié)果啟動就報(bào)錯(cuò)了,最后發(fā)現(xiàn)是版本不兼容的鍋。
gateway項(xiàng)目:
啟動類: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項(xiàng)目:
啟動類: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是一個(gè)簡單的靜態(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)配置的實(shí)現(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; } //注冊了一個(gè)路由刷新監(jiān)聽器,默認(rèn)實(shí)現(xiàn)是ZuulRefreshListener.class,這個(gè)是我們動態(tài)路由的關(guān)鍵 @Bean public ApplicationListenerzuulRefreshRoutesListener() { 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 filters; @Bean public ZuulFilterInitializer zuulFilterInitializer() { return new ZuulFilterInitializer(this.filters); } } //上面提到的路由刷新監(jiān)聽器 private static class ZuulRefreshListener implements ApplicationListener { @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è)置為臟,下一次匹配到路徑時(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)路由的難題,第一步就得理解路由定位器的作用。
很失望,因?yàn)閺慕涌陉P(guān)系來看,spring考慮到了路由刷新的需求,但是默認(rèn)實(shí)現(xiàn)的SimpleRouteLocator沒有實(shí)現(xiàn)RefreshableRouteLocator接口,看來我們只能借鑒DiscoveryClientRouteLocator去改造SimpleRouteLocator使其具備刷新能力。
public interface RefreshableRouteLocator extends RouteLocator { void refresh(); }
DiscoveryClientRouteLocator比SimpleRouteLocator多了兩個(gè)功能,第一是從DiscoveryClient(如Eureka)發(fā)現(xiàn)路由信息,之前的架構(gòu)圖已經(jīng)給大家解釋清楚了,我們不想使用eureka這種侵入式的網(wǎng)關(guān)模塊,所以忽略它,第二是實(shí)現(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
重寫過后的自定義路由定位器如下:
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)提供了這個(gè)方法,這里寫出來只是為了說明這一個(gè)方法很重要?。?! // @Override // protected void doRefresh() { // super.doRefresh(); // } @Override public void refresh() { doRefresh(); } @Override protected MaplocateRoutes() { LinkedHashMap routesMap = new LinkedHashMap (); //從application.properties中加載路由信息 routesMap.putAll(super.locateRoutes()); //從db中加載路由信息 routesMap.putAll(locateRoutesFromDB()); //優(yōu)化一下配置 LinkedHashMap values = new LinkedHashMap<>(); for (Map.Entry 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 locateRoutesFromDB(){ Map routes = new LinkedHashMap<>(); List 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; } } }
配置這個(gè)自定義的路由定位器:
@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í),就可以從數(shù)據(jù)庫和配置文件中一起加載路由信息了,離動態(tài)路由還差最后一步,就是實(shí)時(shí)刷新,前面已經(jīng)說過了,默認(rèn)的ZuulConfigure已經(jīng)配置了事件監(jiān)聽器,我們只需要發(fā)送一個(gè)事件就可以實(shí)現(xiàn)刷新了。
public class RefreshRouteService { @Autowired ApplicationEventPublisher publisher; @Autowired RouteLocator routeLocator; public void refreshRoute() { RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator); publisher.publishEvent(routesRefreshedEvent); } }
看完上述內(nèi)容是否對您有幫助呢?如果還想對相關(guān)知識有進(jìn)一步的了解或閱讀更多相關(guān)文章,請關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝您對創(chuàng)新互聯(lián)的支持。
新聞名稱:怎么在springcloud中使用Zuul實(shí)現(xiàn)動態(tài)路由
本文路徑:http://fisionsoft.com.cn/article/iedoih.html