Tổng Quan về Spring Framework 5
Spring là một framework nhẹ, không xâm lấn, nổi tiếng với việc cung cấp các công cụ mạnh mẽ để xây dựng ứng dụng Java. Hai nguyên lý cốt lõi của nó là Inversion of Control (IOC) và Aspect-Oriented Programming (AOP).
1. Nền Tảng về Inversion of Control (IOC)
Hãy xem xét cách chúng ta quản lý các thành phần và sự phụ thuộc theo cách thủ công trước khi hiểu được giá trị của IOC.
Giả sử chúng ta có một giao diện để truy cập dữ liệu người dùng:
package com.example.dataaccess;
public interface UserRepository {
void retrieveUserData();
}
Và một triển khai cụ thể cho cơ sở dữ liệu mặc định:
package com.example.dataaccess;
public class DefaultUserRepository implements UserRepository {
@Override
public void retrieveUserData() {
System.out.println("Lấy dữ liệu người dùng từ cơ sở dữ liệu mặc định.");
}
}
Một giao diện cho tầng dịch vụ xử lý logic kinh doanh:
package com.example.businesslogic;
public interface AccountService {
void displayAccountInfo();
}
Trong một triển khai dịch vụ ban đầu, chúng ta sẽ tạo sự phụ thuộc trực tiếp:
package com.example.businesslogic;
import com.example.dataaccess.UserRepository;
import com.example.dataaccess.DefaultUserRepository;
// import com.example.dataaccess.PostgresUserRepository; // Có thể có nhiều triển khai
public class AccountServiceImpl implements AccountService {
private UserRepository dataRepo;
@Override
public void displayAccountInfo() {
dataRepo.retrieveUserData();
}
// Phương thức setter để thiết lập sự phụ thuộc
public void setDataRepo(UserRepository dataRepo) {
this.dataRepo = dataRepo;
}
}
Và cách sử dụng trong ứng dụng chính:
import com.example.businesslogic.AccountServiceImpl;
import com.example.dataaccess.MySQLUserRepository; // Một triển khai khác
import com.example.dataaccess.OracleUserRepository; // Hoặc một triển khai khác
public class ApplicationLauncher {
public static void main(String[] args) {
// Ứng dụng thực tế chỉ tương tác với tầng dịch vụ
AccountServiceImpl accountService = new AccountServiceImpl();
// Thiết lập triển khai phụ thuộc một cách thủ công (setter injection)
accountService.setDataRepo(new OracleUserRepository());
accountService.displayAccountInfo();
}
}
Trong ví dụ trên, việc thay đổi triển khai UserRepository đòi hỏi thay đổi mã trong ApplicationLauncher. Đây là lúc IOC của Spring phát huy tác dụng, cho phép một container quản lý các đối tượng và sự phụ thuộc của chúng, giảm ghép nối chặt chẽ.
2. Chào Mừng Spring
Để bắt đầu với Spring, chúng ta cần một POJO (Plain Old Java Object) và cấu hình Spring XML.
package com.example.model;
public class GreetingMessage {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public String toString() {
return "GreetingMessage{" +
"content='" + content + '\'' +
'}';
}
}
Tệp cấu hình Spring (ví dụ: app-context.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Sử dụng Spring để tạo đối tượng, chúng được gọi là Bean trong Spring.
Tương tự như: GreetingMessage myGreeting = new GreetingMessage();
'id' là tên biến
'class' là kiểu đối tượng được tạo
'property' dùng để thiết lập giá trị cho các thuộc tính của đối tượng thông qua setter.
-->
<bean id="myGreeting" class="com.example.model.GreetingMessage">
<property name="content" value="Xin chào từ Spring!"/>
</bean>
</beans>
Và mã để lấy bean từ Spring container:
import com.example.model.GreetingMessage;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SpringHelloApp {
public static void main(String[] args) {
// Lấy đối tượng context của Spring
ApplicationContext context = new ClassPathXmlApplicationContext("app-context.xml");
// Lấy bean "myGreeting" từ container và ép kiểu
GreetingMessage message = (GreetingMessage) context.getBean("myGreeting");
System.out.println(message.toString());
}
}
3. Các Cách Khởi Tạo Đối Tượng trong IOC
Spring container có thể tạo đối tượng theo nhiều cách:
- Sử dụng hàm tạo mặc định (không tham số): Đây là cách mặc định và phổ biến nhất.
- Sử dụng hàm tạo có tham số: Bạn có thể chỉ định các tham số cho hàm tạo trong XML.
Ví dụ với một lớp Person có hàm tạo:
package com.example.model;
public class Person {
private String fullName;
private int age;
public Person(String fullName, int age) {
this.fullName = fullName;
this.age = age;
}
// Getters và setters (có thể bỏ qua nếu chỉ dùng hàm tạo)
public String getFullName() { return fullName; }
public void setFullName(String fullName) { this.fullName = fullName; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
@Override
public String toString() {
return "Person{" +
"fullName='" + fullName + '\'' +
", age=" + age +
'}';
}
}
Cấu hình XML cho hàm tạo có tham số:
<?xml version="1.0" encoding="UTF-8"?>
<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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Cách 1: Gán giá trị theo chỉ mục của tham số (ít được dùng) -->
<!--
<bean id="personByIndex" class="com.example.model.Person">
<constructor-arg index="0" value="Nguyen Van A"/>
<constructor-arg index="1" value="30"/>
</bean>
-->
<!-- Cách 2: Gán giá trị theo tên tham số (nên dùng) -->
<bean id="personByName" class="com.example.model.Person">
<constructor-arg name="fullName" value="Tran Thi B"/>
<constructor-arg name="age" value="25"/>
</bean>
</beans>
4. Cấu Hình Bean Nâng Cao
Alias cho Bean
Bạn có thể định nghĩa một alias (bí danh) cho bean để tham chiếu nó bằng nhiều tên khác nhau.
<alias name="personByName" alias="individualProfile"/>
Thuộc Tính của Bean
Các thuộc tính quan trọng khi định nghĩa một bean:
id: Định danh duy nhất cho bean trong container.class: Tên lớp đầy đủ của đối tượng bean.name: Cũng là một bí danh, có thể là nhiều tên cách nhau bằng dấu phẩy, dấu chấm phẩy hoặc khoảng trắng.
<bean id="mainPerson" class="com.example.model.Person" name="currentPerson, newPersonAlias">
<constructor-arg name="fullName" value="Le Van C"/>
<constructor-arg name="age" value="40"/>
</bean>
Import Cấu Hình
Trong các dự án lớn, để quản lý cấu hình tốt hơn, bạn có thể chia cấu hình thành nhiều tệp XML và import chúng vào một tệp cấu hình chính.
<import resource="another-config.xml"/>
Sau đó, bạn chỉ cần tải tệp cấu hình chính:
ApplicationContext context = new ClassPathXmlApplicationContext("main-config.xml");
5. Dependency Injection (DI) với Setter
DI là một khía cạnh của IOC, nơi Spring container "tiêm" các phụ thuộc vào một đối tượng. Setter injection (tiêm qua phương thức setter) là một cách phổ biến.
- Sự phụ thuộc (Dependency): Việc tạo đối tượng bean phụ thuộc vào container.
- Tiêm (Injection): Tất cả các thuộc tính của đối tượng bean được quyết định bởi container.
Hãy tạo một lớp Location và Profile để minh họa.
package com.example.entities;
public class Location {
private String city;
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
@Override
public String toString() {
return "Location{" +
"city='" + city + '\'' +
'}';
}
}
package com.example.entities;
import java.util.*;
public class Profile {
private String userName;
private Location residence;
private String[] favoriteColors;
private List<String> hobbies;
private Map<String, String> contactInfo;
private Set<String> skills;
private String maritalStatus; // Có thể null
private Properties systemSettings;
// Getters và setters cho tất cả các thuộc tính
public String getUserName() { return userName; }
public void setUserName(String userName) { this.userName = userName; }
public Location getResidence() { return residence; }
public void setResidence(Location residence) { this.residence = residence; }
public String[] getFavoriteColors() { return favoriteColors; }
public void setFavoriteColors(String[] favoriteColors) { this.favoriteColors = favoriteColors; }
public List<String> getHobbies() { return hobbies; }
public void setHobbies(List<String> hobbies) { this.hobbies = hobbies; }
public Map<String, String> getContactInfo() { return contactInfo; }
public void setContactInfo(Map<String, String> contactInfo) { this.contactInfo = contactInfo; }
public Set<String> getSkills() { return skills; }
public void setSkills(Set<String> skills) { this.skills = skills; }
public String getMaritalStatus() { return maritalStatus; }
public void setMaritalStatus(String maritalStatus) { this.maritalStatus = maritalStatus; }
public Properties getSystemSettings() { return systemSettings; }
public void setSystemSettings(Properties systemSettings) { this.systemSettings = systemSettings; }
@Override
public String toString() {
return "Profile{" +
"userName='" + userName + '\'' +
", residence=" + residence.toString() +
", favoriteColors=" + Arrays.toString(favoriteColors) +
", hobbies=" + hobbies +
", contactInfo=" + contactInfo +
", skills=" + skills +
", maritalStatus='" + maritalStatus + '\'' +
", systemSettings=" + systemSettings +
'}';
}
}
Cấu hình XML để tiêm các loại dữ liệu khác nhau:
<?xml version="1.0" encoding="UTF-8"?>
<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
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Bean Location -->
<bean id="currentLocation" class="com.example.entities.Location">
<property name="city" value="Hanoi"/>
</bean>
<!-- Bean Profile với các kiểu tiêm khác nhau -->
<bean id="userProfile" class="com.example.entities.Profile">
<property name="userName" value="Nguyen Minh"/>
<!-- Tiêm bean khác (ref) -->
<property name="residence" ref="currentLocation"/>
<!-- Tiêm mảng -->
<property name="favoriteColors">
<array>
<value>Xanh</value>
<value>Đỏ</value>
<value>Vàng</value>
</array>
</property>
<!-- Tiêm danh sách (List) -->
<property name="hobbies">
<list>
<value>Đọc sách</value>
<value>Chơi game</value>
<value>Du lịch</value>
</list>
</property>
<!-- Tiêm bản đồ (Map) -->
<property name="contactInfo">
<map>
<entry key="Email" value="user@example.com"/>
<entry key="Điện thoại" value="0987654321"/>
</map>
</property>
<!-- Tiêm tập hợp (Set) -->
<property name="skills">
<set>
<value>Java</value>
<value>Spring</value>
<value>SQL</value>
</set>
</property>
<!-- Tiêm giá trị null -->
<property name="maritalStatus">
<null/>
</property>
<!-- Tiêm Properties -->
<property name="systemSettings">
<props>
<prop key="app.version">1.0</prop>
<prop key="db.timeout">3000</prop>
</props>
</property>
</bean>
</beans>
Kiểm tra cấu hình này:
import com.example.entities.Profile;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestDI {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("profile-config.xml");
Profile myProfile = (Profile) context.getBean("userProfile");
System.out.println(myProfile.toString());
}
}
6. Phạm Vi của Bean (Bean Scopes)
Phạm vi (scope) của bean định nghĩa vòng đời và số lượng các instance của bean sẽ được tạo bởi Spring container.
singleton(Mặc định): Chỉ một instance duy nhất của bean được tạo và được chia sẻ trên toàn bộ ứng dụng.prototype: Mỗi khi bạn yêu cầu bean từ container, một instance mới sẽ được tạo.request,session,application: Chỉ dùng trong môi trường ứng dụng web.
Ví dụ thiết lập phạm vi cho bean:
<!-- Bean này sẽ luôn trả về một instance mới mỗi khi được yêu cầu -->
<bean id="tempItem" class="com.example.model.Item" scope="prototype"/>
<!-- Bean này sẽ trả về cùng một instance mỗi khi được yêu cầu (mặc định) -->
<bean id="sharedService" class="com.example.services.SharedService" scope="singleton"/>
7. Tự Động Gán (Autowiring) Bean
Autowiring là một cách Spring tự động đáp ứng các phụ thuộc của bean bằng cách tìm kiếm và gán các bean khác từ context.
Có ba cách để cấu hình autowiring:
- Cấu hình rõ ràng trong XML (ví dụ: setter injection thủ công).
- Cấu hình bằng Java.
- Autowiring ngầm định (thông qua XML hoặc annotation).
Để minh họa, hãy tạo các lớp Pet, DogPet, CatPet và Owner.
package com.example.pets;
public class CatPet {
public void makeSound(){
System.out.println("Meow meow");
}
}
package com.example.pets;
public class DogPet {
public void makeSound(){
System.out.println("Gau gau");
}
}
Lớp Owner có các thuộc tính cat và dog:
package com.example.pets;
public class Owner {
private String ownerName;
private CatPet cat;
private DogPet dog;
public String getOwnerName() { return ownerName; }
public void setOwnerName(String ownerName) { this.ownerName = ownerName; }
public CatPet getCat() { return cat; }
public void setCat(CatPet cat) { this.cat = cat; }
public DogPet getDog() { return dog; }
public void setDog(DogPet dog) { this.dog = dog; }
}
Cấu hình XML ban đầu (tiêm thủ công):
<bean id="myCat" class="com.example.pets.CatPet"/>
<bean id="myDog" class="com.example.pets.DogPet"/>
<bean id="petOwner" class="com.example.pets.Owner">
<property name="ownerName" value="Kien"/>
<property name="cat" ref="myCat"/>
<property name="dog" ref="myDog"/>
</bean>
Kiểm tra:
import com.example.pets.Owner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.junit.Test;
public class AutowireTest {
@Test
public void testAutowireXML() {
ApplicationContext context = new ClassPathXmlApplicationContext("autowire-config.xml");
Owner owner = context.getBean("petOwner", Owner.class);
System.out.println("Chủ sở hữu: " + owner.getOwnerName());
owner.getCat().makeSound();
owner.getDog().makeSound();
}
}
Autowire theo tên (byName)
Spring sẽ tự động tìm kiếm bean trong context có id trùng khớp với tên thuộc tính trong lớp (hoặc phần sau 'set' trong setter method).
<bean id="catPet" class="com.example.pets.CatPet"/> <!-- id = tên thuộc tính -->
<bean id="dogPet" class="com.example.pets.DogPet"/> <!-- id = tên thuộc tính -->
<bean id="petOwner" class="com.example.pets.Owner" autowire="byName">
<property name="ownerName" value="Minh"/>
</bean>
Lưu ý: Để byName hoạt động, id của bean phụ thuộc phải khớp với tên thuộc tính trong lớp Owner (cat, dog). Tôi đã đổi id="myCat" thành id="cat" và id="myDog" thành id="dog" trong ví dụ trên.
Autowire theo kiểu (byType)
Spring sẽ tự động tìm kiếm bean trong context có kiểu dữ liệu khớp với kiểu thuộc tính trong lớp.
<bean class="com.example.pets.CatPet"/> <!-- Không cần id, chỉ cần kiểu duy nhất -->
<bean class="com.example.pets.DogPet"/>
<bean id="petOwner" class="com.example.pets.Owner" autowire="byType">
<property name="ownerName" value="Lam"/>
</bean>
Tóm tắt autowiring XML:
byName: Cần đảm bảoidcủa bean phụ thuộc khớp với tên thuộc tính được tiêm.byType: Cần đảm bảo chỉ có một bean duy nhất của kiểu đó trong container. Nếu có nhiều bean cùng kiểu, sẽ gây lỗi.
7.1 Tự Động Gán bằng Annotation
Sử dụng annotation là cách hiện đại và được ưa chuộng để autowire các phụ thuộc.
Để sử dụng annotation, bạn cần:
- Thêm namespace
contextvào tệp XML. - Kích hoạt hỗ trợ annotation với
<context:annotation-config/>.
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- Kích hoạt hỗ trợ annotation của Spring -->
<context:annotation-config/>
<bean id="catPet" class="com.example.pets.CatPet"/>
<bean id="specialDog" class="com.example.pets.DogPet"/>
<bean id="anotherDog" class="com.example.pets.DogPet"/> <!-- Bean Dog thứ hai -->
<!-- Bean Owner không cần cấu hình property nữa -->
<bean id="petOwnerAnnotated" class="com.example.pets.OwnerAnnotated"/>
</beans>
Với lớp OwnerAnnotated:
package com.example.pets;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
public class OwnerAnnotated {
private String ownerName = "Phong"; // Giá trị mặc định
@Autowired // Tự động gán CatPet
private CatPet catPet;
@Autowired // Tự động gán DogPet
@Qualifier("specialDog") // Chỉ định bean DogPet cụ thể theo id
private DogPet dogPet;
// Getters và setters
public String getOwnerName() { return ownerName; }
public void setOwnerName(String ownerName) { this.ownerName = ownerName; }
public CatPet getCatPet() { return catPet; }
public DogPet getDogPet() { return dogPet; }
// Phương thức setter cho CatPet (không cần thiết với @Autowired trên trường)
// public void setCatPet(CatPet catPet) { this.catPet = catPet; }
}
@Autowired có thể được đặt trên trường, setter method, hoặc constructor. Khi đặt trên trường, bạn không cần phải viết phương thức setter cho thuộc tính đó. Nếu có nhiều bean cùng kiểu và không thể phân biệt bằng tên, bạn cần sử dụng @Qualifier("beanId") để chỉ định bean cụ thể.
8. Phát Triển Dựa trên Annotation
Annotation không chỉ dùng cho autowiring mà còn để định nghĩa bean và cấu hình các khía cạnh khác của Spring.
- Đảm bảo đã thêm namespace
contextvà kích hoạt<context:annotation-config/>hoặc<context:component-scan/>.
Annotation Định Nghĩa Bean và Tiêm Thuộc Tính
package com.example.core;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
// @Component tương đương với <bean id="userService" class="com.example.core.UserService"/>
@Component("userComponent") // Có thể chỉ định id cho bean
public class UserService {
// Tiêm giá trị trực tiếp vào trường
@Value("Nguyen Van Dat")
private String defaultUserName;
// Hoặc tiêm thông qua setter method
// @Value("Nguyen Van Dat")
// public void setDefaultUserName(String defaultUserName) {
// this.defaultUserName = defaultUserName;
// }
public String getDefaultUserName() {
return defaultUserName;
}
public void processData() {
System.out.println("Processing data for user: " + defaultUserName);
}
}
Các Annotation Phái Sinh
@Component có các annotation phái sinh được sử dụng trong phát triển web theo kiến trúc MVC ba tầng:
@Repository: Dành cho tầng truy cập dữ liệu (DAO).@Service: Dành cho tầng dịch vụ/logic nghiệp vụ.@Controller: Dành cho tầng trình điều khiển (controller) trong Spring MVC.
Tất cả các annotation này đều có chức năng tương tự như @Component: đăng ký lớp vào Spring container dưới dạng một bean.
Annotation Phạm Vi
Bạn có thể chỉ định phạm vi của bean bằng annotation @Scope:
@Component
@Scope("prototype") // Mỗi lần yêu cầu sẽ tạo instance mới
public class TempObject {
// ...
}
Tóm Tắt về XML và Annotation
- XML: Thường dùng để quản lý định nghĩa bean và các cấu hình tổng quát.
- Annotation: Chủ yếu dùng để tiêm thuộc tính, định nghĩa bean trực tiếp trên lớp.
- Để annotation có hiệu lực, bạn phải bật hỗ trợ annotation và chỉ định gói để Spring quét tìm các annotation.
<!-- Chỉ định gói để Spring quét tìm các annotation như @Component, @Service, v.v. -->
<context:component-scan base-package="com.example"/>
<!-- Kích hoạt xử lý các annotation như @Autowired, @Value -->
<context:annotation-config/>
9. Cấu Hình Spring Bằng Java
Bạn có thể cấu hình Spring hoàn toàn bằng Java, không cần tệp XML. Điều này mang lại lợi ích về kiểu an toàn và khả năng tái cấu trúc.
Lớp POJO User:
package com.example.models;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component // Đánh dấu lớp này được Spring quản lý
public class User {
@Value("Hoang") // Tiêm giá trị cho thuộc tính name
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
Lớp cấu hình Java:
package com.example.config;
import com.example.models.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration // Đánh dấu đây là một lớp cấu hình Spring, tương tự như tệp XML
@ComponentScan("com.example.models") // Quét các bean trong gói này
@Import(AdditionalConfig.class) // Import các lớp cấu hình khác
public class MainAppConfig {
// Định nghĩa một bean, tương đương với thẻ <bean>
// Tên phương thức là id của bean
// Kiểu trả về là class của bean
@Bean
public User defaultUser() {
// Trả về đối tượng bean mà Spring sẽ quản lý
User user = new User();
user.setName("Default Java User"); // Có thể cấu hình thêm
return user;
}
// Một ví dụ khác của bean được quét tự động từ @ComponentScan nếu User cũng có @Component
// Để tránh xung đột, thường chỉ định @Bean cho những bean không thể được quét tự động
// hoặc cần cấu hình phức tạp.
}
Lớp cấu hình bổ sung (có thể import vào MainAppConfig):
package com.example.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AdditionalConfig {
// Các bean hoặc cấu hình khác có thể được định nghĩa tại đây
}
Kiểm tra ứng dụng:
import com.example.config.MainAppConfig;
import com.example.models.User;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class JavaConfigTest {
public static void main(String[] args) {
// Tải context từ lớp cấu hình Java
ApplicationContext context = new AnnotationConfigApplicationContext(MainAppConfig.class);
// Lấy bean "defaultUser" (tên phương thức)
User javaUser = (User) context.getBean("defaultUser");
System.out.println("Tên người dùng từ Java config: " + javaUser.getName());
// Lấy bean "user" (nếu lớp User có @Component và @ComponentScan hoạt động)
User scannedUser = (User) context.getBean("user"); // id mặc định là tên lớp viết thường
System.out.println("Tên người dùng từ @Component: " + scannedUser.getName());
}
}
10. Mẫu Thiết Kế Proxy
Mẫu Proxy là nền tảng của Spring AOP (Aspect-Oriented Programming). Nó cung cấp một đối tượng đại diện (proxy) cho một đối tượng khác (đối tượng thực) để kiểm soát quyền truy cập hoặc thêm các hành vi phụ trợ.
Có hai loại proxy chính:
- Static Proxy (Proxy Tĩnh)
- Dynamic Proxy (Proxy Động)
10.1 Static Proxy (Proxy Tĩnh)
Trong proxy tĩnh, lớp proxy được định nghĩa trước khi chạy ứng dụng và thường là một lớp riêng biệt.
Các vai trò:
- Abstract Subject (Chủ thể Trừu tượng): Thường là một interface hoặc lớp trừu tượng, định nghĩa giao diện chung cho đối tượng thực và proxy.
- Real Subject (Chủ thể Thực): Đối tượng mà proxy đại diện.
- Proxy Subject (Chủ thể Proxy): Đối tượng đại diện cho chủ thể thực, thực hiện các hoạt động phụ trợ trước hoặc sau khi gọi phương thức của chủ thể thực.
- Client (Khách hàng): Đối tượng sử dụng proxy để tương tác với chủ thể thực.
Ví dụ: Quản lý tác vụ dịch vụ
1. Interface định nghĩa dịch vụ:
package com.example.proxy.staticproxy;
public interface DataService {
void processData(String input);
}
2. Triển khai dịch vụ thực tế:
package com.example.proxy.staticproxy;
public class RealDataService implements DataService {
@Override
public void processData(String input) {
System.out.println("Xử lý dữ liệu thực tế: " + input);
}
}
3. Lớp proxy:
package com.example.proxy.staticproxy;
public class DataServiceProxy implements DataService {
private RealDataService realService;
public DataServiceProxy(RealDataService realService) {
this.realService = realService;
}
@Override
public void processData(String input) {
logBeforeProcessing(input); // Hành động phụ trợ trước
realService.processData(input); // Gọi phương thức của đối tượng thực
logAfterProcessing(); // Hành động phụ trợ sau
}
private void logBeforeProcessing(String data) {
System.out.println("Proxy: Bắt đầu xử lý dữ liệu: " + data);
}
private void logAfterProcessing() {
System.out.println("Proxy: Kết thúc xử lý dữ liệu.");
}
}
4. Client sử dụng:
package com.example.proxy.staticproxy;
public class ClientStaticProxy {
public static void main(String[] args) {
RealDataService real = new RealDataService();
DataService proxy = new DataServiceProxy(real);
proxy.processData("Thông tin quan trọng");
}
}
Ưu điểm của Static Proxy:
- Tách biệt rõ ràng trách nhiệm giữa đối tượng thực và các hành động phụ trợ.
- Dễ hiểu và triển khai.
Nhược điểm:
- Mỗi đối tượng thực cần một lớp proxy riêng, dẫn đến trùng lặp mã nếu có nhiều dịch vụ cần các hành động phụ trợ tương tự.
- Cần phải sửa đổi hoặc tạo mới lớp proxy mỗi khi giao diện hoặc hành vi phụ trợ thay đổi.
10.2 Dynamic Proxy (Proxy Động)
Proxy động được tạo ra tại thời gian chạy (runtime) bằng cách sử dụng Java Reflection API. Điều này giải quyết vấn đề trùng lặp mã của proxy tĩnh.
Dynamic Proxy chủ yếu dựa vào hai lớp:
Proxy: Lớp tiện ích để tạo các đối tượng proxy.InvocationHandler: Một interface mà bạn triển khai để định nghĩa logic xử lý các lời gọi phương thức trên đối tượng proxy.
Ví dụ: Quản lý tác vụ dịch vụ chung
1. Interface định nghĩa dịch vụ:
package com.example.proxy.dynamicproxy;
public interface TaskExecutor {
void executeTask(String taskName);
String getTaskStatus(int taskId);
}
2. Triển khai dịch vụ thực tế:
package com.example.proxy.dynamicproxy;
public class ConcreteTaskExecutor implements TaskExecutor {
@Override
public void executeTask(String taskName) {
System.out.println("Thực hiện tác vụ: " + taskName);
}
@Override
public String getTaskStatus(int taskId) {
System.out.println("Kiểm tra trạng thái tác vụ ID: " + taskId);
return "Task " + taskId + " completed.";
}
}
3. Lớp xử lý gọi phương thức (Invocation Handler):
package com.example.proxy.dynamicproxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class LoggingInvocationHandler implements InvocationHandler {
// Đối tượng thực tế mà proxy sẽ ủy quyền cho
private Object targetObject;
public void setTargetObject(Object targetObject) {
this.targetObject = targetObject;
}
// Phương thức tạo đối tượng proxy
public Object createProxy() {
return Proxy.newProxyInstance(
targetObject.getClass().getClassLoader(), // ClassLoader của đối tượng thực
targetObject.getClass().getInterfaces(), // Các interface mà đối tượng thực triển khai
this // InvocationHandler này
);
}
// Logic xử lý mỗi khi một phương thức được gọi trên đối tượng proxy
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Hành động trước khi gọi phương thức
logMethodCall(method.getName(), args);
// Gọi phương thức thực tế trên đối tượng mục tiêu
Object result = method.invoke(targetObject, args);
// Hành động sau khi gọi phương thức
logMethodReturn(method.getName(), result);
return result;
}
private void logMethodCall(String methodName, Object[] args) {
System.out.print("Proxy LOG: Gọi phương thức '" + methodName + "'");
if (args != null && args.length > 0) {
System.out.println(" với các tham số: " + Arrays.toString(args));
} else {
System.out.println();
}
}
private void logMethodReturn(String methodName, Object result) {
System.out.println("Proxy LOG: Phương thức '" + methodName + "' đã trả về: " + result);
}
}
4. Client sử dụng:
package com.example.proxy.dynamicproxy;
import java.util.Arrays;
public class ClientDynamicProxy {
public static void main(String[] args) {
// Đối tượng thực tế
ConcreteTaskExecutor realExecutor = new ConcreteTaskExecutor();
// Handler xử lý gọi phương thức
LoggingInvocationHandler handler = new LoggingInvocationHandler();
handler.setTargetObject(realExecutor); // Thiết lập đối tượng mục tiêu
// Tạo đối tượng proxy động
TaskExecutor proxyExecutor = (TaskExecutor) handler.createProxy();
// Gọi các phương thức thông qua proxy
proxyExecutor.executeTask("Dọn dẹp database");
String status = proxyExecutor.getTaskStatus(101);
System.out.println("Trạng thái nhận được: " + status);
}
}
Ưu điểm của Dynamic Proxy:
- Rất linh hoạt, có thể áp dụng cho bất kỳ interface nào.
- Giảm thiểu code trùng lặp: một lớp
InvocationHandlercó thể phục vụ nhiều loại đối tượng thực miễn là chúng triển khai cùng một interface hoặc có các hành vi chung. - Có thể thêm các chức năng chéo (cross-cutting concerns) như logging, bảo mật, transaction một cách tập trung.
11. Triển Khai AOP với Spring
Aspect-Oriented Programming (AOP) cho phép bạn tách biệt các chức năng chéo (ví dụ: logging, bảo mật, quản lý giao dịch) khỏi logic nghiệp vụ cốt lõi. Spring AOP sử dụng proxy để thực hiện điều này.
Cách 1: Sử dụng các Interface API của Spring AOP
Spring cung cấp các interface như MethodBeforeAdvice, AfterReturningAdvice, v.v. để triển khai các lời khuyên (advice) AOP.
1. Định nghĩa giao diện dịch vụ và triển khai:
package com.example.aop.service;
public interface ProductService {
void addProduct(String name);
void deleteProduct(int id);
void updateProduct(int id, String name);
String getProductDetails(int id);
}
package com.example.aop.service;
public class ProductServiceImpl implements ProductService {
@Override
public void addProduct(String name) {
System.out.println("Thêm sản phẩm: " + name);
}
@Override
public void deleteProduct(int id) {
System.out.println("Xóa sản phẩm ID: " + id);
}
@Override
public void updateProduct(int id, String name) {
System.out.println("Cập nhật sản phẩm ID " + id + " thành: " + name);
}
@Override
public String getProductDetails(int id) {
System.out.println("Truy vấn chi tiết sản phẩm ID: " + id);
return "Product " + id + " details";
}
}
2. Tạo các lớp Advice:
package com.example.aop.advice;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
public class BeforeLoggingAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("[BEFORE ADVICE] Phương thức '" + method.getName() +
"' của đối tượng " + target.getClass().getName() + " sắp được thực thi.");
}
}
package com.example.aop.advice;
import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;
public class AfterReturningResultAdvice implements AfterReturningAdvice {
@Override
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("[AFTER RETURNING ADVICE] Phương thức '" + method.getName() +
"' đã thực thi, trả về kết quả: " + returnValue);
}
}
3. Cấu hình AOP trong 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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- Định nghĩa bean dịch vụ -->
<bean id="productService" class="com.example.aop.service.ProductServiceImpl"/>
<!-- Định nghĩa các lời khuyên -->
<bean id="beforeLog" class="com.example.aop.advice.BeforeLoggingAdvice"/>
<bean id="afterResultLog" class="com.example.aop.advice.AfterReturningResultAdvice"/>
<!-- Cấu hình AOP -->
<aop:config>
<!-- Định nghĩa điểm cắt (Pointcut) -->
<aop:pointcut id="servicePointcut" expression="execution(* com.example.aop.service.ProductServiceImpl.*(..))"/>
<!-- Gán lời khuyên cho điểm cắt -->
<aop:advisor advice-ref="beforeLog" pointcut-ref="servicePointcut"/>
<aop:advisor advice-ref="afterResultLog" pointcut-ref="servicePointcut"/>
</aop:config>
</beans>
4. Kiểm tra:
import com.example.aop.service.ProductService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestAOPAPI {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("aop-config-api.xml");
ProductService service = context.getBean("productService", ProductService.class);
service.addProduct("Laptop ABC");
service.getProductDetails(1);
}
}
Cách 2: Tùy Chỉnh AOP với XML (Sử dụng Aspect tùy chỉnh)
Bạn có thể tạo các lớp Java thông thường chứa các phương thức "advice" và cấu hình chúng trong XML.
1. Sử dụng lại giao diện và triển khai ProductService/ProductServiceImpl từ ví dụ trên.
2. Tạo lớp Aspect tùy chỉnh:
package com.example.aop.custom;
public class CustomLoggingAspect {
public void logBeforeExecution() {
System.out.println("[CUSTOM AOP] --- Phương thức sắp được thực thi ---");
}
public void logAfterExecution() {
System.out.println("[CUSTOM AOP] --- Phương thức đã thực thi xong ---");
}
}
3. Cấu hình AOP trong 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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- Định nghĩa bean dịch vụ -->
<bean id="productService" class="com.example.aop.service.ProductServiceImpl"/>
<!-- Định nghĩa bean Aspect tùy chỉnh -->
<bean id="myCustomAspect" class="com.example.aop.custom.CustomLoggingAspect"/>
<!-- Cấu hình AOP với aspect tùy chỉnh -->
<aop:config>
<aop:aspect ref="myCustomAspect">
<!-- Định nghĩa điểm cắt -->
<aop:pointcut id="allServiceMethods" expression="execution(* com.example.aop.service.ProductServiceImpl.*(..))"/>
<!-- Gán các lời khuyên cho điểm cắt -->
<aop:before method="logBeforeExecution" pointcut-ref="allServiceMethods"/>
<aop:after method="logAfterExecution" pointcut-ref="allServiceMethods"/>
</aop:aspect>
</aop:config>
</beans>
4. Kiểm tra tương tự như trên.
Cách 3: Sử dụng Annotation để Triển Khai AOP
Đây là cách phổ biến và gọn gàng nhất để sử dụng AOP trong Spring.
1. Sử dụng lại giao diện và triển khai ProductService/ProductServiceImpl.
2. Tạo lớp Aspect với Annotation:
package com.example.aop.annotation;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect // Đánh dấu lớp này là một Aspect
@Component // Để Spring quét và quản lý Aspect này như một bean
public class MethodPerformanceMonitor {
// Định nghĩa điểm cắt dùng chung
@Pointcut("execution(* com.example.aop.service.ProductServiceImpl.*(..))")
public void serviceMethods() {}
@Before("serviceMethods()")
public void beforeMethodExecution() {
System.out.println(">>> [Annotation AOP] Trước khi phương thức thực thi <<<");
}
@After("serviceMethods()")
public void afterMethodExecution() {
System.out.println(">>> [Annotation AOP] Sau khi phương thức thực thi <<<");
}
// Advice Around bao bọc việc thực thi phương thức
@Around("serviceMethods()")
public Object aroundMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println(">>> [Annotation AOP] Bắt đầu Around Advice <<<");
Signature signature = joinPoint.getSignature(); // Lấy thông tin về phương thức
System.out.println(">>> [Annotation AOP] Phương thức bị chặn: " + signature.getName());
// Thực thi phương thức mục tiêu
Object result = joinPoint.proceed();
System.out.println(">>> [Annotation AOP] Kết thúc Around Advice <<<");
return result;
}
@AfterReturning(pointcut = "serviceMethods()", returning = "retVal")
public void afterReturningAdvice(Object retVal) {
System.out.println(">>> [Annotation AOP] Kết quả trả về: " + retVal + " <<<");
}
@AfterThrowing(pointcut = "serviceMethods()", throwing = "ex")
public void afterThrowingAdvice(Throwable ex) {
System.out.println(">>> [Annotation AOP] Ngoại lệ xảy ra: " + ex.getMessage() + " <<<");
}
}
3. Cấu hình AOP trong XML để kích hoạt Annotation:
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- Định nghĩa bean dịch vụ -->
<bean id="productService" class="com.example.aop.service.ProductServiceImpl"/>
<!-- Kích hoạt quét các component để tìm Aspect -->
<context:component-scan base-package="com.example.aop.annotation"/>
<!-- Kích hoạt hỗ trợ AOP với @Aspect -->
<aop:aspectj-autoproxy/>
</beans>
4. Kiểm tra:
import com.example.aop.service.ProductService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestAOPAnnotation {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("aop-config-annotation.xml");
ProductService service = context.getBean("productService", ProductService.class);
service.addProduct("Camera Sony");
service.getProductDetails(10);
}
}
12. Tích Hợp Spring và MyBatis
Để tích hợp Spring với MyBatis, chúng ta cần cấu hình DataSource, SqlSessionFactory, và SqlSessionTemplate của MyBatis trong Spring.
1. Cấu hình Datasource trong Spring (thay thế cấu hình Datasource của MyBatis):
<!-- DataSource: Sử dụng DataSource của Spring -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/my_database?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
</bean>
2. Cấu hình SqlSessionFactoryBean:
<!-- SqlSessionFactory: Tích hợp với DataSource của Spring -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- Liên kết với tệp cấu hình MyBatis (nếu có) -->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!-- Chỉ định vị trí các tệp mapper XML -->
<property name="mapperLocations" value="classpath:com/example/mapper/*.xml"/>
</bean>
3. Cấu hình SqlSessionTemplate:
<!-- SqlSessionTemplate: Là SqlSession của MyBatis, được quản lý bởi Spring -->
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<!-- Phải sử dụng hàm tạo để tiêm SqlSessionFactory -->
<constructor-arg index="0" ref="sqlSessionFactory"/>
</bean>
4. Định nghĩa giao diện Mapper (ví dụ: ProductMapper) và lớp POJO (ví dụ: Product).
package com.example.domain;
public class Product {
private int productId;
private String productName;
private double price;
// Constructors, Getters, Setters, toString
public Product() {}
public Product(int productId, String productName, double price) {
this.productId = productId;
this.productName = productName;
this.price = price;
}
public int getProductId() { return productId; }
public void setProductId(int productId) { this.productId = productId; }
public String getProductName() { return productName; }
public void setProductName(String productName) { this.productName = productName; }
public double getPrice() { return price; }
public void setPrice(double price) { this.price = price; }
@Override
public String toString() {
return "Product{" +
"productId=" + productId +
", productName='" + productName + '\'' +
", price=" + price +
'}';
}
}
package com.example.mapper;
import com.example.domain.Product;
import java.util.List;
public interface ProductMapper {
List<Product> findAllProducts();
int insertProduct(Product product);
}
5. Viết tệp Mapper XML (ví dụ: ProductMapper.xml):
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.ProductMapper">
<select id="findAllProducts" resultType="com.example.domain.Product">
SELECT product_id, product_name, price FROM products;
</select>
<insert id="insertProduct" parameterType="com.example.domain.Product">
INSERT INTO products (product_id, product_name, price) VALUES (#{productId}, #{productName}, #{price});
</insert>
</mapper>
6. Tạo tệp cấu hình MyBatis cơ bản (mybatis-config.xml):
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<typeAlias type="com.example.domain.Product" alias="product"/>
</typeAliases>
</configuration>
7. Triển khai Mapper bằng SqlSessionTemplate:
package com.example.mapper;
import com.example.domain.Product;
import org.mybatis.spring.SqlSessionTemplate;
import java.util.List;
public class ProductMapperImpl implements ProductMapper {
// SqlSessionTemplate sẽ được Spring tiêm vào
private SqlSessionTemplate sqlSession;
public void setSqlSession(SqlSessionTemplate sqlSession) {
this.sqlSession = sqlSession;
}
@Override
public List<Product> findAllProducts() {
// Lấy mapper từ SqlSessionTemplate để thực hiện các thao tác
ProductMapper mapper = sqlSession.getMapper(ProductMapper.class);
return mapper.findAllProducts();
}
@Override
public int insertProduct(Product product) {
ProductMapper mapper = sqlSession.getMapper(ProductMapper.class);
return mapper.insertProduct(product);
}
}
8. Cấu hình bean ProductMapperImpl trong Spring XML (ví dụ: dao-config.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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Import cấu hình DataSource, SqlSessionFactory, SqlSessionTemplate -->
<import resource="spring-mybatis-core.xml"/>
<!-- Định nghĩa bean cho ProductMapperImpl -->
<bean id="productMapper" class="com.example.mapper.ProductMapperImpl">
<property name="sqlSession" ref="sqlSession"/>
</bean>
</beans>
9. Hoặc dùng SqlSessionDaoSupport để đơn giản hóa việc triển khai Mapper:
package com.example.mapper;
import com.example.domain.Product;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import java.util.List;
public class ProductMapperImpl2 extends SqlSessionDaoSupport implements ProductMapper {
@Override
public List<Product> findAllProducts() {
ProductMapper mapper = getSqlSession().getMapper(ProductMapper.class);
return mapper.findAllProducts();
}
@Override
public int insertProduct(Product product) {
ProductMapper mapper = getSqlSession().getMapper(ProductMapper.class);
return mapper.insertProduct(product);
}
}
Cấu hình ProductMapperImpl2 trong Spring XML:
<!-- Định nghĩa bean cho ProductMapperImpl2 -->
<bean id="productMapper2" class="com.example.mapper.ProductMapperImpl2">
<!-- SqlSessionDaoSupport yêu cầu SqlSessionFactory -->
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>
10. Kiểm tra tích hợp:
import com.example.domain.Product;
import com.example.mapper.ProductMapper;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import java.util.List;
public class TestSpringMyBatis {
@Test
public void testProductListing() {
ApplicationContext context = new ClassPathXmlApplicationContext("dao-config.xml");
ProductMapper productMapper = context.getBean("productMapper", ProductMapper.class);
productMapper.insertProduct(new Product(101, "Điện thoại Samsung", 1500.0));
productMapper.insertProduct(new Product(102, "Máy tính bảng", 800.0));
List<Product> products = productMapper.findAllProducts();
for (Product product : products) {
System.out.println(product);
}
}
}
13. Giao Dịch Khai Báo (Declarative Transaction)
Giao dịch là một chuỗi các thao tác được thực hiện như một đơn vị logic. Tất cả các thao tác trong giao dịch phải thành công hoặc tất cả phải thất bại (nguyên tắc ACID).
Tại sao cần giao dịch?
- Đảm bảo tính nhất quán của dữ liệu.
- Ngăn chặn các lỗi do thay đổi dữ liệu không đầy đủ hoặc không đồng bộ.
Trong Spring, chúng ta thường sử dụng giao dịch khai báo, nơi bạn định nghĩa các quy tắc giao dịch trong cấu hình (XML hoặc Annotation) thay vì mã hóa thủ công trong logic nghiệp vụ.
Các bước cấu hình:
1. Thêm các thư viện cần thiết vào pom.xml:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version> <!-- Đã cập nhật version -->
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.9</version> <!-- Đã cập nhật version -->
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId> <!-- Thay vì spring-webmvc nếu không dùng web -->
<version>5.3.15</version> <!-- Đã cập nhật version -->
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId> <!-- Cần cho quản lý transaction -->
<version>5.3.15</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId> <!-- Cần cho quản lý transaction -->
<version>5.3.15</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId> <!-- Cần cho Spring AOP -->
<version>1.9.8</version> <!-- Đã cập nhật version -->
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.7</version> <!-- Đã cập nhật version -->
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<scope>provided</scope>
</dependency>
</dependencies>
2. Lớp POJO Order:
package com.example.transaction.domain;
public class Order {
private int orderId;
private String customerName;
private double totalAmount;
// Constructors, Getters, Setters, toString
public Order() {}
public Order(int orderId, String customerName, double totalAmount) {
this.orderId = orderId;
this.customerName = customerName;
this.totalAmount = totalAmount;
}
public int getOrderId() { return orderId; }
public void setOrderId(int orderId) { this.orderId = orderId; }
public String getCustomerName() { return customerName; }
public void setCustomerName(String customerName) { this.customerName = customerName; }
public double getTotalAmount() { return totalAmount; }
public void setTotalAmount(double totalAmount) { this.totalAmount = totalAmount; }
@Override
public String toString() {
return "Order{" +
"orderId=" + orderId +
", customerName='" + customerName + '\'' +
", totalAmount=" + totalAmount +
'}';
}
}
3. Giao diện Mapper OrderMapper:
package com.example.transaction.mapper;
import com.example.transaction.domain.Order;
import java.util.List;
public interface OrderMapper {
List<Order> getAllOrders();
int addOrder(Order order);
int deleteOrder(int id);
}
4. Tệp Mapper XML OrderMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.transaction.mapper.OrderMapper">
<select id="getAllOrders" resultType="com.example.transaction.domain.Order">
SELECT order_id, customer_name, total_amount FROM orders;
</select>
<insert id="addOrder" parameterType="com.example.transaction.domain.Order">
INSERT INTO orders (order_id, customer_name, total_amount) VALUES (#{orderId}, #{customerName}, #{totalAmount});
</insert>
<delete id="deleteOrder" parameterType="int">
DELETE FROM orders WHERE order_id = #{id};
</delete>
</mapper>
5. Lớp triển khai Mapper (sử dụng SqlSessionDaoSupport):
package com.example.transaction.mapper;
import com.example.transaction.domain.Order;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import java.util.List;
public class OrderMapperImpl extends SqlSessionDaoSupport implements OrderMapper {
@Override
public List<Order> getAllOrders() {
return getSqlSession().getMapper(OrderMapper.class).getAllOrders();
}
@Override
public int addOrder(Order order) {
// Ví dụ một thao tác thứ hai trong cùng một "giao dịch" logic
// Nếu dòng này gây lỗi, cả addOrder và deleteOrder sẽ rollback (nếu có)
// int x = 1/0; // Uncomment để thử lỗi
// Giả sử có thêm một thao tác nào đó ở đây, ví dụ:
// getSqlSession().getMapper(OtherMapper.class).doSomethingElse();
return getSqlSession().getMapper(OrderMapper.class).addOrder(order);
}
@Override
public int deleteOrder(int id) {
return getSqlSession().getMapper(OrderMapper.class).deleteOrder(id);
}
}
6. Cấu hình Spring XML cho giao dịch (ví dụ: transaction-config.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:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- DataSource -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/my_database?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="password"/>
</bean>
<!-- SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<property name="mapperLocations" value="classpath:com/example/transaction/mapper/*.xml"/>
</bean>
<!-- Định nghĩa OrderMapperImpl -->
<bean id="orderMapper" class="com.example.transaction.mapper.OrderMapperImpl">
<property name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>
<!-- Cấu hình Transaction Manager -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- Cấu hình lời khuyên giao dịch -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- Áp dụng giao dịch cho các phương thức có tên bắt đầu bằng 'add', 'update', 'delete' -->
<tx:method name="add*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<!-- Các phương thức 'query' hoặc 'get' chỉ đọc -->
<tx:method name="get*" read-only="true"/>
<!-- Mọi phương thức khác cũng cần giao dịch REQUIRED -->
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>
<!-- Kết hợp AOP để đưa giao dịch vào -->
<aop:config>
<!-- Định nghĩa điểm cắt cho tất cả các phương thức trong gói mapper -->
<aop:pointcut id="transactionPointCut" expression="execution(* com.example.transaction.mapper.*.*(..))"/>
<!-- Gán lời khuyên giao dịch cho điểm cắt -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="transactionPointCut"/>
</aop:config>
</beans>
7. Kiểm tra giao dịch:
import com.example.transaction.domain.Order;
import com.example.transaction.mapper.OrderMapper;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestDeclarativeTransaction {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("transaction-config.xml");
OrderMapper orderMapper = context.getBean("orderMapper", OrderMapper.class);
System.out.println("--- Trước khi thêm đơn hàng ---");
orderMapper.getAllOrders().forEach(System.out::println);
try {
// Thao tác này sẽ được bao bọc trong một giao dịch
orderMapper.addOrder(new Order(1001, "Khach Hang A", 1250.0));
orderMapper.deleteOrder(5); // Xóa một order không tồn tại để kiểm tra
System.out.println("Thêm và xóa đơn hàng thành công.");
} catch (Exception e) {
System.err.println("Lỗi trong giao dịch: " + e.getMessage());
}
System.out.println("--- Sau khi thêm đơn hàng ---");
orderMapper.getAllOrders().forEach(System.out::println);
}
}
Trong ví dụ OrderMapperImpl.addOrder, nếu bạn bỏ comment dòng int x = 1/0;, một ngoại lệ sẽ được ném ra. Với cấu hình giao dịch, cả thao tác addOrder và các thao tác khác trong cùng một phương thức sẽ được rollback, đảm bảo tính nguyên tử của giao dịch.