Zuul动态路由

spring-cloud-netflix-core:1.3.0.RELEASE

原文:https://note.youdao.com/share/?id=fbd1d5bda015ae807a00c55edb2d3ed8&type=note#/

先说明两个概念:路由配置和路由规则,
路由配置是指配置某请求路径路由到指定的目的地址;
路由规则是指匹配到路由配置之后,再进行自定义的规则判断,规则判断可以更改路由目的地址

动态路由需要达到可持久化配置,动态刷新的效果。不仅要能满足从spring的配置文件properties加载路由信息,还需要从Redis加载我们的配置。另外一点是,路由信息在容器启动时就已经加载进入了内存,我们希望配置完成后,实施发布,动态刷新内存中的路由信息,达到不停机维护路由信息的效果

为了避免Eureka的侵入性设计,这里没有使用spring-cloud的服务的注册与发现的Eureka,而直接使用了Zuul。

需要配置如下属性使Zuul不依赖Eureka:

#关闭zuul的eureka注册
eureka.client.enabled=false

Zuul动态路由_第1张图片

CubeRouteLocator 自定义 动态路由

这里采取的方式修改路由定位器,借鉴DiscoveryClientRouteLocator去改造SimpleRouteLocator使其具备刷新能力。

DiscoveryClientRouteLocator比SimpleRouteLocator多了两个功能,第一是从DiscoveryClient(如Eureka)发现路由信息,我们不想使用eureka这种侵入式的网关模块,所以忽略它,第二是需要实现可刷新的路由定位器接口(RefreshableRouteLocator),并可以继承默认的实现(SimpleRouteLocator)再进行扩展,能够实现动态刷新。

实现动态路由主要关注两个方法

  • protected Map locateRoutes():此方法是加载路由配置的,父类中是获取properties中的路由配置,可以通过扩展此方法,达到动态获取配置的目的

    • 加载方式:1.zk 2.DB 3.Redis 4.配置文件
  • public Route getMatchingRoute(String path):此方法是根据访问路径,获取匹配的路由配置,父类中已经匹配到路由,可以通过路由id查找自定义配置的路由规则,以达到根据自定义规则动态分流的效果(了解)

public class CubeRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator{
	private LookupService lookupService;

	public CubeRouteLocator(LookupService lookupService,ServerProperties server, ZuulProperties properties) {
		super(server.getServletPrefix(), properties);
		this.lookupService = lookupService;
	}

    protected Map locateRoutes() {
    	List serviceInfos = lookupService.getServiceInfos();//从配置文件中查询路由信息
		LinkedHashMap routesMap = new LinkedHashMap();
		for(ServiceInfo info : serviceInfos){
			ZuulRoute zuulRoute = new ZuulRoute();
			zuulRoute.setId( info.getServiceId() );
			zuulRoute.setServiceId( info.getServiceId() );
			zuulRoute.setPath( "/api/"+info.getServiceId() + info.getPath() +"/**" );
			zuulRoute.setUrl( "http://" + info.getHostName() + ":" + info.getPort()  + info.getPath() );
			routesMap.put("/api/"+info.getServiceId()+ info.getPath() +"/**", zuulRoute);
		}

        return routesMap;
    }
    //刷新路由。
	@Override
	public void refresh() {
		doRefresh();
	}

}

在spring容器启动完成后就刷新了路由规则(重启)。因此我们如果要主动刷新路由规则,只需要发布一个RoutesRefreshedEvent事件即可(源码:ZuulRefreshListener 如下) , 由源码可知在发生ContextRefreshedEvent(Spring重启)和RoutesRefreshedEvent(可以手动发布)事件时会执行

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

看下源码,zuul是怎么做到转发,路由的。

@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass(ZuulServlet.class)
@Import(ServerPropertiesAutoConfiguration.class)
public class ZuulConfiguration {

	@Autowired
	protected ZuulProperties zuulProperties; //zuul的配置文件,对应了application.properties中的配置信息

	@Autowired
	protected ServerProperties server;

	@Autowired(required = false)
	private ErrorController errorController;

	@Bean
	public HasFeatures zuulFeature() {
		return HasFeatures.namedFeature("Zuul (Simple)", ZuulConfiguration.class);
	}

	@Bean
	@Primary
	public CompositeRouteLocator primaryRouteLocator(
			Collection routeLocators) {
		return new CompositeRouteLocator(routeLocators);
	}
    //核心类,路由定位器,最最重要
	@Bean
	@ConditionalOnMissingBean(SimpleRouteLocator.class)
	public SimpleRouteLocator simpleRouteLocator() {
	    //默认配置的实现是SimpleRouteLocator.class
		return new SimpleRouteLocator(this.server.getServletPrefix(),
				this.zuulProperties);
	}
    //zuul的控制器,负责处理链路调用
	@Bean
	public ZuulController zuulController() {
		return new ZuulController();
	}

	@Bean
	public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
		ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());
		mapping.setErrorController(this.errorController);
		return mapping;
	}
	
    //注册了一个路由刷新监听器,默认实现是ZuulRefreshListener.class,这个是我们动态路由的关键
	@Bean
	public ApplicationListener zuulRefreshRoutesListener() {
		return new ZuulRefreshListener();
	}


    //上面提到的路由刷新监听器 
	private static class ZuulRefreshListener
			implements ApplicationListener {

		@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); //setDirty方法源码如下
			}
			else if (event instanceof HeartbeatEvent) {
				if (this.heartbeatMonitor.update(((HeartbeatEvent) event).getValue())) {
					this.zuulHandlerMapping.setDirty(true);
				}
			}
		}

	}

}


	public void setDirty(boolean dirty) {
		this.dirty = dirty;
		if (this.routeLocator instanceof RefreshableRouteLocator) {
			((RefreshableRouteLocator) this.routeLocator).refresh();
		}
	}

分析下平台路由配置类

@Configuration
@EnableZuulProxy
public class CubeRouteConfiguration {
    @Autowired
    private ServerProperties server;
    
    @Autowired
    private ZuulProperties properties;
    
    //映射规则写入到配置文件中
    @Value("${cube.lookupservice.path?:file:config/cube-service.json}")
    private Resource resouce;

    public CubeRouteConfiguration() {
    }

    @Bean
    public CubeRouteLocator getCubeRouteLocator() throws IOException {
        return new CubeRouteLocator(this.getLookupService(), this.server, this.properties);
    }
    
    //读取配置文件中的路由规则
    @Bean
    public LookupService getLookupService() throws IOException {
        if (this.resouce.exists()) {
            FileLookupService lookupService = new FileLookupService(this.resouce.getInputStream());
            return lookupService;
        } else {
            return new FileLookupService();
        }
    }
    //路由过滤器
    @Bean
    public CubeZuulFilter getCubeZuulFilter() {
        return new CubeZuulFilter();
    }
    // ****  CubeRestClient 服务间调用工具 (平台自己使用(如记录系统日志))
    @Bean
    @ConditionalOnMissingBean
    public CubeRestClient getCubeRestClient() throws IOException {
        RestTemplateBuilder builder = new RestTemplateBuilder(new RestTemplateCustomizer[0]);
        RestTemplate restTemplate = builder.additionalMessageConverters(new HttpMessageConverter[]{new FormHttpMessageConverter(), new FastJsonHttpMessageConverter()}).build();
        return new CubeRestClient(this.getLookupService(), restTemplate); 
        // 请注意这块,this.getLookupService()参数,会把从配置文件读取到的路由规则传给CubeRestClient
        //如果有兴趣 请接着阅读源码,这块不多介绍了
    }
}
    
////////////////////////////////////////////////////我是分隔符////////////////////////////////////////////////////////////////////////////////    
    //服务间调用工具 我们目前使用的 
    @Bean
	@ConditionalOnMissingBean
	public CubeRestClient getCubeRestClient() throws IOException{
		RestTemplateBuilder builder = new RestTemplateBuilder();
		RestTemplate restTemplate = builder.additionalMessageConverters(new FormHttpMessageConverter(),new FastJsonHttpMessageConverter())
			.additionalInterceptors(new ClientHttpRequestInterceptor(){
				@Override
				public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
						throws IOException {
					CubeContext cubeContext = CubeContextManager.getCurrentContext();
					if(null != cubeContext){
						Locale locale = cubeContext.getLanguage();
						String remoteAddress =  cubeContext.getRemoteAddress();
						String jwt = cubeContext.getHeaderString("Authorization");

						request.getHeaders().add("x-forwarded-for", remoteAddress); //获取IP

						if(locale != null){
							request.getHeaders().add("Content-Language", locale.toString()); //获取local 本地化
						}

						if(StringUtils.isNotBlank(jwt)){
							request.getHeaders().add("Authorization", jwt); //访问该系统的用户是谁 jwt里面有一个user实体(用户名等)
						}
					}

					
					return execution.execute(request, body);
				}
		}).build();
		return new CubeRestClient(getLookupService(), restTemplate);
	}
	
	
////////////////////////////////////////////////////我是分隔符////////////////////////////////////////////////////////////////////////////////
    //这个是WebUI里面 调用后台服务的时候 使用的 看区别
    @Bean
    @ConditionalOnMissingBean
    public CubeRestClient getCubeRestClient() throws IOException {
        RestTemplateBuilder builder = new RestTemplateBuilder(new RestTemplateCustomizer[0]);
        RestTemplate restTemplate = builder.additionalMessageConverters(new HttpMessageConverter[]{new FormHttpMessageConverter(), new FastJsonHttpMessageConverter()}).additionalInterceptors(new ClientHttpRequestInterceptor[]{new ClientHttpRequestInterceptor() {
            public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
                HttpServletRequest httpServletRequest = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
                Locale locale = httpServletRequest.getLocale();
                String remoteAddress = httpServletRequest.getRemoteHost();
                if (locale != null) {
                    request.getHeaders().add("Content-Language", locale.toString());
                }

                request.getHeaders().add("x-forwarded-for", remoteAddress);
                String jwt = (String)RequestContextHolder.currentRequestAttributes().getAttribute("CUBE_AUTHORIZATION_TOKEN", 0);
                if (StringUtils.isNotBlank(jwt) && !request.getHeaders().containsKey("CUBE_AUTHORIZATION_TOKEN")) {
                    request.getHeaders().add("Authorization", jwt);
                }

                return execution.execute(request, body);
            }
        }}).build();
        return new CubeRestClient(this.getLookupService(), restTemplate);
    }

回顾一下 自助审计日志

关注一下这个CUBE_AUTHORIZATION_TOKEN 参数

你可能感兴趣的:(SpringCloud)