背景
有这样一个场景,对于同一个业务领域,面向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的作用原理和包含过滤以及排出过滤,以及对机房信息和应用环境信息的理解和信息提取。并且这种改造能带来以下几点收益:
- 节省人力和时间资源,不需要搞专项做C端和B端服务分离改造
- 解决C端和B端流量不对等带来的机器配置和节点数量不对等部署问题
- 一定程度上节省内存资源,如果C端和B端有几十上百个接口服务,那么分机房扫描和注册服务,能够节省相当可观的内存资源,对于寸土寸金的机器RAM,算是一笔不小的收益。
如果您喜欢或者对您有帮助,辛苦关注公众号!