spring号码归属地批量查询,批量查询号码归属地,在线工具,可按省份城市运营商号段分类分开分别导出excel表格

简介: 简介:文章探讨Spring Boot项目启动优化策略,通过自定义监听器、异步初始化及分库分表加载优化等手段,将项目启动时间从280秒缩短至159秒,提升约50%,显著提高开发效率。

批量快速查询号码归属地系统(haomashiwu或chahaoxitong)。

1.1:导入号码文件txt。
打开网站后,点击 “导入号码并批量查询” 功能,选择 “导入文件txt”,将存储号码的文本文件上传。上传过程大约30秒,并自动执行查询号码归属地,等待弹出提示框“查询完成,请导出”。

1.2:查询完成后,就可以 “导出查询结果”,可将查询的归属地结果保存为 Excel 表格,其中有多种导出分类的选项可供选择,按全部来导出、按省份来导出、按城市来导出、按号段来导出、按运营商来导出(按移动、联通和电信分别导出)。

支持几万个、几十万个、上百万个等大级别数量的号码批量快速一键查询归属地,可导出excel表格。

1.3:提示,如果你的号码是杂乱的,也就是在大量混杂的文本里面有号码,那么可以使用网站上的“号码提取筛选”模块,来帮你快速批量提取出11位号码,并自动排成一行一列的干净整齐的格式,这样才能适合拿去批量查询号码归属地。

-----------分割线--------------

一、前言
随着业务的发展,笔者项目对应的Spring Boot工程的依赖越来越多。随着依赖数量的增长,Spring 容器需要加载更多组件、解析复杂依赖并执行自动装配,导致项目启动时间显著增长。在日常开发或测试过程中,一旦因为配置变更或者其他热部署不生效的变更时,项目重启就需要等待很长的时间影响代码的交付。加快Spring项目的启动可以更好的投入项目中,提升开发效率。

整体环境介绍:

Spring版本:4.3.22

Spring Boot版本:1.5.19

CPU:i5-9500

内存:24GB

优化前启动耗时:280秒

二、Spring Boot项目启动流程介绍
Spring Boot项目主要启动流程都在org.spring-

framework.boot.SpringApplication#run(java.lang.String...)方法中:

public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// Spring上下文
ConfigurableApplicationContext context = null;
FailureAnalyzers analyzers = null;
configureHeadlessProperty();
// 初始化SpringApplicationRunListener监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
// 环境准备
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
// 打印banner
Banner printedBanner = printBanner(environment);
// 创建上下文
context = createApplicationContext();
analyzers = new FailureAnalyzers(context);
// 容器初始化
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
// 刷新容器内容
refreshContext(context);
afterRefresh(context, applicationArguments);
// 结束监听广播
listeners.finished(context, null);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
return context;
} catch (Throwable ex) {
handleRunFailure(context, listeners, analyzers, ex);
throw new IllegalStateException(ex);
}
}
可以看到在启动流程中,监听器应用在了应用的多个生命周期中。并且Spring Boot中也预留了针对listener的扩展点。我们可以借此实现一个自己的扩展点去监听Spring Boot的每个阶段的启动耗时,实现如下:

@Slf4j
public class MySpringApplicationRunListener implements SpringApplicationRunListener{
private Long startTime;
public MySpringApplicationRunListener(SpringApplication application, String[] args){
}
@Override
public void starting(){
startTime = System.currentTimeMillis();
log.info("MySpringListener启动开始 {}", LocalTime.now());
}
@Override
public void environmentPrepared(ConfigurableEnvironment environment){
log.info("MySpringListener环境准备 准备耗时:{}毫秒", (System.currentTimeMillis() - startTime));
startTime = System.currentTimeMillis();
}
@Override
public void contextPrepared(ConfigurableApplicationContext context){
log.info("MySpringListener上下文准备 耗时:{}毫秒", (System.currentTimeMillis() - startTime));
startTime = System.currentTimeMillis();
}
@Override
public void contextLoaded(ConfigurableApplicationContext context){
log.info("MySpringListener上下文载入 耗时:{}毫秒", (System.currentTimeMillis() - startTime));
startTime = System.currentTimeMillis();
}
@Override
public void finished(ConfigurableApplicationContext context, Throwable exception){
log.info("MySpringListener结束 耗时:{}毫秒", (System.currentTimeMillis() - startTime));
startTime = System.currentTimeMillis();
}
}
接着还需要在classpath/META-INF目录下新建spring.factories文件,并添加如下文件内容:

org.springframework.boot.SpringApplicationRunListener=com.vivo.internet.gameactivity.api.web.MySpringApplicationRunListener
至此,借助Listener机制,我们能够追踪Spring Boot启动各阶段的耗时分布,为后续性能优化提供数据支撑。

图片

contextLoaded事件是在run方法中的prepareContext()结束时调用的,因此contextLoaded事件和finished事件之间仅存在两个语句:refreshContext(context)和afterRefresh

(context,applicationArguements)消耗了285秒的时间,调试一下就能发现主要耗时在refreshContext()中。

三、AbstractApplicationContext#refresh
refreshContext()最终调用到org.spring-framework.context.support.AbstractApplicationContext#refresh方法中,这个方法主要是beanFactory的预准备、对beanFactory完成创建并进行后置处理、向容器添加bean并且给bean添加属性、实例化所有bean。通过调试发现,finishBeanFactoryInitialization(beanFactory) 方法耗时最久。该方法负责实例化容器中所有的单例 Bean,是启动性能的关键影响点。

四、找出实例化耗时的Bean
Spring Boot也是利用的Spring的加载流程。在Spring中可以实现InstantiationAwareBeanPost-

Processor接口去在Bean的实例化和初始化的过程中加入扩展点。因此我们可以实现该接口并添加自己的扩展点找到处理耗时的Bean。

@Service
public class TimeCostCalBeanPostProcessor implements InstantiationAwareBeanPostProcessor {
private Map costMap = Maps.newConcurrentMap();

@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
    if (!costMap.containsKey(beanName)) {
        costMap.put(beanName, System.currentTimeMillis());
    }
    return null;
}
@Override
public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {
    return true;
}
@Override
public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {
    return pvs;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
     if (costMap.containsKey(beanName)) {
        Long start = costMap.get(beanName);
        long cost = System.currentTimeMillis() - start;
        // 只打印耗时长的bean
         if (cost > 5000) {
            System.out.println("bean: " + beanName + "\ttime: " + cost + "ms");
        }
    }
     return bean;
}

}
具体原理就是在Bean开始实例化之前记录时间,在Bean初始化完成后记录结束时间,打印实例化到初始化的时间差获得Bean的加载总体耗时。结果如图:

图片

可以看到有许多耗时在10秒以上的类,接下来可以针对性的做优化。值得注意的是,统计方式为单点耗时计算,未考虑依赖链上下文对整体加载顺序的影响,实际优化还需结合依赖关系分析。

五、singletonDataSource

@Bean(name = "singletonDataSource")
public DataSource singletonDataSource(DefaultDataSourceWrapper dataSourceWrapper) throws SQLException {
//先初始化连接
dataSourceWrapper.getMaster().init();
//构建分库分表数据源
String dataSource0 = "ds0";
Map dataSourceMap = new HashMap<>();
dataSourceMap.put(dataSource0, dataSourceWrapper.getMaster());
//分库分表数据源
DataSource shardingDataSource = ShardingDataSourceFactory.createDataSource
(dataSourceMap,shardingRuleConfiguration, prop);
return shardingDataSource;
}
singletonDataSource是一个分库分表的数据源,连接池采用的是Druid,分库分表组件采用的是公司内部优化后的中间件。通过简单调试代码发现,整个Bean耗时的过程发生在createDataSource方法,该方法中会调用createMetaData方法去获取数据表的元数据,最终运行到loadDefaultTables方法。该方法如下图,会遍历数据库中所有的表。因此数据库中表越多,整体就越耗时。

图片

笔者的测试环境数据库中有很多的分表,这些分表为了和线上保持一致,分表的数量都和线上是一样的。

图片

因此在测试环境启动时,为了加载这些分表会更加的耗时。可通过将分表数量配置化,使测试环境在不影响功能验证的前提下减少分表数量,从而加快启动速度。

六、初始化异步
activityServiceImpl启动中,主要会进行活动信息的查询初始化,这是一个耗时的操作。类似同样的操作在工程的其他类中也存在。

@Service
public class ActivityServiceImpl implements ActivityService, InitializingBean{
// 省略无关代码
@Override
public void afterPropertiesSet() throws Exception {
initActivity();
}
// 省略无关代码
}
可以通过将afterPropertiesSet()异步化的方式加速项目的启动。

观察Spring源码可以注意到afterPropertiesSet方法是在AbstractAutowireCapableBeanFactory#

invokeInitMethods中调用的。在这个方法中,不光处理了afterPropertiesSet方法,也处理了init-method。

因此我们可以写一个自己的BeanFactory继承AbstractAutowireCapableBeanFactory,将invokeInitMethods方法进行异步化重写。考虑到AbstractAutowireCapableBeanFactory是个抽象类,有额外的抽象方法需要实现,因此继承该抽象类的子类DefaultListableBeanFactory。具体实现代码如下:

public class AsyncInitListableBeanFactory extends DefaultListableBeanFactory{
public AsyncInitBeanFactory(ConfigurableListableBeanFactory beanFactory){
super(beanFactory);
}
@Override
protected void invokeInitMethods(String beanName, Object bean, RootBeanDefinition mbd)throws Throwable {
if (beanName.equals("activityServiceImpl")) {
AsyncTaskExecutor.submitTask(() -> {
try {
super.invokeInitMethods(beanName, bean, mbd);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
});
} else {
super.invokeInitMethods(beanName, bean, mbd);
}
}
}

又因为Spring在refreshContext()方法之前的prepareContext()发放中针对initialize方法提供了接口扩展(applyInitializers())。因此我们可以通过实现该接口并将我们的新的BeanFactory通过反射的方式更新到Spring的初始化流程之前。

public interface ApplicationContextInitializer {
/**

 * Initialize the given application context.
 * @param applicationContext the application to configure
 */
void initialize(C applicationContext);

}

改造后的代码如下,新增AsyncAccelerate-

Initializer类实现ApplicationContextInitializer接口:

public class AsyncBeanFactoryInitializer implements ApplicationContextInitializer {
@SneakyThrows
@Override
public void initialize(ConfigurableApplicationContext applicationContext){
if (applicationContext instanceof GenericApplicationContext) {
AsyncInitListableBeanFactory beanFactory = new AsyncInitListableBeanFactory(applicationContext.getBeanFactory());
Field field = GenericApplicationContext.class.getDeclaredField("beanFactory");
field.setAccessible(true);
field.set(applicationContext, beanFactory);
}
}
}
public class AsyncBeanInitExecutor{
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final AtomicReference THREAD_POOL_REF = new AtomicReference<>();
private static final List> FUTURES = new ArrayList<>();
/**

  * 创建线程池实例
  */
 private static ThreadPoolExecutor createThreadPoolExecutor(){
     int poolSize = CPU_COUNT + 1;
     return new ThreadPoolExecutor(poolSize, poolSize, 50L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy()
    );
}
/**
 * 确保线程池已初始化(线程安全)
 */
 private static void ensureThreadPoolExists(){
     if (THREAD_POOL_REF.get() != null) {
          return;
    }
    ThreadPoolExecutor executor = createThreadPoolExecutor();
     if (!THREAD_POOL_REF.compareAndSet(null, executor)) {
        executor.shutdown(); // 另一线程已初始化成功
    }
}
/**
 * 提交异步初始化任务
 *
 * @param task 初始化任务
 * @return 提交后的 Future 对象
 */
public static Future<?> submitInitTask(Runnable task) {
    ensureThreadPoolExists();
    Future<?> future = THREAD_POOL_REF.get().submit(task);
    FUTURES.add(future);
    return future;
}
/**
 * 等待所有初始化任务完成并释放资源
 */
public static void waitForInitTasks(){
    try {
        for (Future<?> future : FUTURES) {
            future.get();
        }
    } catch (Exception ex) {
        throw new RuntimeException("Async init task failed", ex);
    } finally {
        FUTURES.clear();
        shutdownThreadPool();
    }
}
 /**
 * 关闭线程池并重置引用
 */
 private static void shutdownThreadPool(){
    ThreadPoolExecutor executor = THREAD_POOL_REF.getAndSet(null);
     if (executor != null) {
        executor.shutdown();
    }
}

}

实现类后,还需要在META-INF/spring.factories下新增说明org.springframework.context.

ApplicationContextInitializer=com.xxx.AsyncAccelerateInitializer,这样这个类才能真正生效。

这样异步化以后还有一个点需要注意,如果该初始化方法执行耗时很长,那么会存在Spring容器已经启动完成,但是异步初始化任务没执行完的情况,可能会导致空指针等异常。为了避免这种问题的发生,还要借助于Spring容器启动中finishRefresh()方法,监听对应事件,确保异步任务执行完成之后,再启动容器。

public class AsyncInitCompletionListener implements ApplicationListener, ApplicationContextAware, PriorityOrdered{
private ApplicationContext currentContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext)throws BeansException {
this.currentContext = applicationContext;
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event){
if (event.getApplicationContext() == currentContext) {
AsyncBeanInitExecutor.waitForInitTasks();
}
}
@Override
public int getOrder(){
return Ordered.HIGHEST_PRECEDENCE;
}
}

七、总结
启动优化后的项目实际测试结果如下:

通过异步化初始化和分库分表加载优化,项目启动时间从 280 秒缩短至 159 秒,提升约 50%。这对于提升日常开发效率、加快测试与联调流程具有重要意义。---------

相关文章
|
6月前
|
Python
Excel中如何批量重命名工作表与将每个工作表导出到单独Excel文件
本文介绍了如何在Excel中使用VBA批量重命名工作表、根据单元格内容修改颜色,以及将工作表导出为独立文件的方法。同时提供了Python实现导出工作表的代码示例,适用于自动化处理Excel文档。
|
关系型数据库 MySQL Shell
不通过navicat工具怎么把查询数据导出到excel表中
不通过navicat工具怎么把查询数据导出到excel表中
182 0
|
数据格式 UED
记录一次NPOI库导出Excel遇到的小问题解决方案
【11月更文挑战第16天】本文记录了使用 NPOI 库导出 Excel 过程中遇到的三个主要问题及其解决方案:单元格数据格式错误、日期格式不正确以及合并单元格边框缺失。通过自定义单元格样式、设置数据格式和手动添加边框,有效解决了这些问题,提升了导出文件的质量和用户体验。
996 3
|
前端开发
实现Excel文件和其他文件导出为压缩包,并导入
实现Excel文件和其他文件导出为压缩包,并导入
271 1
|
SQL C# 数据库
EPPlus库的安装和使用 C# 中 Excel的导入和导出
本文介绍了如何使用EPPlus库在C#中实现Excel的导入和导出功能。首先,通过NuGet包管理器安装EPPlus库,然后提供了将DataGridView数据导出到Excel的步骤和代码示例,包括将DataGridView转换为DataTable和使用EPPlus将DataTable导出为Excel文件。接着,介绍了如何将Excel数据导入到数据库中,包括读取Excel文件、解析数据、执行SQL插入操作。
EPPlus库的安装和使用 C# 中 Excel的导入和导出
|
Java API Apache
|
存储 Java API
Java实现导出多个excel表打包到zip文件中,供客户端另存为窗口下载
Java实现导出多个excel表打包到zip文件中,供客户端另存为窗口下载
968 4
|
JavaScript 前端开发 数据处理
Vue导出el-table表格为Excel文件的两种方式
Vue导出el-table表格为Excel文件的两种方式
914 6
|
easyexcel Java UED
SpringBoot中大量数据导出方案:使用EasyExcel并行导出多个excel文件并压缩zip后下载
在SpringBoot环境中,为了优化大量数据的Excel导出体验,可采用异步方式处理。具体做法是将数据拆分后利用`CompletableFuture`与`ThreadPoolTaskExecutor`并行导出,并使用EasyExcel生成多个Excel文件,最终将其压缩成ZIP文件供下载。此方案提升了导出效率,改善了用户体验。代码示例展示了如何实现这一过程,包括多线程处理、模板导出及资源清理等关键步骤。

热门文章

最新文章