【加精】Spring Cloud Zuul路由动态配置

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> 【加精】Spring Cloud Zuul路由动态配置

Spring Cloud Zuul动态路由配置

  • Zuul配置
  • 在mysql中创建路由信息表,对于类如下:
  • 定义CustomRouteLocator类
  • 增加CustomZuulConfig类,主要是为了配置CustomRouteLocator
  • RefreshRouteService类,用于实现数据库路由信息的刷新
  • 当然也要提供RefreshController,提供从浏览器访问的刷新功能
  • 问题
  • 后记

上一篇初步记录了Spring Cloud GateWay的动态路由配置,这一篇说一下Zuul的动态路由配置。ZuulNetflix 开源的微服务网关,Spring CloudZuul 进行了整合和增强。在 SpringCloud 体系中,Zuul 担任着网关的角色,对发送到服务端的请求进行一些预处理,比如安全验证、动态路由、负载分配等。还是那句话,由于水平有限,难免有不当或者错误之处,请大家指正,谢谢。

Zuul配置

一般的,我们如果使用Spring Cloud Zuul 进行路由配置,类似于下面的样子:

12345
zuul:  routes:    users:      path: /myusers/**      stripPrefix: false

zuul:
routes:
users:
path: /myusers/**
stripPrefix: false

当我们要新增或者改变一个网关路由时,我们不得不停止网关服务,修改配置文件,保存再重新启动网关服务,这样才能让我们新的设置生效。设想一样,如果是在生产环境,为了一个小小的路由变更,这样的停止再重启恐怕谁也受不了吧。接下来,看看我们怎么能做到动态配置网关路由,让网关路由配置在服务不需要重启的情况生效。(废话一堆啊)

在mysql中创建路由信息表,对于类如下:

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

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

定义CustomRouteLocator类

CustomRouteLocator集成SimpleRouteLocator,实现了RefreshableRouteLocator接口

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
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;        System.out.println(properties.toString());        logger.info("servletPath:{}", servletPath);    }     @Override    public void refresh() {        doRefresh();    }     @Override    protected MapString, ZuulProperties.ZuulRoute locateRoutes() {        LinkedHashMapString, ZuulProperties.ZuulRoute routesMap = new LinkedHashMap();        System.out.println("start " + new Date().toLocaleString());        //从application.properties中加载路由信息        routesMap.putAll(super.locateRoutes());        //从db中加载路由信息        routesMap.putAll(locateRoutesFromDB());        //优化一下配置        LinkedHashMapString, ZuulProperties.ZuulRoute values = new LinkedHashMap();        for (Map.EntryString, ZuulProperties.ZuulRoute entry : routesMap.entrySet()) {            String path = entry.getKey();            System.out.println(path);            // 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 MapString, ZuulProperties.ZuulRoute locateRoutesFromDB() {        MapString, ZuulProperties.ZuulRoute routes = new LinkedHashMap();        ListZuulRouteVO results = jdbcTemplate.query("select * from gateway_api_define where enabled = true ", new                BeanPropertyRowMapper(ZuulRouteVO.class));        for (ZuulRouteVO result : results) {            if (StringUtils.isEmpty(result.getPath()) ) {                continue;            }            if (StringUtils.isEmpty(result.getServiceId()) && StringUtils.isEmpty(result.getUrl())) {                continue;            }            ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();            try {                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 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;
    System.out.println(properties.toString());
    logger.info("servletPath:{}", servletPath);
}

@Override
public void refresh() {
    doRefresh();
}

@Override
protected MapString, ZuulProperties.ZuulRoute locateRoutes() {
    LinkedHashMapString, ZuulProperties.ZuulRoute routesMap = new LinkedHashMap();
    System.out.println("start " + new Date().toLocaleString());
    //从application.properties中加载路由信息
    routesMap.putAll(super.locateRoutes());
    //从db中加载路由信息
    routesMap.putAll(locateRoutesFromDB());
    //优化一下配置
    LinkedHashMapString, ZuulProperties.ZuulRoute values = new LinkedHashMap();
    for (Map.EntryString, ZuulProperties.ZuulRoute entry : routesMap.entrySet()) {
        String path = entry.getKey();
        System.out.println(path);
        // 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 MapString, ZuulProperties.ZuulRoute locateRoutesFromDB() {
    MapString, ZuulProperties.ZuulRoute routes = new LinkedHashMap();
    ListZuulRouteVO results = jdbcTemplate.query("select * from gateway_api_define where enabled = true ", new
            BeanPropertyRowMapper(ZuulRouteVO.class));
    for (ZuulRouteVO result : results) {
        if (StringUtils.isEmpty(result.getPath()) ) {
            continue;
        }
        if (StringUtils.isEmpty(result.getServiceId()) && StringUtils.isEmpty(result.getUrl())) {
            continue;
        }
        ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
        try {
            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;
}   

}

主要的是locateRoutes和locateRoutesFromDB这两个函数,locateRoutes是从SimpleRouteLocator Override过来的,先装载配置文件里面的路由信息,在从数据库里面获取路由信息,最后都是保存在SimpleRoteLocatorAtomicReferenceMapString, ZuulRoute routes属性中,注意routes是类型,它是可以保证线程俺去的。

增加CustomZuulConfig类,主要是为了配置CustomRouteLocator

12345678910111213141516
@Configurationpublic class CustomZuulConfig {    @Autowired    ZuulProperties zuulProperties;    @Autowired    ServerProperties server;    @Autowired    JdbcTemplate jdbcTemplate;     @Bean    public CustomRouteLocator routeLocator() {        CustomRouteLocator routeLocator = new CustomRouteLocator(this.server.getServlet().getPath(), this.zuulProperties);        routeLocator.setJdbcTemplate(jdbcTemplate);        return routeLocator;    }}

@Configuration
public class CustomZuulConfig {
@Autowired
ZuulProperties zuulProperties;
@Autowired
ServerProperties server;
@Autowired
JdbcTemplate jdbcTemplate;


@Bean
public CustomRouteLocator routeLocator() {
    CustomRouteLocator routeLocator = new CustomRouteLocator(this.server.getServlet().getPath(), this.zuulProperties);
    routeLocator.setJdbcTemplate(jdbcTemplate);
    return routeLocator;
}

}

CustomerRouteLocator 去数据库获取路由配置信息,需要一个JdbcTemplate Bean。this.zuulProperties 就是配置文件里面的路由配置,应该是网关服务启动时自动就获取过来的。

RefreshRouteService类,用于实现数据库路由信息的刷新

1234567891011121314
@Servicepublic class RefreshRouteService {    @Autowired    ApplicationEventPublisher publisher;     @Autowired    RouteLocator routeLocator;     public void refreshRoute() {        RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);        publisher.publishEvent(routesRefreshedEvent);     }}

@Service
public class RefreshRouteService {
@Autowired
ApplicationEventPublisher publisher;


@Autowired
RouteLocator routeLocator;

public void refreshRoute() {
    RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
    publisher.publishEvent(routesRefreshedEvent);

}

}

当然也要提供RefreshController,提供从浏览器访问的刷新功能

1234567891011121314151617181920
@RestControllerpublic class RefreshController {    @Autowired    RefreshRouteService refreshRouteService;     @Autowired    ZuulHandlerMapping zuulHandlerMapping;     @GetMapping("/refreshRoute")    public String refresh() {        refreshRouteService.refreshRoute();        return "refresh success";    }     @RequestMapping("/watchRoute")    public Object watchNowRoute() {        //可以用debug模式看里面具体是什么        return zuulHandlerMapping.getHandlerMap();    }}

@RestController
public class RefreshController {
@Autowired
RefreshRouteService refreshRouteService;


@Autowired
ZuulHandlerMapping zuulHandlerMapping;

@GetMapping("/refreshRoute")
public String refresh() {
    refreshRouteService.refreshRoute();
    return "refresh success";
}

@RequestMapping("/watchRoute")
public Object watchNowRoute() {
    //可以用debug模式看里面具体是什么
    return zuulHandlerMapping.getHandlerMap();
}

}

上面两个实现的功能是,在数据库里面新增或者修改路由信息,通过上面的功能进行刷新。

问题

网关服务跑起来了,也能实现正常的路由功能。但是,等等,查看日志,发现每隔30秒,服务自动从数据库再次加载路由配置,这是为什么呢?

这个问题在于ZuulRefreshListener 这个类,这个类j实现了ApplicationListener 接口,监听系统的Event,然后进行刷新。

让我们来更改这个类的代码:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
private static class ZuulRefreshListener implements ApplicationListenerApplicationEvent {        @Autowired        private ZuulHandlerMapping zuulHandlerMapping;        private HeartbeatMonitor heartbeatMonitor;         private ZuulRefreshListener() {            this.heartbeatMonitor = new HeartbeatMonitor();        }         @Override        public void onApplicationEvent(ApplicationEvent event) {            if (!(event instanceof ContextRefreshedEvent) && !(event instanceof RefreshScopeRefreshedEvent) && !(event instanceof RoutesRefreshedEvent) && !(event instanceof InstanceRegisteredEvent)) {                if (event instanceof ParentHeartbeatEvent) {                    ParentHeartbeatEvent e = (ParentHeartbeatEvent)event;                    this.resetIfNeeded(e.getValue());                 } else if (event instanceof HeartbeatEvent) {                    HeartbeatEvent e = (HeartbeatEvent)event;                    this.resetIfNeeded(e.getValue());                 }            } else {                /**                 * 原来代码                 * this.reset();                 */                if ((event instanceof ContextRefreshedEvent) || (event instanceof RefreshScopeRefreshedEvent) || (event instanceof RoutesRefreshedEvent)) {                     if (event instanceof ContextRefreshedEvent) {                        ContextRefreshedEvent contextRefreshedEvent = (ContextRefreshedEvent) event;                        ApplicationContext context = contextRefreshedEvent.getApplicationContext();                         String eventClassName = context.getClass().getName();                         /**                         * 为了服务启动只执行一次从数据库里面获取路由信息,这儿进行判断                         */                        if (eventClassName.equals("org.springframework.context.annotation.AnnotationConfigApplicationContext")) {                            this.reset();                        }                    } else {                        this.reset();                    }                }            }         }         private void resetIfNeeded(Object value) {            /**             * 发送监控心态信息接收到注册服务中心的数据后,只更新心态的相关信息,不再从新load整个路由             * 原来是从新load路由信息,可以把新注册的服务都动态load进来。             * 现在要求新的服务的路由在数据库里面配置。             *             * 否则的话每30秒发送心态检测,就会更新一次路由信息,没有必要             *             */            if (!this.heartbeatMonitor.update(value)) {                return;            }            /* 原来代码            if (this.heartbeatMonitor.update(value)) {                this.reset();            }*/         }

private static class ZuulRefreshListener implements ApplicationListenerApplicationEvent {
@Autowired
private ZuulHandlerMapping zuulHandlerMapping;
private HeartbeatMonitor heartbeatMonitor;


    private ZuulRefreshListener() {
        this.heartbeatMonitor = new HeartbeatMonitor();
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (!(event instanceof ContextRefreshedEvent) && !(event instanceof RefreshScopeRefreshedEvent) && !(event instanceof RoutesRefreshedEvent) && !(event instanceof InstanceRegisteredEvent)) {
            if (event instanceof ParentHeartbeatEvent) {
                ParentHeartbeatEvent e = (ParentHeartbeatEvent)event;
                this.resetIfNeeded(e.getValue());

            } else if (event instanceof HeartbeatEvent) {
                HeartbeatEvent e = (HeartbeatEvent)event;
                this.resetIfNeeded(e.getValue());

            }
        } else {
            /**
             * 原来代码
             * this.reset();
             */
            if ((event instanceof ContextRefreshedEvent) || (event instanceof RefreshScopeRefreshedEvent) || (event instanceof RoutesRefreshedEvent)) {

                if (event instanceof ContextRefreshedEvent) {
                    ContextRefreshedEvent contextRefreshedEvent = (ContextRefreshedEvent) event;
                    ApplicationContext context = contextRefreshedEvent.getApplicationContext();

                    String eventClassName = context.getClass().getName();

                    /**
                     * 为了服务启动只执行一次从数据库里面获取路由信息,这儿进行判断
                     */
                    if (eventClassName.equals("org.springframework.context.annotation.AnnotationConfigApplicationContext")) {
                        this.reset();
                    }
                } else {
                    this.reset();
                }
            }
        }

    }

    private void resetIfNeeded(Object value) {
        /**
         * 发送监控心态信息接收到注册服务中心的数据后,只更新心态的相关信息,不再从新load整个路由
         * 原来是从新load路由信息,可以把新注册的服务都动态load进来。
         * 现在要求新的服务的路由在数据库里面配置。
         *
         * 否则的话每30秒发送心态检测,就会更新一次路由信息,没有必要
         *
         */
        if (!this.heartbeatMonitor.update(value)) {
            return;
        }
        /* 原来代码
        if (this.heartbeatMonitor.update(value)) {
            this.reset();
        }*/

    }

为什么会30秒一次频繁的获取路由配置,上面的注释已经说的很清楚了。

测试,一切顺利!

后记

写博客很累,主要是没有经验,又担心有的地方理解错误,误导大家。出现问题,有的时候需要去从源码哪里找到答案。幸好我的博客只记录我的心得,没有任何其他的目的,聊以自慰吧。

本文作者:【Java知音】辣椒

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> 【加精】Spring Cloud Zuul路由动态配置


 上一篇
【加精】Spring Cloud GateWay动态路由配置 【加精】Spring Cloud GateWay动态路由配置
目录 GateWay配置 在mysql定义表gateway_define, 表结构如下面的GatewayDefine实体类: 定义repository和service,采用JPA实现 定义MysqlRouteDefinitionReposi
下一篇 
Spring全家桶—SpringCloud之Feign(Finchley版) Spring全家桶—SpringCloud之Feign(Finchley版)
点击上方“Java知音”,选择“置顶公众号” 技术文章第一时间送达! Feign是一个声明式的Web服务客户端。 是什么? 例如我在一个服务的interace上注解@FeignClient(value = "eureka-client"