Bean代理是Spring提供的基本功能,也是最重要的基礎結構功能之一。 它是如此重要和低級,以至于在大多數情況下我們甚至都沒有意識到它的存在。 但是,如果沒有交易,面向方面的編程,高級作用域, @ Async支持以及其他各種國內用例,將無法實現。 那么什么是代理 ?
這是一個示例:將DAO注入服務時,Spring將獲取DAO實例并將其直接注入。 而已。 但是有時候,Spring需要了解服務(以及任何其他bean)對DAO的每一次調用。 例如,如果DAO被標記為事務性的,則它需要在調用并提交之前啟動事務,或者在之后回滾。 當然,您可以手動執行此操作,但是這很繁瑣,容易出錯,并且混雜了許多問題。 這就是為什么我們首先使用聲明式事務的原因。
那么Spring如何實現這種攔截機制呢? 從最簡單到最高級的三種方法。 我不會討論它們的優缺點,我們將在一個具體示例中很快看到它們。
Java動態代理
最簡單的解決方案。 如果DAO實現了任何接口,Spring將創建一個Java 動態代理來實現該接口,并注入它而不是真實的類。 真正的代理仍然存在,并且代理引用了它,但是對于外部世界–代理就是bean。 現在,每次您在DAO上調用方法時,Spring都可以攔截它們,添加一些AOP魔術并調用原始方法。
CGLIB生成的類
Java動態代理的缺點是要求Bean至少實現一個接口。 CGLIB通過動態子類化原始bean并通過覆蓋每種可能的方法直接添加攔截邏輯來解決此限制。 可以將其視為原始類的子類并在其中調用超級版本:
class DAO {def findBy(id: Int) = //...
}class DAO$EnhancerByCGLIB extends DAO {override def findBy(id: Int) = {startTransactiontry {val result = super.findBy(id)commitTransaction()result} catch {case e =>rollbackTransaction()throw e}}
}
但是,此偽代碼并未說明其在現實中的工作方式-引入了另一個問題,請繼續關注。
AspectJ編織
從開發人員的角度來看,這是最具侵入性但也是最可靠和直觀的解決方案。 在這種模式下,偵聽直接應用于您的類字節碼,這意味著您的JVM運行的類與您編寫的類不同。 在構建–編譯時編織(CTW)或在加載類–加載時間編織(LTW)期間,AspectJ weaver通過直接修改類的字節碼來添加攔截邏輯。
如果您對如何在后臺實現AspectJ魔術感到好奇,則可以使用預先通過AspectJ編織編譯的,經過反編譯和簡化的.class文件:
public void inInterfaceTransactional()
{try{AnnotationTransactionAspect.aspectOf().ajc$before$1$2a73e96c(this, ajc$tjp_2);throwIfNotInTransaction();}catch(Throwable throwable){AnnotationTransactionAspect.aspectOf().ajc$afterThrowing$2$2a73e96c(this, throwable);throw throwable;}AnnotationTransactionAspect.aspectOf().ajc$afterReturning$3$2a73e96c(this);
}
使用加載時編織,在加載類時,將在運行時發生相同的轉換。 如您所見,這里沒有任何干擾,實際上,這正是您手動編程事務的方式。 旁注:您還記得病毒在操作系統將可執行文件加載后將其代碼附加到可執行文件中或動態注入自身的時候嗎?
了解代理技術對于了解代理如何工作以及如何影響代碼非常重要。 讓我們堅持聲明式事務劃分示例,這是我們的戰場:
trait FooService {def inInterfaceTransactional()def inInterfaceNotTransactional();
}@Service
class DefaultFooService extends FooService {private def throwIfNotInTransaction() {assume(TransactionSynchronizationManager.isActualTransactionActive)}def publicNotInInterfaceAndNotTransactional() {inInterfaceTransactional()publicNotInInterfaceButTransactional()privateMethod();}@Transactionaldef publicNotInInterfaceButTransactional() {throwIfNotInTransaction()}@Transactionalprivate def privateMethod() {throwIfNotInTransaction()}@Transactionaloverride def inInterfaceTransactional() {throwIfNotInTransaction()}override def inInterfaceNotTransactional() {inInterfaceTransactional()publicNotInInterfaceButTransactional()privateMethod();}
}
方便的throwIfNotInTransaction()方法…如果未在事務中調用,則引發異常。 誰曾想到? 從不同的地方和不同的配置調用此方法。 如果您仔細檢查方法的調用方式,那么所有方法都應該起作用。 但是,我們的開發人員的生活往往是殘酷的。 第一個障礙是意外的: ScalaTest不支持通過專用運行器進行 Spring集成測試 。 幸運的是,這可以通過簡單的特征輕松地移植(處理依賴注入以測試用例和應用程序上下文緩存):
trait SpringRule extends AbstractSuite { this: Suite =>abstract override def run(testName: Option[String], reporter: Reporter, stopper: Stopper, filter: Filter, configMap: Map[String, Any], distributor: Option[Distributor], tracker: Tracker) {new TestContextManager(this.getClass).prepareTestInstance(this)super.run(testName, reporter, stopper, filter, configMap, distributor, tracker)}}
請注意,我們不會像原始測試框架那樣開始和回退事務。 不僅因為它會干擾我們的演示,而且因為我發現事務測試有害–將來還會對此產生更多影響。 回到我們的示例,這是一個煙霧測試。 完整的源代碼可以在這里從proxy-problem分支下載。 不要抱怨缺少斷言–在這里,我們僅測試未引發異常:
@RunWith(classOf[JUnitRunner])
@ContextConfiguration
class DefaultFooServiceTest extends FunSuite with ShouldMatchers with SpringRule{@Resourceprivate val fooService: FooService = nulltest("calling method from interface should apply transactional aspect") {fooService.inInterfaceTransactional()}test("calling non-transactional method from interface should start transaction for all called methods") {fooService.inInterfaceNotTransactional()}}
令人驚訝的是,測試失敗。 好吧,如果您閱讀我的文章已有一段時間了,您應該不會感到驚訝: Spring AOP謎語和Spring AOP謎語揭開了神秘面紗 。 實際上,Spring參考文檔對此進行了詳細解釋,也請查看此SO問題 。 簡而言之,非事務方法調用事務方法,但繞過事務代理。 即使似乎很明顯,當inInterfaceNotTransactional()調用inInterfaceTransactional()時,事務也應該開始,但事實并非如此。 抽象泄漏。 順便說一句,還請查看引人入勝的交易策略:了解交易陷阱的更多信息。
還記得我們展示CGLIB工作原理的例子嗎? 也知道多態性是如何工作的,似乎使用基于類的代理應該會有所幫助。 現在,inInterfaceNotTransactional()調用被CGLIB / Spring覆蓋的inInterfaceTransactional(),后者依次調用原始類。 沒有機會! 這是偽代碼的真正實現:
class DAO$EnhancerByCGLIB extends DAO {val target: DAO = ...override def findBy(id: Int) = {startTransactiontry {val result = target.findBy(id)commitTransaction()result} catch {case e =>rollbackTransaction()throw e}}
}
Spring首先創建原始bean,然后創建一個將原始bean(某種Decorator模式)包裝在一個后處理器中的子類,而不是對其進行實例化和實例化。 再次,這意味著bean內部的self調用會繞過我們的類的AOP代理。 當然,使用CGLIB會改變bean的行為方式。 例如,我們現在可以注入具體的類而不是接口,實際上甚至不需要接口,并且在這種情況下需要CGLIB代理。 還有一些缺點–不再可以進行構造函數注入,請參閱SPR-3150 ,這是一個遺憾 。 那么,一些更徹底的測試呢?
@RunWith(classOf[JUnitRunner])
@ContextConfiguration
class DefaultFooServiceTest extends FunSuite with ShouldMatchers with SpringRule {@Resourceprivate val fooService: DefaultFooService = nulltest("calling method from interface should apply transactional aspect") {fooService.inInterfaceTransactional()}test("calling non-transactional method from interface should start transaction for all called methods") {fooService.inInterfaceNotTransactional()}test("calling transactional method not belonging to interface should start transaction for all called methods") {fooService.publicNotInInterfaceButTransactional()}test("calling non-transactional method not belonging to interface should start transaction for all called methods") {fooService.publicNotInInterfaceAndNotTransactional()}}
請選擇將失敗的測試(準確選擇兩個)。 你能解釋為什么嗎? 同樣,常識表明一切都應該通過,但事實并非如此。 您可以自己玩耍,請參閱基于類的代理分支。
我們不是在這里揭露問題,而是要克服它們。 不幸的是,我們糾纏不清的服務等級只能使用重型火炮來解決-真正的AspectJ編織。 編譯和加載時編織均使測試通過。 相應地請參見aspectj-ctw和aspectj-ltw分支。
您現在應該問自己幾個問題。 我應該采取哪種方法(或:我真的需要使用AspectJ?),為什么還要打擾? - 在其他人中。 我會說–在大多數情況下,簡單的Spring代理就足夠了。 但是,您絕對必須知道傳播是如何工作的,何時不起作用。 否則會發生壞事。 提交和回滾發生在意外的地方,跨越了意外的數據量,ORM 臟檢查不起作用,看不見的記錄–相信,這種事情是瘋狂發生的。 請記住,我們在這里介紹的主題不僅適用于交易,還適用于AOP的所有方面。
參考: Spring陷阱: NoBlogDefFound博客上的 JCG合作伙伴 Tomasz Nurkiewicz的代理 。
- Spring聲明式事務示例
- Spring依賴注入技術的發展
- Spring和AspectJ的領域驅動設計
- Spring 3使用JUnit 4進行測試– ContextConfiguration和AbstractTransactionalJUnit4SpringContextTests
- 使用Spring AOP進行面向方面的編程
- Java教程和Android教程列表
翻譯自: https://www.javacodegeeks.com/2011/11/spring-pitfalls-proxying.html