Implementation of spring-cloud-zuul dynamic routing

Posted by bradley252 on Sun, 19 May 2019 22:39:04 +0200

First, two concepts are explained: routing configuration and routing rules. Routing configuration refers to configuring a request path to route to a specified destination address. Routing rules refer to matching to the routing configuration and then making a custom rule judgment. Rule judgment can change the routing destination address.

zuul's default routing is configured in properties. If dynamic routing is needed, it needs to be implemented by itself. From the source code analysis above, it can be seen that the implementation of dynamic routing needs to implement Refreshable Route Locator, and can inherit the default implementation (Simple Route Locator) and expand it.

There are two main ways to implement dynamic routing

  • Protected Map < String, ZuulRoute > locateRoutes (): This method loads the routing configuration, and the parent class obtains the routing configuration in properties. This method can be extended to achieve the purpose of dynamic acquisition of configuration.

  • Public Route getMatching Route (String path): This method is based on access path to obtain the matched routing configuration. The parent class has been matched to the routing. The routing rules of the customized configuration can be found through the routing id to achieve the dynamic flow effect according to the customized rules.

In order to implement dynamic routing for different storage modes, abstract classes are defined to implement basic functions. The code is as follows.

package com.itopener.zuul.route.spring.boot.common;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import com.alibaba.fastjson.JSON;
import com.itopener.zuul.route.spring.boot.common.rule.IZuulRouteRule;
import com.itopener.zuul.route.spring.boot.common.rule.IZuulRouteRuleMatcher;
 
public abstract class ZuulRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
    public final static Logger logger = LoggerFactory.getLogger(ZuulRouteLocator.class);
     
    private ZuulProperties properties;
     
    @Autowired
    private IZuulRouteRuleMatcher zuulRouteRuleMatcher;
     
    public ZuulRouteLocator(String servletPath, ZuulProperties properties) {
        super(servletPath, properties);
        this.properties = properties;
        logger.info("servletPath:{}", servletPath);
    }
     
    /**
     * @description Refresh routing configuration
     * @author fuwei.deng
     * @date 2017 3 July 2000, 6:04:42 p.m.
     * @version 1.0.0
     * @return
     */
    @Override
    public void refresh() {
        doRefresh();
    }
     
    @Override
    protected Map<String, ZuulRoute> locateRoutes() {
        LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
        // Load routing information from application.properties
        // routesMap.putAll(super.locateRoutes());
        // Load routing configuration
        routesMap.putAll(loadLocateRoute());
        // Optimize the configuration
        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());
        }
        return values;
    }
     
    /**
     * @description Load routing configuration, implemented by subclasses
     * @author fuwei.deng
     * @date 2017 3 July 2000, 6:04:42 p.m.
     * @version 1.0.0
     * @return
     */
    public abstract Map<String, ZuulRoute> loadLocateRoute();
     
    /**
     * @description Get routing rules and implement them by subclasses
     * @author fuwei.deng
     * @date 2017 3 July 2000, 6:04:42 p.m.
     * @version 1.0.0
     * @return
     */
    public abstract List<IZuulRouteRule> getRules(Route route);
     
    /**
     * @description Changing the routing destination address by configuring rules
     * @author fuwei.deng
     * @date 2017 3 July 2000, 6:04:42 p.m.
     * @version 1.0.0
     * @return
     */
    @Override
    public Route getMatchingRoute(String path) {
        Route route = super.getMatchingRoute(path);
        // Adding Custom Routing Rule Judgment
        List<IZuulRouteRule> rules = getRules(route);
        return zuulRouteRuleMatcher.matchingRule(route, rules);
    }
     
    /**
     * @description Priority of Router Locator
     * @author fuwei.deng
     * @date 2017 3 July 2000, 6:04:42 p.m.
     * @version 1.0.0
     * @return
     */
    @Override
    public int getOrder() {
        return -1;
    }
     
    /**
     * @description The entity of storage routing is converted to Zuul Route required by zuul
     * @author fuwei.deng
     * @date 2017 6:19:40 p.m. on July 3, 2000
     * @version 1.0.0
     * @param locateRouteList
     * @return
     */
    public Map<String, ZuulRoute> handle(List<ZuulRouteEntity> locateRouteList){
        if(CollectionUtils.isEmpty(locateRouteList)){
            return null;
        }
        Map<String, ZuulRoute> routes = new LinkedHashMap<>();
        for (ZuulRouteEntity locateRoute : locateRouteList) {
            if (StringUtils.isEmpty(locateRoute.getPath())
                    || !locateRoute.isEnable()
                    || (StringUtils.isEmpty(locateRoute.getUrl()) && StringUtils.isEmpty(locateRoute.getServiceId()))) {
                continue;
            }
            ZuulRoute zuulRoute = new ZuulRoute();
            try {
                zuulRoute.setCustomSensitiveHeaders(locateRoute.isCustomSensitiveHeaders());
                zuulRoute.setSensitiveHeaders(locateRoute.getSensitiveHeadersSet());
                zuulRoute.setId(locateRoute.getId());
//              zuulRoute.setLocation("");
                zuulRoute.setPath(locateRoute.getPath());
                zuulRoute.setRetryable(locateRoute.isRetryable());
                zuulRoute.setServiceId(locateRoute.getServiceId());
                zuulRoute.setStripPrefix(locateRoute.isStripPrefix());
                zuulRoute.setUrl(locateRoute.getUrl());
                logger.info("add zuul route: " + JSON.toJSONString(zuulRoute));
            } catch (Exception e) {
                logger.error("=============load zuul route info with error==============", e);
            }
            routes.put(zuulRoute.getPath(), zuulRoute);
        }
        return routes;
    }
}

Subclasses are then defined to implement routing configuration and access to routing rules, such as stored in Zookeeper

package com.itopener.zuul.route.zk.spring.boot.autoconfigure;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import com.alibaba.fastjson.JSON;
import com.itopener.zuul.route.spring.boot.common.ZuulRouteEntity;
import com.itopener.zuul.route.spring.boot.common.ZuulRouteLocator;
import com.itopener.zuul.route.spring.boot.common.ZuulRouteRuleEntity;
import com.itopener.zuul.route.spring.boot.common.rule.IZuulRouteRule;
/**
 * @author fuwei.deng
 * @date 2017 11:11:19 a.m. on 30 June 2001
 * @version 1.0.0
 */
public class ZuulRouteZookeeperLocator extends ZuulRouteLocator {
    public final static Logger logger = LoggerFactory.getLogger(ZuulRouteZookeeperLocator.class);
    @Autowired
    private CuratorFrameworkClient curatorFrameworkClient;
 
    private List<ZuulRouteEntity> locateRouteList;
    public ZuulRouteZookeeperLocator(String servletPath, ZuulProperties properties) {
        super(servletPath, properties);
    }
    @Override
    public Map<String, ZuulRoute> loadLocateRoute() {
        locateRouteList = new ArrayList<ZuulRouteEntity>();
        try {
            locateRouteList = new ArrayList<ZuulRouteEntity>();
            //Get the id of all routing configurations
            List<String> keys = curatorFrameworkClient.getChildrenKeys("/");
            //Traverse to get all routing configurations
            for(String item : keys){
                String value = curatorFrameworkClient.get("/" + item);
                if(!StringUtils.isEmpty(value)){
                    ZuulRouteEntity route = JSON.parseObject(value, ZuulRouteEntity.class);
                    //Only enabled routing configuration is required
                    if(!route.isEnable()){
                        continue;
                    }
                    route.setRuleList(new ArrayList<IZuulRouteRule>());
                    //Get the ID of all routing rules corresponding to the routing configuration
                    List<String> ruleKeys = curatorFrameworkClient.getChildrenKeys("/" + item);
                    //Traverse to get all the routing rules
                    for(String ruleKey : ruleKeys){
                        String ruleStr = curatorFrameworkClient.get("/" + item + "/" + ruleKey);
                        if(!StringUtils.isEmpty(ruleStr)){
                            ZuulRouteRuleEntity rule = JSON.parseObject(ruleStr, ZuulRouteRuleEntity.class);
                            //Retain only available routing rules
                            if(!rule.isEnable()){
                                continue;
                            }
                            route.getRuleList().add(rule);
                        }
                    }
                    locateRouteList.add(route);
                }
            }
        } catch (Exception e) {
            logger.error("load zuul route from zk exception", e);
        }
        logger.info("load zuul route from zk : " + JSON.toJSONString(locateRouteList));
        return handle(locateRouteList);
    }
    @Override
    public List<IZuulRouteRule> getRules(Route route) {
        if(CollectionUtils.isEmpty(locateRouteList)){
            return null;
        }
        for(ZuulRouteEntity item : locateRouteList){
            if(item.getId().equals(route.getId())){
                return item.getRuleList();
            }
        }
        return null;
    }
}

Above is the main code encapsulating zuul dynamic routing. The complete code is attached. The attachment code includes routing configuration and routing rules stored by db, zk and redis, as well as management pages and sample codes.

Method of use and effect achieved

Introduce the corresponding starter in the application of configuring routing.

<!-- zookeeper -->
<dependency>
    <groupId>com.itopener</groupId>
    <artifactId>zuul-route-zk-starter</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <type>pom</type>
</dependency>
<!-- db-->
<dependency>
    <groupId>com.itopener</groupId>
    <artifactId>zuul-route-db-starter</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <type>pom</type>
</dependency>
<!-- redis-->
<dependency>
    <groupId>com.itopener</groupId>
    <artifactId>zuul-route-redis-starter</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <type>pom</type>
</dependency>

Configure the corresponding data source

  • zk: spring.zuul.route.zk.serverLists
  • db: Normally configured data source DataSource
  • Redis: Normal configuration redis (RedisTemplate)

Start zuul-route-admin, go to the management page, configure routing and routing rules (you can configure only routing, if the corresponding routing rules are empty, no rule judgment), the routing rules are js syntax, built-in obj objects, you can directly get the parameters in the request through obj, such as

The routing configuration page is as follows. The configuration fields are consistent with the properties configuration.

Routing and rules can be disabled, enabled, deleted, etc., or can be viewed by switching data sources

Routing configuration and rule configuration refresh

  • After routing configuration and rule configuration, the local machine can call the refresh method directly, but considering that routing gateway usually deploys multiple nodes, there is no direct call to the refresh method.

  • zk can monitor data changes, if using zk storage, after modifying the data, each node will automatically refresh

  • redis and db do not have a listening method, so you need to configure the automatic refresh time, spring.zuul.route.refreshCron, with a default value of 0/30* * * *? (refresh every 30 seconds)

Achieved results

  • If you need to allocate shunting according to time (for example, 66 previous activities shunt to different applications according to the time in the morning and afternoon), you can configure the corresponding rules, the content of which is: new Date (). getHours ()>12?'true':'false'. The expected result of the rules is: true. After matching the rules, you can route to the corresponding routing destination address (blue text is admin management page field).

  • If you need to shunt according to the parameter name, you can configure the corresponding rules: obj. name =='honey'?'1':'2', and then configure the corresponding expected results and routing destination address.

  • If all the rules are not matched, 404 will be returned. So try to match at least one rule to avoid bad experience for users.

Current known problems

  • Configuration rules can only be used to shunt to specified services at present, but can not be refined to some nodes of specified services.

  • Because spring can easily listen to Heartbeat Event events and trigger refresh loading routing configuration multiple times, the actual refresh time may be inconsistent with the refresh time of its own configuration and lead to repeated refreshes.

Topics: Spring Java JSON Redis