Command Palette

Search for a command to run...

[Advanced Spring Boot] Mở rộng container của Spring: BeanFactoryPostProcessor, BeanPostProcessor và Aware

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ốn điểm đánh số trên một timeline từ SpringApplication.run tới lúc application sẵn sàng

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.

src/main/java/com/example/demo/trace/Trace.java
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:

Bash
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --lab.mode=full --tag=alpha --tag=beta -v report.csv

cho 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:

Text
 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

Tám phase của một lần startup Spring Boot 4.1.1 kèm số thứ tự mà mỗi phase in ra

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 ConfigurableEnvironmentSpringApplication, ngoài ra chưa có gì khác:

src/main/java/com/example/demo/startup/LabEnvironmentPostProcessor.java
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ì:

Text
Deprecated: true
RuntimeVisibleAnnotations:
    java.lang.Deprecated(
      since="4.0.0"
      forRemoval=true

Cá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.

src/main/resources/META-INF/spring.factories
org.springframework.boot.EnvironmentPostProcessor=\
com.example.demo.startup.LabEnvironmentPostProcessor
 
org.springframework.context.ApplicationContextInitializer=\
com.example.demo.startup.FactoriesContextInitializer

ApplicationContextInitializer chạy ngay sau đó, khi object context vừa được tạo và trước refresh():

src/main/java/com/example/demo/startup/LabContextInitializer.java
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:

src/main/java/com/example/demo/DemoApplication.java
@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:

Bash
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --context.initializer.classes=com.example.demo.startup.PropertyContextInitializer

Initializer đượ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 đó.

src/main/java/com/example/demo/startup/TraceBeanFactoryPostProcessor.java
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 getBeanNamesForTypeallowEagerInit, 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:

Bash
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --lab.mark-primary=false
Text
APPLICATION 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 consumed

Cù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.

src/main/resources/application.properties
lab.reports.daily=0 0 6 * * *
lab.reports.weekly=0 0 7 * * MON
lab.reports.monthly=0 0 8 1 * *
src/main/java/com/example/demo/startup/ReportRegistrar.java
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:

src/main/java/com/example/demo/startup/LabConfig.java
@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:

Text
    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:

src/main/java/com/example/demo/startup/TimingBeanPostProcessor.java
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:

Text
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

và runner inject PriceService đang giữ proxy, không phải service:

Text
31  ApplicationRunner @Order(3)      injected PriceService is a jdk.proxy2.$Proxy94
    [timed] ListPriceService.price took 15.1 ms
    price(SKU-1)     -> 19.90

Một bean đi qua hai method của BeanPostProcessor, method thứ hai trả về proxy mà caller sẽ giữ

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ý:

src/main/java/com/example/demo/trap/BrokenAuditBeanPostProcessor.java
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:

Text
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:

Text
    injected AuditService : com.example.demo.trap.AuditService
    AopUtils.isAopProxy   : false
    @PostConstruct ran = false, @Autowired field set = false
    record(from the runner) transaction active = false

Ba 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.

Cùng một AuditService, có và không có ObjectProvider, đặt cạnh nhau

Phiên bản BeanFactoryPostProcessor của cùng lỗi này còn tệ hơn, vì nó im lặng:

src/main/java/com/example/demo/trap/EagerBeanFactoryPostProcessor.java
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());
    }
}
Text
    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 = false

Thiệ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 getBean bên trong BeanFactoryPostProcessor là 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.

src/main/java/com/example/demo/trap/FixedAuditBeanPostProcessor.java
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:

Text
    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 = true

Mọ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:

ClassCách khai báo order
Alphaimplements PriorityOrdered, getOrder() trả về 5000 — một số ưu tiên thấp
Betaimplements Ordered, getOrder() trả về HIGHEST_PRECEDENCE
Gamma@Order(Ordered.HIGHEST_PRECEDENCE) trên class, không interface
Deltakhô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:

Text
    Alpha   PriorityOrdered, getOrder() = 5000
    Beta    Ordered, getOrder() = HIGHEST_PRECEDENCE
    Delta   no ordering at all
    Gamma   @Order(HIGHEST_PRECEDENCE), no interface

Bố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 PriorityOrderedOrdered, 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:

Java
@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:

Text
     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.ApplicationListenerDetector

Mụ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 OrderedLOWEST_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, BeanFactoryAwareApplicationContextAware. Còn hơn chục cái nữa; bốn cái đáng biết:

src/main/java/com/example/demo/aware/AwareBean.java
@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:

Text
10  BeanClassLoaderAware             LaunchedClassLoader
11  EnvironmentAware                 lab.origin=EnvironmentPostProcessor
12  ResourceLoaderAware              AnnotationConfigServletWebServerApplicationContext
13  ApplicationEventPublisherAware   AnnotationConfigServletWebServerApplicationContext
14  BPP.before  awareBean            AwareBean

BeanClassLoaderAware do chính bean factory gọi, trong invokeAwareMethods, cùng chỗ với BeanNameAwareBeanFactoryAware. 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: ResourceLoaderApplicationEventPublisher đề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.

InterfaceNó đưa cho bạn cái gìDùng khi
EnvironmentAwareEnvironmenthạ tầng cần đọc configuration trước khi có binding
ResourceLoaderAwaremột ResourceLoader cho URL classpath:file:nạp resource mà đường dẫn được tính ra
ApplicationEventPublisherAwarebộ publish eventpublish event từ một class không được phụ thuộc vào context
BeanClassLoaderAwareclassloader đã nạp class của beanreflection, 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, ApplicationEventPublisherApplicationContext đề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.

src/main/java/com/example/demo/runner/ArgumentRunner.java
@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:

Text
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")false-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 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:

Text
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 returned

Web 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:

Text
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:

src/main/java/com/example/demo/runner/RunnerFailure.java
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;
    }
}
Bash
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --lab.fail-runner=true ; echo "exit=$?"
Text
exit=42

Mộ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 pointChạy khi nàoNhìn thấy gìDùng điển hìnhLựa chọn rẻ hơn
EnvironmentPostProcessortrước khi context được tạoEnvironmentSpringApplicationthêm property source từ vault, file, config service từ xaspring.config.import, một giá trị mặc định trong @ConfigurationProperties
ApplicationContextInitializercontext đã tạo, trước refresh()object context, profile, bean factory của nóđăng ký một BeanFactoryPostProcessor, set profile từ codeSpringApplication.setAdditionalProfiles, một auto-configuration
BeanDefinitionRegistryPostProcessorviệc đầu tiên trong refresh()BeanDefinitionRegistryđăng ký N bean từ configuration hoặc từ một lần scanmột method @Bean trả về Map hay List, khi N biết lúc compile
BeanFactoryPostProcessorsau 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
BeanPostProcessorquanh callback khởi tạo của từng beantừng instance bean, hai lầnbọc bean vào proxy, validate, xử lý annotation riêngmột @Aspect, một method @Bean bọc nó một lần
Các interface Awaretrong lúc tạo beanmỗi cái một object của containerclass hạ tầng được tạo trước khi injection chạy đượcconstructor injection, với gần như mọi code application
SmartInitializingSingletoncuối refresh(), trước các runnermọi singleton không lazy, đã hoàn chỉnhnối các bean với nhau khi cần đủ mặt@EventListener(ContextRefreshedEvent.class)
ApplicationRunner / CommandLineRunnersau refresh(), trong run()toàn bộ context đang sống cùng các argumentviệ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 PriorityOrderedOrdered, 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 EnvironmentPostProcessorApplicationContextInitializer, 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.

Bài viết liên quan

[Advanced Spring Boot] Locking và concurrency trong Spring Boot: optimistic @Version, pessimistic lock và race condition

Locking và concurrency trong Spring Boot 4.1.1 trên PostgreSQL: lost update khi hai người cùng sửa một product, @Version và câu update … where version=? nó gửi đi, chuỗi exception tới được code của bạn, saveAll, dirty checking và bulk update @Modifying bỏ qua version, version qua HTTP trả về 409 ProblemDetail, retry một conflict bao quanh cả transaction và cách đặt retry khiến nó không bao giờ retry, PESSIMISTIC_WRITE so với PESSIMISTIC_READ, NOWAIT và jakarta.persistence.lock.timeout thành set local lock_timeout, SKIP LOCKED cho work queue, một deadlock thật (40P01) và cách sửa, atomic update có điều kiện, CHECK constraint, và một bảng để chọn giữa chúng.

[Advanced Spring Boot] Truy vấn động với Spring Data JPA: Specification, Criteria API và Querydsl

Dynamic query cho một search sản phẩm có filter tùy chọn trên Spring Boot 4.1.1 và PostgreSQL: vì sao derived query và mẹo @Query (:x is null or …) không đủ, kèm lỗi lower(bytea) và generic plan đọc 200.403 buffer, Specification kết hợp được với API của Spring Data 4 (allOf, unrestricted, PredicateSpecification, UpdateSpecification, where(null) giờ throw), escape LIKE, join to-many làm count phình ra và page co lại, distinct so với subquery exists, Criteria API trong repository fragment tự viết với metamodel của hibernate-processor và đếm theo category, Querydsl với jakarta classifier, QuerydslPredicateExecutor và JPAQueryFactory, code generation của jOOQ, whitelist cho sort, 400 ProblemDetail khi minPrice lớn hơn maxPrice, và Query by Example.

[Advanced Spring Boot] Tự dựng authorization server: Spring Authorization Server và Keycloak

Dựng một authorization server OAuth2 và OpenID Connect bằng Spring Authorization Server, nay là một module của Spring Security, trên Spring Boot 4.1.1: starter mà Initializr chọn và starter đã deprecated, một server chỉ từ property, hai discovery document và các endpoint chúng công bố, hai filter chain Boot đăng ký và những gì thay đổi khi bạn tự khai báo, client_credentials và authorization_code với PKCE từng bước, body lỗi thật, RSA key đổi sau mỗi lần restart, key cố định và key rotation với JWK selector, JWKS cache của resource server, client, authorization và consent lưu bằng JDBC trên PostgreSQL với schema script lấy từ jar, claim roles từ OAuth2TokenCustomizer và cái bẫy allowlist của Jackson, opaque token với introspection được đo so với việc validate JWT, và phép so sánh có đo đạc với Keycloak.

[Advanced Spring Boot] NoSQL với Spring Data: MongoDB và Redis

Spring Data MongoDB và Spring Data Redis trên Spring Boot 4.1.1: @Document, id là String hay ObjectId, field _class, embedded hay @DocumentReference cùng các query mỗi cách gửi đi, MongoRepository và MongoTemplate kèm command đã log, $push/$inc so với load-modify-save dưới 50 thread, @Version, Boot có tạo index từ @Indexed không, COLLSCAN và IXSCAN trên 300.000 document, một aggregation pipeline, transaction trên replica set, một field bị đổi tên, serializer của RedisTemplate, INCR, sorted set, hash, TTL, key của @RedisHash và phantom key, pipelining và Lettuce.