ioc container & application context
Tung Bui Thanh (TungDaDev)17 mins read

Nếu bạn từng thắc mắc Spring Boot làm sao biết phải tạo object nào, inject vào đâu và quản lý lifecycle ra sao, thì câu trả lời nằm ở package này.
org.springframework.context là trái tim của Spring Framework. Nó chứa toàn bộ cơ chế Inversion of Control (IoC): bạn không còn new Object() thủ công mà chỉ khai báo cần gì, Spring lo tạo và nối cho bạn.
Thực tế package này có mặt ở khắp nơi: khai báo configuration class, publish/listen event, chạy background job theo lịch, quản lý profile (dev/staging/prod), đọc properties từ file config. Nắm được nó thì cũng hiểu được Spring Boot khởi động và vận hành application của bạn thế nào.
# @Configuration: java-based configuration
Trước Spring 3, config bằng XML là chuẩn, khai báo bean có khi mất cả trăm dòng XML. @Configuration thay XML bằng Java code: type-safe, IDE hỗ trợ tốt, refactor được.
Một class đánh @Configuration thực chất là một "factory". Spring quét nó, tìm các method đánh @Bean, gọi chúng và đưa return value vào IoC container. Từ đó class nào cần dependency đó đều inject được.
Trong method @Bean bạn viết Java bình thường: tạo bean có điều kiện, đặt giá trị mặc định, nối dependency giữa các bean. Mọi thứ tường minh, đặt breakpoint debug được.
// Thay thế XML config bằng Java class
@Configuration
public class AppConfig {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.connectTimeout(Duration.ofSeconds(5)) // Boot 3.4+; bản cũ hơn là setConnectTimeout (đã deprecated)
.readTimeout(Duration.ofSeconds(10)) // Boot 3.4+; bản cũ hơn là setReadTimeout
.build();
}
// Bean với dependency
@Bean
public ProductService productService(ProductRepository repo, ObjectMapper mapper) {
return new ProductService(repo, mapper);
}
}# @Configuration(proxyBeanMethods)
Hai chế độ khác nhau ở chỗ: gọi trực tiếp một method @Bean từ method @Bean khác thì có qua container hay không.
// proxyBeanMethods = true (default) — CGLIB proxy, method calls return same instance
@Configuration
public class FullConfig {
@Bean
public ServiceA serviceA() { return new ServiceA(commonDep()); }
@Bean
public ServiceB serviceB() { return new ServiceB(commonDep()); }
@Bean
public CommonDep commonDep() { return new CommonDep(); }
// serviceA và serviceB dùng CÙNG instance commonDep (proxied)
}
// proxyBeanMethods = false — Lite mode, no proxy, faster startup
@Configuration(proxyBeanMethods = false)
public class LiteConfig {
@Bean
public ServiceA serviceA(CommonDep dep) { return new ServiceA(dep); }
// Inject qua parameter thay vì gọi method → an toàn hơn
}# @bean: khai báo bean từ method
@Bean là cách bạn bảo Spring: "object này để container quản lý giùm". Method return gì, container giữ cái đó.
Khi nào dùng @Bean thay vì @Component? Khi class không phải của bạn (thư viện ngoài như ObjectMapper, RestTemplate) nên không gắn annotation vào được, hoặc khi việc khởi tạo cần logic riêng. Class tự viết thì @Service/@Component tiện hơn, còn @Bean cho bạn toàn quyền kiểm soát cách tạo object.
Một điểm quan trọng: mặc định mỗi bean là singleton. Trong class @Configuration, gọi method objectMapper() 10 lần ở 10 method @Bean khác nhau vẫn nhận cùng 1 instance. Đó là nhờ @Configuration tạo CGLIB proxy (một subclass sinh ra lúc runtime) cho class: proxy chặn lời gọi method, bean đã có trong container thì trả lại bean đó chứ không chạy lại thân method. Với proxyBeanMethods = false hoặc method @Bean nằm trong class @Component thường, lời gọi trực tiếp chỉ là gọi hàm Java, mỗi lần tạo một object mới.
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
// Bean name (default = method name)
@Bean("customEncoder")
public PasswordEncoder anotherEncoder() {
return new Argon2PasswordEncoder(16, 32, 1, 65536, 3);
}
// Init & destroy methods
@Bean(initMethod = "start", destroyMethod = "shutdown")
public ConnectionPool connectionPool() {
return new ConnectionPool();
}
// Conditional bean (annotation của Spring Boot)
// Lưu ý: Spring Boot khuyến nghị chỉ dùng @ConditionalOnBean/@ConditionalOnMissingBean trong
// auto-configuration; trong config của ứng dụng, kết quả phụ thuộc thứ tự đăng ký bean
@Bean
@ConditionalOnProperty(name = "cache.enabled", havingValue = "true")
public CacheManager cacheManager(RedisConnectionFactory factory) {
return RedisCacheManager.builder(factory).build();
}
@Bean
@ConditionalOnMissingBean(CacheManager.class)
public CacheManager noOpCacheManager() {
return new NoOpCacheManager();
}
// Profile-specific
@Bean
@Profile("dev")
public DataSource devDataSource() {
return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
}
@Bean
@Profile("prod")
public DataSource prodDataSource(DataSourceProperties props) {
return props.initializeDataSourceBuilder().build();
}
}# @ComponentScan: quét và đăng ký beans
Spring không có phép màu gì: nó cần biết quét ở đâu để tìm bean. @ComponentScan chỉ định package để Spring quét, tìm tất cả class có @Component, @Service, @Repository, @Controller và đăng ký chúng vào container.
Trong Spring Boot hiếm khi phải viết @ComponentScan vì @SpringBootApplication đã gồm sẵn nó, base package là package chứa main class (và các package con). Chỉ khi cần loại bớt một số class (test double, code legacy) hoặc quét thêm package nằm ngoài thì mới cần tới. Lưu ý: tự khai báo @ComponentScan thì các filter mặc định của Boot (như TypeExcludeFilter) không còn tác dụng.
@Configuration
@ComponentScan(
basePackages = "com.tungdadev.csp",
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class),
@ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Test.*")
},
includeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = CustomService.class)
}
)
public class AppConfig {}
// @SpringBootApplication đã include @ComponentScan với base package = package chứa main class# @Profile: conditional bean theo environment
Dự án thật luôn có nhiều môi trường: dev chạy local, staging để test tích hợp, prod phục vụ user thật. Mỗi môi trường cần config khác nhau: database URL, API key, feature flag, thậm chí cả implementation (mock mail hay SMTP thật).
@Profile xử lý chuyện này khá gọn: đánh dấu beans/configurations chỉ hoạt động ở profile cụ thể. Spring chỉ tạo bean nếu profile đang active khớp. Không cần if/else trong code, không cần comment/uncomment.
@Configuration
@Profile("dev")
public class DevConfig {
@Bean
public MailService mailService() {
return new MockMailService(); // Không gửi mail thật
}
}
@Configuration
@Profile("prod")
public class ProdConfig {
@Bean
public MailService mailService(MailProperties props) {
return new SmtpMailService(props);
}
}
// NOT profile
@Configuration
@Profile("!prod") // Mọi env trừ prod
public class NonProdConfig { ... }
// Multiple profiles (OR)
@Service
@Profile({"dev", "staging"})
public class DebugService { ... }
// Profile expression (Spring 5.1+): @Profile("dev & !local"), @Profile("dev | staging")
// Active profiles
// application.properties: spring.profiles.active=dev,local
// CLI: --spring.profiles.active=prod
// ENV: SPRING_PROFILES_ACTIVE=prod# @PropertySource & @Value: external configuration
Hard-code giá trị config trong code Java là anti-pattern kinh điển: mỗi lần đổi timeout, URL hay feature flag lại phải compile và deploy lại.
@Value inject giá trị từ file properties (hoặc biến môi trường, tham số dòng lệnh) vào field Java. Giá trị được đọc một lần lúc tạo bean, nên đổi config chỉ cần restart app chứ không phải build lại (muốn đổi khi app đang chạy thì cần thêm cơ chế như @RefreshScope của Spring Cloud). @PropertySource cho phép nạp thêm properties từ file khác; lưu ý nó chỉ đọc định dạng .properties, không đọc YAML.
Nhưng nếu có nhiều properties liên quan (cùng prefix) thì @ConfigurationProperties hợp hơn: type-safe, validate được, IDE autocomplete. Coi nó như bản nâng cấp từ "lấy từng giá trị rời rạc" lên "bind cả nhóm config vào 1 object có cấu trúc".
@Configuration
@PropertySource("classpath:custom-config.properties")
@PropertySource(value = "file:/opt/config/override.properties", ignoreResourceNotFound = true)
public class ExternalConfig {
@Value("${app.name:CSP}") // Default value
private String appName;
@Value("${app.max-upload-size:10485760}")
private long maxUploadSize;
@Value("${app.allowed-origins}") // Bắt buộc phải có
private String[] allowedOrigins;
@Value("#{${app.feature-flags}}") // SpEL → Map, property dạng: app.feature-flags={'dark-mode': true}
private Map<String, Boolean> featureFlags;
@Value("${app.timeout:30}000") // Placeholder ghép chuỗi: "30" + "000" → 30000
private long timeoutMs;
@Value("#{systemProperties['user.home']}")
private String userHome;
@Value("#{T(java.util.UUID).randomUUID().toString()}")
private String instanceId;
}# @ConfigurationProperties: type-safe config (recommended)
@Data
@ConfigurationProperties(prefix = "app.upload")
@Validated
public class UploadProperties {
@NotBlank
private String storagePath;
@Min(1)
private long maxFileSize = 10_485_760;
private List<String> allowedTypes = List.of("pdf", "png", "jpg");
@DurationUnit(ChronoUnit.SECONDS)
private Duration timeout = Duration.ofSeconds(30);
@Valid
private Retry retry = new Retry();
@Data
public static class Retry {
@Min(1) @Max(10)
private int maxAttempts = 3;
private Duration backoff = Duration.ofSeconds(1);
}
}
// Enable
@Configuration
@EnableConfigurationProperties(UploadProperties.class)
public class AppConfig {}
// Usage
@Service
@RequiredArgsConstructor
public class UploadService {
private final UploadProperties props;
}# event system: application events
Đây là một trong những tính năng bị đánh giá thấp nhất của Spring. Thay vì service A gọi thẳng service B, C, D (dính chặt vào nhau), A chỉ cần "hô lên" là có chuyện vừa xảy ra, ai quan tâm thì tự nghe.
Pattern này gọi là Observer hay Pub/Sub, Spring làm khá gọn qua ApplicationEventPublisher + @EventListener. Code ít phụ thuộc nhau hơn, dễ test (mock publisher), dễ mở rộng (thêm listener mới mà không sửa publisher).
Giữa các microservice, event thường đi qua RabbitMQ/Kafka. Còn trong cùng một service (cùng process), Spring Events nhẹ và đủ dùng, không cần hạ tầng gì thêm. Có điều event chỉ nằm trong bộ nhớ: app chết giữa chừng là mất, không có retry hay lưu lại như message broker.
Một lưu ý quan trọng: dùng @TransactionalEventListener(phase = AFTER_COMMIT) cho các side effect (gửi email, push notification). Transaction mà rollback thì chẳng ai muốn khách nhận email xác nhận cho một đơn hàng không hề tồn tại. Hai cái bẫy đi kèm: nếu lúc publish không có transaction nào đang chạy thì listener này không chạy (trừ khi đặt fallbackExecution = true); và listener chạy sau commit nên muốn ghi tiếp xuống DB thì phải mở transaction mới (@Transactional(propagation = REQUIRES_NEW)).
# built-in events
@Component
@Slf4j
public class AppLifecycleListener {
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
log.info("Application started and ready to serve");
}
@EventListener(ContextRefreshedEvent.class)
public void onRefresh() {
log.info("Context refreshed");
}
@EventListener(ContextClosedEvent.class)
public void onShutdown() {
log.info("Application shutting down");
}
}# custom events
// Event class
@Getter
public class OrderCreatedEvent extends ApplicationEvent {
private final UUID orderId;
private final UUID customerId;
private final BigDecimal amount;
public OrderCreatedEvent(Object source, UUID orderId, UUID customerId, BigDecimal amount) {
super(source);
this.orderId = orderId;
this.customerId = customerId;
this.amount = amount;
}
}
// Publisher
@Service
@RequiredArgsConstructor
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public OrderDTO create(CreateOrderRequest request) {
Order order = orderRepository.save(buildOrder(request));
// Publish event
eventPublisher.publishEvent(new OrderCreatedEvent(
this, order.getId(), order.getCustomerId(), order.getTotalAmount()));
return toDTO(order);
}
}
// Listeners
@Component
@Slf4j
public class OrderEventListeners {
// Sync listener (same thread, same TX)
@EventListener
public void onOrderCreated(OrderCreatedEvent event) {
log.info("Order created: {} | amount={}", event.getOrderId(), event.getAmount());
}
// Async listener (different thread, cần @EnableAsync)
@Async
@EventListener
public void sendNotification(OrderCreatedEvent event) {
notificationService.notifyCustomer(event.getCustomerId(), "Order placed");
}
// Transactional listener (after TX commits)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void afterOrderCommitted(OrderCreatedEvent event) {
// Chỉ chạy khi TX commit thành công
analyticsService.trackOrder(event.getOrderId(), event.getAmount());
}
// Conditional listener
@EventListener(condition = "#event.amount.compareTo(T(java.math.BigDecimal).valueOf(1000000)) > 0")
public void onHighValueOrder(OrderCreatedEvent event) {
alertService.alertHighValueOrder(event.getOrderId());
}
// Return value → publish as new event
@EventListener
public NotificationSentEvent handleOrder(OrderCreatedEvent event) {
sendEmail(event);
return new NotificationSentEvent(this, event.getOrderId());
}
}# generic events (spring 4.2+)
// Generic event — no need to extend ApplicationEvent
// Do type erasure, lúc runtime Spring không biết T là gì → phải implement ResolvableTypeProvider,
// nếu không thì listener EntityChangedEvent<Product> sẽ không nhận được event nào
@Getter
@AllArgsConstructor
public class EntityChangedEvent<T> implements ResolvableTypeProvider {
private final T entity;
private final ChangeType changeType;
@Override
public ResolvableType getResolvableType() {
return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(entity));
}
public enum ChangeType { CREATED, UPDATED, DELETED }
}
// Publish
eventPublisher.publishEvent(new EntityChangedEvent<>(product, ChangeType.CREATED));
// Listen (match by generic type)
@EventListener
public void onProductChanged(EntityChangedEvent<Product> event) { ... }
@EventListener
public void onOrderChanged(EntityChangedEvent<Order> event) { ... }# @Scheduled: task scheduling
Ứng dụng production nào cũng có background job: xoá file tạm, gửi báo cáo hằng ngày, đồng bộ dữ liệu, health check. Thay vì dựng cron job trên server hay kéo Quartz vào, Spring có @Scheduled: gắn annotation lên method là xong.
fixedDelay với fixedRate là câu hỏi phỏng vấn kinh điển. fixedDelay chờ lần chạy trước xong rồi mới đếm khoảng nghỉ. fixedRate tính mốc theo thời điểm bắt đầu, cứ đều đặn mỗi X giây. Nhưng Spring không chạy chồng cùng một task: nếu một lần chạy lâu hơn chu kỳ, lần sau sẽ chạy ngay khi lần trước xong chứ không chạy song song. Muốn chạy song song thật thì phải kết hợp thêm @Async.
Lưu ý: scheduler mà Spring Boot cấu hình sẵn chỉ có 1 thread. Có 5 scheduled task thì chúng phải xếp hàng, một task chạy lâu sẽ làm trễ cả các task khác. Tăng spring.task.scheduling.pool.size (hoặc tự khai báo TaskScheduler) nếu cần chạy song song. Thêm nữa, app chạy nhiều instance thì mỗi instance đều chạy job, muốn chỉ một nơi chạy thì cần khoá phân tán (ví dụ ShedLock).
@Configuration
@EnableScheduling
public class SchedulingConfig {}
@Component
@Slf4j
public class ScheduledTasks {
// Fixed delay — chờ xong rồi mới đếm tiếp
@Scheduled(fixedDelay = 60_000) // 60s SAU KHI method trước hoàn thành
public void cleanExpiredSessions() {
sessionService.cleanExpired();
}
// Fixed rate — chạy đều đặn bất kể execution time
@Scheduled(fixedRate = 30_000) // Mỗi 30s
public void sendHeartbeat() {
healthService.sendHeartbeat();
}
// Cron expression
@Scheduled(cron = "0 0 2 * * *") // Mỗi ngày lúc 2:00 AM
public void dailyCleanup() {
fileService.purgeOldFiles(30);
}
@Scheduled(cron = "0 */5 * * * *") // Mỗi 5 phút
public void syncData() {
syncService.sync();
}
// Initial delay
@Scheduled(initialDelay = 10_000, fixedDelay = 60_000)
public void afterStartup() {
// Chờ 10s sau khi app start, rồi chạy mỗi 60s
}
// Cron timezone
@Scheduled(cron = "0 0 9 * * MON-FRI", zone = "Asia/Ho_Chi_Minh")
public void weekdayMorningReport() {
reportService.generateDaily();
}
}# @Async: non-blocking method execution
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("async-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
@Service
@Slf4j
public class NotificationService {
@Async("taskExecutor")
public void sendEmail(String to, String subject, String body) {
// Chạy trên thread khác, method return ngay lập tức
emailClient.send(to, subject, body);
}
@Async("taskExecutor")
public CompletableFuture<NotificationResult> sendAsync(NotificationRequest request) {
// Caller có thể await result
NotificationResult result = process(request);
return CompletableFuture.completedFuture(result);
}
}
// Usage
CompletableFuture<NotificationResult> future = notificationService.sendAsync(request);
NotificationResult result = future.get(10, TimeUnit.SECONDS); // Optional wait@Async hoạt động qua proxy, nên gọi một method @Async từ chính class đó (this.sendEmail(...)) sẽ chạy đồng bộ như hàm thường. Exception ném ra từ method @Async trả về void cũng không tới được caller, nó chỉ đi vào AsyncUncaughtExceptionHandler (mặc định là ghi log).
# @Scope: bean scope control
Mặc định mọi bean trong Spring là singleton: một instance duy nhất dùng chung cho cả application. Vì vậy KHÔNG nên giữ state thay đổi được trong bean @Service (nhiều thread cùng truy cập là dính race condition).
Nhưng đôi khi bạn cần mỗi lần lấy là một instance mới (prototype), hoặc mỗi HTTP request một instance. @Scope cho bạn kiểm soát chuyện này.
Thực tế gần như mọi bean đều là singleton. Prototype chỉ cần khi bean giữ state (kiểu report builder gom dữ liệu dần), còn request scope dùng cho ngữ cảnh riêng của từng request (user hiện tại, workspace ID).
Cái bẫy hay gặp: inject bean prototype vào một singleton thì việc inject chỉ xảy ra một lần lúc tạo singleton, nên singleton đó dùng mãi một instance. Muốn mỗi lần dùng là một instance mới thì inject ObjectProvider<ReportGenerator> rồi gọi getObject(), hoặc dùng proxyMode như ví dụ request scope bên dưới.
@Component
@Scope("prototype") // New instance mỗi lần inject
public class ReportGenerator {
private final List<String> lines = new ArrayList<>();
public void addLine(String line) { lines.add(line); }
public String generate() { return String.join("\n", lines); }
}
// Request scope (1 instance per HTTP request)
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private UUID requestId;
private UUID workspaceId;
private String userId;
}
// Custom scope
@Bean
@Scope("refresh") // Spring Cloud — refresh without restart
public ExternalServiceConfig externalConfig() { ... }# @Lazy: lazy initialization
Spring mặc định tạo TẤT CẢ singleton bean ngay khi application start, gọi là eager initialization. Ưu điểm là fail fast: lỗi config lộ ra ngay lúc start. Nhược điểm là start chậm nếu có nhiều bean nặng (nạp dữ liệu lớn, kết nối dịch vụ ngoài).
@Lazy hoãn việc tạo bean tới lần đầu nó được dùng, hợp với bean ít dùng hoặc tạo tốn kém. Nhưng cẩn thận: lỗi chỉ lộ ra khi có code gọi tới bean đó, không còn fail fast nữa. Và @Lazy trên class chỉ có tác dụng khi không ai inject nó một cách eager: một singleton bình thường inject nó qua constructor thì bean vẫn bị tạo ngay lúc start, nên chỗ inject cũng phải đánh @Lazy.
// Lazy bean — chỉ tạo khi lần đầu được inject/sử dụng
@Service
@Lazy
public class HeavyReportService {
// Constructor chỉ chạy khi service lần đầu được gọi
public HeavyReportService() {
loadLargeDataset(); // Expensive
}
}
// Lazy injection
// @Lazy phải đặt ở tham số constructor; Lombok @RequiredArgsConstructor KHÔNG chép @Lazy từ field sang
@Service
public class OrderService {
private final HeavyReportService reportService;
public OrderService(@Lazy HeavyReportService reportService) {
this.reportService = reportService; // Spring inject proxy, bean thật chỉ tạo khi gọi method lần đầu
}
}
// Global lazy initialization
// application.yml: spring.main.lazy-initialization=true# @Import & @ImportResource
Project lớn dần thì config được tách thành nhiều class @Configuration. @Import cho một config class "kéo" các config khác vào, chia config thành từng module.
@ImportResource là cầu nối với config XML cũ. Đang chuyển dần từ XML sang Java config thì đây là cách để hai bên cùng sống chung.
// Import config classes
@Configuration
@Import({SecurityConfig.class, CacheConfig.class, AsyncConfig.class})
public class AppConfig {}
// Import XML config (legacy)
@Configuration
@ImportResource("classpath:legacy-beans.xml")
public class LegacyConfig {}# MessageSource: Internationalization (i18n)
Ứng dụng phục vụ nhiều ngôn ngữ (Việt, Anh, Nhật...) thì không hardcode message trong code, mà dùng MessageSource nạp message từ file properties (messages.properties, messages_en.properties...) theo locale hiện tại.
Trong CSP, mỗi service có error code dạng CATEGORY.INDEX.ID (12 chữ số). MessageSource map error code sang message dễ hiểu theo ngôn ngữ, để API luôn trả message đúng ngữ cảnh. Với Spring Boot, chỉ cần đặt spring.messages.basename là có sẵn MessageSource; tự khai báo bean như dưới đây khi cần thứ Boot không có, ví dụ reload file message không cần restart.
@Configuration
public class I18nConfig {
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource source = new ReloadableResourceBundleMessageSource();
source.setBasename("classpath:messages");
source.setDefaultEncoding("UTF-8");
source.setCacheSeconds(3600);
return source;
}
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(Locale.forLanguageTag("vi"));
return resolver;
}
}
// Usage
@Service
@RequiredArgsConstructor
public class ValidationMessageService {
private final MessageSource messageSource;
public String getMessage(String code, Object... args) {
return messageSource.getMessage(code, args, LocaleContextHolder.getLocale());
}
}# quick reference
| Annotation/Interface | Mục đích |
|---|---|
| @Configuration | Java-based config class |
| @Bean | Khai báo bean từ method |
| @ComponentScan | Quét package đăng ký beans |
| @Profile | Bean theo environment |
| @PropertySource | Load external properties |
| @Value | Inject property value |
| @ConfigurationProperties | Type-safe config binding |
| @EventListener | Listen application events |
| @TransactionalEventListener | Listen after TX commit |
| @Scheduled | Cron/fixed-rate scheduling |
| @EnableScheduling | Bật scheduling |
| @Async | Non-blocking execution |
| @EnableAsync | Bật async support |
| @Scope | Bean lifecycle scope |
| @Lazy | Deferred initialization |
| @Import | Import config classes |
| @Conditional* | Conditional bean creation |
| ApplicationEventPublisher | Publish events |
| MessageSource | i18n messages |
| Environment | Access properties/profiles |
Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!
Happy coding 😎 👍🏻 🚀 🔥.