Welcome to MikeDaDev's Blog – notes on Java, backend & system design.

Explore
MikeDaDev's Blog

spring autowired

Tung Bui Thanh (TungDaDev)11 mins read

Spring autowired intro.webp

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.xml trê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.

Bean là object do Container quả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:

  1. Thấy Girl có @Component nên nó phải tạo 1 Bean Girl.

  2. Thấy Girl còn thiếu mấy thuộc tính được đánh dấu @Autowired, nó tìm trong Container các giá trị phù hợp rồi inject vào.

  3. Tạo một new Girl từ những thứ đã inject. Bean singleton thì được cất luôn vào Container để lần sau dùng lại; riêng Girl đang là prototype (xem phần dưới) nên Container tạ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ủa Spring 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ại new một Object.

  • @Scope("singleton"): không nói gì thì Spring mặc định dùng scope này. Singleton nghĩa là object chỉ được tạo đúng một lần trong mỗi Container.

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 consumed

Biế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 😎 👍🏻 🚀 🔥.