前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于ComponentScan实现接口分环境和机房注册

基于ComponentScan实现接口分环境和机房注册

作者头像
叔牙
修改2021-12-30 18:37:15
5630
修改2021-12-30 18:37:15
举报

一、背景

有这样一个场景,对于同一个业务领域,面向C端用户和B端商家或者管理人员,而C端和B端使用的接口能力不同,举个例子,对于电商场景的FAQ,由商家或者管理人员维护更新,而C端用户只有查看的诉求和能力,并且C端用户和管理人员不在同样的区域,用户可能在欧洲,商家和管理人员在国内,那么如果同一份代码在两个区域部署,当然会解决网络延时问题,但是也带来了资源浪费问题,对于部署在欧洲针对用户开放的服务,管理侧相关接口永远不可能被调用到,对于部署在国内的面向商家和管理侧的服务,C端接口也是基本不可能被调用到,我们都知道服务接口和实现都是承载到应用容器中的,最直接的就是带来内存空耗的资源浪费。

二、技术方案

针对上述场景的问题,我们可以把应用细分,区分C端和B端应用的方式来解决,当然如果在人力紧缺和时间紧张的情况下,我们还有另外一种方案,基于spring框架层的能力做扩展,使用同一份代码针对C端和B端呈现出不同的服务能力。

1.包路径区分C端和B端接口

在编写接口实现的时候,根据其服务面向使用者或者区域,将C端接口和B端接口以及实现放到不同的包路径下,比如FAQ场景,C端用户只有查询场景,我们简单的将读写接口分离,C端查询接口放到C端接口路径,B端查询和更新接口放到B端接口路径,对于缓存和业务流转实现根据需要决定是否需要共享。

2.应用启动扫描区分环境和路径

不同机房的机器,我们都可以通过拿到其机房和集群信息,在应用启动时我们识别到机房信息,然后识别出机房与用户群体的映射关系,扫描和注册接口以及实现的时候实现分机房注册,比如跨境电商场景,欧洲机房面向C端用户,那么我们就在应用启动的时候识别到机房信息,只扫描和注册C端用户用到的接口和实现到容器中,对于管理侧的接口直接忽略,反之对于国内机房只扫描和注册管理侧相关接口和实现到容器中,这样就可以实现资源有效利用和非必要接口能力透出管控,也能解决C端和B端服务你能力不对等部署问题,比如C端服务对性能和响应能力要求高,那么最直接的体现就是机器性能和节点数量,而管理侧接口对于这些指标就没有那么敏感,就可以针对性的将机器配置和节点数量根据需要缩减。

另外一点,除了分机房注册我们还提到了分环境注册,可以这么说,除了生产环境,开发、测试和灰度(也叫预发)环境都是我们自己在用,没必要搞那么复杂的集群和机房部署,大多情况下都是单机房单机器部署,这样就不用区分机房注册服务,同一个服务实例即是C端服务,也是B端服务,也就是说在应用启动的时候我们识别机房信息的同时,也识别出环境信息,对于非生产环境我们不做机房区分,对于C端和B端接口服务做全量扫描和注册。

三、代码实现

从@ComponentScan注解中看到一个属性:

代码语言:javascript
复制

  /**
   * Specifies which types are not eligible for component scanning.
   * @see #resourcePattern
   */
  Filter[] excludeFilters() default {};

我们就从这里做文章,如果从机房和环境信息中识别出,不需要全量注册API服务,那么直接使用自定义Filter过滤掉对应的包路径。

1.自定义包路径过滤器

代码语言:javascript
复制
@Slf4j
public class CustomScanPackageExcludeFilter implements TypeFilter, EnvironmentAware {

    private static final int idcId = MapUtils.getIntValue(ServerFileUtil.getServerInfo(), "idc_id", 0);


    private CurrentEnv currentEnv;

    private boolean shouldNotFilter;

    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
        if(this.shouldNotFilter) {
            //如果当前环境参数未注入或者非生产环境,不进行过滤
            return false;
        }
        IdcEnum idcEnum = IdcEnum.getByIdcId(idcId);
        if(null == idcEnum) {
            return false;
        }
        String className = metadataReader.getClassMetadata().getClassName();
        boolean result = idcEnum.getFilter().filter(className);
        log.info("|||||||||||| filter class name={},shouldFilter={}",className,result);
        return result;
    }

    @Override
    public void setEnvironment(Environment environment) {
        String env =  environment.getActiveProfiles()[0];
        log.info("CustomScanPackageExcludeFilter.setEnvironment current env is {}",env);
        this.currentEnv = CurrentEnv.of(env.toLowerCase());
        this.shouldNotFilter = (null == currentEnv || currentEnv.getValue() < CurrentEnv.PROD.getValue());
    }
}

这里环境变量的优先级比机房优先级要高,如果识别出来非生产环境,直接全量扫描和注册,如果是生产环境,根据机房信息注册对应的服务能力。

IdcEnum维护机房与包路径映射以及过滤逻辑:

代码语言:javascript
复制
@Getter
@AllArgsConstructor
public enum IdcEnum {
    EU_FILTER_COD_CONSUMER(Arrays.asList(1,2,3),(c) -> c.startsWith("xxx.consumer.api")),
    LOCAL(Arrays.asList(0),(c) -> c.startsWith("xxx.manager.api")),
    ;

    private List<Integer> idcList;

    private IFilter filter;

    private static final Map<Integer,IdcEnum> map = new HashMap<>();

    static {
        for (IdcEnum idcEnum : IdcEnum.values()) {
            for (Integer idcId : idcEnum.getIdcList()) {
                map.put(idcId,idcEnum);
            }
        }
    }
    public static final IdcEnum getByIdcId(Integer idcId) {
        return map.get(idcId);
    }
}

@FunctionalInterface
interface IFilter {

    boolean filter(String className);
}

*注意:IdcEnum维护对应的机房信息,机房信息基本不会变,如果有变更或者信息可以改成动态配置。

2.开启自定义包过滤能力

在启动类上调整@ComponentScan配置项:

代码语言:javascript
复制
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@ComponentScan(basePackages = {"xxx"},excludeFilters = {@ComponentScan.Filter(type = FilterType.CUSTOM,classes = CustomScanPackageExcludeFilter.class)})
@Slf4j
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

通过观察生产环境和非生产环境的启动日志,我们能看到对应的包路径是否过滤掉,另外如果被过滤调了,肯定不会注册BeanDefinition和实例化,也可以在启动之后尝试调用接口的方式来验证改造是否生效。

总结

当然我们可以使用更简单粗暴的方式来替代上述改造,比如C端和B端服务做细分,C端接口能力收敛到C端应用中,B端应用保留管理侧能力,前提是有人力和时间资源,如果想改造比较小,本篇所描述的方案也不失为一个优雅的临时解决方案,并且成本相对较小,只需要熟悉@ComponentScan的作用原理和包含过滤以及排出过滤,以及对机房信息和应用环境信息的理解和信息提取。并且这种改造能带来以下几点收益:

  1. 节省人力和时间资源,不需要搞专项做C端和B端服务分离改造
  2. 解决C端和B端流量不对等带来的机器配置和节点数量不对等部署问题
  3. 一定程度上节省内存资源,如果C端和B端有几十上百个接口服务,那么分机房扫描和注册服务,能够节省相当可观的内存资源,对于寸土寸金的机器RAM,算是一笔不小的收益
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-12-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PersistentCoder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、背景
  • 二、技术方案
    • 1.包路径区分C端和B端接口
      • 2.应用启动扫描区分环境和路径
      • 三、代码实现
        • 1.自定义包路径过滤器
          • 2.开启自定义包过滤能力
          • 总结
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档