Java基础 | SpringBoot实现自启动的方式
Java基础 | SpringBoot实现自启动的方式
- 一、什么是自启动?
- 二、方式一:@PostConstruct注解(常用度★★★★★)
- 场景一:初始化当前Bean的内部状态或变量(加载专属配置)
- 场景二:校验当前Bean的依赖是否合法
- 场景三:注册当前Bean的内部监听器
- 场景四:执行当前Bean的预加载逻辑
- 三、方式二:CommandLineRunner接口(常用度★★★★)
- 场景一:初始化全局共享资源(所有服务可用)
- 场景二:根据命令行参数执行特定操作
- 场景三:启动全局定时任务
- 场景四:执行跨Bean的初始化逻辑
- 四、方式三:ApplicationRunner接口(常用度★★★)
- 场景一:根据命名参数切换环境配置
- 场景二:处理带键的复杂参数
- 场景三:处理多值参数
- 五、方式四:监听Spring容器事件(常用度★★★)
- 场景一:`ApplicationReadyEvent`(应用完全就绪):通知外部系统应用已启动
- 场景二:`ContextRefreshedEvent`(容器刷新完成):扫描所有Bean并检查规范
- 场景三:`ApplicationReadyEvent`(应用完全就绪):打印应用启动最终信息
- 场景四:`ContextRefreshedEvent`(容器刷新完成):注册全局Bean监听器
- 六、方式五:SmartInitializingSingleton接口(常用度★★)
- 场景一:收集所有实现特定接口的Bean(构建策略工厂)
- 场景二:对所有单例Bean执行统一初始化操作
- 场景三:校验所有单例Bean的依赖关系
- 七、方式六:自定义init-method(常用度★)
- 场景一:@Bean注解指定初始化方法(替代XML配置)
- 场景二:XML配置中指定初始化方法(老项目)
- 场景三:框架源码中显式指定初始化方法
- 总结
一、什么是自启动?
在SpringBoot项目中,“自启动”指的是应用程序在启动过程中或启动完成后,自动执行特定方法的机制。
这些方法无需手动触发,通常用于完成初始化操作(如加载配置、初始化缓存)、执行启动任务(如数据同步、服务注册)等,确保应用就绪后处于预期的运行状态。
二、方式一:@PostConstruct注解(常用度★★★★★)
项目常用度:★★★★★(最常用)
特点:
- 属于JSR-250 Java标准注解,不依赖Spring特定API,通用性强;
- 执行时机:单个Bean的构造方法执行后→依赖注入(@Autowired等)完成后,仅针对当前Bean执行;
- 多个Bean的@PostConstruct方法执行顺序与Bean加载顺序相关(无依赖时顺序不确定,无法通过@Order控制);
- 方法无参数、无返回值,方法名可自定义。
涉及场景(按常用度排序):
场景一:初始化当前Bean的内部状态或变量(加载专属配置)
当Bean需要加载自身专属的配置参数或初始化内部变量时,@PostConstruct能确保在依赖注入完成后执行,避免空指针。
具体例子:
@Component
public class PaymentConfig {@Value("${payment.timeout:3000}") // 从配置文件取值,默认3000msprivate int payTimeout;private long payTimeoutMillis; // 内部变量(转换为毫秒)// 依赖注入完成后,初始化内部变量@PostConstructpublic void initTimeout() {this.payTimeoutMillis = payTimeout * 1000L; // 转换为毫秒System.out.println("PaymentConfig初始化:支付超时时间 = " + this.payTimeoutMillis + "ms");}// 提供获取方法供其他地方使用public long getPayTimeoutMillis() {return payTimeoutMillis;}
}
场景二:校验当前Bean的依赖是否合法
当Bean依赖其他组件时,需确保依赖已正确注入,避免后续使用中出现空指针,@PostConstruct可用于依赖校验。
@Autowired(required = false) 该做法不提倡,在特定场景下不得不使用,加上以后就相当于这个对象可用可不用。
以下只为用于举例说明,意思就是可以告诉你可以通过@PostConstruct去判断一下这个对象是否可用。
具体例子:
@Component
public class OrderService {@Autowired(required = false) // required = false 表示:能找到就塞进来,找不到也不报错,会赋予为nullprivate OrderMapper orderMapper;// 校验依赖是否存在@PostConstructpublic void checkDependencies() {if (orderMapper == null) {throw new IllegalStateException("OrderService依赖的OrderMapper未注入!请检查配置");}if (orderMapper != null) { //不为null,就可以执行这个里面的方法orderMapper.xxxx();}}
}
场景三:注册当前Bean的内部监听器
某些Bean需要在初始化后注册自身的内部监听器(如缓存过期监听),@PostConstruct能保证监听器在Bean可用时正确注册。
具体例子:
@Component
public class CacheService {private final LocalCache localCache = new LocalCache(); // 内部缓存// 注册缓存过期监听器(仅当前Bean的缓存生效)@PostConstructpublic void registerCacheListener() {localCache.addExpireListener((key, value) -> {System.out.println("缓存" + key + "已过期,值:" + value);// 执行过期后的处理(如重新加载)});System.out.println("CacheService初始化:已注册缓存过期监听器");}
}
场景四:执行当前Bean的预加载逻辑
对于需要预先加载少量数据(仅当前Bean使用)的场景,可通过@PostConstruct在Bean初始化后执行预加载。
具体例子:
@Component
public class DictService {@Autowiredprivate DictMapper dictMapper;private Map<String, String> genderDict; // 仅当前Bean使用的性别字典缓存@PostConstructpublic void preloadGenderDict() {// 预加载性别字典(仅当前Bean业务用)genderDict = dictMapper.selectByType("gender").stream().collect(Collectors.toMap(Dict::getCode, Dict::getName));System.out.println("DictService预加载性别字典:" + genderDict);}public String getGenderName(String code) {return genderDict.getOrDefault(code, "未知");}
}
三、方式二:CommandLineRunner接口(常用度★★★★)
项目常用度:★★★★☆(非常常用)
特点:
- SpringBoot提供的接口,执行时机:应用启动完成后(所有Bean初始化就绪);
- 接收原始命令行参数(String[] args),可根据参数执行不同操作;
- 支持通过@Order注解指定多个实现类的执行顺序(值越小越先执行)。
涉及场景(按常用度排序):
场景一:初始化全局共享资源(所有服务可用)
应用启动后,需初始化全局共享的缓存、配置等资源(供多个服务使用),CommandLineRunner适合这种全局初始化。
具体例子:
@Component
@Order(1)
public class GlobalDictRunner implements CommandLineRunner {@Autowiredprivate DictMapper dictMapper;@Autowiredprivate RedisTemplate<String, Object> redisTemplate; // 全局缓存(Redis)@Overridepublic void run(String... args) throws Exception {System.out.println("开始初始化全局字典缓存...");// 查询所有字典数据(依赖dictMapper就绪)List<Dict> allDicts = dictMapper.selectAll();// 存入Redis(所有服务可通过Redis获取)redisTemplate.opsForValue().set("GLOBAL:DICTS", allDicts);System.out.println("全局字典缓存初始化完成,共" + allDicts.size() + "条数据");}
}
场景二:根据命令行参数执行特定操作
启动时可通过命令行参数(如java -jar app.jar initDb)触发特定初始化(如数据库初始化),CommandLineRunner能方便处理这些参数。
具体例子:
@Component
@Order(2)
public class DbInitRunner implements CommandLineRunner {@Autowiredprivate JdbcTemplate jdbcTemplate;@Overridepublic void run(String... args) throws Exception {// 命令行参数包含"initDb"时执行初始化if (Arrays.asList(args).contains("initDb")) {System.out.println("开始执行数据库初始化...");// 创建基础表jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS sys_log (id BIGINT PRIMARY KEY AUTO_INCREMENT, content VARCHAR(255))");// 插入初始数据jdbcTemplate.update("INSERT INTO sys_log (content) VALUES (?)", "系统启动初始化日志");System.out.println("数据库初始化完成");}}
}
场景三:启动全局定时任务
应用中常需要全局定时任务(如日志清理、数据同步),这些任务依赖多个业务Bean就绪,CommandLineRunner可在启动后启动任务。
具体例子:
@Component
@Order(3)
public class ScheduledTaskRunner implements CommandLineRunner {@Autowiredprivate ScheduledExecutorService scheduler;@Autowiredprivate LogCleanService logCleanService; // 日志清理服务@Overridepublic void run(String... args) throws Exception {System.out.println("启动全局定时任务...");// 每天凌晨3点执行日志清理(依赖logCleanService就绪)scheduler.scheduleAtFixedRate(() -> logCleanService.cleanExpiredLogs(30), // 清理30天前的日志0, 1, TimeUnit.DAYS);System.out.println("全局定时任务启动完成:每天凌晨3点清理过期日志");}
}
定时任务可替代方案:
- 使用 CompletableFuture.runAsync+自定义线程池
- 使用 @Async+@EnableAsync+自定义线程池
场景四:执行跨Bean的初始化逻辑
当初始化需要多个Bean协作时,CommandLineRunner能确保所有参与Bean就绪,顺利执行跨Bean操作。
具体例子:
@Component
@Order(4)
public class CrossBeanInitRunner implements CommandLineRunner {@Autowiredprivate UserService userService;@Autowiredprivate RoleService roleService;@Overridepublic void run(String... args) throws Exception {System.out.println("执行跨Bean初始化:为默认用户分配角色");// 依赖userService和roleService都已就绪User defaultUser = userService.getDefaultUser();Role adminRole = roleService.getAdminRole();userService.assignRole(defaultUser.getId(), adminRole.getId());System.out.println("跨Bean初始化完成:默认用户已分配管理员角色");}
}
四、方式三:ApplicationRunner接口(常用度★★★)
项目常用度:★★★☆☆(较常用)
特点:
- 与CommandLineRunner功能类似,执行时机相同(应用启动完成后,所有Bean就绪);
- 区别:接收
ApplicationArguments对象,支持解析命名参数(如--env=dev、--type=test),参数处理更灵活; - 支持通过@Order注解控制执行顺序。
涉及场景(按常用度排序):
场景一:根据命名参数切换环境配置
多环境部署时,通过命名参数指定环境(如生产、测试),ApplicationRunner能方便解析并切换配置。
启动命令:java -jar app.jar --syncType=user --pageSize=100
具体例子:
@Component
public class EnvConfigRunner implements ApplicationRunner {@Autowiredprivate ConfigService configService;@Overridepublic void run(ApplicationArguments args) throws Exception {// 获取命名参数--env的值(如--env=prod → [prod])List<String> envList = args.getOptionValues("env");if (envList != null && envList.contains("prod")) {configService.loadProdConfig(); // 加载生产环境配置System.out.println("已加载生产环境配置");} else if (envList != null && envList.contains("test")) {configService.loadTestConfig(); // 加载测试环境配置System.out.println("已加载测试环境配置");}}
}
可替代方案:
使用 Spring Boot 原生支持的–spring.profiles.active=env参数,直接激活环境(无需自定义 Runner),官方推荐且更简洁:
java -jar myapp.jar --spring.profiles.active=prod # 直接激活生产环境
场景二:处理带键的复杂参数
当命令行参数为键值对形式(如指定同步类型、分页大小)时,ApplicationRunner解析更清晰。
具体例子:
@Component
public class ComplexParamRunner implements ApplicationRunner {@Autowiredprivate DataSyncService syncService;@Overridepublic void run(ApplicationArguments args) throws Exception {// 获取命名参数--syncType和--pageSizeList<String> syncTypeList = args.getOptionValues("syncType");List<String> pageSizeList = args.getOptionValues("pageSize");if (syncTypeList != null && !syncTypeList.isEmpty() && pageSizeList != null && !pageSizeList.isEmpty()) {String syncType = syncTypeList.get(0);int pageSize = Integer.parseInt(pageSizeList.get(0));System.out.println("开始同步:类型=" + syncType + ",分页大小=" + pageSize);syncService.sync(syncType, pageSize);}}
}
最优替代方案:
通过@ConfigurationProperties将命令行参数绑定到实体类,参数管理更规范,尤其适合参数较多的场景:
启动命令:java -jar app.jar --syncType=user --pageSize=100
@ConfigurationProperties(prefix = "") // 绑定根级参数
public class SyncParams {private String syncType;private int pageSize;// getters/setters
}// 使用时直接注入
@Component
public class SyncService {@Autowiredprivate SyncParams syncParams; // 直接使用参数
}
场景三:处理多值参数
对于同一键对应多个值的参数(如--ids=1 --ids=2),ApplicationRunner能直接获取所有值,无需手动解析。
具体例子:
@Component
public class MultiParamSyncRunner implements ApplicationRunner {@Autowiredprivate DataSyncService syncService;@Overridepublic void run(ApplicationArguments args) throws Exception {// 获取多值参数--syncTypes(如--syncTypes=user --syncTypes=order → [user, order])List<String> syncTypes = args.getOptionValues("syncTypes");if (syncTypes != null) {System.out.println("开始同步类型:" + syncTypes);syncService.syncByTypes(syncTypes); // 按类型同步数据}}
}
最优替代方案:
同场景二,通过@ConfigurationProperties绑定到List类型,更符合 “面向对象” 编程习惯,参数访问更直观。
五、方式四:监听Spring容器事件(常用度★★★)
项目常用度:★★★☆☆(中等常用)
特点:
- 基于Spring事件发布/监听机制,可监听容器启动特定阶段的事件;
- 常用事件(按执行顺序):
ContextRefreshedEvent:容器刷新完成(所有单例Bean就绪);ApplicationStartedEvent:SpringBoot启动完成(容器就绪,Runner未执行);ApplicationReadyEvent:应用完全就绪(所有Runner执行完成);
- 可通过
@EventListener注解或实现ApplicationListener接口实现监听。
涉及场景(按常用度排序):
场景一:ApplicationReadyEvent(应用完全就绪):通知外部系统应用已启动
应用完全就绪后,需通知外部系统(如服务发现中心、监控系统),以便外部系统感知应用状态。
具体例子:
@Component
public class ServiceRegistryListener {@Autowiredprivate EurekaClient eurekaClient;@Value("${spring.application.name}")private String appName;@Value("${server.port}")private int port;// 应用完全就绪后,注册到Eureka@EventListener(ApplicationReadyEvent.class)public void registerToEureka(ApplicationReadyEvent event) {InstanceInfo instance = InstanceInfo.Builder.newBuilder().setAppName(appName).setHostName("localhost").setPort(port).setStatus(InstanceStatus.UP).build();eurekaClient.registerInstance(appName, instance);System.out.println("应用" + appName + "已注册到Eureka,端口:" + port);}
}
最优替代方案:
服务发现中心(如 Nacos、Eureka)提供自动注册机制,通过配置spring.cloud.nacos.discovery.server-addr等参数即可自动注册,无需手动监听事件,更可靠且无需重复开发。
场景二:ContextRefreshedEvent(容器刷新完成):扫描所有Bean并检查规范
容器刷新完成后,所有单例Bean已就绪,可扫描Bean检查是否符合项目规范(如特定注解是否添加)。
具体例子:
@Component
public class BeanCheckListener {// 容器刷新完成后,检查所有@Service是否加了@Transactional@EventListener(ContextRefreshedEvent.class)public void checkServiceAnnotations(ContextRefreshedEvent event) {ApplicationContext context = event.getApplicationContext();// 获取所有@Service注解的BeanMap<String, Object> serviceBeans = context.getBeansWithAnnotation(Service.class);for (Map.Entry<String, Object> entry : serviceBeans.entrySet()) {Class<?> beanClass = entry.getValue().getClass();if (!beanClass.isAnnotationPresent(Transactional.class)) {System.out.println("警告:Service Bean " + entry.getKey() + " 未加@Transactional注解");}}System.out.println("Bean规范检查完成");}
}
场景三:ApplicationReadyEvent(应用完全就绪):打印应用启动最终信息
应用完全就绪后,打印启动耗时、环境信息等日志,方便了解启动情况。
具体例子:
@Component
public class StartupInfoListener {@Value("${spring.profiles.active:default}")private String activeProfile;@EventListener(ApplicationReadyEvent.class)public void printStartupInfo(ApplicationReadyEvent event) {long startupTime = System.currentTimeMillis() - event.getTimestamp();System.out.println("======================================");System.out.println("应用启动完成!");System.out.println("环境:" + activeProfile);System.out.println("启动耗时:" + startupTime + "ms");System.out.println("======================================");}
}
场景四:ContextRefreshedEvent(容器刷新完成):注册全局Bean监听器
容器刷新完成后,可注册全局监听器,监听所有Bean的创建、销毁等事件,用于全局监控。
具体例子:
@Component
public class GlobalBeanListener {@EventListener(ContextRefreshedEvent.class)public void registerGlobalListener(ContextRefreshedEvent event) {ApplicationContext context = event.getApplicationContext();// 获取Bean工厂ConfigurableListableBeanFactory beanFactory = ((ConfigurableApplicationContext) context).getBeanFactory();// 注册全局Bean后置处理器(监听Bean初始化)beanFactory.addBeanPostProcessor(new BeanPostProcessor() {@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {System.out.println("Bean初始化完成:" + beanName);return bean;}});System.out.println("全局Bean监听器注册完成");}
}
最优替代方案:
直接定义BeanPostProcessor组件(无需监听事件),Spring 会自动注册并生效,代码更简洁:
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) {System.out.println("Bean初始化完成:" + beanName);return bean;}
}
六、方式五:SmartInitializingSingleton接口(常用度★★)
项目常用度:★★☆☆☆(中低常用)
特点:
- Spring提供的接口,执行时机:所有非懒加载的单例Bean初始化完成后(每个Bean的@PostConstruct执行完毕);
- 确保所有单例Bean都已存在,适合依赖全部单例Bean的操作。
涉及场景(按常用度排序):
场景一:收集所有实现特定接口的Bean(构建策略工厂)
当应用中存在多个实现同一接口的Bean(如策略模式中的策略类),可通过该接口收集所有实现类,构建策略工厂。
具体例子:
@Component
public class PaymentStrategyCollector implements SmartInitializingSingleton {@Autowiredprivate ApplicationContext context;private Map<String, PaymentStrategy> strategyMap = new HashMap<>(); // 策略映射(支付类型→处理器)@Overridepublic void afterSingletonsInstantiated() {// 收集所有实现PaymentStrategy接口的单例Bean(确保所有策略Bean已初始化)Map<String, PaymentStrategy> beans = context.getBeansOfType(PaymentStrategy.class);// 构建映射(key:支付类型,如"alipay"、"wechat")beans.values().forEach(strategy -> strategyMap.put(strategy.getPayType(), strategy));System.out.println("已收集" + strategyMap.size() + "种支付策略:" + strategyMap.keySet());}// 提供获取策略的方法(供其他服务使用)public PaymentStrategy getStrategy(String payType) {return strategyMap.get(payType);}
}// 支付策略接口
public interface PaymentStrategy {String getPayType(); // 返回支付类型(如"alipay")void pay(BigDecimal amount);
}
场景二:对所有单例Bean执行统一初始化操作
需要为所有特定类型的单例Bean(如所有Service的子类)执行统一初始化(如设置日志对象)时,可使用该接口。
具体例子:
@Component
public class BaseServiceInitializer implements SmartInitializingSingleton {@Autowiredprivate ApplicationContext context;@Overridepublic void afterSingletonsInstantiated() {// 获取所有BaseService的子类(假设所有业务Service继承BaseService)Map<String, BaseService> serviceBeans = context.getBeansOfType(BaseService.class);// 统一设置日志对象serviceBeans.values().forEach(service -> service.setLogger(LoggerFactory.getLogger(service.getClass())));System.out.println("已为" + serviceBeans.size() + "个BaseService子类设置日志对象");}
}
场景三:校验所有单例Bean的依赖关系
在大型项目中,可通过该接口校验所有单例Bean之间的依赖关系是否合法,避免依赖缺失问题。
具体例子:
@Component
public class DependencyCheck implements SmartInitializingSingleton {@Autowiredprivate ApplicationContext context;@Overridepublic void afterSingletonsInstantiated() {// 获取所有单例Bean名称String[] singletonNames = context.getBeanDefinitionNames();for (String beanName : singletonNames) {Object bean = context.getBean(beanName);// 检查Bean的@Autowired字段是否都已注入(简化逻辑)Field[] fields = bean.getClass().getDeclaredFields();for (Field field : fields) {if (field.isAnnotationPresent(Autowired.class) && !field.getAnnotation(Autowired.class).required()) {field.setAccessible(true);try {if (field.get(bean) == null) {System.out.println("警告:Bean " + beanName + " 的字段 " + field.getName() + " 注入为null");}} catch (IllegalAccessException e) {// 处理异常}}}}System.out.println("单例Bean依赖关系校验完成");}
}
七、方式六:自定义init-method(常用度★)
项目常用度:★☆☆☆☆(低常用)
特点:
- Spring传统的初始化方式,可通过XML配置或@Bean注解指定Bean的初始化方法;
- 执行时机与@PostConstruct类似(构造方法→依赖注入→init-method);
- 现代项目中较少使用,多在老项目或框架源码中出现。
涉及场景(按常用度排序):
场景一:@Bean注解指定初始化方法(替代XML配置)
在Java配置类中,通过@Bean的initMethod属性指定初始化方法,适用于需要显式指定方法名称的场景。
具体例子:
@Configuration
public class StorageConfig {// 显式指定init-method为init()@Bean(initMethod = "init")public StorageManager storageManager() {return new StorageManager();}
}public class StorageManager {// 初始化方法(与@Bean(initMethod)一致)public void init() {System.out.println("StorageManager初始化:连接分布式存储服务");// 实际连接逻辑...}
}
场景二:XML配置中指定初始化方法(老项目)
传统XML配置项目中,通过标签的init-method属性指定初始化方法,是早期Spring项目的常用方式。
具体例子:
<!-- src/main/resources/applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!-- 指定init-method为init() --><bean id="fileUploader" class="com.example.FileUploader" init-method="init"/>
</beans>// 对应的类
public class FileUploader {// 初始化方法(与XML中init-method一致)public void init() {System.out.println("FileUploader初始化:创建上传目录 /upload");new File("/upload").mkdirs();}
}
场景三:框架源码中显式指定初始化方法
框架开发中,为避免依赖特定注解(如@PostConstruct),常通过init-method显式指定初始化方法,增强通用性。
具体例子:
// 框架中的配置类
@Configuration
public class FrameworkConfig {// 框架内部Bean,显式指定init-method@Bean(initMethod = "start")public FrameworkServer frameworkServer() {return new FrameworkServer();}
}// 框架内部服务类
public class FrameworkServer {// 初始化方法(框架启动逻辑)public void start() {System.out.println("FrameworkServer启动:初始化核心组件");// 框架内部初始化逻辑...}
}
总结
SpringBoot提供了多种自启动方式,选择时需结合执行时机和业务需求:
- 单个Bean的初始化逻辑(如加载自身配置)→ 优先用
@PostConstruct; - 全局初始化+简单命令行参数 → 用
CommandLineRunner; - 全局初始化+命名参数 → 用
ApplicationRunner; - 容器特定阶段操作(如通知外部系统)→ 用事件监听;
- 依赖所有单例Bean的操作 → 用
SmartInitializingSingleton; - 老项目或框架兼容 → 用
init-method。
合理选择自启动方式,能确保应用启动过程中完成必要的初始化,保证应用正常运行。
