Aspect-Oriented Programming trong Spring
Xem thửMột nhóm có mười hai service và yêu cầu mới: ghi log thời gian chạy của mọi thao tác ghi dữ liệu. Cách đầu tiên là thêm hai dòng đo thời gian vào đầu và cuối của từng phương thức: bốn mươi chỗ sửa, và một tuần sau lại có người thêm phương thức mới mà quên. Cách thứ hai: viết một đoạn mã đo thời gian duy nhất và nói "áp dụng cho mọi phương thức có annotation @Audited". Đó là lập trình hướng khía cạnh (AOP): một mối quan tâm cắt ngang (log, thời gian, kiểm toán, giao dịch, bảo mật) được viết một lần và áp dụng bằng quy tắc thay vì chép vào từng nơi. Nhưng cách thứ hai có một cái bẫy mà cả @Transactional và @PreAuthorize bạn đã gặp đều mang: nó hoạt động qua proxy, và có những lời gọi proxy không bao giờ thấy.
Bài này dựng một aspect nhỏ và đo mọi thứ nó làm: bean thật sự là gì khi bạn tiêm nó, thứ tự các loại advice, thứ tự giữa hai aspect, và ba loại lời gọi mà aspect bỏ qua (gọi bằng this, phương thức private, phương thức final). Phần final cho một kết quả đáng ngạc nhiên.
Sau bài này, bạn có thể
- Giải thích aspect, advice, pointcut và join point bằng ví dụ chạy được.
- Phân biệt proxy CGLIB và proxy JDK, và biết cái nào được dùng mặc định.
- Nêu thứ tự
@Around,@Before,@AfterReturning,@Aftervà thứ tự lồng của nhiều aspect bằng@Order. - Nhận ra ba loại lời gọi không được tăng cường (
this,private,final) và thiết kế tránh chúng.
Chuẩn bị môi trường và dự án
Mọi thí nghiệm của bài chạy trong một dự án Maven nhỏ. Mục này nói rõ cần cài gì, khai báo thư viện nào và cấu hình ra sao; mã của từng lớp được trình bày ngay tại chỗ nó được dùng, kèm giải thích, nên bạn không cần mở nơi nào khác. Số liệu và thông báo lỗi trong bài được đo từ chính mã đó (JDK 17, Spring Boot 4.1.1, Spring Framework 7.0.9). Bản sao lưu cả dự án nằm ở kho mã của khóa học, thư mục 12-aop/01-aop-proxy, chỉ để bạn clone cho nhanh nếu không muốn tự gõ lại.
Yêu cầu môi trường
| Thành phần | Phiên bản đã kiểm chứng | Ghi chú |
|---|---|---|
| JDK | 17 trở lên | java -version |
| Maven | 3.9 trở lên | mvn -v |
| Spring Boot | 4.1.1 | khai báo ở parent trong pom.xml; quản lý Spring Framework 7.0.9 và AspectJ |
| Docker | không cần | bài này không dùng cơ sở dữ liệu |
Cấu trúc
01-aop-proxy/
pom.xml parent Boot 4.1.1, starter aspectj, test
src/main/java/dev/canhdominich/boot/aop/
AopApplication.java @SpringBootApplication
Events.java ghi lại các sự kiện theo thứ tự
Audited.java annotation đánh dấu phương thức cần ghi kiểm toán
OrderService.java lớp có phương thức public, @Audited, gọi nội bộ, final và private
PriceCalculator.java, PricingService.java interface và cài đặt để so proxy JDK với CGLIB
LoggingAspect.java @Around, @Before, @AfterReturning, @After (thứ tự 2)
OuterAspect.java aspect bên ngoài (thứ tự 1)
AopDemo.java chạy mọi thí nghiệm
Khai báo thư viện: pom.xml
pom.xml
Dự án Maven dùng spring-boot-starter-parent 4.1.1 với spring-boot-starter-aspectj (Spring AOP và AspectJ cho cú pháp @Aspect và biểu thức pointcut) và spring-boot-starter-test. Không có web, cơ sở dữ liệu hay Docker. Plugin exec-maven-plugin chạy AopDemo bằng mvn -q compile exec:exec.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/>
</parent>
<groupId>dev.canhdominich.javabackend</groupId>
<artifactId>aop-proxy</artifactId>
<version>1.0.0</version>
<name>AOP - Aspect va proxy</name>
<properties>
<java.version>17</java.version>
<main.class>dev.canhdominich.boot.aop.AopDemo</main.class>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<configuration>
<executable>java</executable>
<arguments>
<argument>-classpath</argument>
<classpath/>
<argument>${main.class}</argument>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
</project>
Các khái niệm qua mã
Bài dùng spring-boot-starter-aspectj: nó kéo spring-aop và aspectjweaver, thư viện cung cấp cú pháp @Aspect và biểu thức pointcut. Bốn khái niệm:
- Aspect: một lớp gom mã xuyên suốt (ở đây
LoggingAspect,OuterAspect), đánh dấu@Aspect. - Advice: đoạn mã chạy tại một điểm (
@Around,@Before,@AfterReturning,@After). - Pointcut: biểu thức chọn những phương thức nào được tăng cường, ví dụ
execution(public * ...OrderService.*(..))hoặc@annotation(Audited). - Join point: một điểm cụ thể trong lúc chạy, ở Spring luôn là một lời gọi phương thức.
Các thành phần của bài. Events ghi lại sự kiện theo thứ tự để mọi điều dưới đây đo được thay vì phỏng đoán:
src/main/java/dev/canhdominich/boot/aop/Events.java
package dev.canhdominich.boot.aop;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class Events {
private static final List<String> LOG = Collections.synchronizedList(new ArrayList<>());
private Events() {
}
public static void record(String event) {
LOG.add(event);
}
public static void clear() {
LOG.clear();
}
public static List<String> all() {
synchronized (LOG) {
return new ArrayList<>(LOG);
}
}
}
Audited là một annotation tự định nghĩa để đánh dấu phương thức cần ghi kiểm toán; pointcut @annotation(Audited) sẽ chọn các phương thức mang nó:
src/main/java/dev/canhdominich/boot/aop/Audited.java
package dev.canhdominich.boot.aop;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Audited {
}
OrderService là lớp được tăng cường. Nó có đủ các loại phương thức để thử: place (public, có @Audited, gọi một phương thức private), cancel (public, không @Audited), outer (gọi this.place), receipt (final), và secret (private). Trường prefix được gán trong constructor để thí nghiệm final thấy được trạng thái của đối tượng thật:
src/main/java/dev/canhdominich/boot/aop/OrderService.java
package dev.canhdominich.boot.aop;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final String prefix;
public OrderService() {
this.prefix = String.valueOf("order-");
}
@Audited
public String place(String item) {
Events.record("body: place " + item);
return secret(item);
}
public String cancel(String item) {
Events.record("body: cancel " + item);
return "cancelled " + item;
}
public String outer(String item) {
Events.record("body: outer " + item);
return this.place(item);
}
public final String receipt(String item) {
Events.record("body: receipt " + item);
return prefix + item;
}
private String secret(String item) {
Events.record("body: private secret " + item);
return prefix + item;
}
}
PriceCalculator và PricingService là một interface và cài đặt của nó, để so sánh proxy JDK với CGLIB ở cuối:
src/main/java/dev/canhdominich/boot/aop/PriceCalculator.java
package dev.canhdominich.boot.aop;
public interface PriceCalculator {
int priceOf(String item);
}
src/main/java/dev/canhdominich/boot/aop/PricingService.java
package dev.canhdominich.boot.aop;
import org.springframework.stereotype.Service;
@Service
public class PricingService implements PriceCalculator {
@Override
public int priceOf(String item) {
return item.length() * 1000;
}
}
LoggingAspect là aspect chính: một @Around cho mọi phương thức public của OrderService, một @Before, @AfterReturning và @After cho phương thức có @Audited, và một @Before cho PricingService. Nó có @Order(2):
src/main/java/dev/canhdominich/boot/aop/LoggingAspect.java
package dev.canhdominich.boot.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Order(2)
public class LoggingAspect {
@Around("execution(public * dev.canhdominich.boot.aop.OrderService.*(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
Events.record("around: before " + point.getSignature().getName());
try {
return point.proceed();
} finally {
Events.record("around: after " + point.getSignature().getName());
}
}
@Before("@annotation(audited)")
public void audit(JoinPoint point, Audited audited) {
Events.record("before (audited): " + point.getSignature().getName());
}
@AfterReturning(pointcut = "@annotation(Audited)", returning = "result")
public void returned(JoinPoint point, Object result) {
Events.record("after returning: " + point.getSignature().getName() + " -> " + result);
}
@After("@annotation(Audited)")
public void finished(JoinPoint point) {
Events.record("after (finally): " + point.getSignature().getName());
}
@Before("execution(* dev.canhdominich.boot.aop.PricingService.priceOf(..))")
public void priced(JoinPoint point) {
Events.record("before: priceOf");
}
}
OuterAspect có @Order(1) và bao quanh riêng cancel, để đo nhiều aspect lồng nhau thế nào:
src/main/java/dev/canhdominich/boot/aop/OuterAspect.java
package dev.canhdominich.boot.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Order(1)
public class OuterAspect {
@Around("execution(* dev.canhdominich.boot.aop.OrderService.cancel(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {
Events.record("outer aspect: before " + point.getSignature().getName());
try {
return point.proceed();
} finally {
Events.record("outer aspect: after " + point.getSignature().getName());
}
}
}
AopApplication là lớp ứng dụng như các bài Boot:
src/main/java/dev/canhdominich/boot/aop/AopApplication.java
package dev.canhdominich.boot.aop;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class AopApplication {
public static void main(String[] args) {
SpringApplication.run(AopApplication.class, args);
}
}
AopDemo chạy các thí nghiệm. Với mỗi lời gọi, nó xóa Events, gọi phương thức qua bean lấy từ container, và in kết quả cùng các sự kiện đã ghi. Phần cuối khởi động lại ứng dụng với spring.aop.proxy-target-class=false:
src/main/java/dev/canhdominich/boot/aop/AopDemo.java
package dev.canhdominich.boot.aop;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.aop.support.AopUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
public class AopDemo {
public static void main(String[] args) {
run();
}
public static List<String> run() {
List<String> lines = new ArrayList<>();
SpringApplication application = new SpringApplication(AopApplication.class);
application.setDefaultProperties(Map.of("spring.main.web-application-type", "none", "spring.main.banner-mode", "off", "logging.level.root", "OFF"));
try (ConfigurableApplicationContext context = application.run()) {
OrderService orders = context.getBean(OrderService.class);
PriceCalculator prices = context.getBean(PriceCalculator.class);
lines.add("-- what the beans really are");
lines.add("OrderService class: " + describe(orders));
lines.add("PriceCalculator class: " + describe(prices));
lines.add("-- advice around a public method with @Audited");
lines.addAll(trace("place", () -> orders.place("book")));
lines.add("-- a public method without @Audited");
lines.addAll(trace("cancel", () -> orders.cancel("book")));
lines.add("-- a method that calls another through this");
lines.addAll(trace("outer", () -> orders.outer("book")));
lines.add("-- a final method");
lines.addAll(trace("receipt", () -> orders.receipt("book")));
lines.add("-- an interface-based bean that an aspect advises");
lines.addAll(trace("priceOf", () -> String.valueOf(prices.priceOf("book"))));
}
lines.add("-- the same beans with spring.aop.proxy-target-class=false");
SpringApplication jdk = new SpringApplication(AopApplication.class);
jdk.setDefaultProperties(Map.of("spring.main.web-application-type", "none", "spring.main.banner-mode", "off", "logging.level.root", "OFF", "spring.aop.proxy-target-class", "false"));
try (ConfigurableApplicationContext context = jdk.run()) {
lines.add("PriceCalculator class: " + describe(context.getBean(PriceCalculator.class)));
lines.add("OrderService class (no interface): " + describe(context.getBean(OrderService.class)));
lines.addAll(trace("priceOf", () -> String.valueOf(context.getBean(PriceCalculator.class).priceOf("book"))));
try {
context.getBean(PricingService.class);
lines.add("getBean(PricingService.class): found");
} catch (RuntimeException e) {
lines.add("getBean(PricingService.class): " + e.getClass().getSimpleName());
}
}
lines.forEach(System.out::println);
return lines;
}
private static String describe(Object bean) {
String name = bean.getClass().getSimpleName();
String kind = AopUtils.isCglibProxy(bean) ? "CGLIB proxy" : AopUtils.isJdkDynamicProxy(bean) ? "JDK dynamic proxy" : "not a proxy";
return name.replaceAll("\\$Proxy\\d+", "\\$Proxy<n>") + " (" + kind + ", target " + AopUtils.getTargetClass(bean).getSimpleName() + ")";
}
private static List<String> trace(String label, java.util.function.Supplier<String> call) {
Events.clear();
String result = call.get();
List<String> lines = new ArrayList<>();
lines.add(label + " returned " + result);
Events.all().forEach(event -> lines.add(" " + event));
return lines;
}
}
Chạy bằng mvn -q compile exec:exec. Kết quả (đã bỏ log ở stderr; số thứ tự của proxy JDK được thay bằng <n>):
-- what the beans really are
OrderService class: OrderService$$SpringCGLIB$$0 (CGLIB proxy, target OrderService)
PriceCalculator class: PricingService$$SpringCGLIB$$0 (CGLIB proxy, target PricingService)
-- advice around a public method with @Audited
place returned order-book
around: before place
before (audited): place
body: place book
body: private secret book
after returning: place -> order-book
after (finally): place
around: after place
-- a public method without @Audited
cancel returned cancelled book
outer aspect: before cancel
around: before cancel
body: cancel book
around: after cancel
outer aspect: after cancel
-- a method that calls another through this
outer returned order-book
around: before outer
body: outer book
body: place book
body: private secret book
around: after outer
-- a final method
receipt returned nullbook
body: receipt book
-- an interface-based bean that an aspect advises
priceOf returned 4000
before: priceOf
-- the same beans with spring.aop.proxy-target-class=false
PriceCalculator class: $Proxy<n> (JDK dynamic proxy, target PricingService)
OrderService class (no interface): OrderService$$SpringCGLIB$$0 (CGLIB proxy, target OrderService)
priceOf returned 4000
before: priceOf
getBean(PricingService.class): NoSuchBeanDefinitionException
Bạn tiêm cái gì: proxy
Hai dòng đầu trả lời câu hỏi nền tảng: lớp thật của OrderService mà bạn lấy từ container là OrderService$$SpringCGLIB$$0, một lớp con do Spring sinh ra lúc chạy, và AopUtils xác nhận đó là proxy CGLIB bọc quanh một OrderService thật. Chuỗi hoạt động như sau: lúc khởi động, Spring thấy có aspect khớp với bean, nên thay bean đó trong container bằng một đối tượng đại diện (proxy). Khi bạn gọi một phương thức, proxy chạy các advice khớp rồi mới (hoặc trong lúc) gọi đối tượng thật. Một bean không khớp aspect nào thì không bị thay: hai bean PricingService ở dòng thứ hai là proxy chỉ vì có @Before nhắm vào nó.
Có hai loại proxy:
- CGLIB (mặc định của Boot): sinh một lớp con của lớp đích. Dùng được cả khi lớp không có interface.
- JDK dynamic proxy: sinh một đối tượng cài đặt các interface của đích. Khi đặt
spring.aop.proxy-target-class=falsevà bean có interface,PriceCalculatortrở thành$Proxy<n>; nhưng nó chỉ có kiểu interface, nêngetBean(PricingService.class)(theo lớp cụ thể) thất bại vớiNoSuchBeanDefinitionException. VớiOrderService(không có interface) Spring vẫn dùng CGLIB.
Cách đọc hình: hàng trên: bean có aspect khớp bị thay bằng lớp con CGLIB, kể cả khi có interface. Hàng dưới: khi tắt proxy-target-class và có interface thì là proxy JDK chỉ mang kiểu interface; không có interface thì vẫn CGLIB.
Thứ tự advice quanh một phương thức
Thí nghiệm place ghi tám sự kiện cho một lời gọi orders.place("book"):
around: before place: phần đầu của@Around(trướcproceed()).before (audited): place:@Before.body: place bookvàbody: private secret book: thân phương thức (và phương thứcprivatenó gọi).after returning: place -> order-book:@AfterReturning, chỉ khi thành công, thấy kết quả trả về.after (finally): place:@After, chạy dù thành công hay ném lỗi (như khốifinally).around: after place: phần sau của@Around.
Hình dung @Around là lớp ngoài cùng quanh tất cả (nó quyết định có gọi proceed() hay không và có thể sửa kết quả hay nuốt lỗi), và @Before, @AfterReturning, @After nằm bên trong nó. Bài sau đo thêm @AfterThrowing và các thay đổi @Around có thể làm.
Cách đọc hình: các ô đi từ trên xuống: vào bằng @Around rồi @Before, chạy thân, rồi ra bằng @AfterReturning, @After, và phần sau của @Around.
Nhiều aspect: thứ tự lồng
cancel bị hai aspect cùng khớp: OuterAspect (@Order(1)) và LoggingAspect (@Order(2)). Sự kiện ghi: outer aspect: before, around: before, thân, around: after, outer aspect: after. Aspect có giá trị @Order nhỏ hơn nằm ở ngoài, gần người gọi hơn: nó vào trước và ra sau, bao quanh aspect kia như lớp hành. Thứ tự này quan trọng khi hai aspect tương tác (một aspect ghi log, một aspect thử lại khi lỗi: cái nào ở ngoài quyết định log thấy mỗi lần thử hay chỉ kết quả cuối).
Cách đọc hình: @Order(1) mở đầu và kết thúc; @Order(2) nằm trong; thân phương thức ở giữa. Lớp nào ngoài cùng là lớp đầu tiên nhìn thấy lời gọi và cuối cùng nhìn thấy kết quả.
Ba loại lời gọi mà proxy không thấy
Đây là phần đáng nhớ nhất: aspect không chạy, và không có thông báo nào.
- Gọi bằng
this.orders.outer("book")chạyaround: before outer, thân củaouter, rồi thân củaplacebên trong, vàaround: after outer. Không cóaround: before place, không cóbefore (audited): place, mặc dùplacecó@Audited. Lý do:outerđang chạy trong đối tượng thật;this.place(...)là lời gọi trực tiếp giữa hai phương thức của cùng đối tượng, không đi qua proxy. Đây chính là bẫy self-invocation của@Transactionalở bài Spring Data JPA. - Phương thức
private.secretluôn được gọi từ bên trong và proxy cũng không thể ghi đè phương thứcprivate, nên không bao giờ có advice quanh nó (bạn thấybody: private secret bookkhông kèm sự kiện advice nào). - Phương thức
final.receiptghibody: receipt bookmà không có sự kiện advice nào, và trả vềnullbookthay vìorder-book. Hai điều đồng thời: CGLIB tạo lớp con nên không ghi đè được phương thứcfinal, vì thế lời gọiorders.receipt(...)chạy ngay trên chính đối tượng proxy, không phải trên đối tượng thật. Và đối tượng proxy được tạo mà không chạy constructor của lớp đích, nên trườngprefixcủa nó lànull. Kết quảnullbooklà một lỗi dữ liệu im lặng: mã chạy, trả về chuỗi sai, và không có exception.
Đang tải Animation…
Cách đọc hình: hàng trên: this.place và private secret không qua proxy. Hàng dưới: final receipt không được tăng cường và trả nullbook vì chạy trên đối tượng proxy. Ô cuối là quy tắc thiết kế.
| Điều muốn | Cách đúng | Đo được khi sai |
|---|---|---|
| Advice chạy cho một lời gọi | Gọi từ bean khác qua proxy | this. bỏ qua advice |
| Advice cho một phương thức | Phương thức public, không final | private và final không được tăng cường |
| Trạng thái đúng khi qua proxy | Không dùng phương thức final trên bean được tăng cường | final trả trạng thái rỗng (nullbook) |
| Chọn bean theo lớp | Tiêm theo interface hoặc giữ CGLIB | Proxy JDK không tìm được theo lớp cụ thể |
| Thứ tự nhiều aspect | @Order rõ ràng | Thứ tự không đảm bảo nếu bỏ trống |
Quyết định thiết kế và đánh đổi
| Quyết định | Lựa chọn | Đánh đổi |
|---|---|---|
| Mối quan tâm xuyên suốt | Aspect với pointcut theo annotation | Mã chạy "vô hình": cần tài liệu và test |
| Loại proxy | CGLIB mặc định | Lớp không được final, cần constructor dùng được |
| Chọn đích | @annotation(Audited) thay vì tên lớp | Phải nhớ gắn annotation |
| Nhiều aspect | @Order tường minh | Phải suy nghĩ về lồng nhau |
| Lời gọi nội bộ | Tách sang bean khác | Cấu trúc lớp phản ánh ranh giới aspect |
Phương thức final | Tránh trên bean có aspect | Một số lớp bất biến muốn final |
Playbook: gặp sự cố aspect thì làm gì
| Triệu chứng | Kiểm tra đầu tiên | Nguyên nhân hay gặp | Cách xử lý |
|---|---|---|---|
| Aspect không chạy | Bean có là proxy không (AopUtils.isAopProxy) | Bean không do Spring quản lý, hoặc pointcut sai | Kiểm tra bean và biểu thức pointcut |
| Aspect chạy ở đây nhưng không ở chỗ kia | Lời gọi có qua proxy không | Gọi bằng this | Chuyển phương thức sang bean khác |
Giá trị null lạ trong phương thức | Phương thức có final không | Chạy trên đối tượng proxy | Bỏ final |
NoSuchBeanDefinitionException theo lớp cụ thể | Loại proxy | Proxy JDK chỉ có kiểu interface | Tiêm theo interface hoặc proxy-target-class=true |
| Thứ tự advice sai giữa hai aspect | @Order | Không đặt, hoặc đặt ngược | Đặt @Order rõ ràng |
| Khởi động chậm khi thêm nhiều aspect | Phạm vi pointcut | Pointcut quá rộng (execution(* *(..))) | Thu hẹp pointcut theo package hoặc annotation |
Lỗi thường gặp khi chạy lab
| Triệu chứng | Nguyên nhân | Cách xử lý |
|---|---|---|
| Aspect không chạy | Thiếu spring-boot-starter-aspectj hoặc bean không phải bean Spring | Giữ starter trong pom.xml; bean phải do container quản lý |
getBean(PricingService.class) báo không tìm thấy | Bean là proxy JDK chỉ có kiểu interface | Tiêm theo interface, hoặc giữ proxy-target-class=true |
Bài thực hành
Làm trong dự án của bài (thư mục 12-aop/01-aop-proxy). Sau mỗi bước, so kết quả với tiêu chí nghiệm thu.
- Chạy thí nghiệm.
mvn -q compile exec:exec. Tiêu chí: các dòng khớp bài, đặc biệtnullbookvà thiếu advice ởthis.place. - Gắn aspect vào phương thức mới. Thêm
@Auditedvàocancelvà chạy lại. Tiêu chí: thêmbefore (audited): cancel,after returning: cancel -> cancelled bookvàafter (finally): cancelquanh thâncancel. - Sửa self-invocation. Chuyển
placesang một beanPlacementServicemới (@Service) và choOrderService.outergọi qua nó. Tiêu chí: khi gắn@AuditedchoPlacementService.place(vàOrderServicenhậnPlacementServicequa constructor),outer returnedcó thêmbefore (audited): place,after returningvàafter (finally)quanh thânplace;around: before placevẫn không có vì pointcut@Aroundchỉ nhắmOrderService. - Bỏ
final. Bỏfinalkhỏireceiptvà chạy lại. Tiêu chí:receipt returned order-bookvà cóaround: before receipt. - Đảo thứ tự. Đổi
@Ordercủa hai aspect (OuterAspectthành 3). Tiêu chí:around: before cancelđứng trướcouter aspect: before cancel(aspect ngoài đã đổi chỗ vào trong). - Chạy bộ test (tùy chọn). Bộ test nằm trong bản sao lưu ở kho mã của khóa học; sau khi clone,
mvn testchoTests run: 6, Failures: 0, Errors: 0. Dọn dẹp bằngmvn clean.
Hiểu lầm và giới hạn
- "Aspect sửa mã của lớp." Spring AOP không sửa lớp; nó đặt một proxy phía trước.
- "Mọi lời gọi phương thức đều được tăng cường." Chỉ lời gọi qua proxy, vào phương thức public không
final. - "
finalan toàn hơn." Trên bean được tăng cường,finallàm mất advice và trạng thái. - Số đo thuộc về Spring Boot 4.1.1 (Spring Framework 7.0.9, AspectJ do Boot quản lý): tên lớp proxy, thứ tự advice và hành vi
finallà của cách cài đặt CGLIB hiện tại. Bài chưa nói về@AfterThrowing,@Aroundsửa kết quả, nhiều aspect trên giao dịch (bài sau), AspectJ weaving lúc biên dịch hay nạp lớp (khắc phục giới hạn proxy), và hiệu năng của proxy.
Tự kiểm tra
- Khi bạn tiêm một bean có aspect, bạn nhận được đối tượng nào?
- Nêu thứ tự
@Around,@Before,@AfterReturning,@Afterquanh một lời gọi thành công. - Vì sao
this.place()bên trongouterkhông có advice? - Vì sao phương thức
finaltrảnullbook? - Aspect nào bao ngoài khi một có
@Order(1)và một có@Order(2)?
Gợi ý đáp án. (1) Một proxy (lớp con CGLIB mặc định) bọc đối tượng thật. (2) @Around mở đầu, @Before, thân, @AfterReturning, @After, @Around kết thúc. (3) Lời gọi trực tiếp trong đối tượng thật không đi qua proxy. (4) Proxy không ghi đè được phương thức final, nên nó chạy trên chính đối tượng proxy, nơi constructor của đích chưa từng chạy. (5) @Order(1) ở ngoài.
Điều cần nhớ
- AOP viết một mối quan tâm xuyên suốt một lần và áp bằng pointcut; ở Spring nó chạy qua proxy.
- Bean được tăng cường là proxy: CGLIB mặc định, JDK khi tắt
proxy-target-classvà có interface. - Thứ tự:
@Aroundngoài cùng, rồi@Before, thân,@AfterReturning,@After; aspect có@Ordernhỏ ở ngoài. this,privatevàfinalthoát khỏi aspect một cách im lặng;finalcòn trả trạng thái rỗng.
Đọc thêm
- Spring Framework Reference: Aspect Oriented Programming with Spring
- Spring Framework Reference: Proxying Mechanisms
- Spring Framework Reference: Advice Ordering
Bài tiếp theo hoàn thiện chương: các loại advice còn lại (@AfterThrowing, @Around sửa kết quả và thử lại) và cách @Transactional chính là một aspect, kể cả thứ tự giữa aspect của bạn và giao dịch.