纯真的社区开源库极大的方便了非商业场景的 ip 定位,且其社区仍然非常活跃,保持着每周一更的频率。本文基于不断更新的社区库,利用定时任务每周获取一次纯真的最新库,再通过代理对象的方式,热更新 Spring 容器中的 bean,保证了项目中所使用到的纯真社区库始终是最新的。
纯真提供的社区库,在一般 SpringBoot 项目中的应用方式通常为:将 db 文件放在 resouces 中,在自动配置的时候,将整个 db 文件读进内存,创建 DbSearcher,然后注入 Spring 容器中,在需要使用的地方调用。5.
这样的应用方式有一个比较大的问题:db 文件放到 resources 文件夹下,最终被打进 jar 包中了,若想替换,一般需要(后面会介绍其他方式)重新在源码中替换并打包,而社区库的更新频率是每周一更,若每周都重新源码打包部署,是一件比较麻烦的事情。
如何将这件事情变得更加优雅一点,是需要开发者去自习思考的。
比较直观的方案是:动态的更新 resouces 文件夹下的资源,然后触发 bean 的重新初始化。
但是问题是:Spring Boot 会将 resources 文件夹下的内容打包到 jar 文件中,而在运行时修改 jar 文件中的资源并不是一个常见的操作。
于是就有以下几种解决方案:
这些方案的都有一些显著的缺陷:
有这些缺陷的根本原因是:这个新的文件放哪比较难管理。
如果能够绕开将文件保存下来的问题,直接将网络请求的文件直接读进内存,就可以避免这些复杂的问题。
理想中最好的方案是:每周自动去获取纯真的最新社区库,读取到内存中后替换掉 spring 中原始的 bean。
要想做到上述的最佳方案,有几个关键问题等我们去解决:
说是两个问题,其实是一个问题,就是如何优雅的动态替换掉 spring 中的 bean。
方法:@RefreshScope 是 Spring Cloud 提供的注解,用于动态刷新 Bean。当条件满足时,可以通过触发刷新操作来重新加载 Bean。
使用方式:
@Configuration
public class MyConfig {
@Bean
@RefreshScope
public MyService myService() {
return new MyService();
}
}
@Autowired
private ContextRefresher contextRefresher;
public void refreshBean() {
contextRefresher.refresh();
}
优点:无需手动管理 Bean 的生命周期,刷新操作简单。
缺点:@RefreshScope 依赖 Spring Cloud,可能不适合所有项目。
方法:使用 ApplicationContext 和 ConfigurableBeanFactory 动态替换已经存在的 Bean。这种方式可以手动替换现有的 Bean。
使用方式:
@Autowired
private ConfigurableBeanFactory beanFactory;
public void replaceBean() {
MyService newMyService = new MyService(); // 新的 Bean 实例
beanFactory.registerSingleton("myService", newMyService);
}
优点:直接操作 Bean 的注册,灵活性高。
缺点:需要手动管理 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;
}
}
@Autowired
private ApplicationContext applicationContext;
public void replaceBean() {
MyServiceProxy proxy = (MyServiceProxy) applicationContext.getBean(MyService.class);
MyService newService = new MyServiceImplNew(); // 新的实现
proxy.setTarget(newService); // 更换实际 Bean 实现
}
优势:
说到这里,其实哪种方法好,一目了然了,通过代理对象的方式,始终通过代理类去访问 Bean,直接解耦了Bean 和业务代码。
更进一步的,我们可以不借助 BeanPostProcessor,直接自己实现代理的过程,自由度更高,更适合我们此处的业务场景。
思路都捋顺了,步骤如下:
通过以上四个步骤就完成了全部流程。
通过纯真的社区库审核后,能得到一个链接,每周去请求即可:
链接如下所示:
https://www.cz88.net/api/communityIpAuthorization/communityIpDbFile?fn=czdb&key=***
注意下载得到的是一个 zip,需在内存中解压并找到指定的那个文件。
下面给出关键代码(部分涉及业务不方便给出,如有疑问可在评论区咨询):
代理类,核心类:
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);
}
}
定时任务每周刷新:
@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 的文件名称: