Spring Bean
-
Spring Bean là gì?
Như đã giới thiệu ở bài trước, Spring Bean là một đối tượng được quản lý bởi Spring IoC Container. Nói cách khác, thay vì tạo và quản lý đối tượng thủ công bằng từ khóa new, bạn để Spring tạo và quản lý vòng đời của đối tượng đó.
Một lớp Java bất kỳ có thể trở thành một Spring Bean nếu bạn khai báo nó trong cấu hình Spring hoặc chú thích nó bằng các annotation phù hợp như @Component, @Service, @Repository hoặc @Controller.
-
Bean hệ thống - Infrastructure Beans
Bean hệ thống là những Bean do chính Spring tạo ra phục vụ cho việc vận hành nội bộ của framework, ví dụ như xử lý AOP, transaction, annotation, web MVC, v.v. Bean hệ thống có các đặc điểm sau:
- Tự động được tạo ra bởi Spring khi bạn bật các chức năng như @EnableTransactionManagement, @EnableWebMvc, v.v.
- Không cần (và không nên) can thiệp hoặc sửa đổi trực tiếp.
- Có vai trò hỗ trợ cấu hình, xử lý lifecycle, proxying, injection, v.v.
Bean hệ thống thường có tên trong Container bắt đầu bằng internal hoặc org.springframework .*. Bảng dưới đây liệt kê một vài Bean thông dụng và vai trò của chúng.
Tên Bean Vai trò RequestMappingHandlerMapping Ánh xạ URL tới phương thức của Controller có @RequestMapping RequestMappingHandlerAdapter Kết nối phương thức của Controller với Request tương ứng ExceptionHandlerExceptionResolver Xử lý ngoại lệ thông qua các phương thức với @ExceptionHandler InternalResourceViewResolver Kết hợp với tên view tạo đường dẫn đầy đủ tới tập tin *.jsp, *.html, v.v. (/WEB-INF/views/*.jsp) AnnotationAwareAspectJAutoProxyCreator Tạo proxy tự động cho các Bean có @Aspect, hỗ trợ AOP TransactionInterceptor Quản lý giao dịch tự động PersistenceAnnotationBeanPostProcessor Xử lý các annotation như @PersistenceContext BeanNameViewResolver Tìm view dựa trên tên Bean CommonAnnotationBeanPostProcessor Xử lý @PostConstruct, @PreDestroy, @Resource, v.v. AutowiredAnnotationBeanPostProcessor Xử lý injection thông qua @Autowired và @Value ConfigurationClassPostProcessor Xử lý các lớp @Configuration, tạo và đăng ký các @Bean PropertySourcesPlaceholderConfigurer Đọc và inject giá trị từ application.properties hoặc yaml WebSocketHandlerMapping Ánh xạ WebSocket endpoint tới các WebSocketHandler được đăng ký trong WebSocketConfigurer DelegatingFilterProxy Cầu nối giữa filter của Container và SecurityFilterChain HandlerExceptionResolverComposite Tổng hợp các resolver để xử lý exception trong Controller Những Bean có tên chứa Handler, Adapter, Resolver, Processor, Proxy, Creator, Configurer thường là Bean hệ thống. Để xem các Bean hệ thống đang tồn tại trong Container, bạn có thể dùng phương thức getBeanDefinitionNames() của ApplicationContext interface như sau:
for (String beanName : context.getBeanDefinitionNames()) { System.out.println(beanName); }
-
Bean do người dùng định nghĩa - Application Beans/User-defined Beans
Bean do người dùng định nghĩa (User-defined Bean) là một đối tượng Java (thường là POJO - Plain Old Java Object) do lập trình viên tạo ra và đăng ký với Spring Container và do đó được Spring Container khởi tạo, cấu hình và quản lý. Bean này không phải là thành phần có sẵn của Spring Framework mà được định nghĩa bằng các annotation (@Component, @Service, @Repository, @Bean, v.v.) hoặc tập tin cấu hình XML.
Dưới đây chúng ta sẽ tìm hiểu các phương pháp phổ biến để định nghĩa một Spring Bean.
-
Sử dụng annotation (@Component, @Service, @Repository, @Controller)
Trong Spring, bạn có thể chỉ định một lớp Java bằng các annotation đặc biệt để Spring tự động nhận diện và đăng ký lớp đó như một Bean thông qua cơ chế component scanning. Các annotation phổ biến được mô tả trong bảng dưới đây:
Annotation Vai trò @Component Đánh dấu Bean là một thành phần độc lập trong ứng dụng và có thể được sử dụng ở bất kỳ đâu @Repository Đánh dấu Bean thuộc tầng thao tác và truy vấn cơ sở dữ liệu (DAO) @Service Đánh dấu Bean thuộc tầng service (hoặc business logic) @Controller Đánh dấu Bean thuộc tầng web (controller) -
@Component
Annotation @Component đánh dấu một lớp là Spring Bean, thường được áp dụng cho các lớp helper/utility (như Email, File, Token, Logger, Encryptor) với mục đích tái sử dụng trong toàn bộ ứng dụng và dễ dàng tiêm vào các đối tượng khác thông qua cơ chế dependency injection của Spring.
Minh họa cách đánh dấu lớp AppLogger là một Bean bằng @Component
package vn.hoctotlamhay.learnspring.logger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Component public class AppLogger { private static final Logger logger = LoggerFactory.getLogger(AppLogger.class); public void info(String message) { logger.info(message); } public void error(String message, Throwable exception) { logger.error(message, exception); } public void debug(String message) { logger.debug(message); } }
-
@Repository
Annotation @Repository dùng để đánh dấu một lớp thuộc tầng truy cập dữ liệu (DAO) là một Spring Bean. Khi đó, Spring sẽ quản lý lớp này bao gồm việc tự động chuyển các ngoại lệ cấp thấp (như JDBC, JPA) thành DataAccessException và cho phép dễ dàng tiêm vào các bean khác như @Service.
Minh họa cách đánh dấu interface ProductRepository là một Bean bằng @Repository.
package vn.hoctotlamhay.learnspring.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; import vn.hoctotlamhay.learnspring.entity.Product; import java.util.List; @Repository public interface ProductRepository extends JpaRepository<Product, Long> { @Query("SELECT p FROM Product p") List<Product> findAllProducts(); @Query("SELECT p FROM Product p WHERE LOWER(p.name) LIKE LOWER(CONCAT('%', ?1, '%'))") List<Product> searchByName(String keyword); // Các truy vấn khác }
Chú ý
- Nếu lớp hoặc interface không làm nhiệm vụ truy xuất dữ liệu thì không nên dùng @Repository.
- Không dùng @Repository cho lớp xử lý logic nghiệp vụ.
- Không dùng @Repository cho lớp giao tiếp người dùng.
-
@Service
Annotation @Service đánh dấu một lớp thuộc tầng xử lý logic nghiệp vụ (business logic) là một Spring Bean. Các lớp này thường chịu trách nhiệm xử lý các yêu cầu từ các tầng khác như tầng repository hoặc controller.
Khi lớp được đánh dấu @Service, Spring sẽ tự động phát hiện và tạo đối tượng cho lớp đó, giúp dễ dàng tiêm (inject) vào các nơi khác trong ứng dụng.
Minh họa cách đánh dấu lớp ProductService là một Bean bằng @Service.
package vn.hoctotlamhay.learnspring.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import vn.hoctotlamhay.learnspring.entity.Product; import vn.hoctotlamhay.learnspring.repository.ProductRepository; import java.util.List; @Service public class ProductService { private final ProductRepository productRepository; @Autowired public ProductService(ProductRepository productRepository) { this.productRepository = productRepository; } public List<Product> getAllProducts() { return productRepository.findAllProducts(); } public List<Product> searchByName(String keyword) { return productRepository.searchByName(keyword); } // Các logic khác }
Chú ý
Trong một số trường hợp chúng ta không cần thiết chỉ định một lớp thuộc tầng xử lý logic nghiệp vụ là một Bean, ví dụ:
- Lớp chỉ chứa các phương thức tĩnh.
- Lớp là một đối tượng đơn giản (DTO) hoặc không có logic nghiệp vụ.
- Khi lớp không cần được quản lý bởi Spring IoC hoặc không cần tính năng tự động tiêm các phụ thuộc (dependency injection).
- Khi bạn muốn kiểm soát vòng đời của đối tượng.
-
@Controller
Annotation @Controller đánh dấu một lớp là một Spring MVC controller, có nhiệm vụ xử lý các yêu cầu từ máy khách, điều hướng đến view hoặc trả về dữ liệu (thường là JSON hoặc XML).
@Controller thường được dùng cùng với @RequestMapping, @GetMapping, @PostMapping, v.v. để ánh xạ các URL tới phương thức của lớp.
Minh họa cách đánh dấu lớp ProductController bằng annotation @Controller.
package vn.hoctotlamhay.learnspring.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import vn.hoctotlamhay.learnspring.entity.Product; import vn.hoctotlamhay.learnspring.service.ProductService; import java.util.List; @Controller public class ProductController { private final ProductService productService; @Autowired public ProductController(ProductService productService) { this.productService = productService; } @GetMapping("/products") public String viewAllProducts(Model model) { List<Product> productList = productService.getAllProducts(); model.addAttribute("products", productList); return "product-list"; } @GetMapping("/products/search") public String searchProducts(@RequestParam("keyword") String keyword, Model model) { List<Product> result = productService.searchByName(keyword); model.addAttribute("products", result); model.addAttribute("keyword", keyword); return "product-list"; } // Các phương thức khác }
Chú ý
- @Controller dùng để trả về view (giao diện HTML) chứ không phải dữ liệu.
- Nếu @Controller nằm ngoài package được @ComponentScan, Spring sẽ không phát hiện ra controller.
- Không sử dụng @Controller gắn với xử lý logic nghiệp vụ trực tiếp.
-
-
Sử dụng annotation @Bean
Annotation @Bean được sử dụng để khai báo một đối tượng là Spring Bean theo kiểu cấu hình Java thuần (Java-based Configuration). Điều này có nghĩa là @Bean phải được đặt trong một lớp có annotation @Configuration. Các đặc điểm nổi bật của Spring Bean khai báo bằng @Bean gồm:
- Mỗi phương thức có @Bean sẽ trả về một đối tượng được Spring quản lý.
- Tên Bean mặc định là tên phương thức nhưng có thể chỉ định cụ thể bằng @Bean(name = "beanName").
- Cho phép kiểm soát logic khởi tạo bean bao gồm cả việc thiết lập giá trị, phụ thuộc hoặc cấu hình nâng cao.
- Phạm vi mặc định là singleton nhưng có thể thay đổi với @Scope("prototype"), @Scope("request"), v.v.
- Bean được quản lý bởi ApplicationContext và có thể được sử dụng thông qua cơ chế dependency injection (@Autowired, constructor, setter hoặc field injection).
- Dễ kiểm soát với cấu hình phức tạp ngay cả khi không thể đánh dấu trực tiếp các lớp bằng @Component.
Minh họa phương pháp dùng @Bean để khai báo các Bean ModelMapper, PasswordEncoder và DateTimeFormatter.
package vn.hoctotlamhay.learnspring.configuration; import org.modelmapper.ModelMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import java.time.format.DateTimeFormatter; @Configuration public class CommonBeanConfig { @Bean public ModelMapper modelMapper() { return new ModelMapper(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public DateTimeFormatter dateFormatter() { return DateTimeFormatter.ofPattern("dd-MM-yyyy"); } }
-
-
Phạm vi của Spring Bean
Trong Spring, phạm vi của một Bean xác định số lượng instance được tạo ra và cách Spring Container quản lý vòng đời của Bean đó trong suốt quá trình hoạt động của ứng dụng.
Khi khai báo một Bean bằng các annotation như @Component, @Repository, @Service hoặc bằng cấu hình Java thuần thông qua @Bean, phạm vi mặc định của Bean sẽ là singleton. Điều này có nghĩa là Spring sẽ tạo một instance duy nhất ngay khi khởi động Container và dùng duy nhất instance đó trong toàn bộ ứng dụng.
Nhìn chung, Spring Bean có thể có nhiều phạm vi, dưới đây là các phạm vi phổ biến:
Phạm vi Mô tả singleton (mặc định) Chỉ tạo một instance duy nhất trong toàn bộ Spring Container, dùng ở mọi nơi của ứng dụng prototype Mỗi lần yêu cầu Bean sẽ tạo một instance mới và không dùng chung request (chỉ dùng với web) Mỗi HTTP Request sẽ có một instance riêng session(chỉ dùng với web) Mỗi HTTP Session sẽ có một instance riêng application (chỉ dùng với web) Một instance được tạo cho toàn bộ ứng dụng web ServletContext websocket Mỗi WebSocket Session có một instance riêng Để chỉ định phạm vi của Spring Bean, chúng ta sử dụng annotation @Scope cùng với phạm vi cần chỉ định, ví dụ:
- Phạm vi singleton
- Phạm vi prototype
- Phạm vi request
@Bean @Scope("singleton") public ModelMapper modelMapper() { return new ModelMapper(); } Hoặc bỏ @Scope, vì singleton là mặc định: @Bean public ModelMapper modelMapper() { return new ModelMapper(); }
@Component @Scope("prototype") public class TransactionCodeGenerator { private final String code; public TransactionCodeGenerator() { this.code = UUID.randomUUID().toString(); } public String getCode() { return code; } }
@Bean @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) public User user() { return new User(); }
-
Dependency Injection
-
Dependency Injection là gì?
Dependency Injection là một nguyên lý trong lập trình hướng đối tượng, theo đó các đối tượng không tự tạo hoặc quản lý các phụ thuộc (dependency) của mình mà chúng được cung cấp từ bên ngoài.
Trong Spring, Dependency Injection là cơ chế giúp Spring Container tự động tạo và cung cấp các đối tượng cần thiết (Bean) cho những nơi cần sử dụng. Phương pháp này giúp cho mã nguồn rõ ràng, dễ kiểm soát và dễ kiểm thử hơn.
Như đã trình bày trong bài trước, Spring sử dụng Inversion of Control (IoC) để quản lý vòng đời và phụ thuộc của các Bean.
- Các lớp trong ứng dụng không cần tự tạo đối tượng mà chỉ cần khai báo phụ thuộc.
- Spring chịu trách nhiệm khởi tạo, cấu hình và liên kết các Bean lại với nhau.
-
Các hình thức tiêm phụ thuộc trong Spring
Giả sử chúng ta có một Spring Bean là PaymentService cung cấp phương thức thanh toán và cần sử dụng trong lớp OrderService.
-
Tiêm thông qua hàm khởi tạo - Constructor Injection
Đặc điểm của phương pháp
- Khi khởi tạo đối tượng bắt buộc phải có phụ thuộc.
- Hỗ trợ từ khóa final.
- Thân thiện với kiểm thử.
@Component public class OrderService { private final PaymentService paymentService; @Autowired public OrderService(PaymentService paymentService) { this.paymentService = paymentService; } }
Chú ý: Từ Spring 4.3+, nếu lớp chỉ có một constructor duy nhất thì có thể bỏ qua @Autowired.
-
Tiêm thông qua phương thức Setter - Setter Injection
Đặc điểm của phương pháp
- Phù hợp với các phụ thuộc tùy chọn.
- Mở rộng dễ dàng.
@Component public class OrderService { private PaymentService paymentService; @Autowired public void setPaymentService(PaymentService paymentService) { this.paymentService = paymentService; } }
-
Tiêm trực tiếp vào trường - Field Injection
Đặc điểm của phương pháp
- Khó kiểm thử.
- Không hỗ trợ final.
- Phụ thuộc ẩn, khó hình dung.
@Component public class OrderService { @Autowired private PaymentService paymentService; }
Chú ý: Không nên dùng Field Injection trong các lớp cần unit test hoặc các lớp sẽ được đóng gói thành thư viện.
-
-
Cơ chế Spring xác định Bean để tiêm phụ thuộc
-
@Autowired
@Autowired là một annotation của Spring dùng để tự động tiêm các phụ thuộc vào một Bean. Khi Spring khởi tạo một Bean, nó sẽ tìm các vị trí được đánh dấu với @Autowired (constructor, setter hoặc thuộc tính) và tìm một Bean phù hợp theo kiểu dữ liệu trong Spring Container để tiêm vào.
@Autowired được xử lý bởi BeanPostProcessor, cụ thể là lớp AutowiredAnnotationBeanPostProcessor. Khi Spring Container khởi tạo Bean, nó sẽ thực hiện các công việc sau:
- Quét các constructor, setter và thuộc tính có @Autowired.
- Tìm Bean tương ứng theo kiểu dữ liệu và tiêm Bean đó vào đối tượng.
- Nếu có nhiều Bean cùng kiểu thì @Autowired sẽ không biết chọn Bean nào, khi đó chúng ta cần sử dụng cơ chế khác.
-
@Qualifier
Giả sử chúng ta có một interface là PaymentService như sau:
package vn.hoctotlamhay.learnspring.repository; public interface PaymentService { boolean processPayment(String orderId, double amount); boolean refund(String orderId, String reason); String getPaymentStatus(String orderId); }
Cài đặt mã nguồn xử lý cho phương thức thanh toán bằng Paypal.
package vn.hoctotlamhay.learnspring.service; import org.springframework.stereotype.Component; import vn.hoctotlamhay.learnspring.repository.PaymentService; @Component("paypalService") public class PaypalPaymentService implements PaymentService { @Override public boolean processPayment(String orderId, double amount) { // Logic xử lý thanh toán } @Override public boolean refund(String orderId, String reason) { // Logic xử lý hoàn tiền } @Override public String getPaymentStatus(String orderId) { // Logic kiểm tra trạng thái thanh toán } }
Cài đặt mã nguồn xử lý cho phương thức thanh toán bằng thẻ Credit.
package vn.hoctotlamhay.learnspring.service; import org.springframework.stereotype.Component; import vn.hoctotlamhay.learnspring.repository.PaymentService; @Component("creditCardService") public class CreditCardPaymentService implements PaymentService { @Override public boolean processPayment(String orderId, double amount) { // Logic xử lý thanh toán } @Override public boolean refund(String orderId, String reason) { // Logic xử lý hoàn tiền } @Override public String getPaymentStatus(String orderId) { // Logic kiểm tra trạng thái thanh toán } }
Bởi vì có hai Bean cùng kiểu PaymentService, nếu chỉ dùng @Autowired thì Spring không biết chọn Bean nào do đó cần kết hợp @Autowired và @Qualifier, ví dụ chúng ta chỉ định Bean paypalService:
@Autowired public OrderService(@Qualifier("paypalService") PaymentService paymentService) { this.paymentService = paymentService; }
-
@Primary
Trong trường hợp có nhiều Bean cùng kiểu, nếu không muốn sử dụng @Qualifier tại mỗi vị trí tiêm phụ thuộc bạn có thể đánh dấu một Bean là mặc định bằng annotation @Primary. Khi đó, Spring sẽ ưu tiên sử dụng Bean được đánh dấu @Primary nếu không có chỉ định cụ thể nào khác. Giả sử chúng ta chỉ định phương thức thanh toán bằng Paypal là phương thức được ưu tiên:
package vn.hoctotlamhay.learnspring.service; import org.springframework.stereotype.Component; import vn.hoctotlamhay.learnspring.repository.PaymentService; @Component("paypalService") @Primary public class PaypalPaymentService implements PaymentService { @Override public boolean processPayment(String orderId, double amount) { // Logic xử lý thanh toán } @Override public boolean refund(String orderId, String reason) { // Logic xử lý hoàn tiền } @Override public String getPaymentStatus(String orderId) { // Logic kiểm tra trạng thái thanh toán } }
Spring sẽ tự động tiêm PaypalPaymentService mà không cần @Qualifier.
@Autowired public OrderService(PaymentService paymentService) { this.paymentService = paymentService; }
-
-