Multi tenant personalized service routing

Scene description

Solution

Design a personalized service table to store the personalized services of the tenant. If the tenant does not have personalized services, go to the general service, and a personalized configuration page may be required. The path is the service route accessed by the tenant; The service name represents a certain type of service. Usually, this service can have multiple versions or personalized services; Each version or personalized service has a unique service ID (serviceid), and the service will be registered with Eureka; Tenant ID is associated with personalized service.

API layer solution

By viewing zuul's relevant source code, it is found that when processing route mapping, zuulhandlermapping will obtain the configured route through the route locator discoveryclientroutelocator, and obtain the route list zuulroute from the configured zuulproperties, that is, the configured zuul Routes, the ultimate purpose is to obtain the service ID corresponding to the route.

At the same time, the service list will be obtained from Eureka and converted into route mapping.

Therefore, we only need to change the service ID corresponding to the route to the tenant's personalized service ID in the step of obtaining the route. Note that the service ID obtained here should be regarded as a service name, because a service name will correspond to multiple service IDs.

/**
 * 自定义路由定位器
 */
public class CustomDiscoveryClientRouteLocator extends DiscoveryClientRouteLocator {

    private DiscoveryClient discovery;

    private ZuulProperties properties;

    public CustomDiscoveryClientRouteLocator(String servletPath,DiscoveryClient discovery,ZuulProperties properties) {
        super(servletPath,discovery,properties);
        this.discovery = discovery;
        this.properties = properties;
    }

    public CustomDiscoveryClientRouteLocator(String servletPath,ZuulProperties properties,ServiceRouteMapper serviceRouteMapper) {
        super(servletPath,properties,serviceRouteMapper);
        this.discovery = discovery;
        this.properties = properties;
    }

	/**
     * 覆盖获取路由的方法
     */
    @Override
    protected LinkedHashMap<String,ZuulProperties.ZuulRoute> locateRoutes() {
        LinkedHashMap<String,ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<String,ZuulProperties.ZuulRoute>();

        // ****** 只改这一处 ****** //
        routesMap.putAll(customLocateRoutes());

        // 其它代码不变 复制即可
    }

    /**
     * 获取当前租户的个性化服务,如果从配置文件获取的服务和租户的服务ID不同,则替换成个性化服务.
     */
    protected Map<String,ZuulProperties.ZuulRoute> customLocateRoutes() {
        // 获取当前用户的(特定)路由信息
        List<TenantRoute> tenantRoutes = getCurrentTenantRoute();
        HashMap<String,TenantRoute> tenantRouteMap = new HashMap<>();
        tenantRoutes.forEach(tenantRoute -> {
            tenantRouteMap.put(tenantRoute.getPath(),tenantRoute);
        });

        LinkedHashMap<String,ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>();
        for (ZuulProperties.ZuulRoute route : this.properties.getRoutes().values()) {
            if (tenantRouteMap.containsKey(route.getPath())) {
                TenantRoute tenantRoute = tenantRouteMap.get(route.getPath());
                // 对于某个路由,如/iam/**,如果服务ID不一样,则将个性化服务ID添加进去.
                if (!org.apache.commons.lang.StringUtils.equalsIgnoreCase(tenantRoute.getServiceId(),route.getServiceId())) {
                    routesMap.put(route.getPath(),new ZuulProperties.ZuulRoute(route.getPath(),tenantRoute.getServiceId()));
                    continue;
                }
            }
            routesMap.put(route.getPath(),route);
        }
        return routesMap;
    }

    /**
     * 模拟获取当前租户的个性化服务
     */
    public List<TenantRoute> getCurrentTenantRoute() {
        List<TenantRoute> routes = new ArrayList<>();
        routes.add(new TenantRoute("/iam/**","iam-service","iam-service-100","tenant100"));

        return routes;
    }

    /**
     * 租户路由
     */
    class TenantRoute {
        private String path;
        private String serviceName;
        private String serviceId;
        private String tenantId;

        public TenantRoute(String path,String serviceName,String serviceId,String tenantId) {
            this.path = path;
            this.serviceName = serviceName;
            this.serviceId = serviceId;
            this.tenantId = tenantId;
        }

        // getter/setter
    }

}

@Bean
public DiscoveryClientRouteLocator discoveryRouteLocator(DiscoveryClient discovery,ServiceRouteMapper serviceRouteMapper) {
    return new CustomDiscoveryClientRouteLocator(this.server.getServletPrefix(),this.zuulProperties,serviceRouteMapper);
}

Inter service invocation solution

This paper mainly studies the service personalization called by feign. By checking feign's relevant source code, it is found that when starting the program, the interface with @ feignclient annotation under the root path will be scanned, and a proxy object will be generated for it and put into the spring container. This proxy class is feign Target's implementation class feign Target. Hardcodedtarget, which encapsulates the service name and address, this type of address( http://service-id )It is used for load balancing, that is, requesting services, not a specific IP address.

feign. Synchronousmethodhandler is a specific execution processor that encapsulates the target and client. In the executeanddecade method, load balancing requests are initiated through the client. The core concern is that request request = targetrequest (template), when calling client Before executing, feign.xml will be generated through targetrequest Request object.

In the targetrequest method, first apply all requestinterceptor interceptors to process the requesttemplate, such as adding some information to the header. Then call target.. Apply handles the address. Note that the address in the requesttemplate does not have a service context address, that is http://file-service 。

In the apply method, judge whether the URL of the requesttemplate has http. If not, splice the service context address in front of the address.

Through the above analysis, it is not difficult to find that we only need to change the behavior of apply. In this step, we can replace the personalized service ID with the general service ID. However, we cannot directly extend the target and modify the apply method, but we can achieve this through a disguised method. Through the interceptor, before entering the apply method, personalize the service context (such as http://file-service-100 )Splice to requesttemplate URL, so that no processing will be done in the apply method.

/**
 * 存储当前线程请求服务的服务名称
 */
public class ServiceThreadLocal {

    private static ThreadLocal<String> serviceNameLocal = new ThreadLocal<>();

    public static void set(String serviceName) {
        serviceNameLocal.set(serviceName);
    }

    public static String get() {
        String serviceName = serviceNameLocal.get();
        serviceNameLocal.remove();
        return  serviceName;
    }
}
@Aspect
@Component
public class FeignClientAspect {
    /**
     * 拦截 *FeignClient 结尾的接口的所有方法
     * 这里无法直接通过注解方式拦截 @FeignClient 注解的接口,因为 FeignClient 只有接口,没有实现(生成的是代理类)
     */
    @Before("execution(* *..*FeignClient.*(..))")
    public void keepServiceName(JoinPoint joinPoint) {
        Type type = joinPoint.getTarget().getClass().getGenericInterfaces()[0];
        Annotation annotation = ((Class)type).getAnnotation(FeignClient.class);
        if (annotation != null && annotation instanceof FeignClient) {
            FeignClient feignClient = (FeignClient) annotation;
            // 将服务名放入ThreadLocal中
            String serviceName = feignClient.value();
            if (StringUtils.isEmpty(serviceName)) {
                serviceName = feignClient.name();
            }
            ServiceThreadLocal.set(serviceName);
        }
    }
}
/**
 * 拦截feign请求,根据服务名称和租户ID动态更改路由
 */
@Component
public class FeignRouteInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        // 当前租户ID
        String currentTenantId = "tenant100";
        // 获取当前请求的服务名称
        String serviceName = ServiceThreadLocal.get();
        // 根据租户ID和服务名称获取真正要请求的服务ID
        String serviceId = getCurrentTenantServiceId(currentTenantId,serviceName);

        // 核心代码
        if (StringUtils.isNotBlank(serviceId)) {
            String url;
            // 拼接http://
            if (!StringUtils.startsWith(serviceId,"http")) {
                url = "http://" + serviceId;
            } else {
                url = serviceId;
            }
            // 将真正要请求的服务上下文路径拼接到url前
            if (!StringUtils.startsWith(template.url(),"http")) {
                template.insert(0,url);
            }
        }
    }

    /**
     * 模拟 根据租户ID和服务名称获取服务ID
     */
    public String getCurrentTenantServiceId(String tenantId,String serviceName) {
        List<TenantRoute> tenantRoutes = getCurrentTenantRoute(tenantId);
        for (TenantRoute tenantRoute : tenantRoutes) {
            if (StringUtils.equalsIgnoreCase(serviceName,tenantRoute.getServiceName())) {
                return tenantRoute.getServiceId();
            }
        }
        return serviceName;
    }

    /**
     * 获取租户的个性化服务路由信息
     */
    public List<TenantRoute> getCurrentTenantRoute(String tenantId) {
        // 根据tenantId获取个性化服务 一般在登录时就获取出来然后放到ThreadLocal中.
        List<TenantRoute> routes = new ArrayList<>();
        routes.add(new TenantRoute("/file/**","file-service","file-service-100","tenant100"));

        return routes;
    }
	
	/**
     * 租户路由信息
     */
    class TenantRoute {
        private String path;
        private String serviceName;
        private String serviceId;
        private String tenantId;

        public TenantRoute(String path,String tenantId) {
            this.path = path;
            this.serviceName = serviceName;
            this.serviceId = serviceId;
            this.tenantId = tenantId;
        }

        // getter/setter
    }
}

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>