在实际开发中,我们经常会遇到这样的需求:当SpringBoot应用启动完成后,需要立即执行一些初始化操作。比如加载配置文件、初始化数据库连接池、预热缓存数据、注册定时任务等等。这些操作如果放在普通的Controller或Service中,可能会因为依赖关系或执行时机问题导致各种异常。
我遇到过这样一个真实案例:有个项目需要在启动时从远程配置中心拉取配置,但由于没有处理好初始化顺序,导致部分服务在配置加载完成前就被调用,结果引发了连锁错误。后来改用ApplicationRunner才完美解决了这个问题。
SpringBoot为我们提供了两种优雅的解决方案:CommandLineRunner和ApplicationRunner。它们都是在应用上下文准备就绪后,但在应用正式接收请求前执行的,这个时机非常关键。今天我们就重点聊聊功能更强大的ApplicationRunner。
创建一个ApplicationRunner实现类非常简单,下面是一个完整的示例:
java复制@Component
@Slf4j
public class DatabaseInitializer implements ApplicationRunner {
@Autowired
private DataSource dataSource;
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("开始初始化数据库连接池...");
// 测试数据库连接
try (Connection conn = dataSource.getConnection()) {
log.info("数据库连接测试成功,版本:{}",
conn.getMetaData().getDatabaseProductVersion());
}
log.info("数据库初始化完成");
}
}
这个示例展示了几个关键点:
@Component注解让Spring管理这个BeanApplicationRunner接口并重写run方法run方法会在应用启动完成后自动调用ApplicationRunner的强大之处在于它对启动参数的专业处理。假设我们这样启动应用:
bash复制java -jar app.jar --spring.profiles.active=dev --db.init=true location=北京
在run方法中,我们可以这样处理参数:
java复制@Override
public void run(ApplicationArguments args) throws Exception {
// 获取所有选项参数名
Set<String> optionNames = args.getOptionNames();
log.info("选项参数:{}", optionNames);
// 获取特定选项参数值
List<String> profiles = args.getOptionValues("spring.profiles.active");
if (!CollectionUtils.isEmpty(profiles)) {
log.info("当前激活的profile:{}", profiles.get(0));
}
// 处理非选项参数
List<String> nonOptionArgs = args.getNonOptionArgs();
log.info("非选项参数:{}", nonOptionArgs);
// 获取原始未处理的参数
String[] sourceArgs = args.getSourceArgs();
log.info("原始参数:{}", Arrays.toString(sourceArgs));
}
这种参数处理方式比直接解析main方法的args数组要方便得多,特别是对于复杂的命令行参数场景。
当有多个初始化任务时,执行顺序就变得非常重要。Spring提供了两种控制顺序的方式:
java复制@Component
@Order(1) // 数字越小优先级越高
public class ConfigLoader implements ApplicationRunner {
// 最先执行
}
@Component
@Order(2)
public class CacheWarmer implements ApplicationRunner {
// 其次执行
}
@Component
@Order(Ordered.LOWEST_PRECEDENCE) // 最后执行
public class HealthChecker implements ApplicationRunner {
// 最后执行
}
在实际项目中,我建议将顺序值定义为常量,而不是直接使用魔法数字:
java复制public interface ExecutionOrder {
int HIGHEST = 1;
int MIDDLE = 5;
int LOWEST = Ordered.LOWEST_PRECEDENCE;
}
除了使用@Order注解,还可以实现Ordered接口:
java复制@Component
public class ThirdRunner implements ApplicationRunner, Ordered {
@Override
public int getOrder() {
return 3;
}
@Override
public void run(ApplicationArguments args) throws Exception {
// 实现逻辑
}
}
这两种方式效果相同,选择哪种主要看个人偏好。我更喜欢@Order注解,因为它更简洁,而且可以把顺序定义放在类上而不是方法里。
这是ApplicationRunner最典型的应用场景之一:
java复制@Component
@RequiredArgsConstructor
public class RemoteConfigLoader implements ApplicationRunner {
private final RestTemplate restTemplate;
@Override
public void run(ApplicationArguments args) throws Exception {
String configUrl = args.getOptionValues("config.url")
.orElse("https://config.center/default");
ConfigData config = restTemplate.getForObject(configUrl, ConfigData.class);
// 将配置应用到系统中
applyConfig(config);
}
}
对于需要初始化数据库表结构或基础数据的场景:
java复制@Component
@Transactional
public class DatabaseInitializer implements ApplicationRunner {
@Autowired
private JdbcTemplate jdbcTemplate;
@Value("${spring.datasource.schema}")
private String schemaScript;
@Override
public void run(ApplicationArguments args) throws Exception {
if (args.containsOption("init-db")) {
jdbcTemplate.execute("DROP SCHEMA IF EXISTS app CASCADE");
jdbcTemplate.execute("CREATE SCHEMA app");
jdbcTemplate.execute(schemaScript);
// 插入基础数据
insertBaseData();
}
}
}
缓存预热可以显著提升系统启动后的响应速度:
java复制@Component
@RequiredArgsConstructor
public class CacheWarmer implements ApplicationRunner {
private final ProductService productService;
private final CacheManager cacheManager;
@Override
public void run(ApplicationArguments args) throws Exception {
Cache cache = cacheManager.getCache("products");
if (cache != null) {
productService.getTop100Products()
.forEach(p -> cache.put(p.getId(), p));
}
}
}
在微服务架构中,服务启动后确认所有组件就绪非常重要:
java复制@Component
@Order(Ordered.LOWEST_PRECEDENCE)
public class StartupHealthChecker implements ApplicationRunner {
@Autowired
private List<HealthIndicator> healthIndicators;
@Override
public void run(ApplicationArguments args) throws Exception {
boolean allHealthy = healthIndicators.stream()
.allMatch(i -> i.health().getStatus() == Status.UP);
if (!allHealthy) {
throw new IllegalStateException("启动健康检查未通过");
}
// 注册服务到注册中心
registerToServiceRegistry();
}
}
在实际使用ApplicationRunner时,我踩过不少坑,这里分享几个典型问题:
循环依赖问题:如果Runner中注入的Bean也间接依赖Runner,会导致启动失败。解决方案是使用@Lazy注解或重构代码结构。
异常处理:Runner中的异常如果没有妥善处理,会导致应用启动失败。建议这样处理:
java复制@Override
public void run(ApplicationArguments args) {
try {
// 业务逻辑
} catch (Exception e) {
log.error("初始化失败", e);
// 根据业务决定是否抛出
if (isCritical(e)) {
throw e;
}
}
}
java复制@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setAwaitTerminationSeconds(30);
return executor;
}
@Component
public class TimeoutRunner implements ApplicationRunner {
@Autowired
private TaskExecutor executor;
@Override
public void run(ApplicationArguments args) throws Exception {
Future<?> future = executor.submit(() -> {
// 长时间任务
});
try {
future.get(10, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
log.warn("初始化任务超时");
}
}
}
对于大型系统,启动时的初始化任务可能会影响启动速度。以下是我总结的优化经验:
java复制@Component
public class ParallelRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
CompletableFuture<Void> task1 = CompletableFuture.runAsync(this::loadConfig);
CompletableFuture<Void> task2 = CompletableFuture.runAsync(this::initCache);
CompletableFuture.allOf(task1, task2).join();
}
}
java复制@Component
public class LazyRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
// 立即执行关键任务
initEssentialComponents();
// 非关键任务延迟执行
Executors.newSingleThreadScheduledExecutor()
.schedule(this::initNonCriticalComponents, 1, TimeUnit.MINUTES);
}
}
java复制@Component
public class ProgressRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
AtomicInteger progress = new AtomicInteger();
int total = 100;
IntStream.range(0, total).parallel().forEach(i -> {
// 处理任务
log.info("进度:{}/{}", progress.incrementAndGet(), total);
});
}
}