Spring bean là các thành phần cốt lõi tạo nên sức mạnh của Spring giúp nó đạt được Inversion of Control (IoC). Trong spring, chúng ta có thể định nghĩa bean thông qua @Bean và @Component annotation. Tuy nhiên chúng ta cần chỉ định rõ cho spring biết đâu là nơi nó nên tìm kiếm các bean do chúng ta định nghĩa, nếu không sẽ chẳng có bean nào được khởi tạo và đăng ký với IoC.
Trong bài viết này chúng ta sẽ cùng nhau tìm hiểu cách để chỉ cho spring những nơi mà nó cần quét để khởi tạo và quản lý các bean đã được định nghĩa từ trước. Cùng với một số tuỳ chỉnh nhỏ dùng trong các trường hợp đặc biệt.
@ComponentScan
Đối với các ứng dụng Spring Boot, @SpringBootApplication annotation được sử dụng ở main class là sự kết hợp của cả 3 annotation:
- @Configuration
- @EnableAutoConfiguration
- @ComponentScan
Như vậy, mặc định Spring Boot sẽ quét tất cả các class ở cùng package ở main class và tất cả các sub-package của nó để tìm và khởi tạo các bean tương ứng đã được định nghĩa.
Giả sử mình có MyBean class được đặt tại com.deft.componentscan package.
package com.deft.componentscan; public class MyBean { public String getName() { return "My Bean"; } }
MyConfig cũng được đặt cùng package com.deft.componentscan để để cấu hình bean sử dụng trong ứng dụng.
package com.deft.componentscan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyConfig { @Bean public MyBean myBean() { return new MyBean(); } }
Ngoài ra chúng ta còn có thể định nghĩa một bean thông qua @Service annotation được chú thích trên class.
package com.deft.componentscan; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MyService { @Autowired private MyBean myBean; public void getMyBeanName() { System.out.println(myBean.getName()); } }
Cuối cùng ComponentScanApplication là main class cũng được đặt cùng package. Do vậy Spring sẽ tìm được MyBean, MyConfig và MyService class để khởi tạo bean tương ứng.
package com.deft.componentscan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; @SpringBootApplication public class ComponentScanApplication { private static ApplicationContext applicationContext; public static void main(String[] args) { applicationContext = SpringApplication.run(ComponentScanApplication.class, args); System.out.println("Contains My Bean: " + applicationContext.containsBean("myBean")); System.out.println("Contains My Service Bean: " + applicationContext.containsBean("myService")); System.out.println("Contains My Config Bean: " + applicationContext.containsBean("myConfig")); } }
Output
Contains My Bean: true Contains My Service Bean: true Contains My Config Bean: true
ComponentScan với basePackages
Giả sử mình có cấu trúc cây thư mục như sau
Như vậy, chúng ta có một com.test package mới được thêm, một điểm chúng ta cần chú ý đó là com.test package không cùng package với ComponentScanApplication cũng không phải sub-package của nó. Trong trường hợp này, AnotherService sẽ không được quét và khởi tạo bean.
@Service public class AnotherService { }
Để AnotherService bean xuất hiện trong IoC, chúng ta cần chỉ định nó trong @ComponentScan cho Spring có thể nhận biết. Trong @ComponentScan chúng ta có thể sử dụng thuộc tính basePackages, nhận vào danh sách các package Spring sẽ quét.
Nếu chúng ta sử dụng @ComponentScan ở ComponentScanApplication , nghĩa là ta đang override lại @CompontScan mặc định mà @SpringBootApplication cung cấp, vậy nên chúng ta cần chỉ định hết các package có chứa bean đã được định nghĩa trước đó.
@SpringBootApplication @ComponentScan(basePackages = {"com.deft.test", "com.deft.componentscan"}) public class ComponentScanApplication { private static ApplicationContext applicationContext; public static void main(String[] args) { applicationContext = SpringApplication.run(ComponentScanApplication.class, args); System.out.println("Contains My Bean: " + applicationContext.containsBean("myBean")); System.out.println("Contains My Service Bean: " + applicationContext.containsBean("myService")); System.out.println("Contains My Config Bean: " + applicationContext.containsBean("myConfig")); System.out.println("Contains AnotherService Bean: " + applicationContext.containsBean("anotherService")); } }
Nếu không muốn override lại @CompoentScan của @SpringBootApplication thì chúng ta có thể sử dụng @ComponentScan ở một @Configuration khác, chẳng hạn như MyConfig class.
@Configuration @ComponentScan(basePackages = {"com.deft.test"}) public class MyConfig { @Bean public MyBean myBean() { return new MyBean(); } }
@ComponentScan filter
@ComponentScan cho phép lọc bỏ các bean với thuộc tính excludeFilters hỗ trợ nhiều kiểu fillter
@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = {MyService.class})) public class ComponentScanApplication { ... }
Như vậy, qua bài viết này chúng ta đã biết thêm một tính năng không kém phần quan trọng trong bộ khung của Spring. Thông thường mình thấy annotation thường ít được sử dụng vì Main Class lúc nào cũng được đặt ở root package nên không ai nhớ và nhiều khi các bạn quên luôn kiến thức này. Đây quả là một điều tai hại, khi dự án mở rộng, định nghĩa thêm nhiều package, module thì lúc đấy có lẽ bạn phải cần đến @ComponentScan nếu không để ý có khi mò cả ngày không biết tại sao không có bean trong container đâu nhé.
Sau cùng, các bạn có thể tham khảo mã nguồn mình công khai trên gitlab để tiện cho các bạn theo dõi và thực hành lại: component-scan
Nguồn tham khảo
https://www.baeldung.com/spring-component-scanning