Hôm nay, chúng ta sẽ tìm hiểu vềDependency Injection trong Spring (tiêm phụ thuộc trong Spring). Hai khái niệm cốt lõi của Spring Framework là Dependency Injection (DI) và Lập trình hướng khía cạnh (Aspect Oriented Programming – AOP).
Trước đây tôi đã viết về Dependency Injection trong Java và cách sử dụng framework Google Guice để tự động hóa quy trình này trong các ứng dụng.

Dependency Injection
Hướng dẫn này nhằm cung cấp chi tiết ví dụ về Spring Dependency Injection với cả hai phương pháp cấu hình bằng annotation và dựa trên file XML.
Tôi cũng sẽ đưa ra ví dụ về test case với JUnit, vì khả năng kiểm thử dễ dàng là một trong những lợi ích chính của dependency injection.
Tôi đã tạo một dự án Maven có tên spring-dependency-injection với cấu trúc như hình minh họa bên dưới.

Spring Dependency Injection – Các dependency Maven
Tôi đã thêm các dependency của Spring và JUnit vào file pom.xml. Dưới đây là mã pom.xml hoàn chỉnh.
<project xmlns="<https://maven.apache.org/POM/4.0.0>" xmlns:xsi="<https://www.w3.org/2001/XMLSchema-instance>"
xsi:schemaLocation="<https://maven.apache.org/POM/4.0.0> <https://maven.apache.org/xsd/maven-4.0.0.xsd>">
<modelVersion>4.0.0</modelVersion>
<groupId>com.journaldev.spring</groupId>
<artifactId>spring-dependency-injection</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Phiên bản ổn định hiện tại của Spring Framework là 4.0.0.RELEASE và của JUnit là 4.8.1. Nếu bạn đang dùng phiên bản khác, có thể sẽ cần thực hiện một số điều chỉnh nhỏ trong dự án. Khi build dự án, Maven sẽ tự động thêm một số tệp JAR khác vào dependency do cơ chế transitive dependencies (phụ thuộc bắc cầu), như hình minh họa.
Spring Dependency Injection – Các lớp dịch vụ
Giả sử chúng ta muốn gửi email và tin nhắn Twitter đến người dùng. Để sử dụng Dependency Injection, trước hết cần có một lớp cơ sở cho các dịch vụ này. Vì vậy, tôi tạo một interface có tên là MessageService với một phương thức duy nhất được khai báo dùng để gửi tin nhắn.
package com.journaldev.spring.di.services;
public interface MessageService {
boolean sendMessage(String msg, String rec);
}
Bây giờ chúng ta sẽ tạo các lớp triển khai thực tế để gửi thông điệp qua email và Twitter.
package com.journaldev.spring.di.services;
public class EmailService implements MessageService {
public boolean sendMessage(String msg, String rec) {
System.out.println("Email Sent to " + rec + " with Message=" + msg);
return true;
}
}
package com.journaldev.spring.di.services;
public class TwitterService implements MessageService {
public boolean sendMessage(String msg, String rec) {
System.out.println("Twitter message Sent to " + rec + " with Message=" + msg);
return true;
}
}
Khi đã có đủ các lớp dịch vụ, chúng ta sẽ chuyển sang các lớp thành phần sử dụng các dịch vụ này.
Spring Dependency Injection – Các lớp thành phần
Bạn hãy viết một lớp consumer sử dụng các dịch vụ trên. Chúng ta sẽ xây dựng hai lớp consumer: một lớp sử dụng annotation của Spring để autowire và một lớp không sử dụng annotation mà cấu hình được định nghĩa trong tệp XML.
package com.journaldev.spring.di.consumer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;
import com.journaldev.spring.di.services.MessageService;
@Component
public class MyApplication {
// dependency injection thông qua thuộc tính (field-based)
//@Autowired
private MessageService service;
// dependency injection thông qua constructor
//@Autowired
//public MyApplication(MessageService svc){
// this.service = svc;
//}
@Autowired
public void setService(MessageService svc){
this.service = svc;
}
public boolean processMessage(String msg, String rec){
// xử lý như kiểm tra hợp lệ, ghi log...
return this.service.sendMessage(msg, rec);
}
}
Một số điểm quan trọng liên quan đến lớp MyApplication:
- Annotation
@Componentđược gán cho lớp, nhờ đó khi Spring quét các thành phần thì lớp này sẽ được nhận diện là một bean. Annotation@Componentchỉ áp dụng cho lớp và có phạm vi tồn tại ở thời gian chạy (Runtime). Nếu bạn chưa quen với chính sách tồn tại (retention policy) của annotation, bạn nên tham khảo tài liệu về annotation trong Java. - Annotation
@Autowiredthông báo cho Spring biết rằng cần thực hiện autowiring. Annotation này có thể áp dụng cho field, constructor hoặc phương thức. Nó cho phép chúng ta triển khai dependency injection theo kiểu constructor-based, field-based hoặc method-based trong các thành phần của ứng dụng. - Trong ví dụ này, chúng ta sử dụng method-based để thực hiện dependency injection. Nếu muốn chuyển sang kiểu constructor-based, bạn có thể bỏ comment phần constructor.
Tiếp theo, chúng ta viết một lớp tương tự nhưng không sử dụng annotation.
package com.journaldev.spring.di.consumer;
import com.journaldev.spring.di.services.MessageService;
public class MyXMLApplication {
private MessageService service;
// dependency injection thông qua constructor
//public MyXMLApplication(MessageService svc) {
// this.service = svc;
//}
// dependency injection thông qua setter
public void setService(MessageService svc){
this.service = svc;
}
public boolean processMessage(String msg, String rec) {
// xử lý như kiểm tra hợp lệ, ghi log...
return this.service.sendMessage(msg, rec);
}
}
Một lớp ứng dụng đơn giản sử dụng dịch vụ. Đối với cấu hình dựa trên XML, chúng ta có thể triển khai Dependency Injection trong Spring theo hai cách: thông qua constructor hoặc thông qua phương thức. Lưu ý phương pháp injection thông qua phương thức và thông qua setter là như nhau, chỉ khác tên gọi, một số người gọi là setter-based injection, những người khác thì gọi là method-based injection.
Cấu hình Dependency Injection trong Spring bằng Annotation
Đối với cấu hình dựa trên annotation, chúng ta cần viết một lớp cấu hình (Configurator) dùng để cung cấp bean triển khai thực tế vào thuộc tính của component.
package com.journaldev.spring.di.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import com.journaldev.spring.di.services.EmailService;
import com.journaldev.spring.di.services.MessageService;
@Configuration
@ComponentScan(value={"com.journaldev.spring.di.consumer"})
public class DIConfiguration {
@Bean
public MessageService getMessageService(){
return new EmailService();
}
}
Một số điểm quan trọng liên quan đến lớp trên:
- Annotation
@Configurationđược sử dụng để thông báo cho Spring rằng đây là một lớp cấu hình. - Annotation
@ComponentScanđược sử dụng cùng với@Configurationđể chỉ định các package mà Spring cần quét tìm các lớp component. - Annotation
@Beanđược sử dụng để thông báo cho Spring Framework biết rằng phương thức này sẽ được dùng để cung cấp đối tượng Bean cần inject vào các lớp component.
Ta sẽ viết một chương trình đơn giản để kiểm tra ví dụ Dependency Injection trong Spring sử dụng annotation.
package com.journaldev.spring.di.test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import com.journaldev.spring.di.configuration.DIConfiguration;
import com.journaldev.spring.di.consumer.MyApplication;
public class ClientApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DIConfiguration.class);
MyApplication app = context.getBean(MyApplication.class);
app.processMessage("Hi Pankaj", "pankaj@abc.com");
// đóng context
context.close();
}
}
AnnotationConfigApplicationContext là một lớp triển khai của lớp trừu tượng AbstractApplicationContext và được sử dụng để autowire các dịch vụ vào component khi sử dụng annotation. Hàm dựng của AnnotationConfigApplicationContext nhận vào một lớp kiểu Class, lớp này sẽ được sử dụng để lấy bean triển khai để inject vào các component. Phương thức getBean(Class) trả về đối tượng component và sử dụng cấu hình đã cung cấp để tự động liên kết các đối tượng. Vì đối tượng context tiêu tốn tài nguyên hệ thống nên chúng ta nên đóng nó sau khi sử dụng xong.
Khi chạy chương trình trên, ta sẽ nhận được kết quả sau:
Dec 16, 2013 11:49:20 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@3067ed13: startup date [Mon Dec 16 23:49:20 PST 2013]; root of context hierarchy
Email Sent to pankaj@abc.com with Message=Hi Pankaj
Dec 16, 2013 11:49:20 PM org.springframework.context.support.AbstractApplicationContext doClose
INFO: Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@3067ed13: startup date [Mon Dec 16 23:49:20 PST 2013]; root of context hierarchy
Cấu hình Dependency Injection trong Spring bằng XML
Chúng ta sẽ tạo một tệp cấu hình Spring với nội dung sau. Tên tệp có thể đặt tùy ý. Dưới đây là mã của applicationContext.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="<https://www.springframework.org/schema/beans>"
xmlns:xsi="<https://www.w3.org/2001/XMLSchema-instance>"
xsi:schemaLocation="
<https://www.springframework.org/schema/beans> <https://www.springframework.org/schema/beans/spring-beans-4.0.xsd>">
<!--
<bean id="MyXMLApp" class="com.journaldev.spring.di.consumer.MyXMLApplication">
<constructor-arg>
<bean class="com.journaldev.spring.di.services.TwitterService" />
</constructor-arg>
</bean>
-->
<bean id="twitter" class="com.journaldev.spring.di.services.TwitterService"></bean>
<bean id="MyXMLApp" class="com.journaldev.spring.di.consumer.MyXMLApplication">
<property name="service" ref="twitter"></property>
</bean>
</beans>
Lưu ý, XML trên bao gồm cấu hình cho cả hai phương pháp dependency injection trong Spring: constructor-based và setter-based. Vì lớp MyXMLApplication sử dụng phương thức setter để inject, nên trong cấu hình bean sẽ dùng phần tử property để thực hiện injection. Nếu sử dụng phương thức constructor, chúng ta cần dùng phần tử constructor-arg.
Tệp cấu hình XML này được đặt trong thư mục nguồn, do đó sau khi biên dịch, nó sẽ nằm trong thư mục classes.
Tiếp theo, ta sẽ xem cách sử dụng cấu hình XML thông qua một chương trình đơn giản.
package com.journaldev.spring.di.test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.journaldev.spring.di.consumer.MyXMLApplication;
public class ClientXMLApplication {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
"applicationContext.xml");
MyXMLApplication app = context.getBean(MyXMLApplication.class);
app.processMessage("Hi Pankaj", "pankaj@abc.com");
// đóng context
context.close();
}
}
ClassPathXmlApplicationContext được sử dụng để lấy đối tượng ApplicationContext thông qua đường dẫn tới tệp cấu hình. Lớp này cung cấp nhiều constructor được nạp chồng, cho phép chỉ định nhiều tệp cấu hình khác nhau. Phần còn lại của mã tương tự như chương trình kiểm thử cấu hình bằng annotation, điểm khác biệt duy nhất là cách khởi tạo đối tượng ApplicationContext dựa trên lựa chọn cấu hình.
Khi chạy chương trình trên, ta sẽ nhận được kết quả sau:
Dec 17, 2013 12:01:23 AM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@4eeaabad: startup date [Tue Dec 17 00:01:23 PST 2013]; root of context hierarchy
Dec 17, 2013 12:01:23 AM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [applicationContext.xml]
Twitter message Sent to pankaj@abc.com with Message=Hi Pankaj
Dec 17, 2013 12:01:23 AM org.springframework.context.support.AbstractApplicationContext doClose
INFO: Closing org.springframework.context.support.ClassPathXmlApplicationContext@4eeaabad: startup date [Tue Dec 17 00:01:23 PST 2013]; root of context hierarchy
Lưu ý, một phần nội dung đầu ra được sinh bởi chính Spring Framework. Vì Spring sử dụng thư viện log4j để ghi log và nếu chưa cấu hình log4j thì log sẽ được in trực tiếp ra console.
Kiểm thử Spring Dependency Injection bằng JUnit
Một trong những lợi ích lớn nhất của dependency injection trong Spring là khả năng sử dụng các lớp dịch vụ giả lập (mock) thay vì các dịch vụ thực tế. Dưới đây là ví dụ kiểm thử kết hợp toàn bộ kiến thức đã trình bày ở trên trong một lớp kiểm thử JUnit 4 duy nhất.
package com.journaldev.spring.di.test;
import org.junit.Assert;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import com.journaldev.spring.di.consumer.MyApplication;
import com.journaldev.spring.di.services.MessageService;
@Configuration
@ComponentScan(value="com.journaldev.spring.di.consumer")
public class MyApplicationTest {
private AnnotationConfigApplicationContext context = null;
@Bean
public MessageService getMessageService() {
return new MessageService(){
public boolean sendMessage(String msg, String rec) {
System.out.println("Mock Service");
return true;
}
};
}
@Before
public void setUp() throws Exception {
context = new AnnotationConfigApplicationContext(MyApplicationTest.class);
}
@After
public void tearDown() throws Exception {
context.close();
}
@Test
public void test() {
MyApplication app = context.getBean(MyApplication.class);
Assert.assertTrue(app.processMessage("Hi Pankaj", "pankaj@abc.com"));
}
}
Lớp kiểm thử này được chú thích bằng annotation @Configuration và @ComponentScan vì phương thức getMessageService() trả về một phiên bản mock của MessageService. Do đó, phương thức này cần được đánh dấu với @Bean để Spring biết sử dụng nó khi tạo bean.
Vì đang kiểm thử lớp MyApplication vốn được cấu hình bằng annotation, nên chúng ta sử dụng AnnotationConfigApplicationContext và khởi tạo nó trong phương thức setUp(). Việc đóng context được thực hiện trong phương thức tearDown().
Trong phương thức test(), chúng ta lấy đối tượng component từ context và kiểm thử hành vi của nó bằng một câu lệnh kiểm tra (assert).
Vậy Spring Framework làm cách nào để tự động liên kết các thành phần và gọi các phương thức mà bản thân Spring không biết trước? Điều này được thực hiện nhờ việc sử dụng rộng rãi cơ chế Reflection của Java, cho phép phân tích và thay đổi hành vi của các lớp tại runtime.
Tải Dự án Spring Dependency Injection
Tải dự án Spring Dependency Injection (DI) mẫu từ đường dẫn bên trên và tự thực hành để hiểu rõ hơn.