spring autowired
Tung Bui Thanh (TungDaDev)11 mins read

Chào các bạn, trước khi vào bài hôm nay, các bạn đọc giúp mình mấy khái niệm này trước đã:
Khái niệm Dependency Injection và Inversion Of Control
Chưa nắm mấy khái niệm này thì bạn đọc ở link trên trước, rồi quay lại đọc tiếp phần này sẽ dễ hiểu hơn nhiều.
# ioc container trong spring
Đọc bài Inversion of Control ở trên rồi thì bạn sẽ thấy Spring làm thay mình việc khởi tạo object và quản lý nó luôn. Object nào cần dependency gì thì nó inject vào ngay lúc khởi tạo.
Thằng chịu trách nhiệm tạo và quản lý đó tên là IoC Container.
Tới đây chắc nhiều bạn thắc mắc: thế làm sao nó biết Outfit lấy ở đâu ra, Accessories ở đâu ra, v.v..
Đúng vậy, nếu mình không nói trước với IoC Container thì nó chịu, không biết lấy mấy thứ đó ở đâu. Vậy nói với nó bằng cách nào?
Mình sẽ vừa code vừa giải thích cho dễ hình dung.
# tạo spring project với maven
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>vn.tungdadev.springboot</groupId>
<artifactId>dependency-injection</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
</parent>
<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>Các bạn tạo project như bình thường rồi copy đoạn xml trên vào file pom.xml. Thế là có một Spring project rồi. Ở đây mình dùng dependencies của Spring boot, chứ về bản chất thì cũng như nhau.
Lưu ý:
pom.xmltrên dùng Spring Boot 2.0.5 và Java 8 từ hồi viết bài. Dòng Spring Boot 2.x đã hết hỗ trợ từ lâu, project mới thì nên tạo bằng start.spring.io với bản Spring Boot mới nhất (cần Java 17 trở lên). Mọi annotation trong bài vẫn dùng y như vậy.
# @component
Tạo mấy Interface cần dùng:
// Có cô gái nào mà không cần phụ kiện trên người cơ chứ :3 bông tai, vòng tay, v.v..
public interface Accessories {
}
// Có tý bồng bềnh phồng phềnh trên tóc ms xinh đc.
public interface HairStyle {
}
// Cuối cùng là Outfit
public interface Outfit {
public void wear();
}Rồi implement hết mấy Interface này:
import org.springframework.stereotype.Component;
// Một bộ Bikini, bạn có thể tạo thêm nhiều bộ quần áo khác, chỉ cần implement Outfit là được
@Component
public class Bikini implements Outfit {
public void wear() {
System.out.println("Đã mặc Bikini");
}
}
// Phụ kiện Gucci nhờ :3
@Component
public class GucciAccessories implements Accessories {
// Class chỉ mang tính minh họa, không có gì cả
}
// Tóc hàn quốc cho xinh
@Component
public class KoreanHairStyle implements HairStyle {
// Class chỉ mang tính minh họa, không có gì cả
}Chỗ lạ mắt nhất ở đây là cái @Component. Nó là một Annotation do Spring cung cấp.
import org.springframework.stereotype.Component;@Component: báo cho IoC Container biết "bọn tao ở đây này". Thế là IoC Container biết có một thằng Bikini thuộc interface Outfit đang tồn tại, nó sẽ tạo ra object Bikini rồi cất vào trong Container.
Nhưng Spring không tự đi lục khắp ổ cứng đâu. @SpringBootApplication (lát nữa sẽ gặp) có bật component scan: Spring quét package chứa class Main và mọi package con của nó để tìm các class có @Component. Class nằm ngoài vùng đó thì có gắn @Component cũng không được tạo, đây là lỗi người mới hay gặp nhất.
Lúc này IoC Container đang quản lý 3 object là Bikini, GucciAccessories, KoreanHairStyle. Mấy object này được gọi là Bean.
Beanlà object doContainerquản lý
# @autowired
Tạo class Girl:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype")
public class Girl {
private Outfit outfit;
private Accessories accessories;
private HairStyle hairStyle;
@Autowired
public Girl(Outfit outfit, Accessories accessories, HairStyle hairStyle) {
this.outfit = outfit;
this.accessories = accessories;
this.hairStyle = hairStyle;
}
@Override
public String toString() {
return "Girl{" +
"outfit=" + outfit +
", accessories=" + accessories +
", hairStyle=" + hairStyle +
'}';
}
}Girl cũng có @Component, nên nó cũng là 1 Bean, IoC Container sẽ tìm tới và tạo ra nó. Cái này nói ở trên rồi.
Cái cần nói ở đây là thằng @Autowired.
@Autowired: là Annotation gắn lên constructor, thuộc tính (field) hoặc setter để bảo IoC Container: "inject mấy thuộc tính này vào hộ tao".
Ở Girl mình gắn nó lên constructor. Thật ra từ Spring 4.3, class chỉ có một constructor thì không cần ghi @Autowired nữa, Spring tự dùng constructor đó. Cách inject qua constructor cũng là cách được khuyên dùng: các dependency có thể để final, thiếu cái nào là app báo lỗi ngay lúc khởi động, viết unit test thì cứ new Girl(...) truyền đồ giả vào là xong.
Lúc này IoC Container sẽ làm thế này:
Thấy
Girlcó@Componentnên nó phải tạo 1Bean Girl.Thấy
Girlcòn thiếu mấy thuộc tính được đánh dấu@Autowired, nó tìm trongContainercác giá trị phù hợp rồiinjectvào.Tạo một
new Girltừ những thứ đãinject. Beansingletonthì được cất luôn vàoContainerđể lần sau dùng lại; riêngGirlđang làprototype(xem phần dưới) nênContainertạo xong là giao cho bạn, không giữ lại.
# chạy chương trình
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
@SpringBootApplication
public class Main implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
@Autowired
private ApplicationContext context;
@Override
public void run(String... args) throws Exception {
Girl girl = context.getBean(Girl.class);
System.out.println(girl);
}
}
@SpringBootApplication: chỉ cần gắn một lần trên class chính, để nói rằng Project này là Spring Project. Thực ra nó gộp ba annotation: @Configuration, @EnableAutoConfiguration (tự config các thứ dựa trên thư viện có trong classpath) và @ComponentScan (quét package như nói ở trên).
// Bạn phải wrapper hàm Class có annotation @SpringBootApplication bằng SpringApplication.run() thì mới được Spring hỗ trợ. Còn không thì cái @SpringBootApplication cũng như vứt đi à
SpringApplication.run(Main.class, args);
Spring Bootđơn giản là bản mở rộng củaSpring Framework: giữ lại cái hay, bỏ bớt mấy đoạn code thừa, dài dòng.
Muốn chạy một đoạn code ngay sau khi Spring khởi động xong và đã tạo đủ Bean, cách tiện là implement CommandLineRunner rồi viết code trong hàm run(String... args): lúc đó các field @Autowired đã được inject đầy đủ. (Cũng có thể lấy ApplicationContext từ giá trị trả về của SpringApplication.run(...) ngay trong main, nhưng main là static nên không dùng được field @Autowired.)
Tới đây sẽ gặp ApplicationContext. Đây chính là IoC Container đấy các bạn.
@Autowired // Bảo Spring tự inject chính cái Container của nó cho mình nghịch chút
private ApplicationContext context; // Cái này có thể hiểu chính là Container đấy các bạn, nó chứa mọi Bean trong này.
@Override
public void run(String... args) throws Exception {
Girl girl = context.getBean(Girl.class); // Lấy Girl đã được tạo ra xem.
System.out.println(girl);
Girl girl2 = context.getBean(Girl.class);
System.out.println(girl == girl2); // Kết quả ra False
// Chứng tỏ mỗi lần lấy ra chúng ta tạo ra 1 Cô gái khác nhau
Outfit outfit = context.getBean(Outfit.class);
System.out.println(outfit);
Outfit outfit2 = context.getBean(Outfit.class);
System.out.println(outfit == outfit2); // Kết quả ra True
// Chứng tỏ Outfit là singleton.
}Chạy chương trình bạn sẽ thấy: class Girl mỗi lần lấy ra là một instance mới tinh, còn Outfit thì lấy bao nhiêu lần cũng chỉ là 1 instance duy nhất (singleton).
Muốn hiểu vì sao thì quay lại xem code Girl và Outfit, bạn sẽ thấy hai đứa chỉ khác nhau đúng một chỗ:
@Scope("prototype")Bạn xóa @Scope("prototype") đi và chạy lại thử xem 😅
Tới đây thì rõ rồi: nếu không nói gì, mọi Bean trong IoC Container đều là Singleton. Singleton ở đây là "mỗi Container một instance", khác với Singleton pattern kiểu một instance cho cả JVM. Vì một instance được dùng chung cho mọi thread, bean singleton đừng giữ trạng thái thay đổi theo từng request trong field.
Còn muốn Bean được tạo theo ý mình thì phải thêm Annotation @Scope:
@Scope("prototype"): giống như mỗi lần lạinewmột Object.@Scope("singleton"): không nói gì thìSpringmặc định dùng scope này.Singletonnghĩa là object chỉ được tạo đúng một lần trong mỗiContainer.
Ngoài ra còn mấy kiểu scope dành riêng cho project Web Application, bài này chưa cần nói tới.
# @Configuration và @Bean
Giờ chạy chương trình tạo Girl ở trên thì có một vấn đề: Cô gái nào cũng tự được inject bộ Bikini vào người. Trong khi thực tế, cô gái nào mới sinh ra cũng phải Naked mới đúng 😗
Mình sẽ config lại chương trình một chút để Girl nào được tạo ra cũng Naked:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration // Đánh dấu một Class là Config, nơi khai báo các @Bean
public class GirlConfig {
@Bean // Gắn đối tượng Outfit trả về trong hàm này là 1 bean. IoC Container sẽ quản lý nó giống @Component luôn
public Outfit defaultOutfit(){
return new Naked(); // function này trả về một đối tượng Naked()
}
}
public class Naked implements Outfit {
@Override
public void wear() {
System.out.println("ngượng quá điii!!!");
}
}
Giải thích chút:
@Configuration: là một Annotation đặc biệt của Spring. Class nào gắn @Configuration thì Spring hiểu đây là chỗ mình cấu hình, cài đặt và tạo ra các Bean cần cho chương trình. Bản thân @Configuration cũng là một @Component, nên nó cũng được tìm thấy qua component scan.
@Bean: gắn trên function, đánh dấu object mà function trả về là bean để IoC Container quản lý. Giống @Component, chỉ khác là @Bean gắn trên function thôi. Tên của bean mặc định là tên function, ở đây là defaultOutfit.
Khi nào dùng @Bean thay vì @Component? Khi class đó không phải của mình (class trong thư viện, không sửa code để gắn @Component được), hoặc khi cần tự tay quyết định cách tạo object, ví dụ đọc cấu hình rồi mới chọn tạo loại nào.
Về cách chạy, IoC Container sẽ lục tìm hết các @Configuration trong vùng component scan, gọi hết các Function có @Bean trong đó, lấy các bean mà hàm trả về rồi quản lý chúng.
Có thể coi đây là cách tự tay tạo bean cho class theo ý mình: trong function mình tạo object thế nào thì Bean của class đó sẽ y như vậy.
Có điều chạy chương trình trên thì bạn sẽ dính lỗi:
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 consumedBiết vì sao không? Vì lúc này trong IoC Container có tới 2 thằng Outfit.
@Component // Có component, nên IoC tạo ra 1 Outfit trong Container
public class Bikini implements Outfit {
public void wear() {
System.out.println("Đã mặc Bikini");
}
}
@Configuration // Vì là Configuration, nên IoC sẽ chạy class này và tạo ra toàn bộ các @Bean trong nó.
public class GirlConfig {
@Bean // Có @Bean => tạo ra 1 Outfit là Naked trong Container.
public Outfit defaultOutfit(){
return new Naked();
}
}
class Girl{
// Lúc này IoC Container không biết nên inject thằng outfit nào vào đây.
@Autowired private Outfit outfit;
}
Có 2 cách xử lý:
# @primary
Đánh dấu Naked là ưu tiên. Khi có nhiều Bean kiểu Outfit, nó sẽ luôn chọn inject Naked trước. Đúng ý mình: tạo Girl ra là phải Naked.
@Bean
@Primary
public Outfit defaultOutfit(){
return new Naked();
}# qualifier
Khi có nhiều Bean cùng kiểu làm Spring rối, bạn đặt tên cho từng cái và chỉ rõ chỗ nào dùng cái nào:
@Bean("naked")
public Outfit defaultOutfit(){
return new Naked();
}
@Component
@Scope("prototype")
public class Girl {
@Autowired
@Qualifier("naked")
private Outfit outfit;
@Autowired private Accessories accessories;
@Autowired private HairStyle hairStyle;
}
// Nhớ bỏ cái dòng lệnh này đi
// Outfit outfit = context.getBean(Outfit.class);
// System.out.println(outfit);
// Outfit outfit2 = context.getBean(Outfit.class);
// System.out.println(outfit == outfit2);
// Thay bằng
// context.getBean("naked", Outfit.class)
// hoặc context.getBean("bikini", Outfit.class)# lời kết
Vậy là mình đã giới thiệu xong cách Spring Framework hoạt động và cách dùng nó trong code.
Nắm được lý thuyết này với mấy Annotation ở trên là bạn đã code xoành xoạch được với Framework này rồi.
Nhớ đón đọc mấy bài sau về Spring boot nhé.
Chúc các bạn học tập tốt và nhớ chia sẻ cho bạn bè học cùng ahehe :v
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 😎 👍🏻 🚀 🔥.