Trước khi tìm hiểu về các phương thức tiêm (injection) bean trong Spring, chúng ta cần hiểu rõ khái niệm Tiêm Phụ Thuộc (Dependency Injection - DI). DI là kỹ thuật mà một lớp nhận các đối tượng phụ thuộc của mình từ một nguồn bên ngoài thay vì tự tạo ra chúng. Điều này giúp giảm sự phụ thuộc trực tiếp giữa các thành phần trong ứng dụng.
Spring Container hỗ trợ ba phương thức tiêm chính: tiêm thuộc tính (property injection), tiêm qua constructor (constructor injection), và tiêm qua phương thức factory (factory method injection). Dưới đây là chi tiết về từng phương thức và cách cấu hình.
1. Tiêm Thuộc Tính (Property Injection)
Tiêm thuộc tính thực hiện việc gán giá trị cho thuộc tính hoặc tiêm các đối tượng phụ thuộc thông qua các phương thức setXXX(). Phương thức này được sử dụng phổ biến nhất trong thực tế bởi tính linh hoạt và tùy chọn cao.
Spring sẽ gọi constructor mặc định của bean để khởi tạo đối tượng, sau đó sử dụng cơ chế phản xạ (reflection) để gọi các phương thức set nhằm tiêm giá trị cho thuộc tính.
Yêu cầu đối với class bean khi sử dụng tiêm thuộc tính là phải có một constructor không tham số (default constructor) và các phương thức set tương ứng với các thuộc tính cần tiêm.
Lưu ý: Constructor không tham số là constructor không nhận bất kỳ đối số nào. Nếu một lớp không định nghĩa bất kỳ constructor nào, JVM sẽ tự động tạo một constructor mặc định. Tuy nhiên, nếu bạn định nghĩa một constructor có tham số, JVM sẽ không tự động tạo constructor mặc định nữa, bạn cần phải tự khai báo một constructor không tham số nếu muốn sử dụng.
Ví dụ minh họa phương thức tiêm thuộc tính trong Spring:
Tạo lớp NguoiDung (User):
package com.example.beans;
/**
* Lớp người dùng để kiểm tra phương thức tiêm thuộc tính.
*/
public class NguoiDung {
private String tenNguoiDung;
public String getTenNguoiDung() {
return tenNguoiDung;
}
public void setTenNguoiDung(String tenNguoiDung) {
this.tenNguoiDung = tenNguoiDung;
}
}
Tệp cấu hình Spring (XML):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Định nghĩa bean người dùng -->
<bean id="nguoiDungBean" class="com.example.beans.NguoiDung">
<property name="tenNguoiDung">
<value>SpringUser</value>
</property>
</bean>
</beans>
Trong tệp cấu hình, mỗi thuộc tính tương ứng với một thẻ <property>. Thuộc tính name của thẻ này chính là tên thuộc tính trong lớp, và phải có một phương thức set tương ứng (ví dụ: setTenNguoiDung()).
Mẹo: Spring chỉ kiểm tra sự tồn tại của phương thức setter, không yêu cầu thuộc tính tương ứng phải được khai báo rõ ràng trong class. Tuy nhiên, theo quy ước, bạn nên khai báo thuộc tính để mã nguồn rõ ràng và dễ bảo trì.
Quy tắc đặt tên cho thẻ <property> trong Spring:
- Tên thuộc tính được chỉ định trong thẻ
<property>và phương thức setter trong lớp bean phải tuân theo quy tắc đặt tên của JavaBean. Cụ thể, thuộc tínhxxxsẽ tương ứng với phương thứcsetXxx(). - Thông thường, tên biến thuộc tính trong Java bắt đầu bằng chữ cái thường. Tuy nhiên, Java Bean cho phép tên biến bắt đầu bằng chữ cái viết hoa đối với một số từ viết tắt có ý nghĩa đặc biệt. Quy tắc này phải đảm bảo:
- Hai chữ cái đầu tiên của tên biến hoặc là viết hoa cả hai, hoặc là viết thường cả hai.
- Tuy nhiên, theo thông lệ lập trình, bạn nên đặt tên thuộc tính bằng chữ thường để dễ lập trình và đọc hiểu.
Với phương thức tiêm thuộc tính, Spring chỉ có thể đảm bảo việc tiêm khi bạn cấu hình trong tệp XML. Để đảm bảo các thuộc tính quan trọng được thiết lập ngay tại thời điểm khởi tạo, chúng ta cần sử dụng phương thức tiêm qua constructor.
2. Tiêm qua Constructor (Constructor Injection)
Tiêm qua constructor là một phương thức tiêm phổ biến khác, nó đảm bảo rằng các thuộc tính cần thiết sẽ được thiết lập ngay khi đối tượng bean được khởi tạo và sẵn sàng sử dụng.
Điều kiện tiên quyết để sử dụng tiêm qua constructor là lớp bean phải có một constructor có tham số.
Có một số cách để cấu hình tiêm qua constructor trong tệp XML:
- Theo kiểu dữ liệu của tham số (by type)
- Theo chỉ số của tham số (by index)
- Kết hợp kiểu dữ liệu và chỉ số
- Tự động khớp kiểu tham số thông qua Reflection
Theo kiểu dữ liệu của tham số
Tạo lớp bean NhanVien (Person):
package com.example.beans;
/**
* Lớp nhân viên để kiểm tra tiêm tham số theo kiểu dữ liệu.
*/
public class NhanVien {
private String hoTen;
private Integer namSinh;
// Constructor có tham số
public NhanVien(String hoTen, Integer namSinh) {
this.hoTen = hoTen;
this.namSinh = namSinh;
}
public String getHoTen() {
return hoTen;
}
public Integer getNamSinh() {
return namSinh;
}
}
Tệp cấu hình Spring (XML):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Định nghĩa bean nhân viên -->
<bean id="nhanVienBean" class="com.example.beans.NhanVien">
<constructor-arg type="String">
<value>SpringEmployee</value>
</constructor-arg>
<constructor-arg type="Integer">
<value>1990</value>
</constructor-arg>
</bean>
</beans>
Spring không quan tâm đến thứ tự của các thẻ <constructor-arg> trong tệp cấu hình, điều này giúp đảm bảo tính nhất quán của cấu hình.
Tuy nhiên, khi một constructor có nhiều tham số cùng kiểu dữ liệu, việc chỉ định theo kiểu dữ liệu có thể gây nhầm lẫn. Trong trường hợp đó, chúng ta cần sử dụng phương thức chỉ định theo chỉ số.
Theo chỉ số của tham số
Tạo lớp bean HocSinh (Student):
package com.example.beans;
/**
* Lớp học sinh để kiểm tra tiêm tham số theo chỉ số.
*/
public class HocSinh {
private String hoTen;
private String gioiTinh;
private Double diemSo;
// Constructor có tham số
public HocSinh(String hoTen, String gioiTinh, Double diemSo) {
this.hoTen = hoTen;
this.gioiTinh = gioiTinh;
this.diemSo = diemSo;
}
// Getters (tùy chọn cho ví dụ này)
}
Tệp cấu hình Spring (XML):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Định nghĩa bean học sinh -->
<bean id="hocSinhBean" class="com.example.beans.HocSinh">
<constructor-arg index="0" value="StudentName"/>
<constructor-arg index="1" value="Nam"/>
<constructor-arg index="2" value="7.5"/>
</bean>
</beans>
Mẹo: Cơ chế Reflection của Java không ghi nhớ tên tham số của constructor. Do đó, bạn không thể cấu hình tiêm qua constructor bằng cách chỉ định tên tham số. Thay vào đó, bạn chỉ có thể hoàn thành việc tiêm bằng cách sử dụng kiểu dữ liệu hoặc chỉ số của tham số.
Kết hợp kiểu dữ liệu và chỉ số
Trong các tệp cấu hình phức tạp, đôi khi cần sử dụng cả index và type để tiêm tham số constructor. Ví dụ sau minh họa trường hợp này.
Tạo lớp bean GiaoVien (Teacher):
package com.example.beans;
/**
* Lớp giáo viên để kiểm tra tiêm tham số kết hợp kiểu và chỉ số.
*/
public class GiaoVien {
private String hoTen;
private String diaChi;
private double luong;
private int tuoi;
// Constructor 1
public GiaoVien(String hoTen, String diaChi, double luong) {
this.hoTen = hoTen;
this.diaChi = diaChi;
this.luong = luong;
}
// Constructor 2
public GiaoVien(String hoTen, String diaChi, int tuoi) {
this.hoTen = hoTen;
this.diaChi = diaChi;
this.tuoi = tuoi;
}
// Getters (tùy chọn)
}
Trong lớp này có hai constructor trùng tên nhưng khác tham số. Nếu chỉ dùng index hoặc type có thể gây nhầm lẫn. Cần kết hợp cả hai để Spring xác định đúng constructor.
Tệp cấu hình Spring (XML):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Định nghĩa bean giáo viên -->
<bean id="giaoVienBean" class="com.example.beans.GiaoVien">
<constructor-arg index="0" type="String">
<value>TeacherName</value>
</constructor-arg>
<constructor-arg index="1" type="String">
<value>Vietnam</value>
</constructor-arg>
<constructor-arg index="2" type="int"> <!-- Tham số thứ 3 là int -->
<value>30</value>
</constructor-arg>
</bean>
</beans>
Điểm mấu chốt là chỉ định rõ kiểu dữ liệu của tham số thứ ba. Khi đó, Spring sẽ biết cần sử dụng constructor nào để tiêm tham số.
Mẹo: Nếu tệp cấu hình của bạn có sự mâu thuẫn hoặc không rõ ràng trong việc xác định constructor, Spring Container vẫn có thể khởi động bình thường và sẽ chọn ngẫu nhiên một constructor phù hợp để khởi tạo bean. Tuy nhiên, constructor được chọn ngẫu nhiên có thể không phải là cái bạn mong muốn. Do đó, bạn nên cẩn thận để tránh các trường hợp gây mâu thuẫn khi lập trình.
Tự động khớp kiểu tham số thông qua Reflection
Nếu kiểu dữ liệu của các tham số constructor có thể phân biệt được, Spring có thể tự động hoàn thành việc tiêm ngay cả khi bạn không chỉ định type hoặc index. Điều này là nhờ khả năng của Reflection trong Java có thể lấy thông tin kiểu dữ liệu của tham số constructor. Ví dụ với lớp QuanLy (Manager) dưới đây.
Tạo lớp bean QuanLy (Manager):
package com.example.beans;
/**
* Lớp quản lý để kiểm tra tiêm tham số tự động khớp kiểu.
*/
public class QuanLy {
private String hoTen;
private Double luong;
private NhanVien nhanVien; // Tham chiếu đến bean NhanVien
// Constructor có tham số
public QuanLy(String hoTen, Double luong, NhanVien nhanVien) {
this.hoTen = hoTen;
this.luong = luong;
this.nhanVien = nhanVien;
}
// Getters (tùy chọn)
}
Tệp cấu hình Spring (XML):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Định nghĩa bean quản lý -->
<bean id="quanLyBean" class="com.example.beans.QuanLy">
<constructor-arg>
<value>ManagerName</value>
</constructor-arg>
<constructor-arg>
<value>5000.0</value>
</constructor-arg>
<constructor-arg>
<ref bean="nhanVienBean"/> <!-- Tham chiếu đến bean NhanVien đã định nghĩa ở trên -->
</constructor-arg>
</bean>
</beans>
Tất cả các phương thức trên đều có thể thực hiện việc tiêm tham số constructor. Tuy nhiên, để tránh các vấn đề có thể xảy ra, bạn nên ưu tiên sử dụng chỉ số (index) và kiểu dữ liệu (type) một cách tường minh khi cấu hình tham số constructor.
3. Tiêm qua Phương thức Factory (Factory Method Injection)
Phương thức factory là một mẫu thiết kế (design pattern) phổ biến, là phương thức triển khai chính cho các nguyên lý Điều Khiển Ngược (Inversion of Control - IoC) và Thiết Kế Đơn Thể (Singleton). Lớp factory chịu trách nhiệm tạo ra một hoặc nhiều instance của lớp đích, và phương thức factory thường trả về instance của lớp đích dưới dạng interface hoặc lớp trừu tượng.
Lớp factory ẩn đi bước khởi tạo instance của lớp đích, người dùng thậm chí không cần biết lớp đích cụ thể là gì. Vì Spring Container cung cấp chức năng của phương thức factory như một framework và mở ra cho nhà phát triển một cách minh bạch, nên hiếm khi cần phải tự viết phương thức factory thủ công. Tuy nhiên, trong một số hệ thống cũ hoặc thư viện của bên thứ ba, bạn có thể gặp các phương thức factory và có thể sử dụng phương thức tiêm factory của Spring.
Spring hỗ trợ hai loại tiêm qua phương thức factory: không tĩnh (non-static) và tĩnh (static).
Phương thức Factory không tĩnh
Một số phương thức factory là không tĩnh, yêu cầu phải khởi tạo lớp factory trước khi gọi phương thức factory. Ví dụ minh họa:
Tạo lớp factory MayInSach (BookFactory):
package com.example.factory;
// Giả định có lớp Sach (Book)
// public class Sach { private String ten; ... }
/**
* Lớp factory để kiểm tra tiêm phương thức factory không tĩnh.
*/
public class MayInSach {
public Sach muaSach() {
Sach sach = new Sach();
sach.setTen("Think in Java");
return sach;
}
}
Tệp cấu hình Spring (XML):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Định nghĩa bean cho lớp factory -->
<bean id="mayInSachFactory" class="com.example.factory.MayInSach"></bean>
<!-- Định nghĩa bean Sach, sử dụng phương thức factory từ mayInSachFactory -->
<bean id="sachBean" class="com.example.beans.Sach"
factory-bean="mayInSachFactory"
factory-method="muaSach"></bean>
</beans>
Vì phương thức factory của mayInSachFactory không tĩnh, chúng ta cần định nghĩa một bean cho lớp factory này trước, sau đó sử dụng thuộc tính factory-bean để tham chiếu đến instance của factory bean và thuộc tính factory-method để chỉ định phương thức factory tương ứng.
Phương thức Factory tĩnh
Nhiều phương thức factory là tĩnh, cho phép gọi trực tiếp phương thức của lớp mà không cần khởi tạo instance của lớp factory. Điều này làm cho phương thức factory tĩnh tiện lợi và gọn gàng hơn phương thức không tĩnh.
Tạo lớp factory NhàMayXe (CarFactory):
package com.example.factory;
// Giả định có lớp Xe (Car)
// public class Xe { private String thuongHieu; ... }
/**
* Lớp factory để kiểm tra tiêm phương thức factory tĩnh.
*/
public class NhaMayXe {
public static Xe taoXe() {
Xe xe = new Xe();
xe.setThuongHieu("Lamborghini");
return xe;
}
}
Tệp cấu hình Spring (XML):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Định nghĩa bean Xe, sử dụng phương thức factory tĩnh -->
<bean id="xeBean" class="com.example.beans.Xe"
factory-method="taoXe"></bean>
</beans>
Với phương thức factory tĩnh, bạn chỉ cần chỉ định factory-method, Spring sẽ gọi trực tiếp phương thức tĩnh đó trên lớp được chỉ định trong thuộc tính class.
Tổng kết
Spring cung cấp ba phương thức tiêm chính. Việc lựa chọn phương thức nào trong thực tế không có một tiêu chuẩn thống nhất, tuy nhiên, có một số lý do để cân nhắc:
Lý do nên dùng Tiêm qua Constructor:
- Đảm bảo các thuộc tính quan trọng được thiết lập trước khi đối tượng được sử dụng.
- Không cần cung cấp phương thức setter cho mọi thuộc tính, giúp giảm số lượng phương thức trong lớp.
- Tăng cường đóng gói cho các biến của lớp, tránh các lỗi gọi ngoài ý muốn.
Lý do nên dùng Tiêm thuộc tính:
- Khi số lượng thuộc tính quá nhiều, constructor có thể trở nên cồng kềnh.
- Tiêm qua constructor kém linh hoạt, đôi khi cần tiêm giá trị
nullcho thuộc tính. - Khi có nhiều constructor, cấu hình có thể trở nên mơ hồ và phức tạp hơn.
- Tiêm qua constructor không thuận lợi cho việc kế thừa và mở rộng lớp.
- Tiêm qua constructor có thể gây ra vấn đề phụ thuộc vòng (circular dependency).
Spring cung cấp nhiều phương thức để tiêm tham số, mỗi phương thức đều có ưu điểm riêng cho những trường hợp cụ thể. Bạn chỉ cần lựa chọn phương thức phù hợp với nhu cầu sử dụng. Tuy nhiên, thông thường, phương thức tiêm qua factory ít được khuyến khích sử dụng hơn.