Take you to realize the dynamic management of Zuul routing rules and related source code interpretation

This article is a long one. The first two parts mainly talk about the principles and source code. Small partners who need to see the implementation directly can jump to the third part through the directory

catalogue

1. Zuul request process

2. interpretation of zuul built-in route interceptor source code

2.1 description of frame interceptor

2.2 source code analysis

3. implementation

3.1 customize RouteLocator

3.2 realize routing management

3.3 effect display

4. summary

1. Zuul request process

Zuul is a gateway component based on Servlet. Zuul defines four types of interceptors: pre interceptor, route interceptor, post interceptor and error interceptor

The routing function of the framework itself is also implemented based on interceptors
When @EnableZuulProxy is enabled, the framework will inject a batch of built-in interceptors into the container when the service is started, including route related interceptors.

When a request comes in, it will enter these interceptors in turn, and the interceptors will forward the request.

The following is the general request flow of a request in the routing function

zuul request simple process

 

It can be seen that after a request comes in, it will first be judged in the front interceptor whether it conforms to the existing routing rules. If the request path matches the routing rules, the request will be marked with some relevant marks, and then the request will be forwarded when entering the framework by the built-in route interceptor

This involves a very important point, that is, when the pre interceptor matches the routing rules of the request, it must first obtain the list of routing rules. Therefore, the pre interceptor must load the routing rules from somewhere. Therefore, to realize the dynamic management of routing rules, our main idea is

  1. Find out where Zuul loads routing rules
  2. Replicate its load logic so that it can load routes from the data source we defined

So let's take a look at what interceptors Zuul has built in and how the interceptors work

2. interpretation of zuul built-in route interceptor source code

2.1 description of frame interceptor

Zuul mainly relies on the following three interceptors to realize Request Routing:

org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter

The front interceptor determines whether the request matches the existing routing rules. If so, it injects relevant routing information into the request container so that the target routing address can be obtained during the actual routing

org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter

The service routing rules automatically discovered from the registry and the configured service routes are forwarded by the route interceptor.

Example:

zuul:
  routes:
    demo1:
      path: /demo1/**
      serviceId: demo1
      stripPrefix: false

When accessing

http://localhost:8080/demo1/test

The interceptor will go to the registry to find the service named demo1 and forward the request to the service,

org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter

If an external link route is configured, it will be forwarded by the route. The default implementation is to call the httpClient to send an http request to the routing address to obtain a response

Example:

zuul:
  routes:
    demo:
      path: /demo/**
      url: http://localhost:8081
      stripPrefix: false

When accessing:

  • http://localhost:8080/demo/test

The interceptor will enter the interceptor, and the interceptor will send the

  • http:localhost:8081/demo/test

Send request

 

2.2 source code analysis

View the source code of PreDecorationFilter

	@Override
	public Object run() {
        // Get request container (stored in ThreadLocal, unique to threads)
		RequestContext ctx = RequestContext.getCurrentContext();
        // Get request uri
		final String requestURI = this.urlPathHelper
				.getPathWithinApplication(ctx.getRequest());
        // Key: judge whether the request uri matches the existing route
		Route route = this.routeLocator.getMatchingRoute(requestURI);
        // If yes, set some route related information to the request container, such as the route destination address and prefix
		if (route != null) {
			String location = route.getLocation();
			if (location != null) {
				ctx.put(REQUEST_URI_KEY, route.getPath());
				ctx.put(PROXY_KEY, route.getId());
            /***********Omit a bunch of non primary code*************/	
			}
		}
		else {
			log.warn("No route found for uri: " + requestURI);
			String forwardURI = getForwardUri(requestURI);

			ctx.set(FORWARD_TO_KEY, forwardURI);
		}
		return null;
	}

The logic of this interceptor is simple

  1. Get request uri
  2. Get existing routing rules
  3. Determine whether the uri matches the existing routing rules
  4. If it matches, set the routing related information in the request container

Here comes the key code

Route route = this.routeLocator.getMatchingRoute(requestURI);

Enter the RouteLocator to see what's inside

package org.springframework.cloud.netflix.zuul.filters;

import java.util.Collection;
import java.util.List;

/**
 * @author Dave Syer
 */
public interface RouteLocator {

	/**
	 * Ignored route paths (or patterns), if any.
	 * @return {@link Collection} of ignored paths
	 */
	Collection<String> getIgnoredPaths();

	/**
	 * A map of route path (pattern) to location (e.g. service id or URL).
	 * @return {@link List} of routes
	 */
	List<Route> getRoutes();

	/**
	 * Maps a path to an actual route with full metadata.
	 * @param path used to match the {@link Route}
	 * @return matching {@link Route} based on the provided path
	 */
	Route getMatchingRoute(String path);

}

From the method name, we can see that this list<route> getroutes() is the method we need to load the route list

View the implementation class relationship uml diagram of this interface

RouteLocator inheritance UML diagram

The interface RouteLocator has three implementation classes, which are

  1. CompositeRouteLocator: By multiple RouteLocator Composed of RouteLocator,Enable@EnableZuulServer The default implementation of,
    There is no specific internal implementation, and there are multiple internal Holdings RouteLocator Object, essentially a decorator
  2. SimpleRouteLocator: Simple route locator, realizing the function of obtaining local route configuration
  3. DiscoveryClientRouteLocator: Enable@EnableZuulProxy The default implementation of the SimpleRouteLocator,
    Realized RefreshableRouteLocator,It also extends the function of getting routing rules from the registry

debug: when the request enters the prefilter:

Prefilter debugging information

It can be seen that the route locator in the prefilter is actually CompositeRouteLocator, and the CompositeRouteLocator object holds the RouteLocator array, and the actual call is discoveryclintroutelocator

Return to the method of getting routes

Look at org Springframework Cloud Netflix Zuul Filters Simpleroutelocator\getroutes

	@Override
	public List<Route> getRoutes() {
		List<Route> values = new ArrayList<>();
        // Mainly focus on the getroutemap () method. The following is just to convert ZuulRoute to Route
		for (Entry<String, ZuulRoute> entry : getRoutesMap().entrySet()) {
			ZuulRoute route = entry.getValue();
			String path = route.getPath();
			try {
				values.add(getRoute(route, path));
			}
			catch (Exception e) {
				if (log.isWarnEnabled()) {
					log.warn("Invalid route, routeId: " + route.getId()
							+ ", routeServiceId: " + route.getServiceId() + ", msg: "
							+ e.getMessage());
				}
				if (log.isDebugEnabled()) {
					log.debug("", e);
				}
			}
		}
		return values;
	}

The getroutes () method does not implement the logic of obtaining routes, but obtains routes from getroutes (). Let's look at getRoutes()

	protected Map<String, ZuulRoute> getRoutesMap() {
		if (this.routes.get() == null) {
			this.routes.set(locateRoutes());
		}
		return this.routes.get();
	}

As you can see, locatoroutes () is the final method to obtain routing rules

protected Map<String, ZuulRoute> locateRoutes() {
		LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>();
		for (ZuulRoute route : this.properties.getRoutes().values()) {
			routesMap.put(route.getPath(), route);
		}
		return routesMap;
	}

The logic here is also very simple, that is, take ZuulRoute from properties and convert it into map<string, ZuulRoute>, and properties are actually Zuul's configuration objects

package org.springframework.cloud.netflix.zuul.filters;

@ConfigurationProperties("zuul")
public class ZuulProperties {

	private String prefix = "";

	private boolean stripPrefix = true;

	private Boolean retryable = false;

	private Map<String, ZuulRoute> routes = new LinkedHashMap<>();
    /**
    * Omit a bunch of similar code
    */

	public static class ZuulRoute {

		private String id;

		private String path;

		private String serviceId;

		private String url;

		private boolean stripPrefix = true;

		private Boolean retryable;

		private Set<String> sensitiveHeaders = new LinkedHashSet<>();

		private boolean customSensitiveHeaders = false;
	}
}

Are you familiar with this? Isn't this the related attribute of zuul that we configured in the yml file?

SimpleRouteLocator obtains the configured routing rules from the configuration file, and

Discoveryclintroutelocator extends this method, adds the function of obtaining registered services from the registry, and implements the RefreshableRouteLocator interface to implement the refresh function.

	@Override
	protected LinkedHashMap<String, ZuulRoute> locateRoutes() {
		LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>();
		routesMap.putAll(super.locateRoutes());
		if (this.discovery != null) {
			Map<String, ZuulRoute> staticServices = new LinkedHashMap<>();
			for (ZuulRoute route : routesMap.values()) {
				String serviceId = route.getServiceId();
				if (serviceId == null) {
					serviceId = route.getId();
				}
				if (serviceId != null) {
					staticServices.put(serviceId, route);
				}
			}
			// Add route for discovery service
			List<String> services = this.discovery.getServices();
			String[] ignored = this.properties.getIgnoredServices()
					.toArray(new String[0]);
                // Convert registry services to ZuulRoute
			for (String serviceId : services) {
				String key = "/" + mapRouteToService(serviceId) + "/**";
				if (staticServices.containsKey(serviceId)
						&& staticServices.get(serviceId).getUrl() == null) {
					// Explicitly configured with no URL, cannot be ignored
					// all static routes are already in routesMap
					// Update location using serviceId if location is null
					ZuulRoute staticRoute = staticServices.get(serviceId);
					if (!StringUtils.hasText(staticRoute.getLocation())) {
						staticRoute.setLocation(serviceId);
					}
				}
				if (!PatternMatchUtils.simpleMatch(ignored, serviceId)
						&& !routesMap.containsKey(key)) {
					// Not ignored
					routesMap.put(key, new ZuulRoute(key, serviceId));
				}
			}
		}
           // Move the default routing rule (/ * *) to the end of the map
		if (routesMap.get(DEFAULT_ROUTE) != null) {
			ZuulRoute defaultRoute = routesMap.get(DEFAULT_ROUTE);
			routesMap.remove(DEFAULT_ROUTE);
			routesMap.put(DEFAULT_ROUTE, defaultRoute);
		}
        // Do some conversion 
		LinkedHashMap<String, ZuulRoute> values = new LinkedHashMap<>();
		for (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;
	}

 

3. implementation

3.1 customize RouteLocator

What we need to do is to inherit discoveryclintroutelocator and rewrite the locateRoutes() method, imitating the way discoveryclintroutelocator extends the route.

/**
 * Custom route locator
 *
 * @author qiudao
 * @date 2020/7/7
 */
@Slf4j
public class CustomRouteLocator extends DiscoveryClientRouteLocator {
    private ZuulProperties properties;
    private ZuulRouteService zuulRouteService;

    public CustomRouteLocator(String servletPath, DiscoveryClient discovery, ZuulProperties properties, ServiceInstance localServiceInstance, ZuulRouteService zuulRouteService) {
        super(servletPath, discovery, properties, localServiceInstance);
        this.properties = properties;
        this.zuulRouteService = zuulRouteService;
    }

    @Override
    protected LinkedHashMap<String, ZuulRoute> locateRoutes() {
        log.info("Custom load route start........................");
        LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>(super.locateRoutes());
        //----------Only this part is different from the parent class, and all others are the same----------------//
        //  Get route from database
        List<ZuulRouteEntity> zuulRouteEntities = zuulRouteService.getActiveRoutes();
        // conversion
        LinkedHashMap<String, ZuulRoute> zuulRouteEntitiesMap = this.convert(zuulRouteEntities);
        routesMap.putAll(zuulRouteEntitiesMap);
        log.info("Get from database{}Routing rules...........", zuulRouteEntitiesMap.size());
        //--------------------------//
        if (routesMap.get(DEFAULT_ROUTE) != null) {
            ZuulRoute defaultRoute = routesMap.get(DEFAULT_ROUTE);
            // Move the defaultServiceId to the end
            routesMap.remove(DEFAULT_ROUTE);
            routesMap.put(DEFAULT_ROUTE, defaultRoute);
        }
        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("End of custom load route........................");
        return values;
    }

    /**
     * Database entities are converted to entities required by Zuul
     *
     * @param zuulRouteEntities
     * @return
     */
    private LinkedHashMap<String, ZuulRoute> convert(List<ZuulRouteEntity> zuulRouteEntities) {
        LinkedHashMap<String, ZuulRoute> result = new LinkedHashMap<>();
        if (zuulRouteEntities == null || zuulRouteEntities.isEmpty()) {
            return result;
        }
        for (ZuulRouteEntity zuulRouteEntity : zuulRouteEntities) {
            ZuulRoute route = new ZuulRoute();
            BeanUtils.copyProperties(zuulRouteEntity, route);
            String id = StringUtils.isEmpty(route.getServiceId()) ? extractId(route.getPath()) : route.getServiceId();
            route.setId(id);
            String regex = ",";
            Set<String> sensitiveHeaders = Arrays.stream(zuulRouteEntity.getSensitiveHeaders().split(regex)).collect(Collectors.toSet());
            route.setSensitiveHeaders(sensitiveHeaders);
            result.put("/" + id + "/**", route);
            if (log.isDebugEnabled()) {
                log.info("Load route:{}", route.toString());
            }
        }
        return result;
    }

    private String extractId(String path) {
        path = path.startsWith("/") ? path.substring(1) : path;
        path = path.replace("/*", "").replace("*", "");
        return path;
    }

}

The main task is to rewrite the locatoroutes method. The parent class gets the service list from the registry and converts it into a route. Here, we get the data from the database and convert it into a route.

3.2 realize routing management

ZuulRouteEntity is our customized database entity, and its properties refer to zuulproperties Zuulroute.

/**
 * @author qiudao
 * @date 2020/7/5
 */
@Data
@Accessors(chain = true)
@TableName("route")
@ToString
public class ZuulRouteEntity {

    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private String id;

    private String path;

    private String serviceId;

    private String url;

    private Boolean stripPrefix = true;

    private Boolean retryable = false;

    private String sensitiveHeaders = "";

    private Boolean customSensitiveHeaders = false;

    private Boolean enable = true;
}

Then add the CRUD method of the entity, and reload the route after the operation is successful

Routing rule CRUD

Because our customized route loader inherits from discoveryclintroutelocator, and it implements the RefreshableRouteLocator refresh interface, if we need to refresh the route, we just need to get this Bean through Spring and call its refresh() method

3.3 effect display

A new test application with port 8082 (Zuul application port 8080)

Test application

At this time, the routing rule has not been configured. First, try to access the interface of 8082 through zuul (the routing rule has not been configured at this time)

Then add a rule through the interface

You can see that the console has printed the relevant logs for obtaining routes. Now let's access this interface again

It can be seen that the routing rules have been adjusted normally. So far, the function of dynamic management of routing rules has been realized.

4. summary

There are a lot of BB. In fact, there are only a few lines of key code

 

Tags: Java Spring

Posted by jweissig on Mon, 30 May 2022 23:45:18 +0530