【技术详谈】纯真社区库的最佳应用实践-利用定时任务和代理对象实现社区库热更新

纯真的社区开源库极大的方便了非商业场景的 ip 定位,且其社区仍然非常活跃,保持着每周一更的频率。本文基于不断更新的社区库,利用定时任务每周获取一次纯真的最新库,再通过代理对象的方式,热更新 Spring 容器中的 bean,保证了项目中所使用到的纯真社区库始终是最新的。


文章目录

  • 1.概述
  • 2.一些思考
  • 3.关键问题
  • 4.动态替换容器中的bean--常见方案
    • 4.1 使用 @RefreshScope 注解
    • 4.2 通过 ApplicationContext 动态替换 Bean
    • 4.3 使用 BeanPostProcessor 动态替换 Bean
  • 5.最佳实践-定时任务+网络请求+代理对象
    • 5.1 处理流程
    • 5.2 前置事项
    • 5.3 核心代码


1.概述

纯真提供的社区库,在一般 SpringBoot 项目中的应用方式通常为:将 db 文件放在 resouces 中,在自动配置的时候,将整个 db 文件读进内存,创建 DbSearcher,然后注入 Spring 容器中,在需要使用的地方调用。5.

这样的应用方式有一个比较大的问题:db 文件放到 resources 文件夹下,最终被打进 jar 包中了,若想替换,一般需要(后面会介绍其他方式)重新在源码中替换并打包,而社区库的更新频率是每周一更,若每周都重新源码打包部署,是一件比较麻烦的事情。

如何将这件事情变得更加优雅一点,是需要开发者去自习思考的。

2.一些思考

比较直观的方案是:动态的更新 resouces 文件夹下的资源,然后触发 bean 的重新初始化。
但是问题是:Spring Boot 会将 resources 文件夹下的内容打包到 jar 文件中,而在运行时修改 jar 文件中的资源并不是一个常见的操作。

于是就有以下几种解决方案:

  • 将需要动态更新的资源文件放置在项目外部的目录中,而不是 resource 中。
  • 在 Spring Boot 中使用 WebMvcConfigurer 自定义静态资源的映射路径,将其指向文件系统中的某个目录。
  • 如果必须直接在 resources 文件夹下更新资源,可以考虑在运行时将文件写入系统的临时目录,并使用自定义的 ResourceLoader 读取这些文件。通过自定义类加载器,或在部署时不打包这些资源文件,而是在项目启动时从某个位置动态加载它们。

这些方案的都有一些显著的缺陷:

  • 方案一:需要维护额外的文件路径配置。
  • 方案二:需要配置静态资源映射。
  • 方案三:实现复杂度高,需要手动管理文件的生命周期。

有这些缺陷的根本原因是:这个新的文件放哪比较难管理。

如果能够绕开将文件保存下来的问题,直接将网络请求的文件直接读进内存,就可以避免这些复杂的问题。

3.关键问题

理想中最好的方案是:每周自动去获取纯真的最新社区库,读取到内存中后替换掉 spring 中原始的 bean

要想做到上述的最佳方案,有几个关键问题等我们去解决:

  • 如何动态的替换 spring 中的 bean。
  • 如何保证原有的 bean 被 GC 回收。

说是两个问题,其实是一个问题,就是如何优雅的动态替换掉 spring 中的 bean

4.动态替换容器中的bean–常见方案

4.1 使用 @RefreshScope 注解

方法:@RefreshScope 是 Spring Cloud 提供的注解,用于动态刷新 Bean。当条件满足时,可以通过触发刷新操作来重新加载 Bean。
使用方式:

  1. 在你的 @Configuration 类中,将需要替换的 Bean 声明为 @RefreshScope:
@Configuration
public class MyConfig {

    @Bean
    @RefreshScope
    public MyService myService() {
        return new MyService();
    }
}
  1. 在满足条件时,调用 ContextRefresher 来刷新 Bean:
@Autowired
private ContextRefresher contextRefresher;

public void refreshBean() {
    contextRefresher.refresh();
}

优点:无需手动管理 Bean 的生命周期,刷新操作简单。
缺点:@RefreshScope 依赖 Spring Cloud,可能不适合所有项目。

4.2 通过 ApplicationContext 动态替换 Bean

方法:使用 ApplicationContext 和 ConfigurableBeanFactory 动态替换已经存在的 Bean。这种方式可以手动替换现有的 Bean。
使用方式:

  1. 注入 ConfigurableBeanFactory:
@Autowired
private ConfigurableBeanFactory beanFactory;
  1. 在触发条件时,通过 beanFactory 替换 Bean:
public void replaceBean() {
    MyService newMyService = new MyService(); // 新的 Bean 实例
    beanFactory.registerSingleton("myService", newMyService);
}

优点:直接操作 Bean 的注册,灵活性高。
缺点:需要手动管理 Bean 的生命周期,容易出错。

4.3 使用 BeanPostProcessor 动态替换 Bean

方法:使用 BeanPostProcessor 结合 ApplicationContext 实现 Bean 的动态替换。
使用方式:
1.自定义 BeanPostProcessor:
创建一个自定义的 BeanPostProcessor,在 Bean 初始化后将其代理包装。

@Component
public class MyServicePostProcessor implements BeanPostProcessor {

    @Autowired
    private ApplicationContext applicationContext;

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof MyService) {
            return new MyServiceProxy((MyService) bean); // 使用代理包装
        }
        return bean;
    }
}
  1. 触发条件下替换 Bean:
    在触发条件时,通过 ApplicationContext 获取代理,并动态更换实际的 Bean 实现。
@Autowired
private ApplicationContext applicationContext;

public void replaceBean() {
    MyServiceProxy proxy = (MyServiceProxy) applicationContext.getBean(MyService.class);
    MyService newService = new MyServiceImplNew(); // 新的实现
    proxy.setTarget(newService); // 更换实际 Bean 实现
}

优势:

  • 代理模式:通过代理类包装实际 Bean 的访问逻辑,业务代码调用代理类,不直接依赖具体实现。这样在需要替换时,只需更换代理类指向的目标对象。
  • 不影响现有业务:业务逻辑始终通过代理类访问 Bean,无需感知到 Bean 的更换,确保替换过程不影响已运行的业务代码。

说到这里,其实哪种方法好,一目了然了,通过代理对象的方式,始终通过代理类去访问 Bean,直接解耦了Bean 和业务代码。
更进一步的,我们可以不借助 BeanPostProcessor,直接自己实现代理的过程,自由度更高,更适合我们此处的业务场景。

5.最佳实践-定时任务+网络请求+代理对象

5.1 处理流程

思路都捋顺了,步骤如下:

  • 1.初始时,加载 resources 文件中的 db 文件,自动配置时,读取此 db 文件到内存中,并初始化一个 DbSearcher,并将其代理 DbSearcherProxy 注入容器中。
  • 2.在需要使用的时候都通过 DbSearcherProxy 去使用。
  • 3.每周一凌晨去网络拉取一下纯真最新的社区库,将其文件解压并读取到内存中。
  • 4.动态的替换掉DbSearcher。

通过以上四个步骤就完成了全部流程。

5.2 前置事项

通过纯真的社区库审核后,能得到一个链接,每周去请求即可:
【技术详谈】纯真社区库的最佳应用实践-利用定时任务和代理对象实现社区库热更新_第1张图片
链接如下所示:

https://www.cz88.net/api/communityIpAuthorization/communityIpDbFile?fn=czdb&key=***

注意下载得到的是一个 zip,需在内存中解压并找到指定的那个文件。

5.3 核心代码

下面给出关键代码(部分涉及业务不方便给出,如有疑问可在评论区咨询):
代理类,核心类:

public class DbSearcherProxy {

    private DbSearcher target;

    private final String czDataKey;

    public DbSearcherProxy(InputStream inputStream, String czDataKey) throws Exception {
        this.target = new DbSearcher(inputStream, QueryType.MEMORY, czDataKey);
        this.czDataKey = czDataKey;
    }

    public void updateDbFile(InputStream inputStream) throws Exception {
        var newSearch = new DbSearcher(inputStream, QueryType.MEMORY, czDataKey);
        target.close();
        target = newSearch;
    }

    public String search(String ip) throws Exception {
        return target.search(ip);
    }
}

这里的czDataKey是你页面中的密钥:
【技术详谈】纯真社区库的最佳应用实践-利用定时任务和代理对象实现社区库热更新_第2张图片

定时任务每周刷新:

@Component
@Slf4j
public class RefreshConfigJob {

    @Autowired
    private DbSearcherProxy dbSearcherProxy;

    @Autowired
    private IpRegionProperties ipRegionProperties;

    @Autowired
    private WebClient.Builder webClientBuilder;

    @XxlJob("refreshRegionDbJob")
    public void refreshRegionDbJob() throws Exception {
        WebClient webClient = webClientBuilder.build();
        var url = ipRegionProperties.getCzUpdateFileUrl();
        var targetName = ipRegionProperties.getCzUpdateFileName();
        Mono<Resource> resourceMono = webClient.get()
                .uri(url)
                .retrieve()
                .bodyToMono(Resource.class);

        Resource resource = resourceMono.block(); // 同步获取 Resource
        if(resource == null) {
            log.error("Failed to get resource from url: {}", url);
            return;
        }
        InputStream is = ZipUtil.unzipTargetFile(resource.getInputStream(), targetName);
        dbSearcherProxy.updateDbFile(is);
        var result = dbSearcherProxy.search("****");
        log.info("result: {}", result);
    }
}

targetName是 ipv4 的文件名称:

【技术详谈】纯真社区库的最佳应用实践-利用定时任务和代理对象实现社区库热更新_第3张图片

你可能感兴趣的:(技术方案分析与抉择,ip地址解析,项目解析,ip,地址,代理模式,动态更新,bean,定时任务)