欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

springcloud之动态路由

程序员文章站 2022-07-13 11:11:39
...

1. 背景

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

 

2. 架构演变

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

springcloud之动态路由
            
    
    博客分类: 架构 springcloudzuul 

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

springcloud之动态路由
            
    
    博客分类: 架构 springcloudzuul 

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

springcloud之动态路由
            
    
    博客分类: 架构 springcloudzuul 

我这边实现的简单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,如下所示:

 

springcloud之动态路由
            
    
    博客分类: 架构 springcloudzuul 

       从这个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 zuul