布玛旁若无人的对着倒在建筑之中的鹤仙凝聚出了一颗能量球直接发射出去形成了气功波命中在那里产生了一阵爆炸,这下子鹤仙人可谓是生死不知了。

“违约这事,说好听点是人为财死鸟为食亡,说难听点,就是没有信用。无信便是无德,往小里说,他可能只是个人私德问题。往大里说,以后谁要兴起点风浪,这就是个把柄,人人都可以藉此咬我一口。那我可能付出的代价,未必会比你给我的开价少。”

吉利棋牌安卓版

“响雷能力者果然厉害,那么远就听到一切了。”没过多久,香克斯所乘坐的海贼船已经出现在刘皓他们一行人的视线当中了,带头的香克斯笑了笑,他的见闻色霸气虽然还没达到当年罗杰的地步,但是也只有一线之差了,可以说已经是超越了雷利,距离聆听万物心声还差一点点而已。
空中的轰炸机不断来回俯冲扫射,引擎的吼叫声和轰炸机俯冲时候摩擦空气产生的巨大轰鸣声,以及机枪火力的“哒哒哒”的射击声交织在一起,打得江面上的鬼子死伤累累,惨号连连。

八根修长的矛在他背后伸展,红白两色光芒遍布矛身,无数蓝银草在背后凝结成伞转形态,减缓着他那下落的速度,冰冷的目光始终盯视在泰诺身上,他背后那八根长矛,在阳光的照耀下闪烁着辉煌的光彩,看上去是那样的奇异。

public List


场景描述

  • 不同租户访问同一个地址,tenant100租户有一个个性化服务service-b-100,在API层需要将其路由到service-b-100服务,其它租户则路由到service-b,达到个性化需求。
  • 在服务间,service-a调用service-b,tenant100租户访问时需要调用他的个性化服务service-b-100

Alt text

解决方案

设计一张个性化服务表存储租户的个性化服务,如果租户没有个性化服务,则走通用服务,可能需要一个个性化配置页面。路由(path)是租户访问的服务路由;服务名(serviceName)代表某类服务,通常这个服务可以有多个版本或者个性化服务;每个版本或者个性化服务都有一个唯一的服务ID(serviceId),并且这个服务会注册到Eureka;租户ID(tenantId)则关联了个性化服务。

Alt text

个性化需求的最终目标是找到租户的个性化服务ID,在保证程序代码不变,低侵入性的情况下, 在API层,可以通过路由+租户ID找到服务ID;在服务间调用时,可以通过服务名+租户ID找到服务ID。API层,就算有个性化服务,版本升级,但配置依然保持不变;服务间FeignClient调用,服务名保持不变。

API层解决方案

通过查看Zuul的相关源码发现,ZuulHandlerMapping在处理路由映射的时候,会通过路由定位器DiscoveryClientRouteLocator获取配置的路由,从配置ZuulProperties获取路由列表ZuulRoute,即配置的zuul.routes,最终目的也是获取路由对应的服务ID。

Alt text

同时,会从Eureka获取服务列表,并转换成路由映射。

Alt text

Alt text

所以,我们只需要在获取路由这一步将路由对应的服务ID更改成租户的个性化服务ID即可。注意这里获取的服务ID要将其看成服务名,因为一个服务名会对应多个服务ID。

  • 自定义DiscoveryClientRouteLocator,覆盖locateRoutes方法,核心是customLocateRoutes将从配置文件获取的路由服务与当前租户的个性化路由服务做对比,如果某个路由存在个性化服务,则将个性化服务ID替换成之前的服务ID即可。
/**
 * 自定义路由定位器
 */
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, DiscoveryClient discovery, ZuulProperties properties, ServiceRouteMapper serviceRouteMapper) {
        super(servletPath, discovery, 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,覆盖默认的DiscoveryClientRouteLocator
@Bean
public DiscoveryClientRouteLocator discoveryRouteLocator(DiscoveryClient discovery, ServiceRouteMapper serviceRouteMapper) {
    return new CustomDiscoveryClientRouteLocator(this.server.getServletPrefix(), discovery, this.zuulProperties, serviceRouteMapper);
}
  • 测试可以看出已经更改成个性化服务了,但是我们的配置文件并没有做任何改动。

Alt text

服务间调用解决方案

  • 服务间调用方式

    • RestTemplate:实现HTTP调用,参数列表比较长。
    • Ribbon:实现客户端负载均衡,添加@LoadBalanced注解让RestTemplate整合Ribbon,使其具备负载均衡的能力。

      @Bean
      @LoadBalanced
      public RestTemplate restTemplate() {
        return new RestTemplate();
      }
    • Feign:声明式、模板化的HTTP客户端,帮助我们更加快捷、优雅地调用HTTP API。
    • RPC:基于TCP的远程过程调用,这种方式可确保调用性能更加高效,能支持更高的并发量。

这里主要研究了Feign调用的服务个性化。通过查看Feign的相关源码发现,在启动程序的时候,会扫描根路径下有@FeignClient注解的接口,并为其生成代理对象,放入Spring容器中。这个代理类就是feign.Target的实现类feign.Target.HardCodedTargetTarget封装了服务名和地址,这种类型的地址(http://service-id)就是用来做负载均衡的,即请求服务,而不是具体的某个IP地址。

Alt text

feign.SynchronousMethodHandler是具体的执行处理器,封装了TargetClient,在executeAndDecode方法里,通过client发起负载均衡请求。核心关注的是Request request = targetRequest(template),在调用client.execute之前,会先通过targetRequest生成feign.Request对象。

Alt text

targetRequest方法里,首先应用所有的RequestInterceptor拦截器对RequestTemplate做处理,比如在Header中加入一些信息等。然后调用target.apply对地址做处理,注意此时RequestTemplate中的地址是不带服务上下文地址的,即http://file-service

Alt text

apply方法中,判断RequestTemplateurl是否带有http,如果没有,则将服务上下文地址拼接到地址前面。

Alt text

通过以上分析不难发现,我们只需要更改apply的行为,在这一步将个性化服务ID替换成通用服务ID即可。但是我们无法直接扩展Target并修改apply方法,但可以通过一种变相的方法达到这个目的。通过拦截器,在进入apply方法之前,将个性化服务上下文(比如http://file-service-100)拼接到RequestTemplate.url前,这样apply方法里就不会做处理了。

  • 首先开发一个存储当前请求服务名称的ThreadLocal
/**
 * 存储当前线程请求服务的服务名称
 */
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;
    }
}
  • 开发@FeignClient的切面AOP,拦截到Feign请求时,将服务名放入ThreadLocal中。
@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);
        }
    }
}
  • 开发RequestInterceptor处理RequestTemplate
/**
 * 拦截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 serviceName, String serviceId, String tenantId) {
            this.path = path;
            this.serviceName = serviceName;
            this.serviceId = serviceId;
            this.tenantId = tenantId;
        }

        // getter/setter
    }
}
  • 测试可以看到RequestTemplate.url已经更改成功了,但是FeignClient的服务名称并没有改变。

Alt text

通过以上设计方式,在API网关处和Feign调用时做一些处理,达到个性化服务的目的,通过配置页面配置租户的个性化服务,当然,所有的服务都需要注册到Eureka,配置时可以从Eureka拉取服务列表。这样一来,无论是服务多版本,还是定制化服务,都可以在不改代码及配置文件的情况下完成特定服务路由。

当前文章:http://hnhdqp.com/html_42909.html

发布时间:2019-02-24 06:17:08

欢乐斗棋牌退市 集结号3d捕鱼手机充值 赖子山庄保定麻将 南通棋牌如东三打二 翻牌机棋牌游戏官网 网络棋牌游戏修改 杰克棋牌官网免费下载 新天地游戏app

编辑:安石顺通

相关新闻

微信5.0新增举报功能 可投诉垃圾信息

2019-02-24 05:09:25

荆州焉睦烟科技有限公司

西北最高智能立体停车综合楼封顶

2019-02-24 07:01:57

常德觅烦教育咨询有限公司

我省2018年招收高水平运动员学校名单出炉

2019-02-24 01:45:56

亳州酌吠俅美术工作室

中国降低日用品进口关税 旨在振兴国内消费

2019-02-24 10:47:07

甘孜辰巢胶科贸有限公司

热门推荐

  • 组图:高三生雨中参加成人礼 双胞胎兄弟抢镜
  • 纪实:一个县城的电竞梦
  • 男女密码大不同:男常用“password”,女偏好爱人姓名
  • 林芝再次发生4.0级地震 周边5公里内村庄有白玛沟雄
  • 余震不断!西藏林芝市巴宜区附近发生4.8级地震
  • 腾讯下狠手:大量手机QQ修改/美化版用户遭封号
  • 组图:大学生用纸造“房子” 成校园别致风景
  • 连生产苹果手机都不赚钱,中国制造怎么才能活下去?
  • 昆明一公交车撞上“小黄车” 骑车男子殒命
  • 台政府拟严审大陆赴台官员反制大陆 被批没须要
  • 河北新闻网版权所有 本站点信息未经允许不得复制或镜像 法律顾问:亲朋棋牌官方个人中心 97棋牌游戏大厅
  • 亲朋游戏官网页面 copyright ? 2000 - 2016
  • 新闻热线:0311-67563366 广告热线:0311-67562966 新闻投诉:0311-67562994
  • 冀ICP备 09047539号-1 | 互联网新闻信息服务许可证编号:1312006002
  • 广播电视节目制作经营许可证(冀)字第101号|信息网络传播视听节目许可证0311618号
  • 快来温岭麻将还能玩吗 不思议棋牌下载官网 熊猫四川麻将换牌秘诀 众亿棋牌平台