纯真的社区开源库极大的方便了非商业场景的 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。
使用方式:
- 在你的 @Configuration 类中,将需要替换的 Bean 声明为 @RefreshScope:
@Configuration
public class MyConfig {@Bean@RefreshScopepublic MyService myService() {return new MyService();}
}
- 在满足条件时,调用 ContextRefresher 来刷新 Bean:
@Autowired
private ContextRefresher contextRefresher;public void refreshBean() {contextRefresher.refresh();
}
优点:无需手动管理 Bean 的生命周期,刷新操作简单。
缺点:@RefreshScope 依赖 Spring Cloud,可能不适合所有项目。
4.2 通过 ApplicationContext 动态替换 Bean
方法:使用 ApplicationContext 和 ConfigurableBeanFactory 动态替换已经存在的 Bean。这种方式可以手动替换现有的 Bean。
使用方式:
- 注入 ConfigurableBeanFactory:
@Autowired
private ConfigurableBeanFactory beanFactory;
- 在触发条件时,通过 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 {@Autowiredprivate ApplicationContext applicationContext;@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {if (bean instanceof MyService) {return new MyServiceProxy((MyService) bean); // 使用代理包装}return bean;}
}
- 触发条件下替换 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 前置事项
通过纯真的社区库审核后,能得到一个链接,每周去请求即可:
链接如下所示:
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
是你页面中的密钥:
定时任务每周刷新:
@Component
@Slf4j
public class RefreshConfigJob {@Autowiredprivate DbSearcherProxy dbSearcherProxy;@Autowiredprivate IpRegionProperties ipRegionProperties;@Autowiredprivate 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(); // 同步获取 Resourceif(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 的文件名称: