java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot初始化数据加载

SpringBoot启动后的初始化数据加载原理解析与实战

作者:不惑_

本文主要围绕 Spring Boot 启动后的初始化数据加载展开,介绍了初始化任务的基本需求,包括全局配置加载、数据库表初始化等,阐述了多种初始化加载方式,分析了它们的优缺点,需要的朋友可以参考下

系统初始化操作是一个非常常见的需求。通常,应用在启动后需要执行一些重要的初始化任务,例如加载全局配置、初始化数据库表、预热缓存、启动后台任务等。而如何选择合适的技术方案,在不同的场景下保证初始化任务的高效执行,尤其在多实例的分布式部署中,如何确保任务只执行一次,成为我们在项目实战中需要深入思考和优化的关键问题。

本文将结合 Spring Boot 框架,从基础的启动机制、核心技术原理到分布式环境下的复杂场景,带领大家逐步深入理解如何通过不同方式进行启动后的初始化任务执行。最终,我们会通过一个项目实战例子,演示如何确保初始化任务在分布式部署环境中只执行一次,解决多实例下的任务重复执行问题。

初始化任务的基本需求

这些任务有一个共同的特性:它们通常只需要在应用启动时执行一次。因此,选择一个合适的机制来执行这些初始化操作,并且在分布式环境中确保任务不会被重复执行,是至关重要的。包括但不限于:

启动后初始化加载的几种方式

Spring Boot 提供了多种机制来处理应用启动后的初始化任务。这些机制涵盖了单机部署和分布式部署的需求,并且具有不同的执行时机和适用场景。

PostConstruct 注解

@PostConstruct 是一种非常简洁且常用的方式,它用于标注在 Spring 管理的 Bean 完成依赖注入后自动调用的方法。这种方式特别适合单个 Bean 的初始化操作。Spring 在完成依赖注入后,自动调用带有 @PostConstruct 注解的方法,确保初始化逻辑在 Bean 初始化完成时执行。

适用于需要在某个特定 Bean 初始化完成后执行的任务。例如,某个服务类需要在加载时读取配置文件或执行特定的初始化操作。

@Component
public class MyService {
    @PostConstruct
    public void init() {
        // 初始化任务,只执行一次
        System.out.println("Bean 初始化后执行");
    }
}

优点:

缺点:

CommandLineRunner / ApplicationRunner

CommandLineRunner 和 ApplicationRunner 是 Spring Boot 中用于在应用启动完成后执行初始化任务的接口。两者的区别在于传递的参数形式,CommandLineRunner 提供原始的 String[] 参数,而 ApplicationRunner 封装了启动参数的上下文信息。

CommandLineRunner:这个接口提供的是原始的 String[] 启动参数,这些参数通常是应用启动时传递给 Java 程序的命令行参数。如果你只关心应用启动时的命令行参数并且需要直接操作它们,可以使用 CommandLineRunner

ApplicationRunner:这个接口封装了启动时的参数,通过 ApplicationArguments 类提供对命令行参数的更高层次的访问。如果你需要更多功能(例如获取非标准格式的命令行参数,处理带有标志的参数等),可以使用 ApplicationRunner

优点

缺点

使用 CommandLineRunner

@Component
public class MyStartupRunner implements CommandLineRunner {
    @Override
    public void run(String... args) {
        // 这是应用启动后要执行的任务
        System.out.println("应用启动后执行初始化任务");
    }
}

使用 ApplicationRunner

@Component
public class MyAppStartupRunner implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 获取命令行参数
        System.out.println("应用启动后执行初始化任务,获取启动参数:" + args.getOptionNames());
    }
}

运行后我们会发现,@Component 的执行顺序确实早于 ApplicationRunner 和 CommandLineRunner

为什么是这个顺序

**@Component注解的 Bean:**任何标注为 @Component(或 @Service、@Repository、@Controller 等)的类会在 Spring Boot 应用启动时被自动扫描并实例化。这些 Bean 会在 Spring 容器启动的初期阶段被创建和初始化,在应用启动过程中最早被加载和执行。

ApplicationRunner CommandLineRunner: 这两个接口的实现类是 Spring Boot 特有的启动钩子,它们在所有@Component Bean 被创建和初始化之后执行,但在 Spring Boot 完成应用启动(即应用的上下文已准备好)后执行。ApplicationRunner 会比 CommandLineRunner 早执行,因为它封装了启动参数上下文的更多信息,所以它们的 run() 方法会在 Spring Boot 完成上下文刷新和 Bean 初始化之后执行。

执行顺序控制

如果你的项目中有多个实现类,且它们都需要在应用启动时执行,使用 @Order 注解可以明确控制执行顺序。例如:

@Component
@Order(1)  // 设置优先级,数字越小优先级越高
public class FirstStartupTask implements CommandLineRunner {
    @Override
    public void run(String... args) {
        System.out.println("第一个启动任务");
    }
}

@Component
@Order(2)
public class SecondStartupTask implements CommandLineRunner {
    @Override
    public void run(String... args) {
        System.out.println("第二个启动任务");
    }
}

这样,FirstStartupTask 会在 SecondStartupTask 之前执行。

Spring Boot 生命周期事件

Spring Boot 提供了事件驱动的编程模型,可以帮助开发者在应用生命周期的不同阶段执行特定的任务。通过监听 Spring 生命周期事件,开发者可以在精确的时机执行初始化任务。这种方式适合需要确保任务执行时机的场景,例如,某些任务必须等到应用完全启动、数据库连接已经建立、服务已经准备好之后才能执行。Spring Boot 中有很多生命周期事件,例如:

**ApplicationReadyEvent:**当应用完全启动并准备好处理请求时触发。此事件表示 Spring 应用上下文已经完全初始化,应用已准备好接收外部请求。适用于在应用启动完成后立即执行的初始化任务,例如启动后台服务、初始化缓存等。

ContextRefreshedEvent:当 Spring 上下文被初始化或刷新时触发。这通常发生在 Spring Boot 启动过程中,用于标志 Spring 容器准备好并且所有 Bean 已初始化完毕。 - 适合在应用启动时进行一些预热操作,如加载配置信息、初始化数据库连接池等。

ApplicationStartedEvent:在 Spring Boot 应用启动时触发,发生在 Spring 上下文加载之前,可以用于执行一些早期的初始化任务。

ApplicationEnvironmentPreparedEvent:在 Spring Boot 启动过程中,当应用的 Environment 配置完成时触发。这时还未初始化 Spring 容器,适用于一些基于环境配置的初始化。

ApplicationFailedEvent:如果 Spring Boot 启动失败,这个事件会被触发。可以用来处理应用启动失败后的清理或日志记录。

监听 Spring 生命周期事件

Spring Boot 提供了 ApplicationListener 接口和 @EventListener 注解来监听这些生命周期事件。我们可以通过这两种方式,在特定的时机执行初始化任务。

1:使用 ApplicationListener 监听 ApplicationReadyEvent

通过实现 ApplicationListener 接口,可以监听指定的事件,例如监听 ApplicationReadyEvent 来确保在应用完全启动后执行任务。

@Component
public class ApplicationListenerExample implements ApplicationListener<ApplicationReadyEvent> {
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        // 应用启动完成后执行的任务
        System.out.println("应用启动完成后执行任务");
    }
}

2:使用 @EventListener 注解监听事件

Spring 5 引入了 @EventListener 注解,它提供了更简洁的方式来监听事件,不需要显式实现 ApplicationListener 接口。

@Component
public class EventListenerExample {
    @EventListener(ApplicationReadyEvent.class)
    public void handleApplicationReady() {
        // 应用启动完成后执行的任务
        System.out.println("使用 @EventListener 在应用启动后执行任务");
    }
}

事件驱动机制的优缺点

优点:

缺点:

使用 @Bean(initMethod) 定义自定义初始化方法

在 Spring 中,@Bean(initMethod) 提供了一种自定义 Bean 初始化方法的方式。通过这种机制,开发者可以为特定的 Bean 指定一个初始化方法,该方法会在 Spring 容器完成该 Bean 的依赖注入和实例化后自动执行。通常,这种方法用于处理一些复杂的初始化操作,例如初始化数据库连接、加载外部配置、启动后台任务等。

适用于需要复杂初始化的 Bean:当 Bean 的初始化需要执行复杂的操作(例如调用外部 API、执行文件加载、数据库连接初始化等)时,可以通过 initMethod 指定一个初始化方法,而不需要在 Bean 类中使用 @PostConstruct 注解或 CommandLineRunner 等方式。

外部配置控制初始化逻辑:@Bean(initMethod = "init") 允许将 Bean 的初始化方法与外部配置绑定,使得开发者能够更灵活地控制 Bean 的初始化过程。

@Configuration
public class AppConfig {
    @Bean(initMethod = "init")
    public MyService myService() {
        return new MyService();
    }
}

public class MyService {
    // 自定义初始化逻辑
    public void init() {
        System.out.println("自定义初始化方法");
    }
}

优缺点分析

优点:

缺点:

综合项目实战

需求说明

假设我们有一个应用,要求在启动时执行以下初始化任务:

方案设计

我们将使用 Spring Boot 的不同初始化方式结合分布式锁来确保任务只执行一次。具体使用以下技术:

1. 项目结构

src
└── main
    ├── java
    │   └── com
    │       └── example
    │           ├── Application.java
    │           ├── config
    │           │   └── AppConfig.java
    │           ├── initializer
    │           │   ├── AppStartupRunner.java
    │           │   ├── CacheInitializer.java
    │           │   └── DistributedLockInitializer.java
    │           └── service
    │               └── MyService.java
    └── resources
        └── application.properties

2. application.properties

# Redis 配置
spring.redis.host=localhost
spring.redis.port=6379

# 数据库配置(假设使用 MySQL)
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=root

3. 主应用入口类 Application.java

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

4. 配置类 AppConfig.java

package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.example.service.MyService;

@Configuration
public class AppConfig {

    @Bean(initMethod = "init")
    public MyService myService() {
        return new MyService();
    }
}

5. 服务类 MyService.java

package com.example.service;

public class MyService {

    public void init() {
        // 这里可以执行复杂的初始化操作,如外部API调用等
        System.out.println("执行 MyService 的自定义初始化方法");
    }
}

6. 启动初始化任务类 AppStartupRunner.java

package com.example.initializer;

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

@Component
public class AppStartupRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        // 应用启动后执行的初始化任务
        System.out.println("执行应用启动时的全局初始化任务");
    }
}

7. 缓存初始化类 CacheInitializer.java

package com.example.initializer;

import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
public class CacheInitializer {

    @PostConstruct
    public void init() {
        // 预热缓存逻辑,例如加载常用数据到缓存
        System.out.println("执行缓存预热任务");
    }
}

8. 分布式锁初始化类 DistributedLockInitializer.java

package com.example.initializer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class DistributedLockInitializer implements ApplicationListener<ApplicationReadyEvent> {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        // 获取分布式锁,确保初始化任务只执行一次
        Boolean success = redisTemplate.opsForValue().setIfAbsent("init_lock", "locked");
        if (Boolean.TRUE.equals(success)) {
            // 模拟执行初始化任务(如初始化数据表、加载配置等)
            System.out.println("执行初始化任务:初始化数据库或加载基础数据");
            // 这里可以执行初始化数据库表、加载默认数据等任务
        } else {
            System.out.println("任务已经在其他实例中执行,当前实例跳过任务");
        }
    }
}

9.关键技术和设计

10.运行效果

关键要点

通过灵活运用这些技术和方法,开发者可以确保应用启动时的初始化任务高效、安全地执行,避免在多实例环境中产生重复执行的情况。最终,这些方案能够帮助开发者在确保系统稳定性和性能的同时,提高系统的可维护性和扩展性。

以上就是SpringBoot启动后的初始化数据加载原理解析与实战的详细内容,更多关于SpringBoot初始化数据加载的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文