Bài trước dựng một auto-configuration và một starter của riêng bạn — cách lịch sự để thêm bean vào application của người khác. Bài này đi xuống một tầng nữa: những callback mà container tự gọi trên đường khởi động, nơi code của bạn đọc và viết lại chính những gì container sắp làm.
Không góc nào của Spring nhiều truyền miệng bằng góc này, và phần lớn được viết từ trước Spring Boot 3, nên bài này trace đúng những gì Spring Boot 4.1.1 trên Java 21 thực sự làm. App chạy ở port 8202 thay vì 8080 mặc định, nên đó là port bạn sẽ thấy trong log.
![]()
Bài 9 của khoá Basics đã trace vòng đời của một bean: constructor, gán property, các callback Aware, postProcessBeforeInitialization, @PostConstruct, afterPropertiesSet(), init method, postProcessAfterInitialization, rồi các hook huỷ theo chiều ngược lại. Thứ tự đó ở đây được coi là đã biết, không nhắc lại. Bài này lùi ra một tầng, nhìn toàn bộ startup chứa vòng đời ấy.
Các phase của một lần startup thật
Application cố tình nhỏ: hai implementation của PriceService, vài bean sinh ra từ configuration, và mỗi extension point một class, tất cả in vị trí của mình qua một bộ đếm chung.
package com.example.demo.trace;
import java.util.concurrent.atomic.AtomicInteger;
/** One global counter so every extension point prints its own position in the startup. */
public final class Trace {
private static final AtomicInteger N = new AtomicInteger();
private Trace() {
}
public static void step(String phase, String detail) {
System.out.printf("%2d %-32s %s%n", N.incrementAndGet(), phase, detail);
}
}Chạy jar với một command line cố tình lộn xộn:
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --lab.mode=full --tag=alpha --tag=beta -v report.csvcho ra kết quả sau. Log của Boot đã được bỏ bớt, chỉ giữ hai dòng quan trọng, đúng vị trí chúng xuất hiện:
1 EnvironmentPostProcessor added property source lab-defaults; context does not exist yet
2 org.springframework.boot.env.EPP the pre-4.0 package still on the classpath
3 ApplicationContextInitializer registered through META-INF/spring.factories
4 ApplicationContextInitializer AnnotationConfigServletWebServerApplicationContext exists, refresh() has not started, 6 bean definitions
5 EnvironmentAware on the BDRPP setEnvironment before postProcessBeanDefinitionRegistry
6 BeanDefinitionRegistryPostProcessor registered 3 ReportJob definitions from lab.reports.*
7 BDRPP.postProcessBeanFactory the same object is called again as a plain BeanFactoryPostProcessor
8 BeanFactoryPostProcessor definitions=240, singletons registered=18, PriceService=[listPriceService, promoPriceService]
9 BeanFactoryPostProcessor marked listPriceService primary; still no PriceService instance exists
10 BeanClassLoaderAware LaunchedClassLoader
11 EnvironmentAware lab.origin=EnvironmentPostProcessor
12 ResourceLoaderAware AnnotationConfigServletWebServerApplicationContext
13 ApplicationEventPublisherAware AnnotationConfigServletWebServerApplicationContext
14 BPP.before awareBean AwareBean
15 BPP.after awareBean AwareBean
16 BPP.before listPriceService ListPriceService
17 BPP.after listPriceService returning a proxy instead: jdk.proxy2.$Proxy94
18 BPP.before promoPriceService PromoPriceService
19 BPP.after promoPriceService PromoPriceService
20 BPP.before reportJob-daily ReportJob
21 BPP.after reportJob-daily ReportJob
22 BPP.before reportJob-weekly ReportJob
23 BPP.after reportJob-weekly ReportJob
24 BPP.before reportJob-monthly ReportJob
25 BPP.after reportJob-monthly ReportJob
26 SmartInitializingSingleton every non-lazy singleton exists, refresh() is nearly done
INFO TomcatWebServer : Tomcat started on port 8202 (http) with context path '/'
27 ContextRefreshedEvent refresh() finished
INFO DemoApplication : Started DemoApplication in 1.228 seconds (process running for 1.42)
28 ApplicationStartedEvent published just before the runners
29 ApplicationRunner @Order(1) optionNames=[lab.mode, tag] nonOptionArgs=[-v, report.csv]
30 CommandLineRunner @Order(2) raw args=[--lab.mode=full, --tag=alpha, --tag=beta, -v, report.csv]
31 ApplicationRunner @Order(3) injected PriceService is a jdk.proxy2.$Proxy94
32 ApplicationRunner @Order(5) the last runner in the chain
33 ApplicationReadyEvent published after the last runner returned
34 main() after run() returned the context is up and the runners are done
Bốn điểm trong bảng trace đó nên nhớ trước tiên.
Bước 1 tới 4 chạy trước khi context có thể giữ bean nào. Ở bước 4 object ApplicationContext đã tồn tại và có sáu bean definition — đúng số mà SpringApplication tự đăng ký. Các class @Component của bạn chưa được scan, vì việc scan chính là thứ bước 5 sắp kích hoạt.
Ở bước 8 có 240 bean definition và không một instance nào của application. Đó là toàn bộ ý nghĩa của BeanFactoryPostProcessor: biết đầy đủ những gì sắp được dựng, mà chưa cam kết dựng gì cả.
Dòng Started … in 1.228 seconds in ra trước các runner, không phải sau. Điều này ngược với rất nhiều lời khuyên viết cho Spring Boot 2. Ở 4.1.1, SpringApplication.run publish ApplicationStartedEvent — chính event đó ghi thời gian ở dòng log này — rồi mới gọi các runner, và ApplicationReadyEvent nằm ở cuối cùng. Nếu runner của bạn chạy chín giây, log vẫn nói application khởi động trong một giây.
Mọi bước được đánh số đều nằm trong SpringApplication.run. Bước 34 là dòng đầu tiên trong main sau khi run trả về.
Đăng ký những gì chạy trước khi có context
Hai callback ở bước 1 tới 4 không thể là bean, vì chưa có container nào giữ chúng. Chúng được tìm trên classpath hoặc đưa thẳng cho SpringApplication, và Boot 4 đã đổi cả hai cơ chế.
EnvironmentPostProcessor là hook sớm nhất tồn tại. Nó nhận ConfigurableEnvironment và SpringApplication, ngoài ra chưa có gì khác:
package com.example.demo.startup;
import com.example.demo.trace.Trace;
import java.util.Map;
import org.springframework.boot.EnvironmentPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
public class LabEnvironmentPostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
environment.getPropertySources().addLast(
new MapPropertySource("lab-defaults", Map.of("lab.origin", "EnvironmentPostProcessor")));
Trace.step("EnvironmentPostProcessor", "added property source lab-defaults; context does not exist yet");
}
}Để ý dòng import. Ở Boot 4, interface này nằm trong org.springframework.boot, không còn ở org.springframework.boot.env. Type cũ vẫn nằm trên classpath và vẫn chạy — một class implement nó đã in ra ở bước 2 của bảng trace trên — nhưng javap -v trên jar 4.1.1 nói rõ nó là gì:
Deprecated: true
RuntimeVisibleAnnotations:
java.lang.Deprecated(
since="4.0.0"
forRemoval=trueCách đăng ký vẫn là META-INF/spring.factories, với key là tên interface. Đây là chỗ duy nhất mà spring.factories không phải di sản: auto-configuration đã chuyển sang file META-INF/spring/….imports từ Boot 2.7, còn EnvironmentPostProcessor thì không, vì nó phải được đọc trước khi bộ máy đó sẵn sàng.
org.springframework.boot.EnvironmentPostProcessor=\
com.example.demo.startup.LabEnvironmentPostProcessor
org.springframework.context.ApplicationContextInitializer=\
com.example.demo.startup.FactoriesContextInitializerApplicationContextInitializer chạy ngay sau đó, khi object context vừa được tạo và trước refresh():
package com.example.demo.startup;
import com.example.demo.trace.Trace;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
public class LabContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext context) {
Trace.step("ApplicationContextInitializer",
context.getClass().getSimpleName() + " exists, refresh() has not started, "
+ context.getBeanFactory().getBeanDefinitionCount() + " bean definitions");
}
}Trong các bài hướng dẫn có ba cách đăng ký, và chỉ hai cách còn chạy trên Boot 4.1.1. Entry trong spring.factories ở trên chạy được — đó là bước 3. API của SpringApplication chạy được — đó là bước 4:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(DemoApplication.class);
application.addInitializers(new LabContextInitializer());
application.run(args);
Trace.step("main() after run() returned", "the context is up and the runners are done");
}
}Cách thứ ba, property context.initializer.classes, đã chết. Đặt vừa trong file property vừa trên command line, nó không tạo ra gì hết:
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --context.initializer.classes=com.example.demo.startup.PropertyContextInitializerInitializer được nêu tên ở đó không bao giờ chạy, và cũng không có warning nào. Lý do nhìn thấy ngay trong jar: class từng hiện thực property này, DelegatingApplicationContextInitializer, không còn trong spring-boot-4.1.1.jar nữa. Điều tương tự đúng với người anh em context.listener.classes. Nếu bạn tiếp quản một file config còn một trong hai dòng đó, nó đang im lặng không làm gì.
Giữa hai cách còn sống: dùng spring.factories khi initializer thuộc về một library phải chạy được ở bất cứ đâu nó được thả vào, và dùng addInitializers khi nó thuộc về chính application này — nó là code bình thường, nhìn thấy được từ main, và nhận được constructor argument.
BeanFactoryPostProcessor: bean definition trước khi có instance
Khi refresh() bắt đầu, thứ đầu tiên code của bạn chạm được là tập bean definition — công thức, chưa phải object. BeanFactoryPostProcessor nhận ConfigurableListableBeanFactory và đọc hay sửa được mọi definition trong đó.
package com.example.demo.startup;
import com.example.demo.pricing.PriceService;
import com.example.demo.trace.Trace;
import java.util.Arrays;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
public class TraceBeanFactoryPostProcessor
implements BeanFactoryPostProcessor, EnvironmentAware, Ordered {
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// allowEagerInit = false: read the definitions without instantiating anything
String[] names = beanFactory.getBeanNamesForType(PriceService.class, true, false);
Trace.step("BeanFactoryPostProcessor",
"definitions=" + beanFactory.getBeanDefinitionCount()
+ ", singletons registered=" + beanFactory.getSingletonCount()
+ ", PriceService=" + Arrays.toString(names));
if (environment.getProperty("lab.mark-primary", Boolean.class, true)) {
beanFactory.getBeanDefinition("listPriceService").setPrimary(true);
Trace.step("BeanFactoryPostProcessor",
"marked listPriceService primary; still no PriceService instance exists");
}
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}Hai chi tiết trong class đó đáng chú ý. Argument thứ ba của getBeanNamesForType là allowEagerInit, và truyền false chính là kỷ luật của extension point này: nó yêu cầu factory trả lời bằng metadata thay vì tạo factory bean ra để biết. Còn setPrimary(true) viết lại một definition do @Component sinh ra, từ bên ngoài class, mà không cần annotation nào trên class đó.
Điều thứ hai không phải trang trí. Có hai bean PriceService và một runner inject interface đó. Tắt việc đánh dấu đi thì application không khởi động nổi:
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --lab.mark-primary=falseAPPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in com.example.demo.runner.PriceRunner required a single bean, but 2 were found:
- listPriceService: defined in URL [jar:nested:/…/demo-0.0.1-SNAPSHOT.jar/!BOOT-INF/classes/!/com/example/demo/pricing/ListPriceService.class]
- promoPriceService: defined in URL [jar:nested:/…/demo-0.0.1-SNAPSHOT.jar/!BOOT-INF/classes/!/com/example/demo/pricing/PromoPriceService.class]
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumedCùng cái tay cầm đó gọi được setLazyInit, setScope, setDependsOn, setRole hay sửa constructor argument, còn removeBeanDefinition thì xoá hẳn một bean. Đây là cách bạn chỉnh những bean mình không sở hữu — một auto-configuration của bên thứ ba, một component được scan trong module dùng chung — mà không phải fork chúng.
18 singleton được báo ở bước 8 là hạ tầng của container, đăng ký trực tiếp chứ không dựng từ definition: Environment, systemProperties, systemEnvironment, springApplicationArguments, logging system, báo cáo auto-configuration, và những post-processor đã được tạo ra để làm chính việc này. Không có bean nào của application trong đó.
BeanDefinitionRegistryPostProcessor: mỗi entry configuration một bean
BeanFactoryPostProcessor sửa được definition. Sub-interface của nó, BeanDefinitionRegistryPostProcessor, thêm được definition, vì chạy sớm hơn và nhận thẳng BeanDefinitionRegistry. Đây là cách biến configuration thành bean — mỗi entry một bean, với số lượng chỉ biết lúc runtime.
lab.reports.daily=0 0 6 * * *
lab.reports.weekly=0 0 7 * * MON
lab.reports.monthly=0 0 8 1 * *package com.example.demo.startup;
import com.example.demo.pricing.ReportJob;
import com.example.demo.trace.Trace;
import java.util.Map;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
/** One ReportJob bean per entry under lab.reports.* in the configuration. */
public class ReportRegistrar implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
Trace.step("EnvironmentAware on the BDRPP", "setEnvironment before postProcessBeanDefinitionRegistry");
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
Map<String, String> reports = Binder.get(environment)
.bind("lab.reports", Bindable.mapOf(String.class, String.class))
.orElseGet(Map::of);
reports.forEach((name, cron) -> {
var definition = BeanDefinitionBuilder.genericBeanDefinition(ReportJob.class)
.addConstructorArgValue(name)
.addConstructorArgValue(cron)
.getBeanDefinition();
registry.registerBeanDefinition("reportJob-" + name, definition);
});
Trace.step("BeanDefinitionRegistryPostProcessor",
"registered " + reports.size() + " ReportJob definitions from lab.reports.*");
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
Trace.step("BDRPP.postProcessBeanFactory", "the same object is called again as a plain BeanFactoryPostProcessor");
}
}Cả hai post-processor đều được khai báo bằng method @Bean static, và đây là chi tiết hay bị làm sai:
@Configuration
public class LabConfig {
@Bean
public static ReportRegistrar reportRegistrar() {
return new ReportRegistrar();
}
@Bean
public static TraceBeanFactoryPostProcessor traceBeanFactoryPostProcessor() {
return new TraceBeanFactoryPostProcessor();
}
}static nghĩa là container dựng được post-processor mà không phải dựng class @Configuration khai báo nó trước. Method không static sẽ kéo class configuration — và qua đó mọi thứ class ấy inject — ra đời quá sớm, đúng cái bẫy ở hai mục dưới.
Từ lúc đó trở đi, các bean vừa đăng ký là bean bình thường. Chúng đi qua đủ chain BeanPostProcessor — bước 20 tới 25 trong bảng trace — và một runner hỏi Map<String, ReportJob> nhận đủ ba:
reportJob-daily -> daily @ 0 0 6 * * *
reportJob-weekly -> weekly @ 0 0 7 * * MON
reportJob-monthly -> monthly @ 0 0 8 1 * *Bước 6 và 7 cho thấy điều còn lại cần biết: một BeanDefinitionRegistryPostProcessor được gọi hai lần — một lần với tư cách chính nó, rồi một lần nữa như BeanFactoryPostProcessor thường, trên cùng một instance. Đặt phần đăng ký ở method thứ nhất và phần sửa definition ở method thứ hai; đừng làm trùng việc giữa hai bên.
Để ý thêm bước 5. EnvironmentAware được gọi trên post-processor trước callback của nó, dù post-processor chạy sớm hơn gần như mọi thứ. Được vậy vì ApplicationContextAwareProcessor được đăng ký trong prepareBeanFactory(), trước khi bất kỳ post-processor nào được tạo. Đây là cách sạch nhất để một BeanFactoryPostProcessor đọc configuration, và nó an toàn chính vì Environment không phải một bean mà bạn đang ép ra đời.
BeanPostProcessor: bọc một bean vào proxy
BeanPostProcessor nhìn thấy mọi bean, hai lần: một lần trước các callback khởi tạo và một lần sau. Cả hai method đều trả về Object, và trả về một object khác sẽ thay thế bean ở mọi nơi. Câu đó là toàn bộ extension point này.
Đây là một cái bọc mọi bean mang annotation @Timed vào một JDK dynamic proxy:
package com.example.demo.startup;
import com.example.demo.pricing.Timed;
import com.example.demo.trace.Trace;
import java.lang.reflect.Proxy;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;
public class TimingBeanPostProcessor implements BeanPostProcessor, Ordered {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
Class<?> type = bean.getClass();
if (!type.isAnnotationPresent(Timed.class) || type.getInterfaces().length == 0) {
return bean;
}
Object proxy = Proxy.newProxyInstance(
type.getClassLoader(),
type.getInterfaces(),
(p, method, args) -> {
long start = System.nanoTime();
try {
return method.invoke(bean, args);
} finally {
System.out.printf(" [timed] %s.%s took %.1f ms%n",
type.getSimpleName(), method.getName(), (System.nanoTime() - start) / 1e6);
}
});
Trace.step("BPP.after " + beanName,
"returning a proxy instead: " + proxy.getClass().getName());
return proxy;
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 10;
}
}ListPriceService mang @Timed và ngủ 12 ms trong price(). PromoPriceService thì không. Bảng trace cho thấy đúng một trong hai bị tráo:
16 BPP.before listPriceService ListPriceService
17 BPP.after listPriceService returning a proxy instead: jdk.proxy2.$Proxy94
18 BPP.before promoPriceService PromoPriceService
19 BPP.after promoPriceService PromoPriceServicevà runner inject PriceService đang giữ proxy, không phải service:
31 ApplicationRunner @Order(3) injected PriceService is a jdk.proxy2.$Proxy94
[timed] ListPriceService.price took 15.1 ms
price(SKU-1) -> 19.90
Có hai hệ quả từ cú tráo đó. Bean đăng ký dưới tên listPriceService chính là proxy — trong container không còn chỗ nào trỏ tới object gốc ngoài closure bên trong InvocationHandler. Và postProcessBeforeInitialization là chỗ sai để làm việc này: object sẽ bị bọc trước khi @PostConstruct của chính nó chạy, nên các callback khởi tạo sẽ bắn vào proxy thay vì target.
Đây chính là cơ chế đằng sau @Transactional, @Async, @Cacheable và mọi annotation khác thay đổi hành vi mà không sửa thân method: AnnotationAwareAspectJAutoProxyCreator là một BeanPostProcessor quyết định trong postProcessAfterInitialization xem có advisor nào áp dụng cho bean không, và trả về proxy khi có. Bài 30 của khoá Basics đã nói proxy đó làm gì lúc gọi method và vì sao self-invocation lọt qua nó; bài AOP sau trong khoá này sẽ đi kỹ vào JDK proxy so với CGLIB.
Giới hạn thực tế của JDK dynamic proxy nhìn thấy ngay trong code: type.getInterfaces(). Một bean không có interface thì không proxy kiểu này được, và đó là lý do hạ tầng của Spring quay sang subclass CGLIB, cũng là lý do AuditService ở mục sau trở về dưới dạng AuditService$$SpringCGLIB$$0.
Cái bẫy khởi tạo quá sớm
BeanPostProcessor được tạo trước những bean nó xử lý — bắt buộc phải thế. Nên mọi thứ nó phụ thuộc cũng được tạo trước nó, tức là trước khi chain post-processor hoàn chỉnh. Một bean sinh ra ở thời điểm đó sẽ bỏ lỡ mọi post-processor chưa kịp đăng ký.
Đây là lỗi đó, dưới một hình dạng trông hoàn toàn hợp lý:
public class BrokenAuditBeanPostProcessor implements BeanPostProcessor, PriorityOrdered {
private final AuditService auditService;
public BrokenAuditBeanPostProcessor(AuditService auditService) {
this.auditService = auditService;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (beanName.equals("listPriceService")) {
auditService.record("listPriceService");
}
return bean;
}
@Override
public int getOrder() {
return PriorityOrdered.HIGHEST_PRECEDENCE;
}
}AuditService là một @Service bình thường với một field @Autowired, một @PostConstruct và một method @Transactional. Khởi động application với post-processor đó bật lên sẽ in ra, ở mức WARN:
WARN PostProcessorRegistrationDelegate$BeanPostProcessorChecker : Bean 'auditService' of type
[com.example.demo.trap.AuditService] is not eligible for getting processed by all BeanPostProcessors
(for example: not eligible for auto-proxying). Is this bean getting eagerly injected/applied to a
currently created BeanPostProcessor [brokenAuditBeanPostProcessor]? Check the corresponding
BeanPostProcessor declaration and its dependencies/advisors. If this bean does not have to be
post-processed, declare it with ROLE_INFRASTRUCTURE.Warning đó thường bị bỏ qua. Đây là cái giá thật của nó, in từ một runner inject đúng AuditService ấy:
injected AuditService : com.example.demo.trap.AuditService
AopUtils.isAopProxy : false
@PostConstruct ran = false, @Autowired field set = false
record(from the runner) transaction active = falseBa hỏng hóc riêng biệt, không cái nào ném exception. Không có AOP proxy, nên @Transactional hoàn toàn vô tác dụng — TransactionSynchronizationManager.isActualTransactionActive() trả về false ngay trong method có annotation. @PostConstruct không bao giờ chạy, vì CommonAnnotationBeanPostProcessor chưa được đăng ký. Và field @Autowired vẫn là null, vì AutowiredAnnotationBeanPostProcessor cũng chưa.
Một application ghi dòng audit ngoài transaction, trên một service chưa hề khởi tạo xong, khởi động ngon lành và qua health check: đó là cái mà một constructor parameter mua được.

Phiên bản BeanFactoryPostProcessor của cùng lỗi này còn tệ hơn, vì nó im lặng:
public class EagerBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
AuditService auditService = beanFactory.getBean(AuditService.class);
System.out.println(" BFPP asked for AuditService: " + auditService.getClass().getName());
}
} BFPP asked for AuditService: com.example.demo.trap.AuditService
injected AuditService : com.example.demo.trap.AuditService
AopUtils.isAopProxy : false
@PostConstruct ran = false, @Autowired field set = false
record(from the runner) transaction active = falseThiệt hại y hệt, và không có warning nào — grep cả log startup tìm WARN không ra dòng nào. Cái checker in ra warning kia được cài ở đầu registerBeanPostProcessors(), tức là sau khi mọi BeanFactoryPostProcessor đã chạy xong. Vậy nên extension point sớm nhất lại là cái ít được bảo vệ nhất.
⚠️ Hãy coi "not eligible for getting processed by all BeanPostProcessors" là lỗi, không phải warning. Và coi một lời gọi
getBeanbên trongBeanFactoryPostProcessorlà bug, dù có ai báo hay không.
Ba cách sửa, và bằng chứng
Cách sửa luôn có cùng một hình dạng: hỏi bean muộn hơn, ở thời điểm chain đã hoàn chỉnh. ObjectProvider là phiên bản nên dùng.
public class FixedAuditBeanPostProcessor implements BeanPostProcessor, PriorityOrdered {
private final AuditService auditService;
private final ObjectProvider<AuditService> auditService;
public FixedAuditBeanPostProcessor(AuditService auditService) {
public FixedAuditBeanPostProcessor(ObjectProvider<AuditService> auditService) {
this.auditService = auditService;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (beanName.equals("listPriceService")) {
auditService.record("listPriceService");
auditService.getObject().record("listPriceService");
}
return bean;
}
@Override
public int getOrder() {
return PriorityOrdered.HIGHEST_PRECEDENCE;
}
}ObjectProvider là một tay cầm của phép tra cứu, không phải kết quả: container inject nó mà chưa giải quyết gì, và getObject() chạy phép tra cứu đúng lúc bạn gọi. Cùng application, cùng bean, khác đúng một parameter:
record(listPriceService) transaction active = true
injected AuditService : com.example.demo.trap.AuditService$$SpringCGLIB$$0
AopUtils.isAopProxy : true
@PostConstruct ran = true, @Autowired field set = true
record(from the runner) transaction active = trueMọi dòng đều lật lại, và WARN biến mất — không còn dòng WARN nào trong cả lần startup.
Các cách khác cũng là ý đó viết theo kiểu khác. @Lazy trên constructor parameter cũng chạy — cùng post-processor đó nhận @Lazy AuditService cho ra một bean đã proxy, khởi tạo đầy đủ và không warning — vì lazy proxy hoãn phép tra cứu thật tới lần gọi method đầu tiên; nó kém rõ ràng hơn ObjectProvider và đặt thêm một proxy nữa vào bức tranh. Implement BeanFactoryAware rồi gọi beanFactory.getBean(AuditService.class) bên trong callback cũng chạy vì lý do tương tự, và là lựa chọn duy nhất khi dependency chỉ xác định được lúc runtime, nhưng nó giấu dependency khỏi mọi người đọc.
Câu trả lời thứ tư thường mới là đúng: đừng có dependency đó. Post-processor là hạ tầng. Nếu nó cần một service của application thì thiết kế thường đã ngược — hãy publish một event từ post-processor, hoặc làm việc đó trong một SmartInitializingSingleton hay một ApplicationRunner, nơi mọi bean đã hoàn chỉnh và chẳng còn chuyện gì trong mục này áp dụng nữa.
Thứ tự post-processor: Ordered, PriorityOrdered và @Order
Bốn BeanPostProcessor, cố tình mâu thuẫn nhau:
| Class | Cách khai báo order |
|---|---|
Alpha | implements PriorityOrdered, getOrder() trả về 5000 — một số ưu tiên thấp |
Beta | implements Ordered, getOrder() trả về HIGHEST_PRECEDENCE |
Gamma | @Order(Ordered.HIGHEST_PRECEDENCE) trên class, không interface |
Delta | không gì cả |
Chúng được khai báo bằng method @Bean theo thứ tự Delta, Gamma, Beta, Alpha. Mỗi cái in tên mình từ postProcessBeforeInitialization. Thứ tự đo được:
Alpha PriorityOrdered, getOrder() = 5000
Beta Ordered, getOrder() = HIGHEST_PRECEDENCE
Delta no ordering at all
Gamma @Order(HIGHEST_PRECEDENCE), no interfaceBốn dòng đó cho ra hai quy tắc, và quy tắc thứ hai mới là cái hay làm người ta vấp.
PriorityOrdered thắng Ordered bất kể con số. Alpha xin ưu tiên 5000 mà vẫn chạy trước Beta, cái xin Integer.MIN_VALUE. PostProcessorRegistrationDelegate chia thành ba nhóm — PriorityOrdered, rồi Ordered, rồi phần còn lại — và chỉ sắp xếp bên trong từng nhóm. Nhóm quyết định trước.
@Order trên một BeanPostProcessor không có tác dụng gì. Gamma xin ưu tiên cao nhất có thể và chạy cuối cùng. Việc chọn nhóm là một phép isTypeMatch với interface PriorityOrdered và Ordered, nên annotation không thể đẩy một processor vào nhóm có order; còn nhóm không order thì được đăng ký mà không sắp xếp, nên annotation cũng vô nghĩa ở đó. Gamma chạy sau Delta chỉ vì method @Bean của Delta đứng trước. Cách sửa là một interface:
@Order(Ordered.HIGHEST_PRECEDENCE)
public static class Gamma implements BeanPostProcessor {
public static class Gamma implements BeanPostProcessor, Ordered {
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
} Một runner duyệt ((AbstractBeanFactory) beanFactory).getBeanPostProcessors() in ra chain hoàn chỉnh, và đây là chỗ hạ tầng của Boot hiện ra:
1 - org.springframework.context.support.ApplicationContextAwareProcessor
2 - org.springframework.boot.web.server.servlet.context.WebApplicationContextServletContextAwareProcessor
3 - org.springframework.context.annotation.ConfigurationClassPostProcessor$ImportAwareBeanPostProcessor
4 - org.springframework.context.support.PostProcessorRegistrationDelegate$BeanPostProcessorChecker
5 PriorityOrdered org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor
6 PriorityOrdered org.springframework.boot.jdbc.autoconfigure.HikariJdbcConnectionDetailsBeanPostProcessor
7 PriorityOrdered com.example.demo.order.OrderedBpps$Alpha
8 Ordered com.example.demo.order.OrderedBpps$Beta
9 Ordered org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator
10 Ordered com.example.demo.startup.TimingBeanPostProcessor
11 Ordered org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor
12 - com.example.demo.order.OrderedBpps$Delta
13 - com.example.demo.order.OrderedBpps$Gamma
14 - com.example.demo.startup.TraceBeanPostProcessor
15 - org.springframework.data.web.config.ProjectingArgumentResolverRegistrar$ProjectingArgumentResolverBeanPostProcessor
16 - org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor
17 - org.springframework.boot.web.error.ErrorPageRegistrarBeanPostProcessor
18 PriorityOrdered org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor
19 PriorityOrdered org.springframework.context.annotation.CommonAnnotationBeanPostProcessor
20 PriorityOrdered org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor
21 - org.springframework.context.support.ApplicationListenerDetectorMục 1 tới 4 được container thêm thẳng vào chain chứ không phải phân giải như bean có order, nên nằm ngoài mọi phép sắp xếp — đó là lý do ApplicationContextAwareProcessor, và do đó EnvironmentAware cùng đồng bọn, luôn chạy đầu tiên. Mục 18 tới 20 là các annotation processor của chính Spring, bị tách khỏi phép sắp xếp và nối lại vào cuối, bất kể PriorityOrdered. Đó là lý do cơ học khiến postProcessBeforeInitialization của bạn luôn thấy bean trước khi @PostConstruct của nó chạy, dù bạn cho nó order nào — bài Basics đã chỉ ra triệu chứng; mục 19 là nguyên nhân.
Mục 9 là cái nên để mắt: auto-proxy creator của AOP là một processor Ordered ở LOWEST_PRECEDENCE, nên một processor của bạn nằm trong nhóm PriorityOrdered sẽ thấy bean trước khi nó được proxy, còn cái nằm ở nhóm không order sẽ thấy proxy.
Các interface Aware ngoài ba cái đầu
Basics 9 đã trace BeanNameAware, BeanFactoryAware và ApplicationContextAware. Còn hơn chục cái nữa; bốn cái đáng biết:
@Component
public class AwareBean implements EnvironmentAware, ResourceLoaderAware,
ApplicationEventPublisherAware, BeanClassLoaderAware {
@Override
public void setEnvironment(Environment environment) {
Trace.step("EnvironmentAware", "lab.origin=" + environment.getProperty("lab.origin"));
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
Trace.step("ResourceLoaderAware", resourceLoader.getClass().getSimpleName());
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
Trace.step("ApplicationEventPublisherAware", publisher.getClass().getSimpleName());
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
Trace.step("BeanClassLoaderAware", classLoader.getClass().getSimpleName());
}
}Lần chạy chia chúng thành hai nhóm, điều không nhìn ra được từ source:
10 BeanClassLoaderAware LaunchedClassLoader
11 EnvironmentAware lab.origin=EnvironmentPostProcessor
12 ResourceLoaderAware AnnotationConfigServletWebServerApplicationContext
13 ApplicationEventPublisherAware AnnotationConfigServletWebServerApplicationContext
14 BPP.before awareBean AwareBeanBeanClassLoaderAware do chính bean factory gọi, trong invokeAwareMethods, cùng chỗ với BeanNameAware và BeanFactoryAware. Ba cái còn lại do ApplicationContextAwareProcessor gọi — mục 1 của chain ở trên — vốn là một BeanPostProcessor làm việc trong postProcessBeforeInitialization. Vì thế mới có khoảng cách ở bước 13: mọi thứ context biết đều đến qua một processor duy nhất, và các processor của bạn thấy bean sau đó.
Cũng để ý bước 12 và 13: ResourceLoader và ApplicationEventPublisher đều chính là ApplicationContext, vì ApplicationContext implement cả hai interface. Các interface hẹp tồn tại để class của bạn khỏi phải nói rằng nó muốn cả container.
| Interface | Nó đưa cho bạn cái gì | Dùng khi |
|---|---|---|
EnvironmentAware | Environment | hạ tầng cần đọc configuration trước khi có binding |
ResourceLoaderAware | một ResourceLoader cho URL classpath: và file: | nạp resource mà đường dẫn được tính ra |
ApplicationEventPublisherAware | bộ publish event | publish event từ một class không được phụ thuộc vào context |
BeanClassLoaderAware | classloader đã nạp class của bean | reflection, Class.forName, dựng JDK proxy |
Với code application, câu trả lời gần như luôn là constructor injection. Environment, ResourceLoader, ApplicationEventPublisher và ApplicationContext đều inject được như constructor parameter bình thường; như vậy field giữ được final, class vẫn test được bằng new thuần, và chữ ký type không dính interface của Spring. Các interface Aware chỉ đáng giá trong đúng một tình huống: một class mà container tạo ra trước khi injection sẵn sàng, tức là BeanFactoryPostProcessor, BeanPostProcessor hay ApplicationContextInitializer. ReportRegistrar ở trên dùng EnvironmentAware đúng vì lý do đó.
ApplicationRunner và CommandLineRunner
Cả hai chạy sau khi context refresh xong và trước khi run() trả về. Khác biệt duy nhất là argument.
@Order(1)
@Component
public class ArgumentRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
Trace.step("ApplicationRunner @Order(1)", "optionNames=" + args.getOptionNames()
+ " nonOptionArgs=" + args.getNonOptionArgs());
System.out.println(" --tag -> " + args.getOptionValues("tag"));
System.out.println(" --lab.mode -> " + args.getOptionValues("lab.mode"));
System.out.println(" containsOption(v)-> " + args.containsOption("v"));
System.out.println(" getSourceArgs -> " + java.util.Arrays.toString(args.getSourceArgs()));
}
}Chạy với --lab.mode=full --tag=alpha --tag=beta -v report.csv:
29 ApplicationRunner @Order(1) optionNames=[lab.mode, tag] nonOptionArgs=[-v, report.csv]
--tag -> [alpha, beta]
--lab.mode -> [full]
containsOption(v)-> false
getSourceArgs -> [--lab.mode=full, --tag=alpha, --tag=beta, -v, report.csv]
30 CommandLineRunner @Order(2) raw args=[--lab.mode=full, --tag=alpha, --tag=beta, -v, report.csv]ApplicationArguments đã parse sẵn. Một option argument phải đúng dạng --name hoặc --name=value; mọi thứ khác là non-option argument. Nên -v một gạch không phải option — containsOption("v") là false và -v rơi vào getNonOptionArgs() cạnh report.csv. Option lặp lại thì cộng dồn: --tag hai lần cho ra một list hai phần tử. CommandLineRunner nhận đúng mảng Boot nhận được, chưa parse, cũng là thứ getSourceArgs() trả về ở interface kia.
Dùng ApplicationRunner trừ khi có lý do khác. CommandLineRunner chỉ đáng khi bạn đưa argument cho một library tự parse lấy.
@Order có tác dụng giữa các runner — khác với trên BeanPostProcessor — vì SpringApplication.callRunners sắp xếp các bean runner bằng AnnotationAwareOrderComparator, và comparator này đọc annotation. Cả hai interface được sắp chung một danh sách, nên một ApplicationRunner ở @Order(1) chạy trước một CommandLineRunner ở @Order(2), đúng như bảng trace.
Vị trí so với log mới là chỗ nhiều người hiểu ngược:
27 ContextRefreshedEvent refresh() finished
INFO DemoApplication : Started DemoApplication in 1.228 seconds (process running for 1.42)
28 ApplicationStartedEvent published just before the runners
29 ApplicationRunner @Order(1) ...
33 ApplicationReadyEvent published after the last runner returnedWeb server đã nhận kết nối từ bước 27, và dòng "Started" đã in xong. Runner không phải cổng chặn startup: traffic có thể tới application trong lúc nó còn đang chạy. Nếu bạn cần một việc xong trước khi instance nhận traffic, chỗ của nó là @PostConstruct, một SmartInitializingSingleton, hay một readiness probe do bạn kiểm soát — không phải runner.
Exception trong một runner gây ra chuyện gì
Một runner ném exception sẽ kéo đổ cả application. Đây là @Order(4) ném, trong khi @Order(5) còn chưa tới lượt:
32 ApplicationRunner @Order(4) about to throw
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
ERROR SpringApplication : Application run failed
com.example.demo.runner.RunnerFailure: the runner failed on purpose
at com.example.demo.runner.FailingRunner.run(FailingRunner.java:18)
at org.springframework.boot.SpringApplication.lambda$callRunner$0(SpringApplication.java:788)
...
INFO GracefulShutdown : Commencing graceful shutdown. Waiting for active requests to complete
INFO GracefulShutdown : Graceful shutdown complete
INFO LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
INFO HikariDataSource : HikariPool-1 - Shutdown initiated...
INFO HikariDataSource : HikariPool-1 - Shutdown completed.Bốn điều quan sát được. Runner @Order(5) không bao giờ chạy — chuỗi dừng ở lỗi đầu tiên. Context được đóng hẳn, không bị bỏ lửng: Tomcat drain rồi dừng, còn entity manager factory và connection pool được tắt qua đúng các destruction callback thông thường. curl vào port 8202 ngay sau đó không trả về gì, kết nối bị từ chối. Và tiến trình thoát với mã khác 0.
Exit code mặc định là 1. Cho exception implement ExitCodeGenerator thì bạn tự chọn:
package com.example.demo.runner;
import org.springframework.boot.ExitCodeGenerator;
public class RunnerFailure extends RuntimeException implements ExitCodeGenerator {
public RunnerFailure(String message) {
super(message);
}
@Override
public int getExitCode() {
return 42;
}
}java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --lab.fail-runner=true ; echo "exit=$?"exit=42Một bean ExitCodeGenerator làm điều tương tự cho lần shutdown bình thường, còn ExitCodeExceptionMapper ánh xạ type exception sang mã mà không phải động vào class exception — đúng thứ bạn cần cho một batch job mà orchestrator đọc mã thoát.
Bảng tổng hợp các extension point
| Extension point | Chạy khi nào | Nhìn thấy gì | Dùng điển hình | Lựa chọn rẻ hơn |
|---|---|---|---|---|
EnvironmentPostProcessor | trước khi context được tạo | Environment và SpringApplication | thêm property source từ vault, file, config service từ xa | spring.config.import, một giá trị mặc định trong @ConfigurationProperties |
ApplicationContextInitializer | context đã tạo, trước refresh() | object context, profile, bean factory của nó | đăng ký một BeanFactoryPostProcessor, set profile từ code | SpringApplication.setAdditionalProfiles, một auto-configuration |
BeanDefinitionRegistryPostProcessor | việc đầu tiên trong refresh() | BeanDefinitionRegistry | đăng ký N bean từ configuration hoặc từ một lần scan | một method @Bean trả về Map hay List, khi N biết lúc compile |
BeanFactoryPostProcessor | sau khi mọi definition đã đăng ký | mọi BeanDefinition, chưa instance nào | đánh dấu primary hay lazy, sửa definition mình không sở hữu | @Primary, @Lazy, @Qualifier trên code của mình |
BeanPostProcessor | quanh callback khởi tạo của từng bean | từng instance bean, hai lần | bọc bean vào proxy, validate, xử lý annotation riêng | một @Aspect, một method @Bean bọc nó một lần |
Các interface Aware | trong lúc tạo bean | mỗi cái một object của container | class hạ tầng được tạo trước khi injection chạy được | constructor injection, với gần như mọi code application |
SmartInitializingSingleton | cuối refresh(), trước các runner | mọi singleton không lazy, đã hoàn chỉnh | nối các bean với nhau khi cần đủ mặt | @EventListener(ContextRefreshedEvent.class) |
ApplicationRunner / CommandLineRunner | sau refresh(), trong run() | toàn bộ context đang sống cùng các argument | việc startup chạy một lần, application kiểu CLI | @EventListener(ApplicationReadyEvent.class) |
Hãy đọc cột cuối như lựa chọn mặc định. Phần lớn việc người ta định dùng post-processor để làm thì một annotation, một auto-configuration hay một event làm tốt hơn; các extension point chỉ đáng dùng khi bạn phải tác động lên bean mình không sở hữu, hoặc lên một số lượng chỉ biết lúc runtime.
FAQ
BeanFactoryPostProcessor và BeanPostProcessor khác nhau ở đâu?
Chúng tác động lên hai thứ khác nhau, ở hai thời điểm khác nhau. BeanFactoryPostProcessor chạy một lần, sớm trong refresh(), và nhìn thấy bean definition — phần metadata — trước khi bất kỳ bean nào của application được tạo; nó sửa, thêm hoặc xoá được chúng. BeanPostProcessor chạy hai lần cho mỗi instance bean, quanh các callback khởi tạo của bean đó, và thay được instance bằng thứ khác. Tên gọi giống nhau đủ để gây nhầm, còn hai phase thì không liền nhau: mọi BeanFactoryPostProcessor trong context đã xong trước khi BeanPostProcessor đầu tiên được đăng ký.
Vì sao BeanPostProcessor của tôi bỏ qua @Order?
Vì việc chọn nhóm dùng isTypeMatch với interface PriorityOrdered và Ordered, còn nhóm còn lại thì được đăng ký mà không sắp xếp. Một processor chỉ mang @Order sẽ rơi vào nhóm không order và chạy sau mọi thứ có order, bất kể annotation ghi số nào — đo được ở trên, nơi @Order(HIGHEST_PRECEDENCE) chạy cuối trong bốn cái. Hãy implement Ordered (hoặc PriorityOrdered) và trả giá trị từ getOrder(). Ngược lại, giữa các runner thì @Order chạy tốt, vì callRunners sắp xếp bằng AnnotationAwareOrderComparator.
Cái gì gây ra "is not eligible for getting processed by all BeanPostProcessors"?
Một thứ đang được tạo trong giai đoạn đăng ký post-processor đã kéo theo một bean thường ra đời cùng — thường là một BeanPostProcessor có dependency ở constructor, hoặc một method @Bean không static khai báo dependency, khiến class @Configuration bao quanh lôi theo cả đống phụ thuộc của nó. Bean đó được dựng với chỉ phần chain đã đăng ký tới thời điểm ấy, nên có thể mất AOP proxy, mất @PostConstruct và mất field @Autowired, tất cả trong im lặng. Hãy inject ObjectProvider<T> và gọi getObject() bên trong callback.
context.initializer.classes còn chạy trên Spring Boot 4 không?
Không. DelegatingApplicationContextInitializer, class từng đọc property đó, không còn trong spring-boot-4.1.1.jar, và đặt property trong application.properties hay trên command line đều không tạo ra output lẫn warning nào ở lần chạy trên. context.listener.classes cũng vậy. Hãy đăng ký ApplicationContextInitializer qua META-INF/spring.factories hoặc SpringApplication.addInitializers.
Đăng ký EnvironmentPostProcessor ở đâu trong Spring Boot 4?
Trong META-INF/spring.factories, với key là org.springframework.boot.EnvironmentPostProcessor — interface đã rời khỏi package org.springframework.boot.env từ Boot 4.0, và type cũ mang @Deprecated(since = "4.0.0", forRemoval = true) dù vẫn còn chạy. Đây không phải cơ chế META-INF/spring/….imports của auto-configuration; environment post-processor chạy rất lâu trước khi bộ máy đó sẵn sàng, nên spring.factories vẫn là chỗ đúng cho chúng.
Nên dùng ApplicationRunner hay một listener của ApplicationReadyEvent?
Chúng chạy gần như cùng lúc, và — đo được ở đây — chúng hỏng giống hệt nhau: exception từ một runner và exception từ một listener của ApplicationReadyEvent đều ghi Application run failed, đóng context một cách êm đẹp rồi thoát với mã khác 0. Nên hãy chọn theo sự tiện tay. Dùng ApplicationRunner khi bạn cần các argument trên command line đã parse sẵn và cần thứ tự giữa nhiều phần việc startup. Dùng @EventListener(ApplicationReadyEvent.class) khi việc đó thuộc về một bean vốn đã tồn tại vì lý do khác, hoặc khi bạn muốn gắn @Async lên nó.
Kết luận
Startup của container không phải một hộp đen với đúng một cái móc. Trace trên Boot 4.1.1 cho thấy nó chạy EnvironmentPostProcessor trước khi có context, ApplicationContextInitializer trước refresh(), BeanDefinitionRegistryPostProcessor rồi BeanFactoryPostProcessor trên các definition, các callback Aware cùng hai method BeanPostProcessor quanh từng bean, SmartInitializingSingleton ở cuối refresh(), và cuối cùng là các runner — sau khi dòng Started … in đã in ra rồi, không phải trước. Cách đăng ký cũng đổi ở Boot 4: spring.factories cho EnvironmentPostProcessor và ApplicationContextInitializer, còn context.initializer.classes thì không làm gì cả.
Hai quy tắc đáng mang theo đều đã đo ở trên. Một BeanPostProcessor trả về object khác sẽ thay bean ở mọi nơi, và đó là cách @Transactional cùng mọi annotation đổi hành vi khác được hiện thực. Còn một post-processor inject một bean thường sẽ tạo bean đó quá sớm — không AOP proxy, không @PostConstruct, không field @Autowired, một method @Transactional không có transaction, kèm theo hoặc là một warning chẳng ai đọc, hoặc — từ một BeanFactoryPostProcessor — không warning nào cả. ObjectProvider tốn đúng một từ và xoá sạch cả nhóm bug đó.
Bài tiếp theo mở chính cái proxy ra: AOP và cơ chế proxy — JDK dynamic proxy vs CGLIB, @Aspect, pointcut, advice, và lỗi self-invocation.