API网关——zuul
zuul是什么
1. API網關
在微服務架構中,通常會有多個服務提供者。設想一個電商系統,可能會有商品、訂單、支付、用戶等多個類型的服務,而每個類型的服務數量也會隨著整個系統體量的增大也會隨之增長和變更。作為UI端,在展示頁面時可能需要從多個微服務中聚合數據,而且服務的劃分位置結構可能會有所改變。網關就可以對外暴露聚合API,屏蔽內部微服務的微小變動,保持整個系統的穩定性。
簡單來說,API網關可以提供一個單獨且統一的API入口用于訪問內部一個或多個API。
當然這只是網關眾多功能中的一部分,它還可以做負載均衡,統一鑒權,協議轉換,監控監測等一系列功能。
2. 網關的選擇
現在市場上有很多的網關可供選擇:
- Spring Cloud Zuul:本身基于Netflix開源的微服務網關,可以和Eureka,Ribbon,Hystrix等組件配合使用。
- Kong : 基于OpenResty的 API 網關服務和網關服務管理層。
- Spring Cloud Gateway:是由spring官方基于Spring5.0,Spring Boot2.0,Project Reactor等技術開發的網關,提供了一個構建在Spring - Ecosystem之上的API網關,旨在提供一種簡單而有效的途徑來發送API,并向他們提供交叉關注點,例如:安全性,監控/指標和彈性。目的是為了替換Spring Cloud Netfilx Zuul的。
在Spring cloud體系中,一般上選擇zuul或者Gateway。當然,也可以綜合自己的業務復雜性,自研一套或者改在一套符合自身業務發展的api網關的,最簡單做法是做個聚合api服務,通過SpringBoot構建對外的api接口,實現統一鑒權、參數校驗、權限控制等功能,說白了就是一個rest服務。
3. zuul
Zuul是Netflix開源的微服務網關,可以和Eureka、Ribbon、Hystrix等組件配合使用,Spring Cloud對Zuul進行了整合與增強,Zuul的主要功能是路由轉發和過濾器。
Zuul是基于JVM的路由器和服務器端負載均衡器。同時,Zuul的規則引擎允許規則和過濾器基本上用任何JVM語言編寫,內置支持Java和Groovy。這個功能,就可以實現動態路由的功能了。當需要添加某個新的對外服務時,一般上不停機更新是通過數據緩存配置或者使用Groovy進行動態路由的添加的。
zuul=路由+過濾器
Zuul的核心一系列的過濾器:
- 身份認證與安全:識別每個資源的驗證要求,并拒絕那些與要求不符的請求。
- 審查與監控:在邊緣位置追蹤有意義的數據和統計結果,從而帶來精確的生產視圖。
- 動態路由:動態地將請求路由到不同的后端集群。
- 壓力測試:逐漸增加指向集群的流量,以了解性能。
- 負載分配:為每一種負載類型分配對應容量,并啟用超出限定值的請求。
- 靜態響應處理:在邊緣位置直接建立部分相應,從而避免其轉發到內部集群。
二、zuul的工作原理
1. 過濾器機制
zuul的核心是一系列的filters, 其作用可以類比Servlet框架的Filter,或者AOP。
zuul把Request route到 用戶處理邏輯 的過程中,這些filter參與一些過濾處理,比如Authentication,Load Shedding等。
Zuul提供了一個框架,可以對過濾器進行動態的加載,編譯,運行。
Zuul的過濾器之間沒有直接的相互通信,他們之間通過一個RequestContext的靜態類來進行數據傳遞的。RequestContext類中有ThreadLocal變量來記錄每個Request所需要傳遞的數據。
Zuul的過濾器是由Groovy寫成,這些過濾器文件被放在Zuul Server上的特定目錄下面,Zuul會定期輪詢這些目錄,修改過的過濾器會動態的加載到Zuul Server中以便過濾請求使用。
2. 過濾器類型
Zuul大部分功能都是通過過濾器來實現的。Zuul中定義了四種標準過濾器類型,這些過濾器類型對應于請求的典型生命周期。
內置的特殊過濾器
zuul還提供了一類特殊的過濾器,分別為:StaticResponseFilter和SurgicalDebugFilter
StaticResponseFilter:StaticResponseFilter允許從Zuul本身生成響應,而不是將請求轉發到源。
SurgicalDebugFilter:SurgicalDebugFilter允許將特定請求路由到分隔的調試集群或主機。
自定義的過濾器
除了默認的過濾器類型,Zuul還允許我們創建自定義的過濾器類型。
例如,我們可以定制一種STATIC類型的過濾器,直接在Zuul中生成響應,而不將請求轉發到后端的微服務。
3. 過濾器生命周期
Zuul請求的生命周期如圖,該圖詳細描述了各種類型的過濾器的執行順序。
4. 與應用的集成方式
ZuulServlet - 處理請求(調度不同階段的filters,處理異常等)
ZuulServlet類似SpringMvc的DispatcherServlet,所有的Request都要經過ZuulServlet的處理
三個核心的方法preRoute(),route(), postRoute(),zuul對request處理邏輯都在這三個方法里
ZuulServlet交給ZuulRunner去執行。
由于ZuulServlet是單例,因此ZuulRunner也僅有一個實例。
ZuulRunner直接將執行邏輯交由FilterProcessor處理,FilterProcessor也是單例,其功能就是依據filterType執行filter的處理邏輯
FilterProcessor對filter的處理邏輯:
- 首先根據Type獲取所有輸入該Type的filter,List list。
- 遍歷該list,執行每個filter的處理邏輯,processZuulFilter(ZuulFilter filter)
- RequestContext對每個filter的執行狀況進行記錄,應該留意,此處的執行狀態主要包括其執行時間、以及執行成功或者失敗,如果執行失敗則對異常封裝后拋出。
- 到目前為止,zuul框架對每個filter的執行結果都沒有太多的處理,它沒有把上一filter的執行結果交由下一個將要執行的filter,僅僅是記錄執行狀態,如果執行失敗拋出異常并終止執行。
ContextLifeCycleFilter - RequestContext 的生命周期管理
ContextLifecycleFilter的核心功能是為了清除RequestContext; 請求上下文RequestContext通過ThreadLocal存儲,需要在請求完成后刪除該對象。
RequestContext提供了執行filter Pipeline所需要的Context,因為Servlet是單例多線程,這就要求RequestContext即要線程安全又要Request安全。
context使用ThreadLocal保存,這樣每個worker線程都有一個與其綁定的RequestContext,因為worker僅能同時處理一個Request,這就保證了Request Context 即是線程安全的由是Request安全的。
三、zuul實踐
1. 自定義Filter
添加一個PreRequestLogFilter:
public class PreRequestLogFilter extends ZuulFilter{@Overridepublic String filterType() {return PRE_TYPE;}@Overridepublic int filterOrder() {return 0;}@Overridepublic boolean shouldFilter() {return true;}@Overridepublic Object run() {RequestContext currentContext = RequestContext.getCurrentContext();HttpServletRequest request = currentContext.getRequest();System.out.print(String.format("send %s request to %s",request.getMethod(),request.getRequestURL()));return null;} }修改啟動類GatewayZuulDemoApplication,添加Filter注入:
@Bean public PreRequestLogFilter preRequestLogFilter(){return new PreRequestLogFilter(); }啟動服務并請求服務,觀察控制臺輸出日志信息。
2. 實現動態路由
動態路由需要達到可持久化配置,動態刷新的效果。不僅要能滿足從spring的配置文件properties加載路由信息,還需要從數據庫加載我們的配置。另外一點是,路由信息在容器啟動時就已經加載進入了內存,我們希望配置完成后,實施發布,動態刷新內存中的路由信息,達到不停機維護路由信息的效果。
網關項目
啟動類:GatewayApplication.Java
@EnableZuulProxy @SpringBootApplication public class GatewayApplication {public static void main(String[] args) {SpringApplication.run(GatewayApplication.class, args);} } 自定義路由定位器: ```java 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(); // }@Overridepublic void refresh() {doRefresh();}@Overrideprotected 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;}} }路由定位器配置:
@Configuration public class CustomZuulConfig {@AutowiredZuulProperties zuulProperties;@AutowiredServerProperties server;@AutowiredJdbcTemplate jdbcTemplate;@Beanpublic CustomRouteLocator routeLocator() {CustomRouteLocator routeLocator = new CustomRouteLocator(this.server.getServletPrefix(),this.zuulProperties);routeLocator.setJdbcTemplate(jdbcTemplate);return routeLocator;} }動態刷新服務:默認的ZuulConfigure已經配置了事件監聽器,我們只需要發送一個事件就可以實現刷新了。
public class RefreshRouteService {@AutowiredApplicationEventPublisher publisher;@AutowiredRouteLocator routeLocator;public void refreshRoute() {RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);publisher.publishEvent(routesRefreshedEvent);} }配置文件:application.properties
#不使用注冊中心,會帶來侵入性 ribbon.eureka.enabled=false#網關端口 server.port=8080業務項目
啟動類: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);} }zuul實現轉發、路由源碼解讀
@Configuration @EnableConfigurationProperties({ZuulProperties.class }) @ConditionalOnClass(ZuulServlet.class) @Import(ServerPropertiesAutoConfiguration.class) public class ZuulConfiguration {@Autowired//zuul的配置文件,對應了application.properties中的配置信息protected ZuulProperties zuulProperties;@Autowiredprotected ServerProperties server;@Autowired(required = false)private ErrorController errorController;@Beanpublic HasFeatures zuulFeature() {return HasFeatures.namedFeature("Zuul (Simple)", ZuulConfiguration.class);}//核心類,路由定位器,最最重要@Bean@ConditionalOnMissingBean(RouteLocator.class)public RouteLocator routeLocator() {//默認配置的實現是SimpleRouteLocator.classreturn new SimpleRouteLocator(this.server.getServletPrefix(),this.zuulProperties);}//zuul的控制器,負責處理鏈路調用@Beanpublic ZuulController zuulController() {return new ZuulController();}//MVC HandlerMapping that maps incoming request paths to remote services.@Beanpublic ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes,zuulController());mapping.setErrorController(this.errorController);return mapping;}//注冊了一個路由刷新監聽器,默認實現是ZuulRefreshListener.class,這個是我們動態路由的關鍵@Beanpublic 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@Beanpublic ServletDetectionFilter servletDetectionFilter() {return new ServletDetectionFilter();}@Beanpublic FormBodyWrapperFilter formBodyWrapperFilter() {return new FormBodyWrapperFilter();}@Beanpublic DebugFilter debugFilter() {return new DebugFilter();}@Beanpublic Servlet30WrapperFilter servlet30WrapperFilter() {return new Servlet30WrapperFilter();}// post filters@Beanpublic SendResponseFilter sendResponseFilter() {return new SendResponseFilter();}@Beanpublic SendErrorFilter sendErrorFilter() {return new SendErrorFilter();}@Beanpublic SendForwardFilter sendForwardFilter() {return new SendForwardFilter();}@Configurationprotected static class ZuulFilterConfiguration {@Autowiredprivate Map<String, ZuulFilter> filters;@Beanpublic ZuulFilterInitializer zuulFilterInitializer() {return new ZuulFilterInitializer(this.filters);}}//上面提到的路由刷新監聽器private static class ZuulRefreshListener implements ApplicationListener<ApplicationEvent> {@Autowiredprivate ZuulHandlerMapping zuulHandlerMapping;private HeartbeatMonitor heartbeatMonitor = new HeartbeatMonitor();@Overridepublic 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);}}}} } 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的方式提供出去,核心實現@Overridepublic 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;}@Overridepublic Collection<String> getIgnoredPaths() {return this.properties.getIgnoredPatterns();}//這個方法在網關產品中也很重要,可以根據實際路徑匹配到Route來進行業務邏輯的操作,進行一些加工@Overridepublic 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. RequestContext
zuul的源碼中是這樣解釋RequestContext的:
RequestContext保存了request、response、狀態信息和數據,以供ZuulFilter來訪問和共享。RequestContext在請求期間有效,并且保存在ThreadLocal中。可以通過設置contextClass替換RequestContext的擴展。RequestContext是ConcurrentHashMap的擴展實現。
setSendZuulResponse
ctx.setSendZuulResponse(false)表示不進行路由。換句話說,如果設置為false,這個請求最終不會被zuul轉發到后端服務器,但是如果當前Filter后面還存在其他Filter,那么其他Filter仍然會被調用到,所以一般我們在Filter的shouldFilter方法中,都會通過
@Override public boolean shouldFilter() {RequestContext ctx = RequestContext.getCurrentContext();if(!ctx.sendZuulResponse()){return false;} }這樣的方法來做判斷,如果這個請求最終被攔截掉,則后面的過濾器邏輯也不需要執行了
總結
以上是生活随笔為你收集整理的API网关——zuul的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 51单片机+L298驱动步进电机+L29
- 下一篇: c语言read有什么作用,c语言read