`

springcloud之动态路由

阅读更多

1. 背景

       Zuul是Netflix提供的一个开源组件,Zuul致力于在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。小弟所在的公司,是使用它来作为网关的重要组成部分。今天就通过一个简单的实例,来具体说明一下是怎么实现的动态路由。

 

2. 架构演变

      为了更好的帮助小伙伴们理解后面的demo,先来做个简单的架构演变,如下图所示:

     上图是没有网关参与的一个最典型的互联网架构。引入网关,为了拉取服务实例,引入springcloud中的eureka组件,作为注册中心,将架构演变后,如下图所示:

       因为Zuul网关是面向众多的外围系统,所以这种服务发现的方式,不适合用在网关产品。因此,将架构继续演变,如下图所示:

我这边实现的简单demo,就是根据上图实现的。

 

3. 动态路由

      既然路由有动态的,那么相对的,也有静态路由。在介绍动态路由之前,先搭建一个静态路由的demo。然后,根据这个示例,我们分析下使用动态路由的优势,再修改下这个demo,最后实现动态路由。

      这里demo的管理工具是maven,整个的项目结构如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.route</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>gate-way</module>
        <module>demo-service</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>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.2</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <executable>true</executable>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

       这里有个需要注意的地方,就是springboot和springcloud的对应版本,如果版本不匹配,会有版本兼容的问题,直接导致服务启动报错。

3.1 gateway项目

       服务启动类:

@EnableZuulProxy
@SpringBootApplication
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

 

       属性配置:

 

# 路由信息
zuul.routes.books.url=http://localhost:8090
zuul.routes.books.path=/book/**

# 不适用注册中心(否则会带来侵入性)
ribbon.eureka.enabled=false

# 网管端口
server.port=8888
 

 

3.2 demo-service项目

      服务启动类:

@RestController
@SpringBootApplication
@Slf4j
public class DemoServiceApplication {

    @RequestMapping(value = "/available")
    public String available() {
        log.info("Spring in Action");
        return "avaliable success";
    }

    @RequestMapping(value = "/checked-out")
    public String checkedOut() {
        return "checkout success";
    }

    public static void main(String[] args) {
        SpringApplication.run(DemoServiceApplication.class, args);
    }
}

     属性配置:

# 服务端口号
server.port=8090

 一个简单的静态路由demo,已经搭建好了,测试下:http://localhost:8888/books/available

 

3.3 静态路由源码分析

       上面是一个简单的静态路由的demo,从源码分析下,实现转发及路由的关键是ZuulConfiguration,下面我们就直接看看这个配置文件的源码:

@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass(ZuulServlet.class)
@Import(ServerPropertiesAutoConfiguration.class)
public class ZuulConfiguration {
    // zuul的配置文件,对应了application.properties中的配置信息
	@Autowired
	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() {
		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
    ........
	
	// post 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);
				}
			}
		}

	}

}

源码中关键的实现,我这里都已经贴出来了,省略号的地方,有兴趣的可以自行查看源码。

 

3.4 动态路由

       动态路由需要达到可持久化配置,动态刷新的效果。如最后一个架构图所示,不仅要能满足从spring的配置文件properties加载路由信息,还需要从数据库加载我们的配置。另外一点是,路由信息在容器启动时就已经加载进入了内存,我们希望配置完成后,实施发布,动态刷新内存中的路由信息,达到不停机维护路由信息的效果。而从ZuulConfiguration的源码上分析,要实现动态路由,第一步需要理解路由定位器,我们画一个关于RouteLocator的UML,如下所示:

 

       从这个UML上,我们查看SimpleRouteLocator的源码,没有实现RefreshableRouteLocator接口。从接口关系来看,spring考虑到了路由刷新的需求,是没法用RouteLocator的默认实现类SimpleRouteLocator来是实现的。所以,我们只能参考DiscoveryClientRouteLocator来改造SimpleRouteLocator使其具备刷新能力。

从DiscoveryClientRouteLocator的源码分析,它是继承SimpleRouteLocator,但是比SimpleRouteLocator多了两个功能:第一是从DiscoveryClient(如Eureka)发现路由信息,代码片段如下所示:

public DiscoveryClientRouteLocator(String servletPath, DiscoveryClient discovery,
		ZuulProperties properties) {
	super(servletPath, properties);

	if (properties.isIgnoreLocalService()) {
		ServiceInstance instance = discovery.getLocalServiceInstance();
		if (instance != null) {
			String localServiceId = instance.getServiceId();
			if (!properties.getIgnoredServices().contains(localServiceId)) {
				properties.getIgnoredServices().add(localServiceId);
			}
		}
	}
	this.serviceRouteMapper = new SimpleServiceRouteMapper();
	this.discovery = discovery;
	this.properties = properties;
}

public DiscoveryClientRouteLocator(String servletPath, DiscoveryClient discovery,
			ZuulProperties properties, ServiceRouteMapper serviceRouteMapper) {
	this(servletPath, discovery, properties);
	this.serviceRouteMapper = serviceRouteMapper;
}

 

       从之前的架构图已经给大家解释清楚了,所以忽略它,第二是实现了RefreshableRouteLocator接口,能够实现动态刷新。

       在自定义实现动态路由之前,先分析下SimpleRouteLocator的源码:

@CommonsLog
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;
	}
	
	// 省略部分实现
	.........

    // 这个方法在网关产品中也很重要,可以根据实际路径匹配到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());
	}

	// 具体就是在这儿定位路由信息的,我们之后从数据库加载路由信息,主要也是从这儿改写
	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;
	}

        // 省略部分实现
	..........
}

省略的部分,有兴趣的小伙伴,可以直接翻查源码。

       分析源码之后,我们就是实现自己的RouteLocator,代码如下所示:

@Slf4j
public class CustomRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator{
    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;
        log.info("servletPath:{}",servletPath);
    }

    //父类已经提供了这个方法,这里写出来只是为了说明这一个方法很重要!!!
    @Override
    public void refresh() {
        super.doRefresh();
    }

    @Override
    protected Map<String, ZuulRoute> locateRoutes() {
        LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>();
        //从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());
        }
        log.info("locateRoutes:{}", values);
        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 = 1 ",new BeanPropertyRowMapper<>(ZuulRouteVO.class));
        for (ZuulRouteVO result : results) {
            if(org.apache.commons.lang3.StringUtils.isAnyEmpty(result.getPath(), result.getUrl())){
                continue;
            }
            ZuulRoute zuulRoute = new ZuulRoute();
            try {
                org.springframework.beans.BeanUtils.copyProperties(result,zuulRoute);
            } catch (Exception e) {
                log.error("=============load zuul route info from db with error==============",e);
            }
            routes.put(zuulRoute.getPath(),zuulRoute);
        }
        return routes;
    }
}

 

在配置文件中添加下DB的配置:

spring.datasource.url=jdbc:mysql://xxxxxx/xxxxx
spring.datasource.username=xxxx
spring.datasource.password=xxxx
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

logging.level.jdbc.sqltiming=INFO
logging.level.jdbc.sqlonly=OFF
logging.level.jdbc.audit=OFF
logging.level.jdbc.resultset=OFF
logging.level.jdbc.connection=OFF

 

配置下CustomRouteLocator

 

 

@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已经配置了事件监听器,我们只需要发送一个事件就可以实现刷新了。

@Service
public class RefreshRouteService {

  @Autowired
  ApplicationEventPublisher publisher;

  @Autowired
  RouteLocator routeLocator;

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

 

4. 总结

       这里实现的动态路由,只是给小伙伴们提供一个思路。当然,解决问题的方法有很多。所以,欢迎小伙伴们大胆尝试。

分享到:
评论

相关推荐

    springcloud getaway 动态路由.doc

    动态路由是 SpringCloud Gateway 的核心特性之一,允许在运行时根据各种条件动态调整路由规则。 在 SpringCloud Gateway 中,动态路由主要通过以下几个组件实现: 1. **RouteDefinitionRepository**: 这是一个接口...

    基于Nacos实现Spring Cloud Gateway实现动态路由的方法示例

    将Nacos与Spring Cloud Gateway结合,可以实现动态路由,即在运行时动态调整路由规则,提高系统的灵活性和可扩展性。 首先,我们要理解Spring Cloud Gateway的核心概念。它是一个基于Spring Framework 5、Project ...

    Spring Cloud Zuul动态路由demo

    本篇文章将深入探讨如何使用Spring Cloud Zuul实现动态路由,并通过实际的代码示例——"Spring Cloud Zuul动态路由demo"来展示这一功能。 1. **Zuul简介** Zuul是Netflix开源的一个边缘服务,主要功能包括路由转发...

    spring_cloud_gateway负载均衡,动态路由

    spring cloud gateway的负载均衡和动态路由的实现 demo_01,demo_02,demo_03 这三个服务相当于是集群的微服务 gateway这个服务是 springcloude gateway + ribbon 做的负载均衡 gateway_01 这个服务 是动态路由的...

    springCloud路由网管负载均衡及拦截过滤的简单实现.

    下面将详细介绍Spring Cloud路由管理、负载均衡和拦截过滤的实现方式。 一、Spring Cloud Gateway Spring Cloud Gateway是Spring Cloud官方推出的下一代API网关,取代了之前的Zuul。它基于Spring Framework 5、...

    Spring Cloud实战 _springcloud实战_springcloud_

    Spring Cloud Config是配置管理工具,它支持配置服务的集中化管理和动态刷新,使得开发者可以在不重启应用的情况下更新配置。另外,Spring Cloud Bus可以将配置变更实时推送到所有关联的服务,进一步提高了配置管理...

    SpringCloud Zuul实现动态路由

    SpringCloud Zuul 实现动态路由 Zuul 是在 Spring Cloud Netflix 平台上提供动态路由、监控、弹性、安全等边缘服务的框架,是 Netflix 基于 JVM 的路由器和服务器端负载均衡器,相当于是设备和 Netflix 流应用的 ...

    spring-cloud项目_springcloud_springcloud项目_springcloud_spring-clou

    3. **Zuul**: Zuul 是 Spring Cloud 的边缘服务和动态路由组件,它可以为微服务架构提供动态路由、过滤和安全等功能。作为 API 网关,Zuul 可以处理所有进入和离开系统的请求。 4. **Hystrix**: Hystrix 是一个延迟...

    Spring Cloud之一 Spring Cloud简介

    它所依赖的基础是Spring Boot框架,因为Spring Cloud是建立在Spring Boot之上的,所以理解Spring Boot是理解和使用Spring Cloud的前提。 Spring Cloud的出现是为了简化分布式系统的开发。在分布式系统中,服务的...

    SpringCloud中文文档

    Spring Cloud 是一个用于快速构建分布式系统的工具集,提供了配置管理、服务发现、断路器、智能路由、微代理、控制总线等多种功能。这些功能可以帮助开发人员快速地支持实现分布式系统中的常见模式,例如服务注册和...

    springcloud hystrix 断路由

    在分布式系统中,服务间的调用异常处理是至关重要的,Spring Cloud Hystrix 就是为了解决这一问题而设计的。Hystrix 是 Netflix 开源的一个延迟和容错库,用于隔离服务间的调用,防止因某个服务的不稳定导致整个系统...

    SpringCloud项目源码下载.docx

    Spring Cloud 是一个基于Spring Boot实现的云应用开发工具包,它为开发者提供了在分布式系统(如配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性令牌、全局锁、领导选举、分布式会话、集群状态)中...

    SpringCloud.pdf

    1. **分布式/版本化配置**:Spring Cloud Config 提供了一个集中式的配置服务器,允许服务动态地获取和更新配置。这使得在分布式环境中管理和维护配置变得更加容易,支持配置的版本控制。 2. **服务注册与发现**:...

    springcloud视频学习

    《SpringCloud视频学习》 SpringCloud作为微服务架构的重要实现框架,深受广大开发者的喜爱。本资源包含了两部关于SpringCloud的视频教程,由尚硅谷出品,内容详实且易于理解,是学习SpringCloud的理想资料。 一、...

    spring cloud视频教程

    Spring Cloud 是一个基于 Spring Boot 实现的云应用开发工具包,它为开发者提供了在分布式系统(如配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性令牌、全局锁、领导选举、分布式会话、集群状态)...

    SpringCloud 15个完整例子

    SpringCloud是中国Java开发者广泛使用的微服务框架,它包含了一系列组件,用于构建分布式系统。这个压缩包文件"SpringCloud 15个完整例子"提供了一系列从基础到进阶的示例项目,帮助用户深入理解并实践SpringCloud的...

    尚硅谷SpringCloud第2季2020版.mmap

    一篇很好的springCloud学习的思维导读,详细的介绍了,springCloud的搭建步骤以及各组件的说明讲解 涵盖 Eureka服务注册与发现 Zookeeper服务注册与发现 Consul服务注册与发现 Ribbon负载均衡服务调用 OpenFeign...

    基于spring cloud项目源码源码.rar

    Spring Cloud是基于Spring Boot实现的一套微服务解决方案,它为开发者提供了在分布式系统(如配置管理、服务发现、断路器、智能路由、微代理、控制总线、一次性令牌、全局锁、领导选举、分布式会话、集群状态)操作...

    尚硅谷周阳老师SpringCloud笔记

    开发者需要学习如何利用SpringCloud Bus实现配置的动态更新。 除此之外,尚硅谷周阳老师的笔记可能还涵盖了SpringCloud Gateway、SpringCloud Stream、SpringCloud Data Flow等更多高级主题。在实际学习过程中,...

Global site tag (gtag.js) - Google Analytics