使用spring zuul 实现动态网关

前言

在微服务的大趋势下,spring cloud相关技术越来越流行,但本人目前负责的业务项目中用spring cloud 的相关技术比较少,刚好组内有同学利用spring zull实现了自己的网关服务,因此借此机会学习学习。
网关致力于提供动态路由,监控,前置安全验证,断路等功能为一体的服务,为其他服务提供统一的外网调用接口。这里只重点介绍网关如何实现动态路由这一个关键点。

spring cloude 通用性架构。
1、外部或者内部的非Spring Cloud项目都统一通过API网关(Zuul)来访问内部服务.
2、网关接收到请求后,从注册中心(Eureka)获取可用服务
3、由Ribbon进行均衡负载后,分发到后端的具体实例
4、微服务之间通过Feign进行通信处理业务
5、Hystrix负责处理服务超时熔断
6、Turbine监控服务间的调用和熔断相关指标

传统互联网服务器结构

使用spring zuul 实现动态网关_第1张图片

image

 

没有网关的结构,Book*代表业务服务

加入eureka的架构图

使用spring zuul 实现动态网关_第2张图片

image

 

book注册到eureka注册中心中,zuul本身也连接着同一个eureka,可以拉取book众多实例的列表。服务中心的注册发现一直是值得推崇的一种方式,但是不适用与网关产品。因为我们的网关是面向众多的其他部门的已有或是异构架构的系统,不应该强求其他系统都使用eureka,这样是有侵入性的设计。

最终架构图

使用spring zuul 实现动态网关_第3张图片

image

 

上图中的gateway最终也会部署多台。properties文件中的路由一般是发布服务是硬编码固定的,容器启动时就一起加入了内存,另外需要加载DB中的路由信息,并且DB中的路由信息更新时需要及时刷新zull内存中的路由信息,那主要的关键点就是两个:1、容器启动时加载DB的路由信息,2、更新DB和zull中的路由信息。
要实现DB动态管理路由,首先简单实现properties方式的路由,了解其管理路由的方式。

Properties路由

gateway项目,即zull网关
启动类:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class,})
public class GatewayApplication {

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

(exclude={DataSourceAutoConfiguration.class,}该注解的作用是,排除自动注入数据源的配置(取消数据库配置),否则启动时会报数据源等错误。
配置:

#配置在配置文件中的路由信息
zuul.routes.books.url=http://localhost:8090
zuul.routes.books.path=/book/**
# 不注册到euraka注册中心
ribbon.eureka.enabled=false
server.port=8080
logging.level.root=debug

book项目,即具体应用服务器
启动类&应用controller:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class BookApplication {

    @RequestMapping(value = "/available")
    public String available() {
        System.out.println("Spring in Action");
        return "Spring in Action";
    }

    @RequestMapping(value = "/checked-out")
    public String checkedOut() {
        return "Spring Boot in Action";
    }

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

配置文件:

server.port=8090

启动两个项目在,访问http://localhost:8080/book/available,即能看到访问信息。

Spring in Action

上面就是一个简单的zull实现的简单静态路由,简单看一下部分源码是怎么实现的。

gateway启动类GatewayApplication.java中的注解,

@EnableZuulProxy ->ZuulProxyConfiguration -> ZuulConfiguration.java

/**
 * @author Spencer Gibb
 * @author Dave Syer
 */
@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass(ZuulServlet.class)
// Make sure to get the ServerProperties from the same place as a normal web app would
@Import(ServerPropertiesAutoConfiguration.class)
public class ZuulConfiguration {

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

    @Autowired
    protected ServerProperties server;

    @Autowired(required = false)
    //在项目中我们遇到404找不到的错误、或者500错误等,需要配置相应的页面给用户一个友好的提示时使用
    private ErrorController errorController;

    @Bean
    public HasFeatures zuulFeature() {
        return HasFeatures.namedFeature("Zuul (Simple)", ZuulConfiguration.class);
    }
    //核心类,路由定位器,最最重要
    @Bean
    @ConditionalOnMissingBean(RouteLocator.class)
    public RouteLocator routeLocator() {
    //系统配置的定位器SimpleRouteLocator.class
        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;
    }
  //注册了一个路由刷新监听器,路由更新后可以通过这个监听刷新路由
    @Bean
    public ApplicationListener 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
    @Bean
    public ServletDetectionFilter servletDetectionFilter() {
        return new ServletDetectionFilter();
    }
    @Bean
    public FormBodyWrapperFilter formBodyWrapperFilter() {
        return new FormBodyWrapperFilter();
    }

    @Bean
    public DebugFilter debugFilter() {
        return new DebugFilter();
    }

    @Bean
    public Servlet30WrapperFilter servlet30WrapperFilter() {
        return new Servlet30WrapperFilter();
    }

    // post filters
    @Bean
    public SendResponseFilter sendResponseFilter() {
        return new SendResponseFilter();
    }

    @Bean
    public SendErrorFilter sendErrorFilter() {
        return new SendErrorFilter();
    }

    @Bean
    public SendForwardFilter sendForwardFilter() {
        return new SendForwardFilter();
    }

    @Configuration
    protected static class ZuulFilterConfiguration {

        @Autowired
        private Map filters;

        @Bean
        public ZuulFilterInitializer zuulFilterInitializer() {
            return new ZuulFilterInitializer(this.filters);
        }

    }
//上面提到的路由刷新监听器
    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);
            }
            else if (event instanceof HeartbeatEvent) {
                if (this.heartbeatMonitor.update(((HeartbeatEvent) event).getValue())) {
                    this.zuulHandlerMapping.setDirty(true);
                }
            }
        }

    }

}

系统默认实现的定位器SimpleRouteLocator.class只是实现了RouteLocator.class中的方法,并没有提供刷新路由的方法,刷洗路由的方法在RefreshableRouteLocator.class中,但是能够借鉴DiscoveryClientRouteLocator.class定义自己的定位器。
RouteLocator.class接口:

public interface RouteLocator {
    /**
     * Ignored route paths (or patterns), if any.
     */
    Collection getIgnoredPaths();
    /**
     * A map of route path (pattern) to location (e.g. service id or URL).
     */
    List getRoutes();
    /**
     * Maps a path to an actual route with full metadata.
     */
    Route getMatchingRoute(String path);
}

RefreshableRouteLocator.class接口:

public interface RefreshableRouteLocator extends RouteLocator {
    void refresh();
}

SimpleRouteLocator.class具体内容:

@CommonsLog
public class SimpleRouteLocator implements RouteLocator {

    private ZuulProperties properties;

    private PathMatcher pathMatcher = new AntPathMatcher();

    private String dispatcherServletPath = "/";
    private String zuulServletPath;

    private AtomicReference> 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 getRoutes() {
        if (this.routes.get() == null) {
//加载properties文件中的路由
            this.routes.set(locateRoutes());
        }
        List 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;
    }

    @Override
    public Collection getIgnoredPaths() {
        return this.properties.getIgnoredPatterns();
    }
//这个方法在网关产品中也很重要,可以根据实际路径匹配到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 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> routes = new AtomicReference<>();也可以达到刷新的效果
    protected void doRefresh() {
        this.routes.set(locateRoutes());
    }

    /**
     * Compute a map of path pattern to route. The default is just a static map from the
     * {@link ZuulProperties}, but subclasses can add dynamic calculations.
     */
//具体就是在这儿定位路由信息的,我们之后从数据库加载路由信息,主要也是从这儿改写
    protected Map locateRoutes() {
        LinkedHashMap routesMap = new LinkedHashMap();
        for (ZuulRoute route : this.properties.getRoutes().values()) {
            routesMap.put(route.getPath(), route);
        }
        return routesMap;
    }

    protected boolean matchesIgnoredPatterns(String path) {
        for (String pattern : this.properties.getIgnoredPatterns()) {
            log.debug("Matching ignored pattern:" + pattern);
            if (this.pathMatcher.match(pattern, path)) {
                log.debug("Path " + path + " matches ignored pattern " + pattern);
                return true;
            }
        }
        return false;
    }

    private String adjustPath(final String path) {
        String adjustedPath = path;

        if (RequestUtils.isDispatcherServletRequest()
                && StringUtils.hasText(this.dispatcherServletPath)) {
            if (!this.dispatcherServletPath.equals("/")) {
                adjustedPath = path.substring(this.dispatcherServletPath.length());
                log.debug("Stripped dispatcherServletPath");
            }
        }
        else if (RequestUtils.isZuulServletRequest()) {
            if (StringUtils.hasText(this.zuulServletPath)
                    && !this.zuulServletPath.equals("/")) {
                adjustedPath = path.substring(this.zuulServletPath.length());
                log.debug("Stripped zuulServletPath");
            }
        }
        else {
            // do nothing
        }

        log.debug("adjustedPath=" + path);
        return adjustedPath;
    }

}

重写之后的路由定位器:

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;
        logger.info("servletPath:{}",servletPath);
    }  

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

    @Override
    protected Map locateRoutes() {
        LinkedHashMap routesMap = new LinkedHashMap();
        //从application.properties中加载路由信息
        routesMap.putAll(super.locateRoutes());
        //从db中加载路由信息
        routesMap.putAll(locateRoutesFromDB());
        //优化一下配置
        LinkedHashMap values = new LinkedHashMap<>();
        for (Map.Entry 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());
        }
        return values;
    }

    private Map locateRoutesFromDB(){
        Map routes = new LinkedHashMap<>();
        List results = jdbcTemplate.query("select * from gateway_api_define where enabled = true ",new BeanPropertyRowMapper<>(ZuulRouteVO.class));
        for (ZuulRouteVO result : results) {
            if(org.apache.commons.lang3.StringUtils.isBlank(result.getPath()) || org.apache.commons.lang3.StringUtils.isBlank(result.getUrl()) ){
                continue;
            }
            ZuulRoute zuulRoute = new ZuulRoute();
            try {
                org.springframework.beans.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 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.eg.http://localhost:8090
         */
        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 String apiName;

        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 String getApiName() {
            return apiName;
        }

        public void setApiName(String apiName) {
            this.apiName = apiName;
        }

        public Boolean getEnabled() {
            return enabled;
        }

        public void setEnabled(Boolean enabled) {
            this.enabled = enabled;
        }
    }

配置自定义的路由定位器:

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

最后只差动态刷新路由器,前面ZuulConfiguration.class中有对路由刷新做时间监控,因此,只需要更新路由后发送一个RoutesRefreshedEvent.class事件,zull就会自动接收监听进行路由刷新。因此简单增加一个路由刷新事件的service即可,

@Service
public class RefreshRouteService {
    @Autowired
    ApplicationEventPublisher publisher;
    @Autowired
    RouteLocator routeLocator;

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

再次启动两个项目,并且手动修改数据库中的路由信息,调用刷新接口后,能够访问到book服务,到此,动态路由的简单实现就已经完成。

这里有一张摘抄于SpringCloud官网的zuul的生命周期图片

 

使用spring zuul 实现动态网关_第4张图片

image.png

小礼物走一走,来简书关注我



作者:黎明_dba5
链接:https://www.jianshu.com/p/a8f111c8f261
来源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

你可能感兴趣的:(spring,boot,themeleaf)