Spring一個強大便捷的代理工廠類,你用過嗎?
環(huán)境:Spring6.1.2
1. 簡介
在Spring框架中,AOP(面向切面編程)是一種強大的編程范式,它允許開發(fā)者在不修改原有代碼的情況下,為程序添加額外的功能,如日志記錄、事務管理、安全控制等。
實際開發(fā)中常用實現(xiàn)AOP配置方式:
- 基于XML
在早期的Spring版本中,開發(fā)者常常使用XML配置文件來定義切面、通知和目標對象之間的關聯(lián)。通過配置<aop:config>、<aop:aspect>、<aop:before>等標簽,可以輕松地實現(xiàn)AOP的各種功能。如下示例:
<aop:config>
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.pack.service.*.*(..))"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
</aop:aspect>
</aop:config>
- 基于注解
通過在切面類和方法上使用如@Aspect、@Before、@After等注解,可以更加簡潔地定義AOP的相關配置。這種方式不僅減少了XML配置的工作量,還使得代碼更加清晰易讀。如下示例:
@Component
@Aspect
public class LogAspect {
@Pointcut("execution(* save(..))")
private void logPc() {}
@Around("logPc()")
public Object process(ProceedingJoinPoint pjp) throws Throwable {
Object ret = null ;
System.out.println("before log...") ;
ret = pjp.proceed() ;
System.out.println("after log...") ;
return ret ;
}
}
以上是Spring提供的2中方式來聲明AOP配置方式。但如果你需要一種更加靈活和可配置性,那么Spring還提供了一個非常方便強大的ProxyFactoryBean類,該類特別適合那些需要更多自定義和控制的場景,例如當你需要為特定的Bean創(chuàng)建代理,或者需要在不修改原始代碼的情況下為現(xiàn)有類添加額外的功能時。
2. 實戰(zhàn)案例
ProxyFactoryBean與其他Spring FactoryBean實現(xiàn)一樣,引入了一個間接級別。如果定義了名為pack的ProxyFactoryBean,那么引用pack的對象看不到ProxyFactoryBean實例本身,而是由ProxyFactoryBean#getObject()方法實現(xiàn)創(chuàng)建的對象。此方法創(chuàng)建一個AOP代理,用于包裝目標對象。
2.1 屬性配置
ProxyFactoryBean提供了很多屬性,讓你可以靈活的配置代理對象。該對象繼承了ProxyConfig,一些關鍵的屬性是由ProxyConfig定義。
- proxyTargetClass:如果要代理目標類,而不是目標類的接口,則為true。如果此屬性值設置為true,則會創(chuàng)建CGLIB代理。
- optimize:控制是否對通過CGLIB創(chuàng)建的代理應用積極的優(yōu)化。除非完全理解相關AOP代理如何處理優(yōu)化,否則不應該輕松地使用此設置。目前僅用于CGLIB代理。它對JDK動態(tài)代理沒有影響。
- frozen:如果代理配置被凍結(jié),則不再允許更改該配置。此屬性的默認值為false,因此允許更改(例如添加額外的通知)。
- exposeProxy:確定是否應在ThreadLocal中公開當前代理,以便目標可以訪問它。如果目標需要獲取代理,并且exposeProxy屬性設置為true,則該目標可以使用AoPontext.currentProxy()方法獲取代理對象。
- proxyInterface:字符串接口名稱的數(shù)組。
- interceptorNames:要應用的Advisor、攔截器或其他建議名稱的字符串數(shù)組。
接下來將從2方面介紹ProxyFactoryBean的使用,代理接口與代理類。2.2 代理接口
要通過ProxyFactoryBean創(chuàng)建代理,你至少需要涉及到下面幾點(類):
- 需要被代理的目標bean類。
- 一個Advisor或者Advice,增強部分。
- 指定要代理的接口。
如下示例:
public interface ICommonDAO {
void save() ;
}
@Component("commonDAOTarget")
public class CommonDAOImpl implements ICommonDAO {
@Override
public void save() {
System.out.println("save operator...") ;
}
}
@Component
public class LogInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("before log...") ;
Object ret = invocation.proceed() ;
System.out.println("after log...") ;
return ret ;
}
}
@Configuration
public class AppConfig {
@Bean
// 由于上面已經(jīng)定義了CommonDAOImpl,而這里的FactoryBean#getObject返回的
// 也是一個實現(xiàn)了ICommonDAO接口的對象,所以需要加上@Primary
@Primary
ProxyFactoryBean commonDAO(@Qualifier("commonDAOTarget") CommonDAOImpl commonDAOTarget) throws Exception {
ProxyFactoryBean proxy = new ProxyFactoryBean() ;
proxy.setProxyInterfaces(new Class<?>[] {ICommonDAO.class}) ;
proxy.setTarget(commonDAOTarget) ;
proxy.setInterceptorNames("logInterceptor") ;
return proxy ;
}
}
測試
ICommonDAO dao = context.getBean(ICommonDAO.class) ;
dao.save() ;
// 輸出
before log...
save operator...
after log...
2.3 代理類
如果我們的目標沒有實現(xiàn)接口,那么我們只能通過CGLIB進行代理,通過設置proxyTargetClass屬性為true。CGLIB代理通過在運行時生成目標類的子類來工作。Spring將這個生成的子類配置為將方法調(diào)用委托給原始目標。如下示例:
@Component("commonDAOTarget")
public class CommonDAO {
public void save() {
System.out.println("save operator...") ;
}
}
@Bean
@Primary
ProxyFactoryBean commonDAO(@Qualifier("commonDAOTarget") CommonDAO commonDAOTarget) throws Exception {
ProxyFactoryBean proxy = new ProxyFactoryBean() ;
proxy.setTarget(commonDAOTarget) ;
proxy.setInterceptorNames("logInterceptor") ;
// 代理類,可以不設置
proxy.setProxyTargetClass(true) ;
return proxy ;
}
查看最終的CommonDAO是否是通過CGLIB代理
CommonDAO dao = context.getBean(CommonDAO.class) ;
System.out.println(dao.getClass()) ;
輸出結(jié)果
class com.pack.aop.create.ProxyFactoryBeanTest2$CommonDAO$$SpringCGLIB$$1
CGLIB代理通過在運行時生成目標類的子類來工作。但需要注意以下事項:
- final 類不能被代理,因為它們不能被擴展。
- final方法無法提供增強,因為它們不能被覆蓋。
- 不能增強private方法,因為它們不能被重寫。
- 不可見的方法,通常是來自不同包的父類中的包私有方法,不能被增強,因為它們實際上是私有的。
2.4 模糊匹配攔截器
在上面配置攔截器時,我們都是指定的具體攔截器,其實我們還可以使用通配符,指定攔截器。如下示例:
@Component("global_log")
public class LogInterceptor implements MethodInterceptor {
}
@Component("global_auth")
public class AuthInterceptor implements MethodInterceptor {
}
// ProxyFactoryBena配置
ProxyFactoryBean commonDAO() throws Exception {
ProxyFactoryBean proxy = new ProxyFactoryBean() ;
// 注意:這里的通配符必須是最后,你不能放到其它位置
proxy.setInterceptorNames("global_*") ;
return proxy ;
}
以上ProxyFactoryBean在初始化時,會自動查找容器中beanName以global_開頭的所有Bean對象。