springcloud----Zuul動態路由界面化

     最近公司要開發一個微服務項目,剛好在研究網關這一塊,網上收了一些內容,然後想把動態網關弄到界面操作,如下圖

項目地址:https://gitee.com/chenxiaoxuan/projects

參考文章1:https://blog.csdn.net/u013815546/article/details/68944039

參考文章2:http://www.ityouknow.com/springboot/2017/09/23/spring-boot-jpa-thymeleaf-curd.html

參考文章3:https://github.com/gaowenhui/SpringDataJPA

 

 

前言

Zuul 是Netflix 提供的一個開源組件,致力於在雲平臺上提供動態路由,監控,彈性,安全等邊緣服務的框架。也有很多公司使用它來作爲網關的重要組成部分,碰巧今年公司的架構組決定自研一個網關產品,集動態路由,動態權限,限流配額等功能爲一體,爲其他部門的項目提供統一的外網調用管理,最終形成產品(這方面阿里其實已經有成熟的網關產品了,但是不太適用於個性化的配置,也沒有集成權限和限流降級)。

不過這裏並不想介紹整個網關的架構,而是想着重於討論其中的一個關鍵點,並且也是經常在交流羣中聽人說起的:動態路由怎麼做?

再闡釋什麼是動態路由之前,需要介紹一下架構的設計。

傳統互聯網架構圖

這裏寫圖片描述 
上圖是沒有網關參與的一個最典型的互聯網架構(本文中統一使用book代表應用實例,即真正提供服務的一個業務系統)

加入eureka的架構圖

這裏寫圖片描述 
book註冊到eureka註冊中心中,zuul本身也連接着同一個eureka,可以拉取book衆多實例的列表。服務中心的註冊發現一直是值得推崇的一種方式,但是不適用與網關產品。因爲我們的網關是面向衆多的其他部門已有或是異構架構的系統,不應該強求其他系統都使用eureka,這樣是有侵入性的設計。

最終架構圖

這裏寫圖片描述 
要強調的一點是,gateway最終也會部署多個實例,達到分佈式的效果,在架構圖中沒有畫出,請大家自行腦補。

本博客的示例使用最後一章架構圖爲例,帶來動態路由的實現方式,會有具體的代碼。

動態路由

動態路由需要達到可持久化配置,動態刷新的效果。如架構圖所示,不僅要能滿足從spring的配置文件properties加載路由信息,還需要從數據庫加載我們的配置。另外一點是,路由信息在容器啓動時就已經加載進入了內存,我們希望配置完成後,實施發佈,動態刷新內存中的路由信息,達到不停機維護路由信息的效果。

zuul–HelloWorldDemo

項目結構

    <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

@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
#網關端口
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是一個簡單的靜態路由,簡單看下源碼,zuul是怎麼做到轉發,路由的。

@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使其具備刷新能力。

public interface RefreshableRouteLocator extends RouteLocator {
    void refresh();
}

DiscoveryClientRouteLocator比SimpleRouteLocator多了兩個功能,第一是從DiscoveryClient(如Eureka)發現路由信息,之前的架構圖已經給大家解釋清楚了,我們不想使用eureka這種侵入式的網關模塊,所以忽略它,第二是實現了RefreshableRouteLocator接口,能夠實現動態刷新。 
對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的方式提供出去,核心實現
    @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;
    }

}

重寫過後的自定義路由定位器如下:

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;
        }
    }
}

配置這個自定義的路由定位器:

@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已經配置了事件監聽器,我們只需要發送一個事件就可以實現刷新了。
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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章