Post

Spring Core Annotations

1. Giới thiệu

Chúng ta có thể tận dụng các khả năng của Spring DI engine bằng cách sử dụng các chú thích trong package org.springframework.beans.factory.annotation và org.springframework.context.annotation. Ở một phần nào đó hôm trước, chúng ta đã tìm hiểu qua Spring Bean Annotations. Hôm nay hãy đi xa hơn một chút để gặt hái một chút mới mẻ với Spring, bạn có biết đó là gì không ? - Chúng ta thường gọi những điều này là “Spring core annotations” và chúng ta sẽ xem xét chúng trong hướng dẫn này.

@Autowired

Chúng ta có thể sử dụng @Autowired để đánh dấu một phần dependency mà Spring sẽ resolveinject. Chúng ta có thể sử dụng chú thích này với một hàm constructor, setter hoặc field injection. Hãy xem chi tiết từng cách bên dưới.

Constructor injection:

1
2
3
4
5
6
7
8
class Car {
    Engine engine;
 
    @Autowired
    Car(Engine engine) {
        this.engine = engine;
    }
}

Setter injection:

1
2
3
4
5
6
7
8
class Car {
    Engine engine;
 
    @Autowired
    void setEngine(Engine engine) {
        this.engine = engine;
    }
}

Field injection:

1
2
3
4
class Car {
    @Autowired
    Engine engine;
}

@Autowired có một đối số boolean bắt buộc với giá trị mặc định là true. Nó điều chỉnh hành vi của Spring khi không tìm thấy bean thích hợp để kết nối. Nếu true - một ngoại lệ được ném ra, nếu false - không có gì được kết nối.

Lưu ý rằng nếu chúng ta sử dụng constructor, thì tất cả các đối số của hàm constructor là bắt buộc.

Bắt đầu với phiên bản 4.3, chúng ta không cần chú thích các hàm tạo với @Autowired một cách rõ ràng trừ khi khai báo ít nhất hai contructor.

@Bean

Annotation này đánh dấu một phương thức factory khởi tạo một Spring bean:

1
2
3
4
@Bean
Engine engine() {
    return new Engine();
}

Spring gọi các phương thức này khi một instance mới của kiểu trả về được yêu cầu. Bean kết quả có cùng tên với phương thức factory. Nếu chúng ta muốn đặt tên khác, chúng ta có thể làm như vậy với tên hoặc các giá trị đối số của chú thích này (giá trị đối số là bí danh cho tên đối số):

1
2
3
4
@Bean("engine")
Engine getEngine() {
    return new Engine();
}

Lưu ý rằng tất cả các phương thức được chú thích bằng @Bean phải nằm trong các lớp @Configuration.

@Qualifier

Chúng ta sử dụng @Qualifier cùng với @Autowired để cung cấp id bean hoặc tên bean mà chúng ta muốn sử dụng trong các tình huống không rõ ràng. Ví dụ: hai bean sau triển khai cùng một interface.

1
2
3
class Bike implements Vehicle {}
 
class Car implements Vehicle {}

Nếu Spring cần inject một bean Vehicle, nó sẽ có nhiều định nghĩa phù hợp. Trong những trường hợp như vậy, chúng ta có thể cung cấp tên bean một cách rõ ràng bằng cách sử dụng chú thích @Qualifier.

Sử dụng constructor injection:

1
2
3
4
@Autowired
Biker(@Qualifier("bike") Vehicle vehicle) {
    this.vehicle = vehicle;
}

Sử dụng setter injection:

1
2
3
4
@Autowired
void setVehicle(@Qualifier("bike") Vehicle vehicle) {
    this.vehicle = vehicle;
}

Sử dụng field injection:

1
2
3
@Autowired
@Qualifier("bike")
Vehicle vehicle;

Hoặc một cách khác:

1
2
3
4
5
@Autowired
@Qualifier("bike")
void setVehicle(Vehicle vehicle) {
    this.vehicle = vehicle;
}

@Required

Annotation này dựa trên các phương thức setter để đánh dấu các dependency thông qua XML

1
2
3
4
@Required
void setColor(String color) {
    this.color = color;
}
1
2
3
<bean class="tungdadev.annotations.Bike">
    <property name="color" value="green" />
</bean>

Nếu không, ngoại lệ BeanInitializationException sẽ được ném ra.

@Value

Chúng ta có thể sử dụng @Value để inject các giá trị property vào bean. Nó tương thích với cả constructor, setter và field injection.

Constructor injection:

1
2
3
Engine(@Value("8") int cylinderCount) {
    this.cylinderCount = cylinderCount;
}

Setter injection:

1
2
3
4
@Autowired
void setCylinderCount(@Value("8") int cylinderCount) {
    this.cylinderCount = cylinderCount;
}

Field injection:

1
2
@Value("8")
int cylinderCount;

Hoặc cách khác như sau:

1
2
3
4
@Value("8")
void setCylinderCount(int cylinderCount) {
    this.cylinderCount = cylinderCount;
}

Tất nhiên, việc inject các giá trị tĩnh không hữu ích. Do đó, chúng ta có thể sử dụng placeholder strings trong @Value để truyền các giá trị được xác định từ các nguồn bên ngoài. Ví dụ: trong file .properties hoặc .yaml.

Giả sử ta có một .properties file:

engine.fuelType=petrol

Chúng ta có thể injeect giá trị của engine.fuelType vào như sau:

1
2
@Value("${engine.fuelType}")
String fuelType;

Chúng ta có thể sử dụng @Value ngay cả với SpEL. Bạn có thể tìm thấy các ví dụ nâng cao hơn về @Value ở các nguồn chính thống của Spring.

@DependsOn

Chúng ta có thể sử dụng chú thích này để làm cho Spring khởi tạo các bean khác trước chú thích. Thông thường, hành vi này là tự động dựa trên dependency rõ ràng giữa các bean.

Chúng ta chỉ cần chú thích này khi các dependency là ngầm định. Ví dụ: tải JDBC driver hoặc khởi tạo biến tĩnh. Chúng ta có thể sử dụng @DependsOn trên lớp dependency chỉ định tên của các dependency bean. Giá trị đối số của chú thích cần một mảng chứa các tên dependency bean:

1
2
@DependsOn("engine")
class Car implements Vehicle {}

Ngoài ra, nếu chúng ta xác định bean bằng chú thích @Bean, thì phương thức factory sẽ được chú thích bằng @DependsOn:

1
2
3
4
5
@Bean
@DependsOn("fuel")
Engine engine() {
    return new Engine();
}

@Lazy

Chúng ta sử dụng @Lazy muốn khởi tạo bean của mình một cách lazy 😊). Theo mặc định, Spring tạo tất cả các singleton bean khi khởi động bao gồm cấu trúc của ngữ cảnh ứng dụng. Tuy nhiên, có những trường hợp chúng ta cần tạo bean khi có yêu cầu chứ không phải lúc khởi động.

Chú thích này hoạt động khác nhau tùy thuộc vào vị trí chúng ta đặt chính xác. Chúng ta có thể đặt nó trên:

  • một phương thức bean factory được chú thích @Bean để trì hoãn việc gọi phương thức (do đó tạo bean)
  • một lớp @Configuration và tất cả các phương thức @Bean chứa trong đó sẽ bị ảnh hưởng
  • một lớp @Component (không phải là lớp @Configuration) - bean này sẽ được khởi tạo một cách lazy
  • một @Autowired constructor, setter hoặc field mong muốn để tải các dependency một cách lazy (thông qua proxy)

Chú thích này có một đối số được đặt tên là value với giá trị mặc định là true. Nó rất hữu ích để ghi đè hành vi mặc định. Ví dụ: đánh dấu các bean được tải khi thiết lập chung bị lazy hoặc cấu hình các phương thức @Bean cụ thể để tải nhanh trong lớp

@Configuration được đánh dấu bằng @Lazy:

1
2
3
4
5
6
7
8
9
10
@Configuration
@Lazy
class VehicleFactoryConfig {
 
    @Bean
    @Lazy(false)
    Engine engine() {
        return new Engine();
    }
}

@Lookup

Một phương thức được chú thích bằng @Lookup sẽ yêu cầu Spring trả về một instance của kiểu trả về của phương thức khi chúng ta gọi nó.

Inject prototype-scoped Bean vào một Singleton Bean

Nếu chúng ta tình cờ quyết định có một prototype Spring bean thì chúng ta gần như ngay lập tức phải đối mặt với vấn đề là làm thế nào để singleton Spring bean có thể truy cập vào những prototype Spring bean này?

Bây giờ, Provider chắc chắn là một cách, mặc dù @Lookup linh hoạt hơn ở một số khía cạnh. Đầu tiên, hãy tạo một prototype bean mà sau này chúng ta sẽ đưa vào một singleton bean:

1
2
3
4
5
@Component
@Scope("prototype")
public class SchoolNotification {
    // ... prototype-scoped state
}

Và nếu chúng ta tạo một bean đơn giản để sử dụng @Lookup:

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class StudentServices {
 
    // ... các biến thành viên, ...
 
    @Lookup
    public SchoolNotification getNotification() {
        return null;
    }
 
    //... getters và setters
}

Sử dụng @Lookup, chúng ta có thể nhận được một instance của Scho`olNotification thông qua singleton bean:

1
2
3
4
5
6
7
8
9
@Test
public void whenLookupMethodCalled_thenNewInstanceReturned() {
    // ... initialize context
    StudentServices first = this.context.getBean(StudentServices.class);
    StudentServices second = this.context.getBean(StudentServices.class);
       
    assertEquals(first, second); 
    assertNotEquals(first.getNotification(), second.getNotification()); 
}

Lưu ý rằng trong StudentServices, chúng ta đã để phương thức getNotification còn sơ khai. Điều này là do Spring ghi đè phương thức bằng lời gọi tới beanFactory.getBean (StudentNotification.class) vì vậy chúng ta có thể để trống nó.

Inject Dependency theo thủ tục

Vẫn còn cách mạnh mẽ hơn @Lookup - cho phép chúng ta inject một phần dependency theo thủ tục, điều mà chúng ta không thể làm với Provider. Hãy nâng cao StudentNotification với một số trạng thái:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
@Scope("prototype")
public class SchoolNotification {
    @Autowired Grader grader;
 
    private String name;
    private Collection<Integer> marks;
 
    public SchoolNotification(String name) {
        // ... set fields
    }
 
    // ... getters và setters
 
    public String addMark(Integer mark) {
        this.marks.add(mark);
        return this.grader.grade(this.marks);
    }
}

Bây giờ, nó phụ thuộc vào một số ngữ cảnh Spring và cả ngữ cảnh bổ sung mà chúng ta sẽ cung cấp theo thủ tục. Sau đó, chúng ta có thể thêm một phương thức vào StudentServices để lấy student data và duy trì nó:

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class StudentServices {
 
    private Map<String, SchoolNotification> notes = new HashMap<>();
 
    @Lookup
    protected abstract SchoolNotification getNotification(String name);
 
    public String appendMark(String name, Integer mark) {
        SchoolNotification notification
          = notes.computeIfAbsent(name, exists -> getNotification(name)));
        return notification.addMark(mark);
    }
}

Trong thời gian chạy, Spring sẽ thực hiện phương thức theo cách tương tự với một vài thủ thuật bổ sung. Đầu tiên, hãy lưu ý rằng nó có thể gọi một constructor phức tạp cũng như đưa các Spring bean khác vào cho phép chúng ta coi SchoolNotification giống như một phương thức Spring-Recognition. Nó thực hiện việc này bằng cách triển khai getSchoolNotification với một lời gọi tới beanFactory.getBean (SchoolNotification.class, name).

Thứ hai, đôi khi chúng ta có thể làm cho phương thức @Lookup-annotated trở nên trừu tượng, giống như ví dụ trên. Sử dụng trừu tượng trông đẹp hơn một chút so với sơ khai nhưng chúng ta chỉ có thể sử dụng nó khi không có scan component hoặc @Bean-manage bao bọc xung quanh:

1
2
3
4
5
6
7
8
9
@Test
public void whenAbstractGetterMethodInjects_thenNewInstanceReturned() {
    // ... initialize context
 
    StudentServices services = context.getBean(StudentServices.class);    
    assertEquals("PASS", services.appendMark("Alex", 89));
    assertEquals("FAIL", services.appendMark("Bethany", 78));
    assertEquals("PASS", services.appendMark("Claire", 96));
}

Với thiết lập này, chúng ta có thể thêm các Spring dependency cũng như các phương thức dependency vào SchoolNotification. Mặc dù @Lookup linh hoạt, nhưng nó cũng có một số hạn chế đáng chú ý:

  • Các phương thức @Lookup-annotated như getNotification phải cụ thể khi lớp xung quanh như Student được scan component. Điều này là do quá trình scan component bỏ qua các bean trừu tượng.
  • Các phương thức @Lookup-annotated sẽ không hoạt động khi được bao bọc bởi @Bean-managed.

Trong những trường hợp đó, nếu chúng ta cần inject một prototype bean vào một singleton, chúng ta có thể tìm đến Provider để thay thế.

@Primary

Đôi khi chúng ta cần xác định nhiều bean cùng loại. Trong những trường hợp này, việc inject sẽ không thành công vì Spring không biết chúng ta cần bean nào. Chúng ta có một giải pháp để giải quyết tình huống này là đánh dấu tất cả các điểm nối @Qualifier và chỉ định tên của bean được yêu cầu.

Tuy nhiên, hầu hết thời gian chúng ta cần một loại bean cụ thể và hiếm khi những có những loại khác. Chúng ta có thể sử dụng @Primary để đơn giản hóa trường hợp này: nếu đánh dấu bean được sử dụng thường xuyên nhất bằng @Primary nó sẽ được chọn trên các điểm injection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Component
@Primary
class Car implements Vehicle {}
 
@Component
class Bike implements Vehicle {}
 
@Component
class Driver {
    @Autowired
    Vehicle vehicle;
}
 
@Component
class Biker {
    @Autowired
    @Qualifier("bike")
    Vehicle vehicle;
}

Trong ví dụ trước Car là phương tiện chính. Do đó, trong lớp Driver, Spring sẽ inject một Car bean. Tất nhiên, trong Biker bean giá trị của vehicle sẽ là đối tượng Bike.

@Scope

Chúng ta sử dụng @Scope để xác định phạm vi của lớp @Component hoặc định nghĩa @Bean. Nó có thể là singleton, prototype, request, session, globalSession hoặc một số scope tùy chỉnh.

Ví dụ:

1
2
3
@Component
@Scope("prototype")
class Engine {}

Context Configuration Annotations – Các chú thích cấu hình Context.

Chúng ta có thể cấu hình ngữ cảnh ứng dụng bằng các chú thích được mô tả trong phần này.

@Profille

Nếu chúng ta muốn Spring chỉ sử dụng lớp @Component hoặc phương thức @Bean khi một cấu hình cụ thể đang hoạt động thì có thể đánh dấu nó bằng @Profile. Chúng ta có thể cấu hình tên của Profile bằng giá trị đối số của chú thích:

1
2
3
@Component
@Profile("sportDay")
class Bike implements Vehicle {}

@Import

Chúng ta có thể sử dụng các lớp @Configuration cụ thể mà không cần scan component với chú thích này. Chúng ta có thể cung cấp các lớp đó với giá trị đối số của @Import:

1
2
@Import(VehiclePartSupplier.class)
class VehicleFactoryConfig {}

@ImportResource

Chúng ta có thể import các cấu hình XML với chú thích này. Và có thể chỉ định các vị trí file XML bằng vị trí đối số hoặc bằng bí danh của nó - giá trị đối số:

1
2
3
@Configuration
@ImportResource("classpath:/annotations.xml")
class VehicleFactoryConfig {}

@PropertySource

Với chú thích này, chúng ta có thể xác định các file property cho việc cài đặt ứng dụng:

1
2
3
@Configuration
@PropertySource("classpath:/annotations.properties")
class VehicleFactoryConfig {}

@PropertySource tận dụng tính năng lặp lại chú thích của Java 8, có nghĩa là chúng ta có thể đánh dấu một lớp bằng nó nhiều lần:

1
2
3
4
@Configuration
@PropertySource("classpath:/annotations.properties")
@PropertySource("classpath:/vehicle-factory.properties")
class VehicleFactoryConfig {}

@PropertySources

Chúng ta có thể sử dụng chú thích này để chỉ định nhiều cấu hình @PropertySource:

1
2
3
4
5
6
@Configuration
@PropertySources({ 
    @PropertySource("classpath:/annotations.properties"),
    @PropertySource("classpath:/vehicle-factory.properties")
})
class VehicleFactoryConfig {}

3. Kết luận

Trong bài viết này, chúng ta đã có cái nhìn tổng quan về các chú thích cốt lõi phổ biến nhất của Spring. Chúng ta đã biết cách cấu hình bean wiring và ngữ cảnh ứng dụng cũng như cách đánh dấu các lớp để scan component.

Bên trên là các Annotation bạn sẽ thường xuyên gặp phải trong quá trình làm việc xuyên suốt với Spring. Còn chuyện sử dụng chúng hiệu quả như thế nào là vấn đề nằm ở sự rèn luyện, đầu tư thời gian cho chuyện thực hành – tự học ở bạn.

Bài viết mang tính chất “ghi chú, lưu trữ, chia sẻ và phi lợi nhuận”.
Nếu bạn thấy hữu ích, đừng quên chia sẻ với bạn bè và đồng nghiệp của mình nhé!

Happy coding! 😎 👍🏻 🚀 🔥

Đọc thêm:

This post is licensed under CC BY 4.0 by the author.