Bài trước kết thúc bằng một BeanPostProcessor tráo bean thành JDK dynamic proxy, và có nhắc rằng AnnotationAwareAspectJAutoProxyCreator của Spring làm đúng chuyện đó nhưng ở quy mô lớn hơn nhiều. Bài này mổ trọn bộ máy ấy: Spring dựng proxy loại nào, mỗi loại không làm được gì, một aspect chọn method để bọc ra sao, và vì sao annotation trên method mà bạn gọi từ bên trong cùng class lại hoàn toàn vô tác dụng.
Các ví dụ dùng Spring Boot 4.1.1 và Java 21. Không góc nào của Spring có nhiều lời truyền miệng từ thời trước Boot 3 như chỗ này, nên bài này đem từng mẩu truyền miệng ra đối chiếu với những gì Boot 4 thực sự làm.
![]()
Phần đầu là một thay đổi đóng gói của Boot 4 mà bạn gặp trước cả khi viết dòng aspect đầu tiên. Từ phần hai trở đi là bản thân cái proxy.
Starter AOP không còn tồn tại
Mọi bài viết về AOP trước 2026 đều mở đầu giống nhau: thêm spring-boot-starter-aop. Trên Boot 4.1.1 artifact đó không nằm trong bill of materials, và Spring Initializr không biết từ này:
curl -s "https://start.spring.io/starter.zip?type=gradle-project&language=java&bootVersion=4.1.1&javaVersion=21&dependencies=aop"{"timestamp":"2026-09-18T03:01:58.679Z","status":400,"error":"Bad Request","message":"Unknown dependency 'aop' check project metadata","path":"/starter.zip"}Không phải gõ nhầm. Metadata của Initializr cho Boot 4.1.1 liệt kê 204 dependency trong 23 nhóm, và tìm chuỗi aop hay aspect trong mọi field của cả 204 mục đó cho ra con số không. BOM cũng vậy — spring-boot-starter-aop không xuất hiện ở bất cứ đâu trong spring-boot-dependencies-4.1.1.pom, còn metadata trên Maven Central dừng ở 4.0.0-M2. Hỏi bản 4.1.1 thì nhận 404.
Bản thay thế nằm trong BOM dưới một cái tên khác, và bạn tự thêm bằng tay:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aspectj'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-aspectj-test'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
}Không ghi version: BOM của Boot plugin lo phần đó. Nó kéo theo những gì, lấy từ ./gradlew dependencies --configuration compileClasspath trên một project mà dependency duy nhất còn lại là spring-boot-starter-webmvc:
+--- org.springframework.boot:spring-boot-starter-aspectj -> 4.1.1
| +--- org.springframework.boot:spring-boot-starter:4.1.1
| +--- org.springframework:spring-aop:7.0.9 (*)
| \--- org.aspectj:aspectjweaver:1.9.25.1Ba thứ, và chỉ một thứ là mới với một ứng dụng thông thường. spring-aop vốn đã có trên classpath của mọi project Spring Boot, vì spring-context phụ thuộc vào nó. spring-boot-starter cũng đã có sẵn. Thứ mà starter thật sự bổ sung là org.aspectj:aspectjweaver, được ghim bởi property aspectj.version = 1.9.25.1 trong BOM.
Starter bật lên chính xác thứ gì
aspectjweaver không phải đồ trang trí. AopAutoConfiguration của 4.1.1 rẽ nhánh dựa trên một class trong đó, và javap -v đọc thẳng ra từ file jar:
AopAutoConfiguration
@ConditionalOnBooleanProperty(name = ["spring.aop.auto"], matchIfMissing = true)
AopAutoConfiguration$AspectJAutoProxyingConfiguration
@ConditionalOnClass(org.aspectj.weaver.Advice.class)
AopAutoConfiguration$AspectJAutoProxyingConfiguration$CglibAutoProxyConfiguration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ConditionalOnBooleanProperty(name = ["spring.aop.proxy-target-class"], matchIfMissing = true)
AopAutoConfiguration$AspectJAutoProxyingConfiguration$JdkDynamicAutoProxyConfiguration
@EnableAspectJAutoProxy(proxyTargetClass = false)
@ConditionalOnBooleanProperty(name = ["spring.aop.proxy-target-class"], havingValue = false)
AopAutoConfiguration$ClassProxyingConfiguration
@ConditionalOnMissingClass("org.aspectj.weaver.Advice")
@ConditionalOnBooleanProperty(name = ["spring.aop.proxy-target-class"], matchIfMissing = true)Không có org.aspectj.weaver.Advice trên classpath thì Boot đi nhánh ClassProxyingConfiguration: proxy vẫn được tạo — nhờ vậy @Transactional vẫn chạy — nhưng @EnableAspectJAutoProxy không bao giờ được áp dụng, nên không class @Aspect nào được đọc. Có class đó thì phần xử lý @Aspect được bật, và spring.aop.proxy-target-class quyết định loại proxy.
Hai property, và chỉ có bấy nhiêu. Lấy từ spring-configuration-metadata.json bên trong spring-boot-autoconfigure-4.1.1.jar:
| Property | Type | Mặc định | Tác dụng |
|---|---|---|---|
spring.aop.auto | Boolean | true | thêm @EnableAspectJAutoProxy |
spring.aop.proxy-target-class | Boolean | true | proxy kiểu CGLIB subclass (true) thay cho JDK proxy dựa trên interface (false) |
Không hề có spring.aop.expose-proxy. Thứ đó chỉ với tới được qua annotation, và điều này quan trọng ở phần self-invocation bên dưới.
Có thật sự cần starter không?
Thường là không, và câu trả lời thành thật này đáng biết trước khi bạn thêm một dependency. spring-boot-starter-data-jpa kéo theo spring-aspects, mà spring-aspects kéo theo aspectjweaver:
| \--- org.springframework:spring-aspects:7.0.9
| \--- org.aspectj:aspectjweaver:1.9.25 -> 1.9.25.1Ứng dụng lab của bài này đã được build lại sau khi xoá hẳn dòng starter AspectJ, và condition report do --debug in ra không đổi:
AopAutoConfiguration matched:
- @ConditionalOnBooleanProperty (spring.aop.auto=true) matched (OnPropertyCondition)
AopAutoConfiguration.AspectJAutoProxyingConfiguration matched:
- @ConditionalOnClass found required class 'org.aspectj.weaver.Advice' (OnClassCondition)
AopAutoConfiguration.AspectJAutoProxyingConfiguration.CglibAutoProxyConfiguration matched:
- @ConditionalOnBooleanProperty (spring.aop.proxy-target-class=true) matched (OnPropertyCondition)Mọi aspect trong ứng dụng vẫn chạy. Còn trên một project không có JPA — chỉ spring-boot-starter-webmvc — thì chính aspect đó thậm chí không compile nổi:
error: package org.aspectj.lang does not exist
import org.aspectj.lang.ProceedingJoinPoint;
^
error: cannot find symbol
@Aspect
^
symbol: class AspectVậy nên: thêm spring-boot-starter-aspectj khi bạn viết aspect, vì nó khai báo đúng thứ mà file source của bạn đang phụ thuộc. Đừng tưởng nó là thứ làm cho AOP chạy được — trên một ứng dụng JPA, AspectJ đã có mặt từ lâu trước khi bạn yêu cầu.
Proxy do ai dựng, và dựng từ đâu
Object dựng mọi proxy là một BeanPostProcessor, chính cái mà bài trước đo được ở vị trí 9 của chuỗi. Nó được đăng ký dưới một bean name cố định, nên bạn hỏi thẳng context là ra:
Trace.note("auto-proxy creator: "
+ context.getBean("org.springframework.aop.config.internalAutoProxyCreator").getClass().getName()); auto-proxy creator: org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreatorTrong postProcessAfterInitialization nó hỏi từng advisor trong context xem có áp dụng cho bean này không, và nếu có thì trả về một proxy thay chỗ bean. Mọi thứ trong bài này đều là hệ quả của một phép tráo duy nhất đó.
Service sẽ bị proxy được cố ý viết thật bình thường — một interface, một implementation, và một aspect in ra mỗi khi nó chạy:
package com.example.demo.pricing;
import java.math.BigDecimal;
public interface PriceService {
BigDecimal quote(String sku, int quantity);
String name();
}package com.example.demo.pricing;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Service;
@Service
public class ListPriceService implements PriceService {
private final Map<String, BigDecimal> catalog = new HashMap<>();
public ListPriceService() {
catalog.put("SKU-1", new BigDecimal("19.90"));
catalog.put("SKU-2", new BigDecimal("4.50"));
System.out.println(" ListPriceService constructor ran on instance @"
+ Integer.toHexString(System.identityHashCode(this)));
}
@Override
public BigDecimal quote(String sku, int quantity) {
return catalog.getOrDefault(sku, BigDecimal.ZERO).multiply(BigDecimal.valueOf(quantity));
}
@Override
public String name() {
return "list";
}
/** final on purpose: CGLIB cannot override it. */
public final String describe() {
return "catalog holds " + catalog.size() + " prices";
}
}JDK dynamic proxy hay CGLIB subclass
Truyền miệng nói rằng Spring dùng JDK dynamic proxy mỗi khi target có implement interface. Trên Boot thì điều đó sai từ bản 2.0, và kết quả chạy nói rõ. ListPriceService implement PriceService, vậy mà với cấu hình mặc định bean lại là một CGLIB subclass:
bean class com.example.demo.pricing.ListPriceService$$SpringCGLIB$$0
AopUtils.isAopProxy true
isJdkDynamicProxy false
isCglibProxy true
ultimateTargetClass com.example.demo.pricing.ListPriceServiceLý do nằm ở phần auto-configuration bên trên: Boot áp @EnableAspectJAutoProxy(proxyTargetClass = true) vì spring.aop.proxy-target-class mặc định là true. Spring Framework thuần, không có Boot, mặc định ngược lại. Đổi property thì đúng bean đó quay về dạng JDK proxy:
server.port=8203
spring.aop.proxy-target-class=false bean class jdk.proxy2.$Proxy103
AopUtils.isAopProxy true
isJdkDynamicProxy true
isCglibProxy false
ultimateTargetClass com.example.demo.pricing.ListPriceService
proxiedInterfaces [PriceService]jdk.proxy2 là module động mà JDK đặt các class proxy sinh ra vào; con số sau $Proxy là bộ đếm nên sẽ đổi khi bạn thêm bean. Bốn helper trả lời câu "tôi đang cầm cái gì", và nên nhớ chúng, vì getClass().getName() trên một proxy chẳng nói lên điều gì hữu ích:
| Lời gọi | Trả lời |
|---|---|
AopUtils.isAopProxy(bean) | object này có phải Spring AOP proxy không |
AopUtils.isJdkDynamicProxy(bean) | có phải java.lang.reflect.Proxy không |
AopUtils.isCglibProxy(bean) | có phải subclass sinh ra không |
AopProxyUtils.ultimateTargetClass(bean) | class của object nằm bên dưới, bóc hết các lớp proxy lồng nhau |

JDK proxy làm hỏng cái gì
JDK dynamic proxy implement các interface của target và không có gì khác. Nó không phải một ListPriceService, nên ép kiểu về implementation class sẽ ném:
cast to ListPriceService threw
java.lang.ClassCastException: class jdk.proxy2.$Proxy103 cannot be cast to class com.example.demo.pricing.ListPriceService (jdk.proxy2.$Proxy103 is in module jdk.proxy2 of loader org.springframework.boot.loader.launch.LaunchedClassLoader @378bf509; com.example.demo.pricing.ListPriceService is in unnamed module of loader org.springframework.boot.loader.launch.LaunchedClassLoader @378bf509)Thực tế bạn hiếm khi tự viết câu ép kiểu đó; container viết hộ bạn ngay khi một bean xin đúng implementation type thay vì interface:
@Component
public class ImplInjectionBean {
private final ListPriceService prices;
ImplInjectionBean(ListPriceService prices) {
this.prices = prices;
}
}Với spring.aop.proxy-target-class=false, ứng dụng đó không khởi động được, và failure analyzer của Boot giải thích rất tử tế:
APPLICATION FAILED TO START
***************************
Description:
The bean 'listPriceService' could not be injected because it is a JDK dynamic proxy
The bean is of type 'jdk.proxy2.$Proxy103' and implements:
com.example.demo.pricing.PriceService
org.springframework.aop.SpringProxy
org.springframework.aop.framework.Advised
org.springframework.core.DecoratingProxy
Expected a bean of type 'com.example.demo.pricing.ListPriceService' which implements:
com.example.demo.pricing.PriceService
Action:
Consider injecting the bean as one of its interfaces or forcing the use of CGLib-based proxies by setting proxyTargetClass=true on @EnableAsync and/or @EnableCaching.Đó là lý do Boot đổi mặc định. CGLIB subclass chính là target type, nên inject theo implementation class vẫn chạy và không ai phải quan tâm proxy nào đã được dựng. Cứ để nguyên spring.aop.proxy-target-class trừ khi bạn có lý do cụ thể — lý do thường gặp là một target class không thể kế thừa được.
CGLIB proxy không làm được gì
CGLIB proxy là một subclass sinh ra để override các method của target. Cái gì nó không override được thì chỗ đó là một lỗ hổng advice. Năm kiểu method, trong một class, mỗi cái đều gọi qua bean với một aspect khớp execution(* com.example.demo.limits.LimitsService.*(..)):
package com.example.demo.limits;
import com.example.demo.lab.Trace;
import org.springframework.stereotype.Service;
@Service
public class LimitsService {
public String publicMethod() {
return "public";
}
public final String finalMethod() {
return "final";
}
protected String protectedMethod() {
return "protected";
}
String packagePrivateMethod() {
return "package-private";
}
private String privateMethod() {
return "private";
}
/** The only way to reach the private method: an advised public method calls it. */
public String callsPrivateMethod() {
Trace.note("callsPrivateMethod(): this = " + this.getClass().getSimpleName());
return privateMethod();
}
}Lần chạy liệt kê những method mà subclass sinh ra có khai báo, rồi gọi từng cái:
bean class: com.example.demo.limits.LimitsService$$SpringCGLIB$$0
protectedMethod [protected] overridden by the proxy class = true
packagePrivateMethod [] overridden by the proxy class = true
callsPrivateMethod [public] overridden by the proxy class = true
publicMethod [public] overridden by the proxy class = true
finalMethod [public final] overridden by the proxy class = false
privateMethod [private] overridden by the proxy class = false
--- calling each one through the bean ---
[LimitsAspect] ADVISED publicMethod
publicMethod() -> public
finalMethod() -> final
[LimitsAspect] ADVISED protectedMethod
protectedMethod() -> protected
[LimitsAspect] ADVISED packagePrivateMethod
packagePrivateMethod() -> package-private
[LimitsAspect] ADVISED callsPrivateMethod
callsPrivateMethod(): this = LimitsService
callsPrivateMethod() -> privateĐoạn output đó chính là cái bảng mà phần này sinh ra để nói:
| Target | Proxy có override | Advice có chạy | Hỏng kiểu gì |
|---|---|---|---|
method public | có | có | — |
method protected | có | có | — |
| method package-private | có | có | — (subclass sinh ra cùng package và cùng class loader) |
method final | không | không | âm thầm, mà còn tệ hơn âm thầm — xem bên dưới |
method private | không | không | âm thầm; bản chất nó đã là self-invocation |
class final | — | — | lỗi to: context không khởi động được |
Hai dòng trong đó ngược hẳn với những gì phần lớn bài viết nói. "Chỉ method public mới được advise" là quy tắc của @Transactional với allowPublicMethodsOnly, không phải của Spring AOP: ở đây protected và package-private đều có advice. Còn method final thì không hề ném lỗi — nó bị bỏ qua lặng lẽ, và đó mới là nửa nguy hiểm.
Class final làm hỏng lúc khởi động
Đặt final lên một class @Service có pointcut khớp thì CGLIB không sinh ra được gì:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'finalPriceService' defined in URL […/demo-0.0.1-SNAPSHOT.jar/!BOOT-INF/classes/!/com/example/demo/limits/FinalPriceService.class]: Could not generate CGLIB subclass of class com.example.demo.limits.FinalPriceService: Common causes of this problem include using a final class or a non-visible class
Caused by: org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class com.example.demo.limits.FinalPriceService: Common causes of this problem include using a final class or a non-visible class
at org.springframework.aop.framework.CglibAopProxy.buildProxy(CglibAopProxy.java:236)
at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.createProxy(AbstractAutoProxyCreator.java:431)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:289)
Caused by: java.lang.IllegalArgumentException: Cannot subclass final class com.example.demo.limits.FinalPriceService
at org.springframework.cglib.proxy.Enhancer.generateClass(Enhancer.java:653)
at org.springframework.aop.framework.ObjenesisCglibAopProxy.createProxyClassAndInstance(ObjenesisCglibAopProxy.java:62)Ca này dễ sống chung, vì nó nổ ngay lần khởi động đầu tiên sau khi bạn viết ra. Nếu class có interface thì spring.aop.proxy-target-class=false cho bạn một JDK proxy; không thì bỏ chữ final đi.
Proxy không bao giờ chạy constructor của bạn
Nhìn khung cuối của stack trace đó: ObjenesisCglibAopProxy. Spring khởi tạo subclass sinh ra bằng Objenesis, thứ cấp phát object mà không gọi bất kỳ constructor nào. Chính nhờ vậy mà một target có constructor nhận tham số, mở connection hay đọc config vẫn proxy được — nhưng đổi lại, các field của chính proxy instance không bao giờ được khởi tạo.
Hai instance, một cái được khởi tạo, và field đọc ra từ mỗi cái bằng reflection:
== 2. does the proxy run the target's constructor
proxy instance @1e489957
target instance @63551c66
target.catalog {SKU-1=19.90, SKU-2=4.50}
proxy.catalog nullDòng println trong constructor xuất hiện đúng một lần trong cả quá trình khởi động — trên target. Bình thường bạn không thấy chuyện này, vì mọi method đã override đều uỷ quyền xuống target và không đụng tới field của proxy. Method final là ngoại lệ: nó không được override, nên gọi nó trên proxy sẽ chạy bytecode của target với this trỏ vào cái proxy chưa khởi tạo.
cast to ListPriceService worked, describe() next
describe() threw java.lang.NullPointerException: Cannot invoke "java.util.Map.size()" because "this.catalog" is nulldescribe() là ba dòng Java hoàn toàn đúng nhưng ném NullPointerException mỗi khi bean bị proxy. Spring có cảnh báo, ở mức WARN cho method implement interface và DEBUG cho các trường hợp còn lại, và thông điệp DEBUG gọi đúng tên vấn đề:
WARN CglibAopProxy: Public final method [public final java.lang.String com.example.demo.pricing.ListPriceService.describe()] cannot get proxied via CGLIB, consider removing the final marker or using interface-based JDK proxies.
DEBUG CglibAopProxy: Final method [public final java.lang.String com.example.demo.pricing.ListPriceService.describe()] cannot get proxied via CGLIB: Calls to this method will NOT be routed to the target instance and might lead to NPEs against uninitialized fields in the proxy instance.⚠️
finaltrên method của một Spring bean không phải một biện pháp an toàn. Nó hoặc làm mất advice, hoặc — nếu method có đọc field — tạo raNullPointerExceptionngay trong đoạn code nhìn rõ ràng là đúng.
Viết một aspect
Aspect là một @Component mang thêm @Aspect. Cần cả hai: @Component biến nó thành bean, @Aspect báo cho auto-proxy creator biết phải đọc advice từ nó. Biểu thức pointcut là cú pháp AspectJ, được Spring đánh giá lúc tạo proxy.
package com.example.demo.ordering;
import com.example.demo.lab.Trace;
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.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/** All five advice kinds on one named pointcut. */
@Aspect
@Component
public class AdviceKindAspect {
@Pointcut("execution(* com.example.demo.ordering.OrderLabService.handle(..))")
void handleCall() {
}
@Around("handleCall()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Trace.step("@Around before proceed()");
try {
Object result = pjp.proceed();
Trace.step("@Around after proceed(), result = " + result);
return result;
} catch (RuntimeException ex) {
Trace.step("@Around proceed() threw " + ex.getClass().getSimpleName());
throw ex;
}
}
@Before("handleCall()")
public void before(JoinPoint jp) {
Trace.step("@Before args = " + java.util.Arrays.toString(jp.getArgs()));
}
@AfterReturning(pointcut = "handleCall()", returning = "result")
public void afterReturning(Object result) {
Trace.step("@AfterReturning result = " + result);
}
@AfterThrowing(pointcut = "handleCall()", throwing = "ex")
public void afterThrowing(Throwable ex) {
Trace.step("@AfterThrowing " + ex.getClass().getSimpleName() + ": " + ex.getMessage());
}
@After("handleCall()")
public void after() {
Trace.step("@After runs either way");
}
}Method rỗng void handleCall() là một named pointcut: không có thân, không bao giờ được gọi, nó chỉ mang biểu thức. Năm advice tham chiếu tới nó bằng tên. Khi biểu thức đổi, bạn sửa một chuỗi thay vì năm chuỗi, và một named pointcut có thể được aspect khác dùng lại dưới dạng com.example.demo.ordering.AdviceKindAspect.handleCall().
Những pointcut designator đáng biết
AspectJ có cả tá designator; Spring AOP hỗ trợ một tập con, và năm cái dưới đây phủ gần hết nhu cầu. Một aspect khai báo một @Before cho mỗi designator, rồi một lần chạy gọi bốn method trên hai bean để bạn thấy rõ mỗi designator chọn cái nào:
@Aspect
@Component
public class DesignatorAspect {
@Before("execution(public String com.example.demo.designator.AlphaService.*(String))")
public void byExecution(JoinPoint jp) {
Trace.note("execution -> " + signature(jp));
}
@Before("within(com.example.demo.designator..*)")
public void byWithin(JoinPoint jp) {
Trace.note("within -> " + signature(jp));
}
@Before("@annotation(com.example.demo.designator.Audited)")
public void byAnnotation(JoinPoint jp) {
Trace.note("@annotation -> " + signature(jp));
}
@Before("bean(betaService)")
public void byBean(JoinPoint jp) {
Trace.note("bean -> " + signature(jp));
}
@Before("within(com.example.demo.designator..*) && args(sku, quantity)")
public void byArgs(JoinPoint jp, String sku, int quantity) {
Trace.note("args -> " + signature(jp) + " sku=" + sku + " quantity=" + quantity);
}
}== 4. pointcut designators
--- alpha.plain("x") ---
execution -> AlphaService.plain
within -> AlphaService.plain
--- alpha.audited("x") ---
@annotation -> AlphaService.audited
execution -> AlphaService.audited
within -> AlphaService.audited
--- beta.plain("x") ---
bean -> BetaService.plain
within -> BetaService.plain
--- beta.withTwoArgs("SKU-1", 3) ---
args -> BetaService.withTwoArgs sku=SKU-1 quantity=3
bean -> BetaService.withTwoArgs
within -> BetaService.withTwoArgs| Designator | Chọn cái gì | Ghi chú |
|---|---|---|
execution(…) | các lần chạy method theo signature | designator duy nhất lọc được theo modifier, return type và kiểu tham số; .. trong package nghĩa là "và mọi subpackage", (..) nghĩa là "tham số bất kỳ" |
within(…) | mọi thứ khai báo trong một type hoặc package | rẻ và thô; lựa chọn quen thuộc cho cả một tầng |
@annotation(…) | method mang một annotation | cách bạn tự dựng @Audited, @Timed, @RateLimited của riêng mình |
bean(…) | method trên các bean có tên khớp | chỉ Spring mới có, không phải AspectJ; hỗ trợ * nên bean(*Repository) chạy được |
args(…) | lời gọi có argument lúc chạy khớp kiểu | đồng thời bind argument vào tham số của advice, như sku và quantity ở trên |
args là cái hay làm người ta bất ngờ: nó được đánh giá theo từng lời gọi chứ không phải theo từng method, nên đây là designator duy nhất trong danh sách tốn chi phí lúc chạy. Cái && within(…) đứng trước nó không phải để cho đẹp — thiếu một designator tĩnh thu hẹp phạm vi trước, args sẽ bị thử trên mọi method của mọi bean trong context.
Ghép các designator bằng &&, || và !. @annotation(Audited) && within(com.example.demo.service..*) là dạng hay gặp: annotation của bạn, nhưng chỉ ở đúng chỗ bạn muốn.
Năm loại advice và thứ tự chúng chạy
Thứ tự đo được cho một lời gọi trả về bình thường, từ aspect bên trên, mỗi dòng do chính advice in ra:
== 5. advice order, normal return
1 @Around before proceed()
2 @Before args = [ok]
3 target method handle("ok")
4 @AfterReturning result = OK
5 @After runs either way
6 @Around after proceed(), result = OK
result = OKvà cho đúng lời gọi đó khi target ném exception:
== 5b. advice order, the target throws
1 @Around before proceed()
2 @Before args = [boom]
3 target method handle("boom")
4 @AfterThrowing IllegalStateException: handle failed on purpose
5 @After runs either way
6 @Around proceed() threw IllegalStateException
7 caller caught IllegalStateException: handle failed on purpose
Bốn điều rút ra từ hai vết chạy đó.
@Around nằm ngoài cùng ở cả hai nhánh. Đây là advice duy nhất sở hữu lời gọi: mọi thứ khác diễn ra giữa proceed() của nó và lúc giá trị quay về.
@After chạy trước khi @Around chạy tiếp. Bước 5 đứng trước bước 6. Tài liệu tham chiếu của Spring Framework ghi thứ tự ưu tiên trong cùng một aspect là @Around, @Before, @After, @AfterReturning, @AfterThrowing, với @After được gọi sau advice returning và throwing, theo đúng ngữ nghĩa "after finally advice" của AspectJ. Rất nhiều tài liệu cũ vẫn vẽ @After nằm ngoài @Around. Nó là finally của target method, không phải của cả chuỗi advice.
@AfterReturning và @AfterThrowing loại trừ nhau, và không cái nào đổi được kết cục. @AfterReturning thấy giá trị nhưng không thay được; @AfterThrowing thấy exception nhưng không nuốt được — bước 7 cho thấy caller nhận đúng IllegalStateException đó.
@Before không chặn được lời gọi, trừ khi nó ném exception. Nếu ném thì target không chạy và exception đi thẳng tới caller.
| Advice | Signature thường dùng | Có đổi được kết cục không |
|---|---|---|
@Around | Object around(ProceedingJoinPoint pjp) throws Throwable | có — argument, giá trị trả về, exception, và cả việc target có chạy hay không |
@Before | void before(JoinPoint jp) | chỉ bằng cách ném exception |
@AfterReturning | void afterReturning(Object result) | không |
@AfterThrowing | void afterThrowing(Throwable ex) | không |
@After | void after() | chỉ bằng cách ném exception |
Hãy chọn loại advice yếu nhất đủ dùng. @Around là loại duy nhất có thể quên gọi proceed(), gọi nó hai lần, hoặc nuốt mất exception mà caller đang cần.
ProceedingJoinPoint: argument, giá trị trả về và exception
@Around nhận một ProceedingJoinPoint, và proceed() có bản nạp chồng nhận mảng argument thay thế. Tất cả những gì một around advice làm được đều nằm trong hai method này:
package com.example.demo.rewrite;
import com.example.demo.lab.Trace;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class RewriteAspect {
@Around("execution(* com.example.demo.rewrite.RewriteService.greet(..))")
public Object rewrite(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
Trace.note("caller passed " + java.util.Arrays.toString(args));
args[0] = ((String) args[0]).toUpperCase(java.util.Locale.ROOT);
Object result = pjp.proceed(args);
Trace.note("target returned \"" + result + "\"");
return result + " [rewritten]";
}
@Around("execution(* com.example.demo.rewrite.RewriteService.fail(..))")
public Object swallow(ProceedingJoinPoint pjp) {
try {
return pjp.proceed();
} catch (Throwable ex) {
Trace.note("swallowed " + ex.getClass().getSimpleName() + ": " + ex.getMessage());
return "fallback";
}
}
}== 6. ProceedingJoinPoint
caller passed [ada, 2]
target sees name="ADA" times=2
target returned "ADA ADA"
caller got "ADA ADA [rewritten]"
swallowed IllegalArgumentException: the target threw
fail() got "fallback"Caller truyền "ada", target nhận "ADA", và caller nhận về một chuỗi mà target chưa từng tạo ra. Advice thứ hai bắt IllegalArgumentException do target ném rồi trả về một giá trị, nên caller không hề biết có gì hỏng.
Cả hai đều chính đáng — retry, circuit breaker và cache đều dựng trên đúng cơ chế này — và cả hai cũng là lý do một giá trị lạ trong debugger thường là do aspect. Ba quy tắc giúp bạn sống sót:
- Sửa
pjp.getArgs()không thôi thì chẳng có tác dụng gì. Mảng đó là bản sao.proceed(args)mới là thứ làm target nhìn thấy. - Hãy rethrow, trừ khi nuốt exception chính là tính năng.
catch (Throwable ex) { return null; }trong một around advice giấu lỗi production hàng năm trời. - Đừng đổi kiểu trả về.
proceed()có kiểuObject; trả về thứ mà caller không ép kiểu được sẽ tạo raClassCastExceptiontại một chỗ gọi trông rất vô tội.
Lưu ý JoinPoint cũng mang theo metadata: getSignature(), getTarget() (object thật), getThis() (proxy) và getArgs(). getSignature().getDeclaringType() là cách một logging aspect biết nó đang trang trí cho class nào.
Nhiều aspect trên cùng một method
Ba aspect cùng một pointcut, hai cái có order, một cái không:
@Aspect
@Component
@Order(10)
public class FirstAspect {
@Around("execution(* com.example.demo.layered.LayeredService.run(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Trace.step("FirstAspect @Order(10) enter");
try {
return pjp.proceed();
} finally {
Trace.step("FirstAspect @Order(10) exit");
}
}
}SecondAspect y hệt nhưng @Order(20), còn UnorderedAspect y hệt nhưng không có @Order nào. Mức lồng nhau đo được:
== 7. three aspects on one method
1 FirstAspect @Order(10) enter
2 SecondAspect @Order(20) enter
3 UnorderedAspect (no @Order) enter
4 target method run()
5 UnorderedAspect (no @Order) exit
6 SecondAspect @Order(20) exit
7 FirstAspect @Order(10) exitGiá trị @Order nhỏ nhất nằm ngoài cùng. Advisor được sắp tăng dần và cái đầu tiên trong chuỗi bọc tất cả phần còn lại, nên @Order(10) thấy lời gọi đầu tiên và trả về sau cùng. Khác với trên BeanPostProcessor — nơi bài trước đo được @Order bị bỏ qua hoàn toàn — ở đây @Order có tác dụng, vì advisor của AOP được sắp bằng AnnotationAwareOrderComparator, thứ có đọc annotation. Implement Ordered cho kết quả giống hệt.
Aspect không có order nằm trong cùng. Spring coi việc thiếu order là Ordered.LOWEST_PRECEDENCE, tức sắp cuối, tức gần target nhất. Đó là mặc định chấp nhận được cho một logging aspect và là mặc định tệ cho một security aspect. Hai aspect cùng không có order thì giữa chúng không có thứ tự xác định nào cả — đừng để một cặp aspect mà vị trí tương đối có ý nghĩa mà lại thiếu @Order tường minh.
Thứ tự thật sự gây đau trên production là giữa aspect của bạn và aspect của chính Spring. @Transactional được áp bởi BeanFactoryTransactionAttributeSourceAdvisor, mặc định ở Ordered.LOWEST_PRECEDENCE, nên hầu như mọi aspect có @Order tường minh đều chạy bên ngoài transaction — nó thấy lời gọi trước khi transaction bắt đầu và sau khi transaction đã commit. Nếu aspect của bạn phải chạy bên trong transaction, hãy cho nó order lớn hơn order của transaction advisor, hoặc đặt @EnableTransactionManagement(order = …) và xếp aspect của bạn lên trên.
Self-invocation, mổ sâu hơn @Transactional
Bài 30 của khoá Basics cho thấy triệu chứng: một method @Transactional gọi từ bên trong cùng class thì chạy mà không có transaction. Phần này là nguyên nhân, và nó gói gọn trong một dòng output.
@Service
public class ReportService {
private final ObjectProvider<ReportService> selfProvider;
private final ReportService lazySelf;
ReportService(ObjectProvider<ReportService> selfProvider, @Lazy ReportService lazySelf) {
this.selfProvider = selfProvider;
this.lazySelf = lazySelf;
}
/** The bug: an internal call runs on the target, so it never re-enters the proxy. */
public void viaThis() {
Trace.note("viaThis(): this = " + id(this));
generate(1);
}
@Reported
@Transactional
public void generate(int n) {
Trace.note(" generate(" + n + "): transaction active = "
+ TransactionSynchronizationManager.isActualTransactionActive());
}
public static String id(Object o) {
return o.getClass().getSimpleName() + "@" + Integer.toHexString(System.identityHashCode(o));
}
}@Reported là một annotation tự viết, được một @Before advice khớp và in ra ADVICE RAN, nhờ vậy lần chạy cho thấy advice của AOP và transaction cùng hỏng một lúc. Danh tính của các object là toàn bộ lời giải thích:
== 8. self-invocation
the bean the caller holds: ReportService$$SpringCGLIB$$1@c247b02
--- viaThis() ---
viaThis(): this = ReportService@272c5abd
generate(1): transaction active = falseCaller giữ ReportService$$SpringCGLIB$$1@c247b02. Bên trong method, this là ReportService@272c5abd — một object khác, thuộc class khác, ở địa chỉ khác. generate(1) biên dịch thành this.generate(1), một lời gọi virtual bình thường trên target. Không advice, không transaction, không cảnh báo, không dòng log. Không có gì trong JVM ở vị trí có thể nhận ra chuyện này.

Ba cách sửa, có đo
Tự inject chính mình. Hỏi container lấy lại bean; thứ quay về là proxy, vì đó mới là thứ được đăng ký dưới tên đó.
/** Fix 1: ask the container for the bean again; what comes back is the proxy. */
public void viaObjectProvider() {
Trace.note("viaObjectProvider(): this = " + id(this) + ", self = " + id(selfProvider.getObject()));
selfProvider.getObject().generate(2);
}
/** Fix 1b: the same idea with @Lazy on a constructor parameter. */
public void viaLazySelf() {
Trace.note("viaLazySelf(): this = " + id(this) + ", self = " + id(lazySelf));
lazySelf.generate(3);
} --- viaObjectProvider() ---
viaObjectProvider(): this = ReportService@272c5abd, self = ReportService$$SpringCGLIB$$1@c247b02
[ReportAspect] ADVICE RAN for generate
generate(2): transaction active = true
--- viaLazySelf() ---
viaLazySelf(): this = ReportService@272c5abd, self = ReportService$$SpringCGLIB$$0@7a85454b
[ReportAspect] ADVICE RAN for generate
generate(3): transaction active = trueCả hai đều chạy, và khác biệt giữa chúng hiện ra ngay ở tên class. ObjectProvider.getObject() trả về chính bean đó, $$SpringCGLIB$$1@c247b02 — đúng object mà caller đang giữ. @Lazy trả về $$SpringCGLIB$$0@7a85454b, một proxy thứ hai được tạo ra để hoãn việc tra cứu, rồi nó mới uỷ quyền xuống proxy thứ nhất. Chạy được, nhưng nó nhét thêm một object và một chặng nữa vào bức tranh vốn đã đủ khó hình dung.
AopContext.currentProxy(). Proxy có thể tự đăng ký vào một ThreadLocal trong suốt lời gọi, nhưng chỉ khi bạn yêu cầu:
@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
public class ExposeProxyConfig {
}Thiếu annotation đó thì lời gọi hỏng, và hỏng to tiếng, ít ra cũng là thành thật:
--- viaAopContext() ---
viaAopContext(): this = ReportService@272c5abd
threw java.lang.IllegalStateException
Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.Có nó rồi thì lời gọi nội bộ được advise như mọi lời gọi khác:
--- viaAopContext() ---
viaAopContext(): this = ReportService@7fcbc336
[ReportAspect] ADVICE RAN for generate
generate(4): transaction active = trueCái giá không nằm trong đoạn code này: exposeProxy = true là công tắc cho toàn context, khiến mọi lời gọi qua proxy trong ứng dụng phải push và pop một ThreadLocal, và AopContext.currentProxy() chỉ chạy trên đúng thread đã đi vào proxy — đẩy lời gọi sang một executor là nó ném lại đúng IllegalStateException đó. Việc ép kiểu về ReportService cũng trói code vào loại proxy: chạy được trên CGLIB proxy và ném ClassCastException trên JDK proxy.
Chuyển method sang bean khác. Khi đó mọi lời gọi đều đi qua proxy, vì nó là lời gọi tới một object khác:
package com.example.demo.selfcall;
import com.example.demo.lab.Trace;
import org.springframework.stereotype.Service;
/** Fix 3: the loop lives in another bean, so every call crosses the proxy. */
@Service
public class ReportBatch {
private final ReportService reports;
ReportBatch(ReportService reports) {
this.reports = reports;
}
public void run() {
Trace.note("ReportBatch.run(): injected ReportService = " + ReportService.id(reports));
reports.generate(5);
}
} --- another bean calls it ---
ReportBatch.run(): injected ReportService = ReportService$$SpringCGLIB$$1@c247b02
[ReportAspect] ADVICE RAN for generate
generate(5): transaction active = true| Cách sửa | Cái giá phải trả | Khi nào hợp lý |
|---|---|---|
| bean khác | thêm một class | mặc định — việc tách ra thường làm thiết kế tốt lên |
ObjectProvider<Self> | thêm một field, một lời gọi getObject() | khi thật sự lặp qua một method có annotation bên trong cùng một service gắn kết |
tự inject bằng @Lazy | thêm một field, cộng một proxy thứ hai | không hơn gì ObjectProvider; hãy chọn cái tường minh hơn |
AopContext.currentProxy() | một ThreadLocal cho mọi lời gọi trong context, một lần ép kiểu, phụ thuộc thread | code cũ không tái cấu trúc được |
Loạt bài này khuyên cách thứ nhất. Một method cần được áp annotation của riêng nó là một method có trách nhiệm riêng, và chuyển nó sang một collaborator nói ra điều đó bằng type system chứ không phải bằng comment. ObjectProvider là lựa chọn thứ hai chấp nhận được khi hai method thật sự thuộc về nhau; exposeProxy là phương án cuối cùng.
Giá phải trả trong thực tế: @Transactional và @Async
Hai annotation hỏng theo kiểu này cũng chính là hai annotation quan trọng nhất, và cách hỏng của chúng khác nhau.
@Transactional hỏng thành transaction active = false ở trên: không transaction nào được mở, nên mỗi lời gọi repository tự mở transaction riêng, dirty checking không ghi gì, và một lỗi sau đó chẳng rollback được gì. Bài Basics 30 đã đo tận SQL nào tới được database và SQL nào không.
@Async hỏng theo kiểu chạy trên chính thread của caller. Đó là mất trắng toàn bộ tính năng, trong im lặng:
--- @Async through this ---
asyncViaThis(): caller thread = main
generateAsync(this): thread = main
--- @Async through the proxy ---
generateAsync(another bean): thread = task-1
generateAsync(proxy): thread = task-2Qua proxy thì method chạy trên task-1 và task-2, các thread của executor pool. Qua this thì nó chạy trên main, đồng bộ, tuần tự, chặn caller đúng bằng thời gian nó cần. Một method @Async lẽ ra bắn đi ba email giờ cộng thêm ba vòng round trip mạng vào chính request đã kích hoạt nó, và triệu chứng duy nhất là endpoint chậm đi.
Một proxy tốn bao nhiêu
Hai method trên cùng một bean: add, khớp với một @Around rỗng, và subtract, không khớp advice nào. Mỗi cái gọi 20 triệu lần, bỏ ba vòng khởi động, lấy vòng tốt nhất trong bảy vòng đo, so với lời gọi trực tiếp trên target lấy từ ((Advised) proxy).getTargetSource().getTarget(). Các con số chỉ để tham khảo.
Với CGLIB proxy, mặc định của Boot:
proxy is com.example.demo.bench.CalculatorService$$SpringCGLIB$$0
advisors matching add(int,int) [ExposeInvocationInterceptor, AspectJAroundAdvice]
advisors matching subtract(int,int) [ExposeInvocationInterceptor]
direct call on the target 0.24 ns/call
CGLIB proxy, no aspect advice 44.97 ns/call
CGLIB proxy, one @Around aspect 65.98 ns/callvà với spring.aop.proxy-target-class=false:
proxy is jdk.proxy2.$Proxy101
advisors matching add(int,int) [ExposeInvocationInterceptor, AspectJAroundAdvice]
advisors matching subtract(int,int) [ExposeInvocationInterceptor]
direct call on the target 0.24 ns/call
JDK proxy, no aspect advice 40.33 ns/call
JDK proxy, one @Around aspect 65.33 ns/callHãy đọc mấy con số này một cách thành thật. Lời gọi trực tiếp là a + b tại một call site monomorphic, thứ mà JIT inline đi gần hết — 0.24 ns không phải một lời gọi method, nó là một lần tăng biến đếm. Vậy nên các con số của proxy gần như là toàn bộ chi phí, chứ không phải phần dôi ra so với một mốc tương đương.
JDK và CGLIB nhanh như nhau. 40 ns so với 45 ns cho phần dispatch, 65 ns so với 66 ns khi có một aspect; một cặp lần chạy trước đó cho 41 và 44. Chênh lệch nằm trong nhiễu giữa các lần chạy. Chọn giữa hai loại là chuyện chúng proxy được cái gì, không bao giờ là chuyện tốc độ.
Phần lớn chi phí nằm ở việc đi qua proxy. Khoảng 40 ns trong số 65 ns là dispatch: box argument vào một Object[], dựng một ReflectiveMethodInvocation, đi hết chuỗi interceptor và gọi target bằng reflection. Advice @Around cộng nốt phần còn lại — 21 ns trên CGLIB proxy, 25 ns trên JDK proxy — chủ yếu là MethodInvocationProceedingJoinPoint mà nó cần. Lưu ý subtract không phải là không có proxy — ExposeInvocationInterceptor mang Pointcut.TRUE và được thêm vào mọi bean mà bất kỳ advisor AspectJ nào chạm tới.
Với một service method thì chuyện này không đáng bận tâm. 65 ns là 0,000065 ms. Một nghìn lời gọi có advice trong một request tốn 0,065 ms; một câu SELECT qua mạng tốn gấp hàng trăm lần. Các trường hợp thật sự đáng lo thì có thật nhưng hẹp: một method bị proxy nằm trong vòng lặp chặt, một lần tra @Cacheable mà nhánh hit chỉ là đọc HashMap, một aspect trên method repository được gọi mỗi dòng dữ liệu. Hãy đo trước khi phỏng đoán; đừng gỡ proxy khỏi service theo nguyên tắc.
Bean này có được advise không, và bởi cái gì?
Mọi Spring AOP proxy đều implement org.springframework.aop.framework.Advised, nên bản thân proxy sẽ khai ra cấu hình của nó. Đây là công cụ debug nên dùng đầu tiên:
Object bean = context.getBean(name);
if (bean instanceof Advised advised) {
Trace.note(name + " -> " + bean.getClass().getSimpleName()
+ ", " + advised.getAdvisors().length + " advisor(s), exposeProxy=" + advised.isExposeProxy());
for (Advisor a : advised.getAdvisors()) {
Trace.note(" " + a);
}
} reportService -> ReportService$$SpringCGLIB$$1, 4 advisor(s), exposeProxy=false
org.springframework.scheduling.annotation.AsyncAnnotationAdvisor@382c90c2
org.springframework.aop.interceptor.ExposeInvocationInterceptor.ADVISOR
org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor: advice org.springframework.transaction.interceptor.TransactionInterceptor@63cd2cd2
InstantiationModelAwarePointcutAdvisor: expression [@annotation(com.example.demo.selfcall.Reported)]; advice method [public void com.example.demo.selfcall.ReportAspect.before(org.aspectj.lang.JoinPoint)]; perClauseKind=SINGLETON
betaService -> BetaService$$SpringCGLIB$$0, 4 advisor(s), exposeProxy=false
org.springframework.aop.interceptor.ExposeInvocationInterceptor.ADVISOR
InstantiationModelAwarePointcutAdvisor: expression [within(com.example.demo.designator..*) && args(sku, quantity)]; advice method [public void com.example.demo.designator.DesignatorAspect.byArgs(org.aspectj.lang.JoinPoint,java.lang.String,int)]; perClauseKind=SINGLETON
InstantiationModelAwarePointcutAdvisor: expression [bean(betaService)]; advice method [public void com.example.demo.designator.DesignatorAspect.byBean(org.aspectj.lang.JoinPoint)]; perClauseKind=SINGLETON
InstantiationModelAwarePointcutAdvisor: expression [within(com.example.demo.designator..*)]; advice method [public void com.example.demo.designator.DesignatorAspect.byWithin(org.aspectj.lang.JoinPoint)]; perClauseKind=SINGLETONMỗi InstantiationModelAwarePointcutAdvisor in ra biểu thức pointcut và đúng method advice của nó, biến "aspect của tôi không chạy" thành "biểu thức của tôi không khớp" chỉ trong một cái liếc.
Một bean không có advisor nào thì đơn giản là không phải proxy, và phép kiểm tra cũng báo luôn điều đó:
listPriceService: not a proxy (com.example.demo.pricing.ListPriceService)Advisor gắn theo từng bean; còn chuyện một advisor có áp cho một method cụ thể hay không là câu hỏi riêng, do chính pointcut của advisor trả lời:
for (Advisor advisor : advised.getAdvisors()) {
boolean applies = !(advisor instanceof PointcutAdvisor pointcutAdvisor)
|| (pointcutAdvisor.getPointcut().getClassFilter().matches(targetClass)
&& pointcutAdvisor.getPointcut().getMethodMatcher().matches(method, targetClass));
if (applies) {
names.add(advisor.getAdvice().getClass().getSimpleName());
}
} --- which advisors apply to one method ---
reportService.generate [ExposeInvocationInterceptor, TransactionInterceptor, AspectJMethodBeforeAdvice]
reportService.viaThis [ExposeInvocationInterceptor]Khi đến thế vẫn chưa đủ, logging.level.org.springframework.aop=TRACE in ra từng quyết định ngay lúc chúng được đưa ra khi khởi động:
TRACE AnnotationAwareAspectJAutoProxyCreator: Creating implicit proxy for bean 'listPriceService' with 0 common interceptors and 2 specific interceptors
TRACE CglibAopProxy: Creating CGLIB proxy: SingletonTargetSource for target object [com.example.demo.pricing.ListPriceService@6aa792]
DEBUG CglibAopProxy: Final method [public final java.lang.String com.example.demo.pricing.ListPriceService.describe()] cannot get proxied via CGLIB: Calls to this method will NOT be routed to the target instance and might lead to NPEs against uninitialized fields in the proxy instance.
TRACE CglibAopProxy: Unable to apply any optimizations to advised method: public java.math.BigDecimal com.example.demo.pricing.ListPriceService.quote(java.lang.String,int)
TRACE AnnotationAwareAspectJAutoProxyCreator: Did not attempt to auto-proxy infrastructure class [org.springframework.transaction.interceptor.TransactionInterceptor]Nó rất dài dòng — riêng CglibAopProxy đã 320 dòng trên ứng dụng nhỏ này — nên chỉ bật khi phép kiểm tra qua Advised chưa trả lời được câu hỏi.
Vượt qua proxy: weaving, aspect trên aspect, và Micrometer
Ba thứ đóng khung chủ đề này, mỗi thứ một câu.
AspectJ weaving là cách vượt qua mọi giới hạn trong bài này. Weaving lúc compile hoặc lúc load sẽ viết lại bytecode của chính class thay vì bọc nó, nên method final, method private, constructor, truy cập field và cả self-invocation đều advise được — đổi lại là một bước weaving trong build hoặc một -javaagent trên dòng lệnh, và đó là lý do gần như không ai chọn nó.
Bản thân aspect thì không bị advise. AnnotationAwareAspectJAutoProxyCreator bỏ qua các bean @Aspect, cũng như các advisor và interceptor mà nó xếp vào loại infrastructure — vết chạy lúc khởi động có dòng Creating implicit proxy for bean cho cả chín service của ứng dụng và không có dòng nào cho bất kỳ aspect nào, nên một aspect không thể vô tình advise chính nó thành vòng lặp.
Phần lớn aspect đáng có thì đã có người viết rồi. io.micrometer.core.aop.TimedAspect trong micrometer-core và io.micrometer.observation.aop.ObservedAspect trong micrometer-observation là các class @Aspect bình thường; đăng ký một trong hai thành bean là bạn có timer, trace và metric trên mọi method mang @Timed hoặc @Observed, được kiểm thử kỹ hơn cái timing aspect mà bạn định tự viết.
FAQ
Vì sao không tìm thấy spring-boot-starter-aop trên Spring Boot 4?
Vì nó không còn tồn tại. Nó vắng mặt trong spring-boot-dependencies-4.1.1.pom, bản cuối cùng công bố trên Maven Central là 4.0.0-M2, và metadata 4.1.1 của Spring Initializr không có mục AOP hay AspectJ nào — hỏi dependencies=aop thì nhận 400 Unknown dependency 'aop' check project metadata. Hãy dùng org.springframework.boot:spring-boot-starter-aspectj (và spring-boot-starter-aspectj-test cho test), nó resolve ra 4.1.1 và kéo theo spring-aop 7.0.9 cùng org.aspectj:aspectjweaver 1.9.25.1.
Spring dùng JDK proxy hay CGLIB proxy?
Trên Spring Boot là CGLIB, kể cả khi target có implement interface: spring.aop.proxy-target-class mặc định true, và class của bean đo được là ListPriceService$$SpringCGLIB$$0. Đặt property thành false thì ra jdk.proxy2.$Proxy103, thứ chỉ implement PriceService nên không inject vào, cũng không ép kiểu về, implementation class được. Spring Framework thuần không có Boot thì mặc định dùng JDK proxy khi có interface, và truyền miệng bắt nguồn từ đó.
Vì sao @Transactional không chạy khi tôi gọi method từ cùng một class?
Vì annotation được hiện thực bởi proxy, mà lời gọi nội bộ không bao giờ tới được proxy. Đo ở đây: caller giữ ReportService$$SpringCGLIB$$1@c247b02 còn this bên trong method là ReportService@272c5abd, nên this.generate(1) là lời gọi virtual bình thường trên target — transaction active = false, và advice của AOP cũng không chạy. Điều tương tự đúng với @Async, @Cacheable, @Retryable và mọi annotation dựa trên proxy khác.
Spring AOP có advise được method final, private hay package-private không?
Method package-private và protected thì có — cả hai đều có advice trong lần chạy này, vì CGLIB subclass được sinh ra trong cùng package và cùng class loader nên override được chúng. Method final và private thì không: subclass không override được cái nào, nên advice bị bỏ qua trong im lặng. Method final là ca tệ hơn, vì khi đó nó chạy trên chính proxy instance, nơi Objenesis chưa bao giờ khởi tạo field — kết quả đo được là NullPointerException: Cannot invoke "java.util.Map.size()" because "this.catalog" is null.
@Before, @Around, @After và @AfterReturning chạy theo thứ tự nào?
Đo trên Spring Framework 7.0.9: @Around cho tới proceed(), rồi @Before, rồi target method, rồi @AfterReturning (hoặc @AfterThrowing), rồi @After, và chỉ sau đó mới tới phần còn lại của @Around. Chỗ người ta hay nhầm là hai bước cuối: @After chạy trước khi @Around chạy tiếp, đây là ngữ nghĩa "after finally advice" của AspectJ và là chỗ các sơ đồ cũ hay vẽ ngược.
Làm sao điều khiển thứ tự của nhiều aspect?
Đặt @Order lên class aspect, hoặc implement Ordered. Giá trị nhỏ nhất nằm ngoài cùng — đo được là @Order(10), rồi @Order(20), rồi target, và tháo ngược lại. Aspect không có order bị coi như LOWEST_PRECEDENCE nên nằm trong cùng, sát target, còn hai aspect cùng không có order thì giữa chúng không có thứ tự xác định. Lưu ý transaction advisor cũng nằm ở LOWEST_PRECEDENCE, nên bất kỳ aspect nào bạn đánh order tường minh đều chạy bên ngoài transaction.
Spring AOP có chậm tới mức đáng lo không?
Không, với bất cứ thứ gì có I/O. Đo trên 20 triệu lời gọi, lấy vòng tốt nhất trong bảy vòng đã khởi động: 0,24 ns cho lời gọi trực tiếp đã bị JIT inline mất, 44,97 ns qua CGLIB proxy không có advice của aspect, 65,98 ns khi có một @Around; con số của JDK proxy là 40,33 và 65,33, bằng nhau trong phạm vi nhiễu. Tức 0,065 ms cho mỗi nghìn lời gọi có advice, so với hàng trăm micro giây cho một vòng round trip tới database. Chỉ lo khi một method bị proxy nằm trong vòng lặp chặt.
spring.aop.auto=false thật sự tắt cái gì?
Aspect, và chỉ aspect. Khi bật property này, AopAutoConfiguration bị bỏ qua nên @EnableAspectJAutoProxy không bao giờ được áp, và auto-proxy creator lùi về InfrastructureAdvisorAutoProxyCreator — đo được là listPriceService quay về class thuần không có advisor nào, còn reportService vẫn là CGLIB proxy mang AsyncAnnotationAdvisor và TransactionInterceptor. Nghĩa là @Transactional và @Async vẫn chạy còn mọi @Aspect trong ứng dụng ngừng hoạt động, mà không có lấy một cảnh báo.
Kết luận
Mọi thứ trong Spring AOP đều bắt nguồn từ một phép tráo: AnnotationAwareAspectJAutoProxyCreator trả về một proxy ở đúng chỗ bean của bạn từng đứng, và advice sống trên proxy. Boot dựng proxy đó thành CGLIB subclass theo mặc định — ListPriceService$$SpringCGLIB$$0 chứ không phải jdk.proxy2.$Proxy103 — nhờ vậy inject theo implementation class vẫn chạy, và cũng vì vậy mà một class final làm chết ứng dụng trong khi một method final lặng lẽ mất advice rồi ném NullPointerException vào các field mà Objenesis chưa bao giờ khởi tạo. Trên Boot 4.1.1 phần đóng gói cũng đổi: spring-boot-starter-aop biến mất, spring-boot-starter-aspectj thay chỗ, và trên ứng dụng JPA thì AspectJ đã nằm sẵn trên classpath trước khi bạn kịp yêu cầu.
Bản thân aspect là phần dễ — @Aspect và @Component, một named @Pointcut, và năm loại advice với thứ tự đo được đặt @Around ngoài cùng và @After trước khi @Around chạy tiếp. Phần khó là cái ranh giới: lời gọi nội bộ là lời gọi trên this, this chính là target, và target thì không có advice nào trên nó. Điều đó được chứng minh ở đây bằng hai danh tính in cạnh nhau, kèm transaction active = false và một method @Async chạy trên main. Hãy chuyển method sang bean khác; dùng ObjectProvider của chính mình khi hai method thật sự thuộc về nhau; để dành exposeProxy cho code bạn không sửa được. Và khi một aspect không chạy, hãy hỏi chính proxy: Advised#getAdvisors in ra mọi biểu thức pointcut và method advice đang gắn vào bean.
Bài tiếp theo vẫn ở trong container nhưng bỏ hẳn proxy: cơ chế event của chính Spring — ApplicationEventPublisher, @EventListener, và @TransactionalEventListener cho công việc chỉ được phép chạy sau khi transaction đã commit.