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

背景

    有这样一个场景,对于同一个业务领域,面向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注解中看到一个属性:


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

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

1.自定义包路径过滤器
@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());
    }
}

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

@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配置项:

@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,算是一笔不小的收益。


如果您喜欢或者对您有帮助,辛苦关注公众号!
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值