Chào mừng bạn quay trở lại với Lộ trình Java Spring! Trong bài viết trước, Tại sao chọn Spring?, chúng ta đã nói về lý do Spring trở thành framework phổ biến và mạnh mẽ trong thế giới Java. Chúng tôi đã đề cập đến những lợi ích của nó – giúp phát triển ứng dụng dễ dàng hơn, linh hoạt hơn và ít đau đầu hơn.
Giờ đây, khi bạn đi sâu vào hệ sinh thái Spring, bạn sẽ bắt gặp một số thuật ngữ nghe có vẻ… hơi chuyên ngành. Những thuật ngữ như “IoC Container”, “Dependency Injection”, “Beans” và “Autowiring”. Chúng là những khối xây dựng cơ bản của Spring, và hiểu rõ chúng là điều cực kỳ quan trọng. Nếu không nắm vững những khái niệm này, hành trình học Spring của bạn sẽ giống như đi trong mê cung mà bịt mắt.
Nhưng đừng lo! Bài viết này được thiết kế để giải mã những khái niệm cốt lõi đó. Chúng tôi sẽ giải thích chúng bằng những phép so sánh đơn giản, như thể chúng tôi đang giải thích điều gì đó phức tạp cho một đứa trẻ 5 tuổi (nhưng là một đứa trẻ 5 tuổi thông minh và hiểu nguyên lý phần mềm một cách trực quan). Mục tiêu không phải là đưa ra định nghĩa học thuật chính xác nhất, mà là xây dựng sự hiểu biết trực quan dễ nhớ.
Sẵn sàng chưa? Hãy cùng khám phá trái tim của Spring Core!
Mục lục
Ý Tưởng Lớn: Đảo Ngược Điều Khiển (IoC)
Được rồi, hãy bắt đầu với một khái niệm nghe có vẻ sang chảnh nhưng thực ra khá đơn giản: Đảo ngược điều khiển (IoC).
Hãy tưởng tượng bạn đang xây dựng một thứ phức tạp, như một siêu robot. Trong lập trình truyền thống, khi robot (một đối tượng) của bạn cần một bộ phận khác (một đối tượng khác) để thực hiện một tác vụ – ví dụ như một cánh tay gắp đặc biệt – bạn thường sẽ tự tạo ra cánh tay đó trong mã của robot. Như thế này:
class Robot {
private GrippingArm arm;
public Robot() {
// Bạn (Robot) tự tạo ra cánh tay
this.arm = new GrippingArm();
}
public void grabSomething() {
arm.grip();
}
}
class GrippingArm {
public void grip() {
System.out.println("Gripping!");
}
}
Bạn, đối tượng Robot
, kiểm soát việc tạo ra phụ thuộc của mình (cánh tay GrippingArm
). Bạn quyết định khi nào và cách tạo ra nó.
IoC đảo ngược điều này. Thay vì *bạn* tạo ra những thứ bạn cần, *ai đó khác* (trong trường hợp này là Spring Framework) tạo chúng và cung cấp cho bạn. Bạn *đảo ngược* quyền kiểm soát việc tạo và quản lý đối tượng từ mã của bạn sang framework.
Hãy nghĩ về việc gọi món trong nhà hàng. Khi bạn muốn pizza, bạn không vào bếp, mua nguyên liệu, nhào bột, thêm topping và tự nướng (đó là kiểm soát truyền thống). Thay vào đó, bạn nói với người phục vụ bạn muốn gì (bạn thể hiện nhu cầu), và nhà bếp (framework/Spring) làm nó và mang đến bàn. Bạn đã đảo ngược quyền kiểm soát việc tạo pizza từ bạn sang nhà hàng.
Vì vậy, IoC là nguyên tắc mà một framework đảm nhận việc tạo và quản lý các đối tượng cùng các phụ thuộc của chúng cho bạn. Các đối tượng của bạn khai báo *chúng cần gì*, và framework tìm cách *cung cấp* nó.
Cách Thực Hiện: Dependency Injection (DI)
IoC là nguyên tắc, và Dependency Injection (DI) là *mẫu hình* hoặc *cơ chế* cụ thể mà Spring sử dụng để đạt được IoC.
Nếu IoC là ý tưởng nhà hàng làm pizza cho bạn, thì Dependency Injection là người phục vụ *mang* pizza đó đến bàn. Người phục vụ *tiêm* pizza (phụ thuộc) vào trải nghiệm ăn uống (đối tượng/lớp của bạn).
Thay vì lớp Robot
tự tạo GrippingArm
, Spring tạo GrippingArm
rồi *tiêm* nó vào lớp Robot
. Lớp Robot
của bạn chỉ cần khai báo rằng nó *cần* một GrippingArm
.
Dưới đây là cách nó có thể trông như thế về mặt khái niệm trong Spring (chúng ta sẽ xem mã thực sự sớm thôi):
class Robot {
// Bạn khai báo bạn cần một GrippingArm
private GrippingArm arm;
// Spring tiêm nó, có thể thông qua constructor
public Robot(GrippingArm arm) {
this.arm = arm;
}
public void grabSomething() {
// Và bạn có thể sử dụng nó!
arm.grip();
}
}
// Spring cũng quản lý đối tượng GrippingArm
class GrippingArm {
public void grip() {
System.out.println("Gripping!");
}
}
Thấy sự khác biệt chưa? Robot
không còn sử dụng new GrippingArm()
. Nó chỉ nhận cánh tay từ “bên ngoài”. Đây chính là Dependency Injection.
Spring thường có ba cách để tiêm phụ thuộc:
- Constructor Injection: Phụ thuộc được cung cấp thông qua constructor của lớp (như trong ví dụ trên). Cách này thường được ưu tiên vì nó đảm bảo đối tượng được tạo ra với tất cả các phụ thuộc cần thiết.
- Setter Injection: Phụ thuộc được cung cấp thông qua một setter method (`setSomething(Dependency dependency)`).
- Field Injection: Phụ thuộc được tiêm trực tiếp vào một trường private bằng các annotation (như `@Autowired`). Dù tiện lợi nhưng cách này đôi khi khiến việc kiểm thử trở nên khó khăn hơn.
Dependency Injection giúp mã của bạn trở nên mô-đun hóa và dễ kiểm thử hơn. Bạn có thể dễ dàng “tiêm” một phiên bản khác của phụ thuộc (như một cánh tay giả để kiểm thử) mà không cần thay đổi logic cốt lõi của Robot.
Người Quản Lý: Spring Container
Vậy, nếu IoC là ý tưởng và DI là phương thức thực hiện, ai là người làm tất cả những việc này? Ai tạo ra các đối tượng và tiêm chúng?
Đó là nhiệm vụ của **Spring Container**.
Hãy nghĩ về Spring Container như hệ thống quản lý trung tâm hay khu bếp nhộn nhịp trong ví dụ nhà hàng của chúng ta. Đó là nơi mọi thứ diễn ra. Container đọc cấu hình của bạn (chúng ta sẽ nói về điều này sau), xác định đối tượng (Bean) nào cần được tạo, tạo chúng, quản lý vòng đời của chúng (từ khi sinh ra đến khi chết đi), và kết nối chúng lại với nhau bằng cách tiêm các phụ thuộc.
Spring Container chịu trách nhiệm:
- Tạo các đối tượng (Beans).
- Cấu hình các đối tượng (thiết lập thuộc tính).
- Kết nối các đối tượng với nhau (Dependency Injection).
- Quản lý vòng đời của đối tượng (khi nào nó được tạo, khi nào sẵn sàng, khi nào bị hủy).
Có hai loại Spring Container chính:
BeanFactory
: Container cơ bản nhất. Nó cung cấp khả năng DI cơ bản.ApplicationContext
: Đây là container nâng cao mà bạn thường sử dụng, đặc biệt là trong Spring Boot. Nó mở rộngBeanFactory
và thêm nhiều tính năng dành riêng cho doanh nghiệp như tích hợp dễ dàng hơn với các tính năng AOP của Spring, xử lý tài nguyên thông báo, xuất bản sự kiện và các ngữ cảnh ứng dụng web.
Trong quá trình phát triển hàng ngày, bạn sẽ thường tương tác với ApplicationContext
. Đó là “môi trường Spring” ứng dụng của bạn chạy trong đó, quản lý tất cả các đối tượng quan trọng của bạn.
Khối Xây Dựng: Beans
Được rồi, Container quản lý mọi thứ. Vậy những “thứ” đó là gì? Chúng được gọi là **Beans**.
Trong Spring, một “Bean” đơn giản là một đối tượng được khởi tạo, lắp ráp và quản lý bởi Spring IoC Container. Hầu như bất kỳ đối tượng Java thông thường nào (POJO – Plain Old Java Object) đều có thể trở thành Spring Bean.
Trong ví dụ nhà hàng của chúng ta, Container là bản thân nhà hàng, còn Beans là các món thực đơn – Bean “Pizza”, Bean “Pasta”, Bean “Waiter”, Bean “Chef”, v.v. Mỗi thứ là một đối tượng riêng biệt mà nhà hàng (Container) biết cách tạo, cấu hình và cung cấp.
Vì vậy, khi bạn nghe “Spring Bean”, chỉ cần nghĩ “một đối tượng mà Spring đang quản lý”.
Đưa Ra Hướng Dẫn: Cấu Hình Spring
Làm thế nào Spring Container biết đối tượng (Bean) nào cần tạo, các phụ thuộc của chúng là gì và làm thế nào để kết nối chúng? Bạn phải nói với nó!
Đây là lúc **Cấu hình Spring** xuất hiện. Bạn cung cấp hướng dẫn cho Spring Container.
Trước đây, cấu hình Spring chủ yếu được thực hiện bằng các tệp XML. Bạn sẽ viết XML để yêu cầu Spring tạo một phiên bản của `com.example.Robot`, một phiên bản của `com.example.GrippingArm`, và sau đó tiêm `GrippingArm` vào `Robot`.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Định nghĩa Bean GrippingArm -->
<bean id="grippingArm" class="com.example.GrippingArm"/>
<!-- Định nghĩa Bean Robot và tiêm cánh tay -->
<bean id="myRobot" class="com.example.Robot">
<constructor-arg ref="grippingArm"/> <!-- Tiêm bằng cách tham chiếu đến Bean arm -->
</bean>
</beans>
Mặc dù XML vẫn được hỗ trợ, nhưng phát triển Spring hiện đại chủ yếu sử dụng **Cấu hình dựa trên Annotation** và **Cấu hình dựa trên Java**.
Cấu hình dựa trên Annotation: Bạn đặt các dấu hiệu đặc biệt (annotation) trực tiếp vào mã Java để nói với Spring về các Beans và phụ thuộc. Các annotation phổ biến bao gồm:
@Component
: Một annotation chung cho biết một lớp là một thành phần (Bean) do Spring quản lý.@Service
,@Repository
,@Controller
: Các phiên bản chuyên biệt của `@Component` cho các tầng cụ thể (tầng dịch vụ, tầng truy cập dữ liệu, tầng trình bày). Chúng mang ý nghĩa ngữ nghĩa.@Autowired
: Báo cho Spring tự động tiêm phụ thuộc vào trường này, constructor, hoặc setter.
Cấu hình dựa trên Java: Bạn tạo các lớp được đánh dấu bằng @Configuration
và định nghĩa các phương thức được đánh dấu bằng @Bean
. Các phương thức này trả về các đối tượng mà bạn muốn Spring quản lý như Beans.
@Configuration
public class AppConfig {
@Bean // Phương thức này tạo ra một Bean
public GrippingArm grippingArm() {
return new GrippingArm(); // Spring sẽ quản lý đối tượng này
}
@Bean // Phương thức này tạo ra một Bean khác
public Robot myRobot(GrippingArm arm) { // Spring tiêm GrippingArm vào đây!
return new Robot(arm); // Tạo Robot bằng cánh tay được tiêm
}
}
Cấu hình dựa trên Java mạnh mẽ vì nó sử dụng mã Java thực sự để định nghĩa cách các bean được tạo và kết nối, mang lại sự linh hoạt hơn so với XML.
Trong Spring Boot, cách tiếp cận phổ biến nhất là kết hợp cấu hình dựa trên annotation (để đánh dấu các thành phần) và thường là một số cấu hình dựa trên Java (để tạo bean phức tạp hơn hoặc thư viện bên thứ ba).
Kết Nối Các Điểm: Autowiring
Chúng tôi đã đề cập sơ qua về @Autowired
. Hãy xem kỹ hơn.
**Autowiring** là một cơ chế trong Spring cho phép bạn để Spring tự động giải quyết và tiêm các beans cộng tác vào bean của bạn.
Cách phổ biến nhất để sử dụng autowiring là với annotation `@Autowired`:
@Component // Báo với Spring quản lý lớp này như một Bean
public class Robot {
// Báo với Spring tự động tìm và tiêm một Bean GrippingArm vào đây
@Autowired
private GrippingArm arm;
public void grabSomething() {
if (arm != null) { // Kiểm tra phòng ngừa, dù Spring đảm bảo nó không null khi thành công
arm.grip();
} else {
System.out.println("Arm chưa được tiêm!");
}
}
// Lưu ý: Với tiêm trường như thế này, constructor thường trống hoặc không có,
// vì Spring tiêm *sau* khi tạo đối tượng. Tiêm constructor thường được ưu tiên:
/*
private final GrippingArm arm; // Đặt là final!
@Autowired // Không bắt buộc nếu chỉ có một constructor
public Robot(GrippingArm arm) {
this.arm = arm;
}
*/
}
@Component // Báo với Spring quản lý lớp này như một Bean nữa
class GrippingArm {
public void grip() {
System.out.println("Gripping!");
}
}
Khi Spring thấy @Autowired
trên trường arm
trong Robot
, nó tìm trong Container một Bean kiểu GrippingArm
và, nếu tìm thấy, sẽ tiêm vào đó. Ma thuật!
Bạn có thể dùng `@Autowired` trên constructor, phương thức setter, hoặc trường. Tiêm constructor thường được khuyến nghị vì nó làm rõ các phụ thuộc và giúp tạo các đối tượng bất biến.
Bao Nhiêu Bản Sao? Phạm Vi Bean
Khi Spring tạo một Bean, bao nhiêu phiên bản của đối tượng đó được tạo ra? Điều này được xác định bởi **phạm vi** của Bean.
Hãy nghĩ lại về nhà hàng. Một số thứ, như Bếp Trưởng, thường chỉ có một người điều hành cả nhà bếp. Những thứ khác, như một món tráng miệng mới, một cái mới được làm *mỗi lần* có người gọi.
Spring định nghĩa một số phạm vi cho bean:
- Singleton (Phạm Vi Mặc Định): Đây là phạm vi mặc định. Spring chỉ tạo **một phiên bản duy nhất** của bean cho mỗi Spring Container. Mỗi lần bạn yêu cầu Container cung cấp bean này, bạn nhận được cùng một phiên bản. Giống như Bếp Trưởng – một phiên bản được chia sẻ bởi mọi người.
- Prototype: Spring tạo **một phiên bản mới** của bean mỗi lần nó được yêu cầu. Giống như món tráng miệng mới – mỗi yêu cầu nhận được một cái hoàn toàn mới.
Đây là hai phạm vi phổ biến nhất bạn sẽ gặp trong Spring Core. Các phạm vi khác như `request`, `session`, và `application` liên quan đến các ứng dụng web và quản lý beans trong thời gian của một yêu cầu web, phiên, hoặc toàn bộ ứng dụng web tương ứng.
Bạn định nghĩa phạm vi bằng annotation `@Scope`:
@Component
@Scope("prototype") // Giờ Spring tạo một GrippingArm mới mỗi lần
public class GrippingArm {
// ...
}
@Component
@Scope("singleton") // Đây là mặc định, nên về mặt kỹ thuật không cần thiết ở đây, nhưng rõ ràng
public class Robot {
// ...
}
Hiểu về phạm vi rất quan trọng vì nó ảnh hưởng đến việc quản lý trạng thái và cách các beans tương tác, đặc biệt trong các ứng dụng đa luồng.
Bảng Tóm Tắt: Thuật Ngữ Cốt Lõi Của Spring Một Cách Ngắn Gọn
Hãy tổng hợp các khái niệm cốt lõi này trong một bảng đơn giản:
Thuật Ngữ | Giải Thích Đơn Giản | Ví Dụ Nhà Hàng |
---|---|---|
IoC (Đảo Ngược Điều Khiển) | Nguyên tắc: để một framework (Spring) tạo và quản lý các đối tượng của bạn thay vì bạn tự làm. | Ý tưởng nhà hàng làm đồ ăn cho bạn, thay vì bạn tự làm. |
DI (Tiêm Phụ Thuộc) | Mẫu hình/cơ chế: Spring cung cấp (tiêm) cho một đối tượng những đối tượng (phụ thuộc) khác mà nó cần để hoạt động. | Người phục vụ mang đồ ăn được yêu cầu (phụ thuộc) đến bàn (đối tượng của bạn). |
Spring Container | Môi trường/quản lý: Nó tạo, cấu hình và kết nối các đối tượng (Beans) của bạn bằng IoC/DI. | Toàn bộ hoạt động nhà hàng: bếp, nhân viên và hệ thống quản lý đơn đặt hàng và bàn. |
Bean | Một đối tượng được quản lý bởi Spring Container. | Một món trong thực đơn – một món ăn, đồ uống, v.v. |
Cấu Hình | Hướng dẫn cho Spring Container biết cách tạo và kết nối các Beans (XML, Annotation, JavaConfig). | Thực đơn và công thức – chi tiết những món có sẵn và cách chúng được làm và kết hợp. |
Autowiring (@Autowired) | Tự động tìm và tiêm phụ thuộc dựa trên kiểu hoặc tên, thay vì cấu hình rõ ràng. | Một người phục vụ thông minh biết bạn cần gì tiếp theo dựa trên đơn đặt hàng và mang đến mà không cần bạn yêu cầu từng món. |
Phạm Vi Singleton | Chỉ một phiên bản của Bean được tạo cho mỗi Container, chia sẻ ở mọi nơi. (Mặc định) | Bếp Trưởng – chỉ có một phiên bản tồn tại trong bếp. |
Phạm Vi Prototype | Một phiên bản mới của Bean được tạo mỗi lần nó được yêu cầu. | Một món tráng miệng mới được chuẩn bị – một cái mới được làm cho mỗi đơn đặt hàng. |
Tại Sao Điều Này Quan Trọng?
Bạn có thể nghĩ, “Có vẻ như rất nhiều bước phụ chỉ để tạo một đối tượng!” Nhưng hiểu được những khái niệm cốt lõi này mở ra sức mạnh thực sự của Spring:
- Giảm Kết Dính: Các đối tượng của bạn không cần biết *cách* tạo các phụ thuộc, chỉ cần biết *chúng cần* chúng. Điều này làm các thành phần ít phụ thuộc vào nhau hơn.
- Khả Năng Kiểm Thử: Vì các phụ thuộc được tiêm, bạn có thể dễ dàng thay thế phụ thuộc thực bằng các đối tượng giả trong quá trình kiểm thử. Bạn có thể kiểm thử logic của
Robot
mà không cần mộtGrippingArm
thực sự. - Dễ Bảo Trì: Thay đổi cách phụ thuộc được tạo hoặc triển khai nào được sử dụng thường chỉ cần thay đổi cấu hình, không phải mã sử dụng phụ thuộc.
- Giảm Boilerplate: Spring xử lý tác vụ lặp đi lặp lại của việc tạo và kết nối đối tượng, cho phép bạn tập trung vào logic nghiệp vụ của ứng dụng.
Những nguyên tắc này, đặc biệt là IoC và DI được quản lý bởi Spring Container, là nền tảng của tất cả các module Spring khác (Spring MVC, Spring Data, Spring Security, Spring Boot). Hiểu rõ những điều này, mọi thứ khác sẽ trở nên dễ hiểu hơn nhiều.
Tổng Kết
Chúng ta đã đề cập đến những điều cơ bản nhất của Spring Core: IoC, DI, Container, Beans, Cấu hình, Autowiring, và Phạm vi. Hãy coi chúng như từ vựng cơ bản bạn cần để bắt đầu nói “ngôn ngữ Spring”.
Đừng lo lắng nếu bạn không cảm thấy mình thành thạo ngay lập tức. Hãy đọc lại các khái niệm này, thử nghiệm với các ví dụ đơn giản (chỉ cần tạo một vài bean và kết nối chúng thủ công hoặc với `@Autowired`), và xem cách chúng hoạt động trong thực tế. Bạn càng xây dựng nhiều với Spring, những thuật ngữ này càng trở nên trực quan.
Trong phần tiếp theo của loạt bài Lộ trình Java Spring, chúng ta sẽ bắt đầu xem các khái niệm cốt lõi này được sử dụng như thế nào trong thế giới thực, đặc biệt trong ngữ cảnh của Spring Boot, giúp việc thiết lập và sử dụng Spring Container cùng các bean trở nên cực kỳ nhanh chóng.
Cho đến lúc đó, hãy tiếp tục thử nghiệm và xây dựng! Chúc bạn mã hóa vui vẻ!