Spring Boot/@Configuration, @Bean, @Component — 3 cách khai báo bean và sự khác biệt
~28 phútSpring là gì & nền tảng IoCMiễn phí

@Configuration, @Bean, @Component — 3 cách khai báo bean và sự khác biệt

3 annotation trông giống nhau nhưng khác bản chất: @Component cho class bạn sở hữu, @Bean cho bean tạo bằng method (third-party), @Configuration tạo class chứa @Bean. Bài này bóc cơ chế CGLIB enhancement, lite mode vs full mode, stereotype @Service/@Repository/@Controller, @Import, ComponentScan filter, @Conditional* family, và meta-annotation tự định nghĩa.

Spring có 3 cách chính khai báo bean: @Component (và bạn bè @Service/@Repository/@Controller), @Bean, và @Configuration. Newcomer thường confused vì các bài blog dùng lẫn lộn: "đặt @Service", "viết @Bean method", "annotate @Configuration class chứa @Bean". 3 cách hoạt động bên dưới khác nhau hoàn toàn — và sự khác nhau ảnh hưởng tới performance, testability, và behavior subtle.

Bài này phân tích 3 annotation ở mức bản chất, giải thích vì sao @Configuration class bị Spring enhance bằng CGLIB, sự khác biệt giữa "lite mode" và "full mode", @Import cho composition config, filter mode của @ComponentScan, family @ConditionalOn*, và cách tự define meta-annotation cho domain.

1. Bảng tổng kết — 3 cách

AnnotationĐặt ở đâuBean class là gìKhi nào dùng
@ComponentTrên classChính class đóClass bạn sở hữu (service, repo, util)
@BeanTrên method (trong class config)Return type của methodBean third-party hoặc cần config phức tạp
@ConfigurationTrên class chứa @Bean methodClass này không phải bean nghiệp vụ — chỉ là factoryKhi bạn dùng nhiều @Bean cùng nhóm

Phân biệt nhanh:

  • "Tôi có class OrderService của tôi viết, muốn nó thành bean" → @Service (sub-class của @Component).
  • "Tôi cần bean DataSource từ Hikari (third-party)" → @Bean method trong class @Configuration.
  • "Tôi muốn nhóm các @Bean method cùng concern" → @Configuration class.

2. @Component và stereotypes

@Component là annotation gốc — đánh dấu class là Spring-managed bean để component scan tìm thấy.

@Component
public class EmailValidator {
    public boolean isValid(String email) { /* ... */ }
}

Spring có 3 alias chuyên biệt theo layer, gọi là stereotype annotations:

AnnotationLayerTính năng đặc biệt
@ServiceService / business logicMarker thuần — không thêm tính năng. Để rõ intent.
@RepositoryData accessMarker + exception translation: tự động convert exception JDBC/JPA cụ thể (vd SQLException) thành DataAccessException hierarchy của Spring.
@ControllerWeb MVCMarker + Spring MVC scan @RequestMapping method. Có biến thể @RestController = @Controller + @ResponseBody.
flowchart TB
    Component["@Component<br/>(generic stereotype)"]
    Service["@Service"]
    Repository["@Repository"]
    Controller["@Controller"]
    RestController["@RestController"]

    Component --> Service
    Component --> Repository
    Component --> Controller
    Controller --> RestController

    style Component fill:#fef3c7

Tất cả đều @Component ở mức code (annotation chain). Component scan tìm cả 4 đều gom vào bean.

Quy tắc thực dụng: chọn stereotype rõ nhất theo layer. Đừng dùng @Component trần khi class có vai trò rõ ràng:

@Service
public class OrderService { ... }       // dung — business layer

@Repository
public class OrderRepository { ... }    // dung — data layer

@Component                                // dung — utility class generic, khong fit layer nao
public class JsonSchemaValidator { ... }

Lợi ích phụ: developer khác đọc code biết ngay class này thuộc layer nào, không phải đọc body.

2.1 Bean naming conventions

Khi @Component không khai báo name, Spring tự sinh:

ClassBean name
OrderServiceorderService (camelCase, chữ đầu lowercase)
XMLParserXMLParser (giữ nguyên — 2+ chữ hoa đầu)
URLURL
aBeanaBean

Spring dùng Introspector.decapitalize để sinh name — chuyển chữ đầu lowercase trừ khi 2 chữ đầu đều uppercase.

Khai báo name explicit:

@Service("orderProcessor")           // override default
public class OrderService { ... }

@Bean("dataSource")                  // override default = methodName
public DataSource buildDataSource() { ... }

Bean name unique trong cùng context. Trùng → từ Spring Boot 2.1 throw BeanDefinitionOverrideException (override mặc định tắt — spring.main.allow-bean-definition-overriding=false).

3. @Bean method — khi class không thuộc về bạn

@Component yêu cầu bạn annotate class. Nhưng nhiều khi bean cần là class third-party — bạn không sửa source được:

@Configuration
public class DataSourceConfig {

    @Bean
    public DataSource dataSource(@Value("${db.url}") String url,
                                  @Value("${db.user}") String user,
                                  @Value("${db.pass}") String pass) {
        var ds = new HikariDataSource();
        ds.setJdbcUrl(url);
        ds.setUsername(user);
        ds.setPassword(pass);
        ds.setMaximumPoolSize(20);
        return ds;
    }

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper()
            .registerModule(new JavaTimeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }
}

Phân tích:

  • Method được annotate @Bean. Method return object — đó là bean.
  • Bean name mặc định = method name (dataSource, objectMapper).
  • Bean class = return type khai báo (DataSource, ObjectMapper).
  • Method parameter cũng được Spring inject — @Value resolve từ properties.

Use case của @Bean:

CaseLý do
Class third-party (HikariDataSource, RestTemplate, ObjectMapper)Không thể annotate @Component
Cần config phức tạp tại creation (set property, register module)@Bean method có thể chứa logic
Cần tạo nhiều bean cùng class với config khác1 method = 1 bean, có thể có 5 method tạo 5 DataSource (read replica, write, archive...)

3.1 Lifecycle qua @Bean

@Bean cho phép cấu hình lifecycle method:

@Bean(initMethod = "start", destroyMethod = "stop")
public CacheCluster cacheCluster() { return new CacheCluster(); }

@Bean(destroyMethod = "")     // tat destroy method tu (inferred)
public DataSource dataSource() { return new HikariDataSource(); }

Spring tự deduce destroyMethod cho:

  • Method close() (Closeable/AutoCloseable).
  • Method shutdown().

Nếu bạn không muốn Spring gọi auto, set destroyMethod = "" (empty string).

@Bean cũng support @Lazy, @Scope, @DependsOn, @Primary, @Qualifier cùng vị trí:

@Bean
@Lazy
@Scope("prototype")
@Qualifier("readReplica")
public DataSource readReplica() { ... }

4. @Configuration — không chỉ là container của @Bean

@Configuration@Component đặc biệt với 1 super-power: Spring enhance class này bằng CGLIB để intercept @Bean method call. Đây là điểm thiết yếu mà 80% dev không biết.

4.1 Vấn đề @Bean trong "lite mode"

public class LiteConfig {                    // KHONG @Configuration

    @Bean
    public Foo foo() { return new Foo(); }

    @Bean
    public Bar bar() {
        return new Bar(foo());                // goi method foo() truc tiep
    }
}

Đoạn này không có @Configuration. Spring vẫn process @Bean method (lite mode), nhưng không enhance class. Kết quả:

  • foo() được Spring gọi 1 lần để tạo bean foo.
  • Khi bar() được Spring gọi tạo bean bar, code new Bar(foo()) chạy — nhưng foo() ở đây là gọi method Java thuần, không qua Spring. Kết quả: tạo 1 instance Foo mới, khác với bean foo đã đăng ký.
  • Bạn có 2 instance Foo trong app: 1 do Spring quản, 1 nằm bên trong Bar. Bug subtle.

4.2 @Configuration với CGLIB enhancement

@Configuration
public class FullConfig {

    @Bean
    public Foo foo() { return new Foo(); }

    @Bean
    public Bar bar() {
        return new Bar(foo());                // call duoc intercept
    }
}

@Configuration, Spring tạo CGLIB subclass của FullConfig với các @Bean method override. Override logic: kiểm tra container có bean đó chưa — có thì return bean cache, không thì call method gốc.

Kết quả: foo() trong body bar() trả về cùng instance với bean foo đã đăng ký. Chỉ 1 instance Foo trong app.

sequenceDiagram
    participant Ctx as Container
    participant Cfg as FullConfig$$EnhancerByCGLIB
    participant Real as FullConfig (real)

    Ctx->>Cfg: bar()
    Cfg->>Cfg: foo() invoked
    alt foo bean exists in cache
        Cfg-->>Cfg: return cached foo
    else first time
        Cfg->>Real: foo() (super call)
        Real-->>Cfg: new Foo()
        Cfg->>Ctx: register as bean foo
    end
    Cfg->>Real: bar() body with foo
    Real-->>Ctx: return Bar

Đây là lý do reference docs Spring luôn khuyến nghị annotate class chứa @Bean bằng @Configuration:

📚 Quote chính chủ

"@Bean methods may also be declared within classes that are not annotated with @Configuration. For example, bean methods may be declared in a @Component class or even in a plain old class. In such cases, @Bean methods are processed in a so-called 'lite' mode."

"In contrast to this, @Bean methods declared in @Configuration classes are recommended for general usage scenarios. They are processed in the 'full' mode where they get the full lifecycle management treatment, including CGLIB-based subclassing."

Nguồn: Spring Framework Reference — Basic Concepts: @Bean and @Configuration

4.3 Hệ quả của CGLIB enhancement

  • Class @Configuration không được final — CGLIB không subclass được class final → throw tại startup.
  • Method @Bean không được private/final — không override được.
  • Hơi tốn memory — 1 class CGLIB subclass cho mỗi config class. Negligible với app thường, đáng cân nhắc cho app native image.
  • Spring 5.2+ thêm proxyBeanMethods = false: tắt CGLIB enhancement nếu bạn chắc chắn không gọi method @Bean lẫn nhau:
@Configuration(proxyBeanMethods = false)
public class FastConfig {

    @Bean
    public Foo foo() { return new Foo(); }

    @Bean
    public Bar bar(Foo foo) {                  // inject qua parameter, KHONG goi foo() body
        return new Bar(foo);
    }
}

Ưu điểm: faster startup (no CGLIB), better cho native image. Nhược: bạn phải kỷ luật — mọi dependency qua method parameter, không qua method call.

Spring Boot 2.2+ tự annotate proxyBeanMethods = false cho hầu hết autoconfig — cho native-friendly.

5. @ComponentScan — chi tiết filter modes

Default @ComponentScan quét mọi class có @Component (hoặc stereotype). Customize qua filter:

@Configuration
@ComponentScan(
    basePackages = {"com.olhub.domain", "com.olhub.api"},
    includeFilters = {
        @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyService.class),
        @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = Repository.class)
    },
    excludeFilters = {
        @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Test.*"),
        @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Deprecated.class)
    },
    useDefaultFilters = false                      // chi dung includeFilters, bo qua @Component default
)
public class AppConfig { }

5 filter type:

FilterTypeMatchUse case
ANNOTATION (default)Class có annotation@Component, custom stereotype
ASSIGNABLE_TYPEClass hoặc subclassMatch tất cả implement 1 interface
ASPECTJAspectJ pointcut expressionPattern phức tạp com.olhub..*Service
REGEXRegex tên classPattern theo tên file
CUSTOMImplement TypeFilterLogic custom

Default Spring Boot dùng @SpringBootApplication với @ComponentScan không filter — đủ 99% case.

5.1 Khi nào dùng filter

Hiếm — default đủ. Nhưng có vài case:

  • Test bypass production bean: excludeFilters để bỏ @Configuration của production khi test slice.
  • Module isolation: lib module muốn scan riêng package — không pull bean từ module khác.
  • Plugin architecture: chỉ scan plugin có annotation @MyPlugin, không bean thường.

6. @Import — composition của config

@Configuration có thể @Import config khác:

@Configuration
@Import({DataSourceConfig.class, SecurityConfig.class, MessagingConfig.class})
public class AppConfig {
    // ...
}

Tương đương khai báo 4 config class với @SpringBootApplication. Khác @ComponentScan:

Aspect@ComponentScan@Import
Phạm viTự động — quét packageExplicit — liệt kê class
Tốc độChậm hơn (scan classpath)Nhanh hơn (no scan)
Use caseApplication codeLibrary config, modular setup

Spring Boot autoconfig không dùng @ComponentScan — nó dùng @Import qua file META-INF/spring/...AutoConfiguration.imports. Lý do: autoconfig phải explicit, không phụ thuộc package layout.

@Import cũng accept ImportSelector (logic chọn class config động) và ImportBeanDefinitionRegistrar (register bean programmatically). Đây là cơ chế của @EnableJpaRepositories, @EnableScheduling, ... — Module 02 đào sâu.

7. Conditional bean — feature mạnh của Spring Boot

Spring Boot mở rộng @Bean với @Conditional* — register bean tuỳ điều kiện:

@Configuration
public class CacheConfig {

    @Bean
    @ConditionalOnClass(name = "org.springframework.data.redis.core.RedisTemplate")
    public CacheManager redisCacheManager() {
        return new RedisCacheManager(/* ... */);
    }

    @Bean
    @ConditionalOnMissingBean(CacheManager.class)
    public CacheManager fallbackCacheManager() {
        return new ConcurrentMapCacheManager();
    }

    @Bean
    @ConditionalOnProperty(name = "feature.audit.enabled", havingValue = "true")
    public AuditLogger auditLogger() {
        return new DefaultAuditLogger();
    }
}

Family @ConditionalOn* đầy đủ:

AnnotationConditionUse case
@ConditionalOnClassClass có trong classpathAuto-config bật khi user thêm starter
@ConditionalOnMissingClassClass không cóFallback khi không có driver
@ConditionalOnBeanBean này đã registerBean phụ thuộc bean khác
@ConditionalOnMissingBeanBean chưa registerFallback default — user override được
@ConditionalOnPropertyProperty có giá trịFeature flag, env-specific bean
@ConditionalOnExpressionSpEL expression trueLogic phức tạp
@ConditionalOnWebApplicationApp là web (servlet/reactive)Bean chỉ cho web
@ConditionalOnNotWebApplicationApp không webBean chỉ cho CLI
@ConditionalOnResourceResource tồn tại trên classpathLicense file, schema file
@ConditionalOnJavaJava version matchCompat với multiple Java version
@ConditionalOnCloudPlatformDetect cloud platformCloud Foundry, K8s specific
@ProfileActive profile matchDev/staging/prod toggle

Đây là cốt lõi của Spring Boot auto-configuration — Module 02 sẽ đào sâu. Bài này chỉ cần biết: @Bean + condition là cách Spring Boot adapt với classpath/config.

7.1 Custom @Conditional

Tự define condition:

public class OnLinuxCondition implements Condition {
    public boolean matches(ConditionContext ctx, AnnotatedTypeMetadata md) {
        return System.getProperty("os.name").toLowerCase().contains("linux");
    }
}

@Configuration
public class FilesystemConfig {
    @Bean
    @Conditional(OnLinuxCondition.class)
    public FileSystemMonitor inotifyMonitor() { ... }
}

Hữu ích cho domain-specific condition (vd "chỉ load bean nếu license valid").

8. Meta-annotation — tự define stereotype

Bạn có thể tạo annotation custom inherit từ @Component:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
@Transactional
public @interface DomainService {
    @AliasFor(annotation = Component.class) String value() default "";
}

// Su dung:
@DomainService
public class OrderService { ... }      // = @Component + @Transactional + naming

Spring nhận diện meta-annotation qua chain. OrderService được scan thành bean (do @Component chain) và auto-tx (do @Transactional chain).

Use case:

  • Domain-specific stereotype: @DomainService, @ApplicationService, @DomainEvent cho DDD codebase.
  • Cross-cutting concern: @Loggable = @Component + AOP advice cho logging.
  • Short-hand: tránh lặp @Service @Validated @Transactional 200 lần.

@AliasFor map attribute từ meta-annotation sang annotation lồng — giữ contract API.

9. Khi nào dùng cái nào — quyết định

flowchart TD
    Start[Toi can declare bean]
    Q1{Class la cua toi?}
    Q2{Co fit layer service/repo/controller?}
    UseStereotype["@Service / @Repository / @Controller"]
    UseComponent["@Component"]
    Q3{Co nhieu bean cung group?}
    UseConfiguration["@Configuration class<br/>+ @Bean methods"]
    UseBeanInExisting["@Bean method<br/>trong @Configuration co san"]

    Start --> Q1
    Q1 -->|Co| Q2
    Q2 -->|Co| UseStereotype
    Q2 -->|Khong| UseComponent
    Q1 -->|Khong third-party| Q3
    Q3 -->|Co| UseConfiguration
    Q3 -->|It, 1-2 bean| UseBeanInExisting

Quy tắc rút gọn:

  • Class của bạn? → stereotype phù hợp layer. Mặc định @Service cho business logic.
  • Class third-party?@Bean method.
  • Nhiều bean cùng group (DataSource + Flyway + JPA config)? → tách @Configuration class riêng, ví dụ DatabaseConfig.
  • 1-2 bean? → bỏ vào @Configuration có sẵn (vd ngay trong @SpringBootApplication class — nó đã là @Configuration).

10. Pitfall tổng hợp

Nhầm 1: Quên @Configuration cho class chứa @Bean gọi nhau.

public class MyConfig {           // KHONG @Configuration
    @Bean Foo foo() { return new Foo(); }
    @Bean Bar bar() { return new Bar(foo()); }   // 2 instance Foo trong app
}

✅ Thêm @Configuration, hoặc đổi sang @Bean Bar bar(Foo foo) (param injection).

Nhầm 2: Class @Configurationfinal.

@Configuration
public final class C { ... }   // CGLIB khong subclass duoc → startup fail

✅ Bỏ final, hoặc dùng proxyBeanMethods = false.

Nhầm 3: @Bean method private/final.

@Configuration class C {
    @Bean private Foo foo() { ... }   // Spring khong override duoc
}

✅ Method public hoặc protected.

Nhầm 4: Annotate @Service cho class third-party.

@Service                          // BAN khong sua source HikariDataSource duoc
public class HikariDataSource { ... }

✅ Class third-party → @Bean method trong config.

Nhầm 5: Đặt @Component cho class chỉ chứa method static utility.

@Component
public class StringUtils {
    public static String camelCase(String s) { ... }
}

✅ Class util tĩnh không cần Spring quản. Bỏ @Component. Hoặc convert thành bean với instance method nếu cần inject dep.

Nhầm 6: Inject @Configuration class qua @Autowired để gọi @Bean method.

@Service class S {
    @Autowired MyConfig cfg;
    public void use() { Foo f = cfg.foo(); }   // anti-pattern
}

✅ Inject bean Foo thẳng. Không dùng config class như factory runtime.

Nhầm 7: @ComponentScan overlap nhiều basePackages.

@ComponentScan(basePackages = {"com.olhub", "com.olhub.api"})   // bao trum

✅ Chỉ scan rộng nhất — com.olhub đã bao gồm com.olhub.api. Khai báo lồng nhau dư thừa và scan trùng.

Nhầm 8: Quên useDefaultFilters = false khi chỉ muốn includeFilters custom.

@ComponentScan(includeFilters = @Filter(MyMarker.class))
// VAN scan tat ca @Component default + class co MyMarker

✅ Set useDefaultFilters = false để chỉ scan theo includeFilters.

11. 📚 Deep Dive Spring Reference

📚 Tài liệu chính chủ

Reference docs:

Source nên đọc:

  • org.springframework.context.annotation.ConfigurationClassPostProcessor — BFPP xử lý @Configuration, parse @Bean method, áp dụng CGLIB enhancement.
  • org.springframework.context.annotation.ConfigurationClassEnhancer — class thực sự sinh CGLIB subclass.
  • org.springframework.boot.autoconfigure.condition.OnClassCondition — implementation @ConditionalOnClass.

Ghi chú: đọc Javadoc của @Configuration (annotation source) — phần Javadoc rất chi tiết, giải thích full mode vs lite mode trực tiếp từ team Spring.

12. Tóm tắt

  • @Component + stereotypes (@Service, @Repository, @Controller): annotate class bạn sở hữu để component scan gom thành bean.
  • @Bean method: tạo bean từ method, dùng cho class third-party hoặc cần config phức tạp. Support initMethod/destroyMethod/@Lazy/@Scope.
  • @Configuration class: container của @Bean method. Spring enhance bằng CGLIB để @Bean method gọi nhau trả cùng instance ("full mode").
  • Không có @Configuration → "lite mode" → method @Bean gọi nhau tạo instance mới mỗi lần (bug subtle).
  • @Configuration class không được final; method @Bean không được private/final.
  • proxyBeanMethods = false tắt CGLIB cho startup nhanh hơn (Boot autoconfig dùng), với điều kiện không gọi method @Bean lẫn nhau trong body.
  • @Repository đặc biệt: thêm exception translation từ JDBC/JPA về DataAccessException.
  • Bean name mặc định = class name decapitalize (OrderServiceorderService). Trùng → throw BeanDefinitionOverrideException từ Spring Boot 2.1+.
  • @ComponentScan có 5 filter type — default đủ 99% case.
  • @Import cho composition explicit — Spring Boot autoconfig dùng @Import qua AutoConfiguration.imports, không scan.
  • @Conditional* family (12+ annotation) cho phép register bean tuỳ classpath/property/profile — cốt lõi của Spring Boot auto-configuration.
  • Meta-annotation + @AliasFor cho phép tự define stereotype domain-specific (@DomainService).
  • Class util tĩnh không cần annotate Spring.

13. Tự kiểm tra

Tự kiểm tra
Q1
Đoạn sau in 1 hay 2? Vì sao?
@Configuration
class Cfg {
  @Bean Counter counter() { return new Counter(); }
  @Bean Worker worker() { return new Worker(counter()); }
}

@Service
class Auditor {
  @Autowired Counter counter;
  @Autowired Worker worker;

  public int distinctCounters() {
      return Set.of(counter, worker.getCounter()).size();
  }
}

In 1.

Vì class Cfg@Configuration, Spring enhance bằng CGLIB. Khi worker() chạy, call counter() bị intercept — proxy kiểm tra "bean counter đã có chưa?" → có → return cached instance. Vậy counter field và worker.getCounter() trỏ cùng object.

Nếu bỏ @Configuration đi (chỉ để class trần): "lite mode". worker() body gọi counter() như method Java thuần → tạo Counter mới. Lúc đó in 2 — distinct counters.

Đây là lý do Spring docs khuyến nghị luôn annotate @Configuration cho class chứa @Bean gọi nhau.

Q2
@Service, @Repository, @Controller — 3 stereotype. Trên runtime container, chúng hoạt động như nhau (đều là @Component). Vậy có lý do thực sự để phân biệt không, hay chỉ là "code style"?

Có lý do thực sự — không chỉ code style.

  • @Repository có hành vi đặc biệt: Spring tự register PersistenceExceptionTranslationPostProcessor wrap bean này. Khi method ném SQLException, HibernateException, ... → tự động chuyển thành DataAccessException (subclass) của Spring. Lợi ích: business code chỉ catch 1 hierarchy DataAccessException, không phụ thuộc DB driver cụ thể.
  • @Controller được Spring MVC scan riêng: chỉ class có @Controller (hoặc @RestController) mới được DispatcherServlet xét @RequestMapping. Class @Service với @RequestMapping trên method sẽ không tạo route — Spring MVC bỏ qua.
  • @Service là marker thuần — không thêm tính năng. Nhưng vẫn nên dùng vì: AOP pointcut có thể match @within(Service), IDE tooling, code reader hiểu intent.

Quy tắc: dùng đúng stereotype cho đúng layer — vừa được tính năng đặc biệt, vừa rõ intent.

Q3
Khi nào dùng @Configuration(proxyBeanMethods = false)? Lợi ích cụ thể là gì? Khi nào KHÔNG nên dùng?

Khi nào dùng:

  • Bạn chắc chắn không gọi @Bean method lẫn nhau trong body — chỉ inject qua method parameter.
  • App có nhiều config class, optimize startup time.
  • Build native image (GraalVM) — CGLIB phát sinh proxy class runtime, không native-friendly.

Lợi ích:

  • Startup nhanh hơn (không sinh CGLIB subclass).
  • Less memory (no extra proxy class).
  • Native image compatibility tốt hơn.
  • Spring Boot 2.2+ tự dùng cho mọi @SpringBootConfiguration autoconfig — startup Boot 3 nhanh hơn Boot 1 đáng kể nhờ điều này.

Khi KHÔNG nên dùng:

  • Code legacy có @Bean method gọi nhau — bật flag sẽ gây bug subtle (instance trùng).
  • Style "tự document" thông qua method gọi method (vd bar() body có foo() để diễn đạt dependency).

Quy tắc: dự án mới với code disciplined → bật. Code cũ hoặc dev team không đồng đều → giữ default.

Q4
Bạn cần 2 instance DataSource trong app: 1 cho write (primary DB), 1 cho read (replica). Cả 2 là HikariDataSource. Code declaration thế nào? Resolve injection ambiguity ra sao?
@Configuration
public class DataSourceConfig {

  @Bean
  @Primary
  @ConfigurationProperties("app.datasource.write")
  public DataSource writeDataSource() {
      return DataSourceBuilder.create().type(HikariDataSource.class).build();
  }

  @Bean
  @Qualifier("read")
  @ConfigurationProperties("app.datasource.read")
  public DataSource readDataSource() {
      return DataSourceBuilder.create().type(HikariDataSource.class).build();
  }
}

Resolve injection:

@Service
public class OrderService {
  private final DataSource write;
  private final DataSource read;

  public OrderService(DataSource write,                          // @Primary - default
                      @Qualifier("read") DataSource read) {      // explicit qualifier
      this.write = write;
      this.read = read;
  }
}

Cách khác: @Qualifier trên cả 2 (không dùng @Primary) — clean, phải khai báo qualifier mọi nơi inject. Hoặc đặt tên bean rõ ràng: @Bean("writeDataSource") rồi inject qua @Qualifier("writeDataSource").

Properties:

app.datasource.write.url=jdbc:postgresql://primary/app
app.datasource.write.username=app
app.datasource.read.url=jdbc:postgresql://replica/app
app.datasource.read.username=app_ro

@ConfigurationProperties bind từng prefix vào DataSource tương ứng — không cần manual setter.

Q5
Class StringUtils với toàn method static: camelCase, truncate, slugify. Có nên annotate @Component không? Vì sao?

Không.

Lý do:

  • Method static không cần object instance để gọi — StringUtils.camelCase("foo") chạy mọi lúc, không cần Spring.
  • Class util không có dependency để inject → không cần Spring quản.
  • Annotate @Component thừa: Spring tạo 1 instance không ai dùng (mọi caller gọi static method qua class, không qua bean). Tốn slot bean cache.
  • Test class util không cần @SpringBootTest — gọi static method từ mọi test trực tiếp.

Khi nào util CẦN thành bean?

  • Util cần inject dependency (vd JsonValidator cần ObjectMapper) → convert thành class với instance method và @Component.
  • Util cần config từ properties (@Value) → tương tự.
  • Cần mock trong test (vd ClockProvider) → bean với interface, mock được. Static method khó mock (cần PowerMock/Mockito-static — anti-pattern).

Quy tắc: stateless + no dep → static utility class, no Spring. Có dep hoặc cần mock → bean instance.

Q6
Codebase team bạn có pattern: mọi service domain đều cần @Service @Validated @Transactional. Lặp 200 lần. Có cách nào DRY không?

Có — meta-annotation.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Service
@Validated
@Transactional
public @interface DomainService {
  @AliasFor(annotation = Service.class, attribute = "value")
  String value() default "";
}

Sử dụng:

@DomainService
public class OrderService { ... }    // = @Service @Validated @Transactional

Cơ chế: Spring resolve meta-annotation theo chain. @DomainService annotated với @Service → class được scan như @Component. @Validated → kích hoạt validation. @Transactional → AOP wrap proxy cho tx.

@AliasFor map attribute từ meta-annotation sang annotation lồng — cho phép @DomainService("custom-name") set bean name.

Lợi ích:

  • DRY — 1 annotation thay 3.
  • Codebase consistency — không ai forget @Transactional trên domain service.
  • Easier refactoring — đổi default behavior 1 chỗ (vd thêm @Loggable) propagate đến mọi service.
  • Domain documentation — annotation tên domain (DomainService) rõ ý nghĩa hơn lặp 3 generic annotation.

Cảnh báo: đừng over-engineer. Chỉ define meta-annotation khi pattern lặp ≥ 5 lần. Codebase nhỏ giữ explicit dễ đọc hơn.

Q7
Đoạn sau làm gì? Bean nào sẽ được register? Khi nào?
@Configuration
public class CacheConfig {

  @Bean
  @ConditionalOnClass(name = "org.springframework.data.redis.core.RedisTemplate")
  @ConditionalOnProperty(name = "cache.type", havingValue = "redis", matchIfMissing = false)
  public CacheManager redisCacheManager(RedisConnectionFactory factory) { ... }

  @Bean
  @ConditionalOnMissingBean(CacheManager.class)
  public CacheManager fallback() {
      return new ConcurrentMapCacheManager();
  }
}

Logic register theo điều kiện:

  • redisCacheManager register nếu cả 2 điều kiện đúng:
    • Class RedisTemplate có trong classpath (user thêm spring-boot-starter-data-redis).
    • Property cache.type=redis set explicit (matchIfMissing = false → property phải có).
  • fallback register nếu không bean CacheManager nào khác đã register — tức là Redis condition fail.

Kết quả 4 scenario:

ScenarioBean register
Redis classpath + cache.type=redisredisCacheManager
Redis classpath + không propertyfallback (ConcurrentMapCacheManager)
Không Redis classpath + property bất kỳfallback
Không Redis + không propertyfallback

Pattern này phổ biến trong autoconfig: primary bean conditional, fallback default. User opt-in bằng cách thêm starter + set property → primary kích hoạt, fallback skip. Default behaviour vẫn work mà không cần config.

Quan trọng — thứ tự evaluation: @ConditionalOnMissingBean phụ thuộc bean order. Spring Boot dùng @AutoConfigureAfter/@AutoConfigureBefore để đảm bảo Redis evaluate trước fallback. Trong cùng @Configuration class, thứ tự method matter — Spring xử lý method theo source order.

Bài tiếp theo: Mini-challenge — tự build mini IoC container 80 dòng

Bài này có giúp bạn hiểu bản chất không?

Bình luận (0)

Đang tải...