Spring Cloud Zuul路由动态配置

目录

  • Zuul配置

  • 在mysql中创建路由信息表

  • 定义CustomRouteLocator类

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

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

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

  • 问题

  • 后记

Zuul 是 Netflix 开源的微服务网关,Spring Cloud 对 Zuul 进行了整合和增强。在 SpringCloud 体系中,Zuul 担任着网关的角色,对发送到服务端的请求进行一些预处理,比如安全验证、动态路由、负载分配等。

还是那句话,由于水平有限,难免有不当或者错误之处,请大家指正,谢谢。

Zuul配置

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

 
  1. zuul:

  2.  routes:

  3.    users:

  4.      path: /myusers/**

  5.      stripPrefix: false

当我们要新增或者改变一个网关路由时,我们不得不停止网关服务,修改配置文件,保存再重新启动网关服务,这样才能让我们新的设置生效。

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

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

 
  1. public static class ZuulRouteVO {

  2.  

  3.        /**

  4.         * The ID of the route (the same as its map key by default).

  5.         */

  6.        private String id;

  7.  

  8.        /**

  9.         * The path (pattern) for the route, e.g. /foo/**.

  10.         */

  11.        private String path;

  12.  

  13.        /**

  14.         * The service ID (if any) to map to this route. You can specify a physical URL or

  15.         * a service, but not both.

  16.         */

  17.        private String serviceId;

  18.  

  19.        /**

  20.         * A full physical URL to map to the route. An alternative is to use a service ID

  21.         * and service discovery to find the physical address.

  22.         */

  23.        private String url;

  24.  

  25.        /**

  26.         * Flag to determine whether the prefix for this route (the path, minus pattern

  27.         * patcher) should be stripped before forwarding.

  28.         */

  29.        private boolean stripPrefix = true;

  30.  

  31.        /**

  32.         * Flag to indicate that this route should be retryable (if supported). Generally

  33.         * retry requires a service ID and ribbon.

  34.         */

  35.        private Boolean retryable;

  36.  

  37.        private Boolean enabled;

  38.  

  39.        public String getId() {

  40.            return id;

  41.        }

  42.  

  43.        public void setId(String id) {

  44.            this.id = id;

  45.        }

  46.  

  47.        public String getPath() {

  48.            return path;

  49.        }

  50.  

  51.        public void setPath(String path) {

  52.            this.path = path;

  53.        }

  54.  

  55.        public String getServiceId() {

  56.            return serviceId;

  57.        }

  58.  

  59.        public void setServiceId(String serviceId) {

  60.            this.serviceId = serviceId;

  61.        }

  62.  

  63.        public String getUrl() {

  64.            return url;

  65.        }

  66.  

  67.        public void setUrl(String url) {

  68.            this.url = url;

  69.        }

  70.  

  71.        public boolean isStripPrefix() {

  72.            return stripPrefix;

  73.        }

  74.  

  75.        public void setStripPrefix(boolean stripPrefix) {

  76.            this.stripPrefix = stripPrefix;

  77.        }

  78.  

  79.        public Boolean getRetryable() {

  80.            return retryable;

  81.        }

  82.  

  83.        public void setRetryable(Boolean retryable) {

  84.            this.retryable = retryable;

  85.        }

  86.  

  87.        public Boolean getEnabled() {

  88.            return enabled;

  89.        }

  90.  

  91.        public void setEnabled(Boolean enabled) {

  92.            this.enabled = enabled;

  93.        }

  94.    }

定义CustomRouteLocator类

CustomRouteLocator集成SimpleRouteLocator,实现了RefreshableRouteLocator接口

 
  1. public class CustomRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {

  2.  

  3.    public final static Logger logger = LoggerFactory.getLogger(CustomRouteLocator.class);

  4.  

  5.    private JdbcTemplate jdbcTemplate;

  6.  

  7.    private ZuulProperties properties;

  8.  

  9.    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {

  10.        this.jdbcTemplate = jdbcTemplate;

  11.    }

  12.  

  13.    public CustomRouteLocator(String servletPath, ZuulProperties properties) {

  14.  

  15.        super(servletPath, properties);

  16.        this.properties = properties;

  17.        System.out.println(properties.toString());

  18.        logger.info("servletPath:{}", servletPath);

  19.    }

  20.  

  21.    @Override

  22.    public void refresh() {

  23.        doRefresh();

  24.    }

  25.  

  26.    @Override

  27.    protected Map locateRoutes() {

  28.        LinkedHashMap routesMap = new LinkedHashMap<>();

  29.        System.out.println("start " + new Date().toLocaleString());

  30.        //从application.properties中加载路由信息

  31.        routesMap.putAll(super.locateRoutes());

  32.        //从db中加载路由信息

  33.        routesMap.putAll(locateRoutesFromDB());

  34.        //优化一下配置

  35.        LinkedHashMap values = new LinkedHashMap<>();

  36.        for (Map.Entry entry : routesMap.entrySet()) {

  37.            String path = entry.getKey();

  38.            System.out.println(path);

  39.            // Prepend with slash if not already present.

  40.            if (!path.startsWith("/")) {

  41.                path = "/" + path;

  42.            }

  43.            if (StringUtils.hasText(this.properties.getPrefix())) {

  44.                path = this.properties.getPrefix() + path;

  45.                if (!path.startsWith("/")) {

  46.                    path = "/" + path;

  47.                }

  48.            }

  49.            values.put(path, entry.getValue());

  50.        }

  51.        return values;

  52.    }

  53.  

  54.    private Map locateRoutesFromDB() {

  55.        Map routes = new LinkedHashMap<>();

  56.        List results = jdbcTemplate.query("select * from gateway_api_define where enabled = true ", new

  57.                BeanPropertyRowMapper<>(ZuulRouteVO.class));

  58.        for (ZuulRouteVO result : results) {

  59.            if (StringUtils.isEmpty(result.getPath()) ) {

  60.                continue;

  61.            }

  62.            if (StringUtils.isEmpty(result.getServiceId()) && StringUtils.isEmpty(result.getUrl())) {

  63.                continue;

  64.            }

  65.            ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();

  66.            try {

  67.                BeanUtils.copyProperties(result, zuulRoute);

  68.            } catch (Exception e) {

  69.                logger.error("=============load zuul route info from db with error==============", e);

  70.            }

  71.            routes.put(zuulRoute.getPath(), zuulRoute);

  72.        }

  73.        return routes;

  74.    }  

  75. }

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

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

 
  1. @Configuration

  2. public class CustomZuulConfig {

  3.    @Autowired

  4.    ZuulProperties zuulProperties;

  5.    @Autowired

  6.    ServerProperties server;

  7.    @Autowired

  8.    JdbcTemplate jdbcTemplate;

  9.  

  10.    @Bean

  11.    public CustomRouteLocator routeLocator() {

  12.        CustomRouteLocator routeLocator = new CustomRouteLocator(this.server.getServlet().getPath(), this.zuulProperties);

  13.        routeLocator.setJdbcTemplate(jdbcTemplate);

  14.        return routeLocator;

  15.    }

  16. }

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

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

 
  1. @Service

  2. public class RefreshRouteService {

  3.    @Autowired

  4.    ApplicationEventPublisher publisher;

  5.  

  6.    @Autowired

  7.    RouteLocator routeLocator;

  8.  

  9.    public void refreshRoute() {

  10.        RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);

  11.        publisher.publishEvent(routesRefreshedEvent);

  12.  

  13.    }

  14. }

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

 
  1. @RestController

  2. public class RefreshController {

  3.    @Autowired

  4.    RefreshRouteService refreshRouteService;

  5.  

  6.    @Autowired

  7.    ZuulHandlerMapping zuulHandlerMapping;

  8.  

  9.    @GetMapping("/refreshRoute")

  10.    public String refresh() {

  11.        refreshRouteService.refreshRoute();

  12.        return "refresh success";

  13.    }

  14.  

  15.    @RequestMapping("/watchRoute")

  16.    public Object watchNowRoute() {

  17.        //可以用debug模式看里面具体是什么

  18.        return zuulHandlerMapping.getHandlerMap();

  19.    }

  20. }

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

问题

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

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

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

 
  1. private static class ZuulRefreshListener implements ApplicationListener {

  2.        @Autowired

  3.        private ZuulHandlerMapping zuulHandlerMapping;

  4.        private HeartbeatMonitor heartbeatMonitor;

  5.  

  6.        private ZuulRefreshListener() {

  7.            this.heartbeatMonitor = new HeartbeatMonitor();

  8.        }

  9.  

  10.        @Override

  11.        public void onApplicationEvent(ApplicationEvent event) {

  12.            if (!(event instanceof ContextRefreshedEvent) && !(event instanceof RefreshScopeRefreshedEvent) && !(event instanceof RoutesRefreshedEvent) && !(event instanceof InstanceRegisteredEvent)) {

  13.                if (event instanceof ParentHeartbeatEvent) {

  14.                    ParentHeartbeatEvent e = (ParentHeartbeatEvent)event;

  15.                    this.resetIfNeeded(e.getValue());

  16.  

  17.                } else if (event instanceof HeartbeatEvent) {

  18.                    HeartbeatEvent e = (HeartbeatEvent)event;

  19.                    this.resetIfNeeded(e.getValue());

  20.  

  21.                }

  22.            } else {

  23.                /**

  24.                 * 原来代码

  25.                 * this.reset();

  26.                 */

  27.                if ((event instanceof ContextRefreshedEvent) || (event instanceof RefreshScopeRefreshedEvent) || (event instanceof RoutesRefreshedEvent)) {

  28.  

  29.                    if (event instanceof ContextRefreshedEvent) {

  30.                        ContextRefreshedEvent contextRefreshedEvent = (ContextRefreshedEvent) event;

  31.                        ApplicationContext context = contextRefreshedEvent.getApplicationContext();

  32.  

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

  34.  

  35.                        /**

  36.                         * 为了服务启动只执行一次从数据库里面获取路由信息,这儿进行判断

  37.                         */

  38.                        if (eventClassName.equals("org.springframework.context.annotation.AnnotationConfigApplicationContext")) {

  39.                            this.reset();

  40.                        }

  41.                    } else {

  42.                        this.reset();

  43.                    }

  44.                }

  45.            }

  46.  

  47.        }

  48.  

  49.        private void resetIfNeeded(Object value) {

  50.            /**

  51.             * 发送监控心态信息接收到注册服务中心的数据后,只更新心态的相关信息,不再从新load整个路由

  52.             * 原来是从新load路由信息,可以把新注册的服务都动态load进来。

  53.             * 现在要求新的服务的路由在数据库里面配置。

  54.             *

  55.             * 否则的话每30秒发送心态检测,就会更新一次路由信息,没有必要

  56.             *

  57.             */

  58.            if (!this.heartbeatMonitor.update(value)) {

  59.                return;

  60.            }

  61.            /* 原来代码

  62.            if (this.heartbeatMonitor.update(value)) {

  63.                this.reset();

  64.            }*/

  65.  

  66.        }

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

后记

写博客很累,主要是没有经验,又担心有的地方理解错误,误导大家。出现问题,有的时候需要去从源码哪里找到答案。本文如果在实践中出现任何问题,欢迎留言指正。

你可能感兴趣的:(Spring,Cloud)