在SpringBoot启动时执行初始化操作,有几种方式


如何在SpringBoot启动时执行初始化操作

遇到一个功能点,数据库中一张很简单的表有一千多条数据,这里的数据主要做到了值域映射的作用,简单来讲就是我可以通过中文名拿到数据库中对应的code值。原本的实现方式是每次用到之后去查一次sql,虽然不会有什么问题,但是只要是走了网络io,都会消耗时间。所以这个方案需要想办法优化。

优化的方式其实很简单,数据量不多,一千多条数据放在内存里也占不了多少空间。因此完全可以把一次性把数据加载到内存中,后面只需要每次去内存里调用就可以了。

实现方案

最直接的方案,在Spring容器初始化时就把这些数据从数据库拿到内存中,后面就直接调用。

SpringBoot中有两个接口能实现该功能:CommandLineRunnerApplicationRunner

  1. CommandLineRunner

    首先了解一下CommandLineRunner的基本用法,CommandLineRunner可以在系统启动后执行里面的run方法

    @Component
    public class DataPrepare implements CommandLineRunner {
        @Override
        public void run(String... args) throws Exception {
            System.out.println("CommandLineRunner执行数据初始化");
        }
    }
    

    如果有多个类的话也可以通过@Order注解指定每个类的执行顺序。

    接着就可以写代码的实现了,首先定义一个类用来将Mysql的数据存到内存里,通过静态的Map存储

    public class DataMap {
        public static Map<String, String> map = new HashMap<>();
        public static void putData(String key, String value) {
            map.put(key, value);
        }
        public static String getDataByKey(String key) {
            return map.get(key);
        }
    }
    

    接着在DataPrepare类中将数据都存入到静态到Map中。

    @Component
    public class DataPrepare implements CommandLineRunner {
        @Autowired
        private DataMapper dataMapper;
        @Override
        public void run(String... args) throws Exception {
            //从数据库中取数据
            List<DataDO> dataDOS = dataMapper.selectList(Wrappers.emptyWrapper());
            //写入到DataMap中
            dataDOS.forEach(item -> DataMap.putData(item.getName(), item.getCode()));
        }
    }
    

    要使用到时候,只需要调用DataMap.getDataByKey()方法就可以直接使用了。

  2. ApplicationRunner

    ApplicationRunner和CommandLineRunner的功能十分相似,实现方式也基本相同。同样继承接口,并实现接口的run方法。

    @Component
    public class ApplicationDataPrepare implements ApplicationRunner {
        @Override
        public void run(ApplicationArguments args) throws Exception {
            System.out.println("ApplicationRunner执行数据初始化");
        }
    }
    

    在不指定@Order注解的情况下,ApplicationRunner会优先于CommandLineRunner执行。

    两者的区别

    CommandLineRunner和ApplicationRunner的功能几乎是相同的,最大的区别在于两者run方法中的入参有所不同,CommandLineRunner通过String数组 来接收启动参数,而ApplicationRunner通过一个ApplicationArguments对象来接收。

    在使用时,不管是String数组还是ApplicationArguments都可以拿到JVM的启动参数。

源码分析

为什么通过实现一个接口,重写run方法就能达到启动程序后就自动执行代码的功能呢?我们可以通过SpringBoot的源码去看:

点进SpringApplication.run()方法,一直进入到public ConfigurableApplicationContext run(String... args)方法中,在执行完一系列初始化方法之后,执行了this.callRunners(context, applicationArguments)方法

8-1.png

callRunners的方法比较简单,首先定义了一个runners集合,并将需要执行的Bean放进去。可以看到ApplicationRunner和CommandLineRunner在这里被放入了runners中,接着对Order注解进行排序,最后遍历执行。

private void callRunners(ApplicationContext context, ApplicationArguments args) {
    List<Object> runners = new ArrayList();
    runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
    runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
    AnnotationAwareOrderComparator.sort(runners);
    Iterator var4 = (new LinkedHashSet(runners)).iterator();

    while(var4.hasNext()) {
        Object runner = var4.next();
        if (runner instanceof ApplicationRunner) {
            this.callRunner((ApplicationRunner)runner, args);
        }

        if (runner instanceof CommandLineRunner) {
            this.callRunner((CommandLineRunner)runner, args);
        }
    }

}

总结

通过一个简单的例子引出ApplicationRunner和CommandLineRunner,实际在使用时也可以通过懒加载,在第一次使用时将数据塞到静态的Map里,也能实现类似缓存的效果。


其他方法

在启动时执行初始化操作有多种方法,还有以下几种常见的:

  1. 实现InitializingBean接口
  2. 实现ApplicationRunner接口
  3. 实现CommandLineRunner接口
  4. 使用ApplicationListener监听ApplicationReadyEvent等事件
  5. 使用@EventListener注解监听上下文事件
  6. 使用@PostConstruct注解

1. 使用CommandLineRunner接口

CommandLineRunner是Spring Boot提供的一个接口,它会在Spring Boot应用启动后执行。

java

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class DatabaseInitializer implements CommandLineRunner {
    
    private final UserRepository userRepository;
    
    public DatabaseInitializer(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Override
    public void run(String... args) throws Exception {
        // 检查并创建默认管理员账户
        if (userRepository.findByUsername("admin").isEmpty()) {
            User admin = new User();
            admin.setUsername("admin");
            admin.setPassword(encodePassword("admin123"));
            admin.setRole("ADMIN");
            userRepository.save(admin);
            System.out.println("默认管理员账户已创建");
        }
        
        // 可以执行其他初始化任务
        System.out.println("数据库初始化完成");
    }
    
    private String encodePassword(String password) {
        // 实际应用中应使用密码编码器
        return "{noop}" + password;
    }
}

2. 使用ApplicationRunner接口

ApplicationRunnerCommandLineRunner类似,但提供了更丰富的ApplicationArguments参数。

java

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class CacheWarmUpRunner implements ApplicationRunner {
    
    private final ProductService productService;
    private final ConfigurationService configurationService;
    
    public CacheWarmUpRunner(ProductService productService, 
                            ConfigurationService configurationService) {
        this.productService = productService;
        this.configurationService = configurationService;
    }
    
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 预热热门商品缓存
        if (args.containsOption("warm-cache")) {
            System.out.println("开始预热缓存...");
            productService.warmUpPopularProductsCache();
        }
        
        // 加载系统配置到缓存
        configurationService.loadAllConfigurationsToCache();
        System.out.println("系统配置已加载到缓存");
    }
}

3. 实现InitializingBean接口

实现InitializingBean接口,重写afterPropertiesSet方法。

import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

@Component
public class DefaultDataLoader implements InitializingBean {
    
    private final CategoryRepository categoryRepository;
    
    public DefaultDataLoader(CategoryRepository categoryRepository) {
        this.categoryRepository = categoryRepository;
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        // 加载默认分类数据
        if (categoryRepository.count() == 0) {
            Category electronics = new Category("电子产品");
            Category clothing = new Category("服装");
            Category books = new Category("图书");
            
            categoryRepository.save(electronics);
            categoryRepository.save(clothing);
            categoryRepository.save(books);
            
            System.out.println("默认分类数据已加载");
        }
    }
}

控制初始化顺序

如果需要控制多个初始化操作的执行顺序,可以使用@Order注解。

java

import org.springframework.core.annotation.Order;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
@Order(1)
public class FirstInitializer implements CommandLineRunner {
    
    @Override
    public void run(String... args) throws Exception {
        System.out.println("首先执行: 系统配置初始化");
        // 初始化系统配置
    }
}

@Component
@Order(2)
public class SecondInitializer implements CommandLineRunner {
    
    @Override
    public void run(String... args) throws Exception {
        System.out.println("其次执行: 数据库初始化");
        // 初始化数据库
    }
}

@Component
@Order(3)
public class ThirdInitializer implements CommandLineRunner {
    
    @Override
    public void run(String... args) throws Exception {
        System.out.println("最后执行: 缓存预热");
        // 预热缓存
    }
}

实际业务场景应用

假设我们有一个电商平台,需要在启动时执行以下操作:

java

import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Component
@Order(1)
public class EcommerceInitializer implements CommandLineRunner {
    
    private final CategoryService categoryService;
    private final ProductService productService;
    private final PromotionService promotionService;
    
    public EcommerceInitializer(CategoryService categoryService,
                               ProductService productService,
                               PromotionService promotionService) {
        this.categoryService = categoryService;
        this.productService = productService;
        this.promotionService = promotionService;
    }
    
    @Override
    public void run(String... args) throws Exception {
        // 1. 初始化商品分类
        initializeCategories();
        
        // 2. 加载热门商品到缓存
        warmUpProductCache();
        
        // 3. 初始化促销活动
        initializePromotions();
        
        // 4. 生成每日报表
        generateDailyReports();
        
        System.out.println("电商平台初始化完成");
    }
    
    private void initializeCategories() {
        // 确保有默认分类存在
        categoryService.ensureDefaultCategoriesExist();
    }
    
    private void warmUpProductCache() {
        // 预热热门商品缓存
        productService.cachePopularProducts();
    }
    
    private void initializePromotions() {
        // 初始化默认促销活动
        promotionService.initializeDefaultPromotions();
    }
    
    private void generateDailyReports() {
        // 生成每日销售报告
        // reportService.generateDailySalesReport();
    }
}

4. 使用@PostConstruct注解

@PostConstruct注解可以标记在方法上,该方法会在依赖注入完成后执行。

import javax.annotation.PostConstruct;
import org.springframework.stereotype.Component;

@Component
public class SystemValidator {
    
    private final Environment environment;
    private final DataSource dataSource;
    
    public SystemValidator(Environment environment, DataSource dataSource) {
        this.environment = environment;
        this.dataSource = dataSource;
    }
    
    @PostConstruct
    public void validateSystem() {
        // 检查必要的环境变量
        checkRequiredProperties();
        
        // 测试数据库连接
        testDatabaseConnection();
        
        System.out.println("系统验证完成");
    }
    
    private void checkRequiredProperties() {
        String[] requiredProps = {"app.version", "app.name"};
        for (String prop : requiredProps) {
            if (!environment.containsProperty(prop)) {
                throw new IllegalStateException("缺少必要的配置属性: " + prop);
            }
        }
    }
    
    private void testDatabaseConnection() {
        try (Connection conn = dataSource.getConnection()) {
            // 简单测试连接是否正常
            conn.createStatement().execute("SELECT 1");
        } catch (Exception e) {
            throw new RuntimeException("数据库连接测试失败", e);
        }
    }
}

5. 使用ApplicationListener监听ContextRefreshedEvent

通过监听应用上下文事件来执行初始化操作。

import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

@Component
public class ElasticsearchIndexInitializer implements ApplicationListener<ContextRefreshedEvent> {
    
    private final ElasticsearchOperations elasticsearchOperations;
    private boolean alreadySetup = false;
    
    public ElasticsearchIndexInitializer(ElasticsearchOperations elasticsearchOperations) {
        this.elasticsearchOperations = elasticsearchOperations;
    }
    
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 防止在子上下文中重复执行
        if (alreadySetup) {
            return;
        }
        
        // 创建Elasticsearch索引和映射
        createIndices();
        createMappings();
        
        alreadySetup = true;
        System.out.println("Elasticsearch索引初始化完成");
    }
    
    private void createIndices() {
        // 创建必要的索引
        // elasticsearchOperations.indexOps(Product.class).create();
        // elasticsearchOperations.indexOps(User.class).create();
    }
    
    private void createMappings() {
        // 创建映射
        // elasticsearchOperations.indexOps(Product.class).putMapping();
        // elasticsearchOperations.indexOps(User.class).putMapping();
    }
}

6. 使用@EventListener注解

使用@EventListener注解来监听应用事件,更加简洁。

import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class ExternalServiceConnector {
    
    private final NotificationService notificationService;
    private final MetricsService metricsService;
    
    public ExternalServiceConnector(NotificationService notificationService,
                                   MetricsService metricsService) {
        this.notificationService = notificationService;
        this.metricsService = metricsService;
    }
    
    @EventListener(ApplicationReadyEvent.class)
    public void connectToExternalServices() {
        // 连接到消息队列
        connectToMessageQueue();
        
        // 连接到监控服务
        connectToMetricsService();
        
        // 发送应用启动通知
        notificationService.sendStartupNotification();
        
        System.out.println("外部服务连接完成");
    }
    
    private void connectToMessageQueue() {
        // 实现消息队列连接逻辑
    }
    
    private void connectToMetricsService() {
        // 实现监控服务连接逻辑
    }
}

7. 使用InitializingBean接口

实现InitializingBean接口,重写afterPropertiesSet方法。

import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

@Component
public class DefaultDataLoader implements InitializingBean {
    
    private final CategoryRepository categoryRepository;
    
    public DefaultDataLoader(CategoryRepository categoryRepository) {
        this.categoryRepository = categoryRepository;
    }
    
    @Override
    public void afterPropertiesSet() throws Exception {
        // 加载默认分类数据
        if (categoryRepository.count() == 0) {
            Category electronics = new Category("电子产品");
            Category clothing = new Category("服装");
            Category books = new Category("图书");
            
            categoryRepository.save(electronics);
            categoryRepository.save(clothing);
            categoryRepository.save(books);
            
            System.out.println("默认分类数据已加载");
        }
    }
}

8. 使用@Bean的initMethod属性

在配置类中定义Bean时,指定初始化方法。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SchedulerConfig {
    
    @Bean(initMethod = "init")
    public TaskScheduler taskScheduler() {
        return new TaskScheduler();
    }
    
    public static class TaskScheduler {
        public void init() {
            // 初始化任务调度器
            System.out.println("任务调度器初始化完成");
            scheduleSystemTasks();
        }
        
        private void scheduleSystemTasks() {
            // 安排系统任务
        }
    }
}

总结

Spring Boot提供了多种在启动时执行初始化操作的方法:

  1. CommandLineRunnerApplicationRunner:适用于简单的启动任务,可以访问命令行参数
  2. @PostConstruct:适用于单个Bean的简单初始化
  3. ApplicationListener和**@EventListener**:适用于基于事件的初始化
  4. InitializingBean:标准的Spring生命周期接口
  5. @Bean的initMethod:适用于配置类中定义的Bean

选择哪种方法取决于具体需求:

  • 如果需要访问命令行参数,使用CommandLineRunner或ApplicationRunner
  • 如果初始化操作依赖于其他Bean,使用@PostConstruct或InitializingBean
  • 如果需要确保在应用完全启动后执行,使用ApplicationListener监听ApplicationReadyEvent
  • 如果需要控制多个初始化操作的顺序,使用@Order注解

在实际项目中,通常会组合使用多种方法,根据初始化操作的性质选择合适的实现方式。

SpringBoot
JAVA-技能点
知识点