使用Spring AOP向領域模型注入依賴
在貧血領域模型這篇譯文中,Martin闡述了這種“反模式”的症狀和問題,並引用了領域驅動設計中的話來說明領域模型和分層設計之間的關係。對於Spring專案的開發人員來說,貧血領域模型十分常見:模型(或實體)僅僅包含對資料表的對映,通常是一組私有屬性和公有getter/setter,所有的業務邏輯都寫在服務層中,領域模型僅僅用來傳遞資料。為了編寫真正的領域模型,我們需要將業務邏輯移至模型物件中,這就引出另一個問題:業務邏輯通常需要呼叫其他服務或模型,而使用new
關鍵字或由JPA建立的物件是不受Spring託管的,也就無法進行依賴注入。解決這個問題的方法有很多,比較之後我選擇使用面向切面程式設計來實現。
面向切面程式設計
面向切面程式設計,或AOP,是一種程式設計正規化,和麵向物件程式設計(OOP)互為補充。簡單來說,AOP可以在不修改既有程式碼的情況下改變程式碼的行為。開發者通過定義一組規則,在特定的類方法前後增加邏輯,如記錄日誌、效能監控、事務管理等。這些邏輯稱為切面(Aspect),規則稱為切點(Pointcut),在呼叫前還是呼叫後執行稱為通知(Before advice, After advice)。最後,我們可以選擇在編譯期將這些邏輯寫入類檔案,或是在執行時動態載入這些邏輯,這是兩種不同的織入方式(Compile-time weaving, Load-time weaving)。
對於領域模型的依賴注入,我們要做的就是使用AOP在物件建立後呼叫Spring框架來注入依賴。幸運的是,Spring AOP已經提供了@Configurable
註解來幫助我們實現這一需求。
Configurable註解
Spring應用程式會定義一個上下文容器,在該容器內建立的物件會由Spring負責注入依賴。對於容器外建立的物件,我們可以使用@Configurable
來修飾類,告知Spring對這些類的例項也進行依賴注入。
假設有一個Report
類(領域模型),其中一個方法需要解析JSON,我們可以使用@Configurable
將容器內的ObjectMapper
物件注入到類的例項中:
@Entity
@Configurable(autowire = Autowire.BY_TYPE)
public class Report {
@Id @GeneratedValue
private Integer id;
@Autowired @Transient
private ObjectMapper mapper;
public String render() {
mapper.readValue(...);
}
}
autowire
引數預設是NO
,因此需要顯式開啟,否則只能使用XML定義依賴。@Autowired
是目前比較推薦的注入方式。@Transient
用於告知JPA該屬性不需要進行持久化。你也可以使用transient
關鍵字來宣告,效果相同。- 專案依賴中需要包含
spring-aspects
。如果已經使用了spring-boot-starter-data-jpa
,則無需配置。 - 應用程式配置中需要加入
@EnableSpringConfigured
:
@SpringBootApplication
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
@EnableSpringConfigured
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- 在
src/main/resources
目錄下,新建META-INF/aop.xml
檔案,用來限定哪些包會用到AOP。否則,AOP的織入操作會作用於所有的類(包括第三方類庫),產生不必要的的報錯資訊。
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
<weaver>
<include within="com.foobar..*"/>
</weaver>
</aspectj>
執行時織入(Load-Time Weaving, LTW)
除了專案依賴和應用程式配置,我們還需要選擇一種織入方式來使AOP生效。Spring AOP推薦的方式是執行時織入,並提供了一個專用的Jar包。執行時織入的原理是:當類載入器在讀取類檔案時,動態修改類的位元組碼。這一機制是從JDK1.5開始提供的,需要使用-javaagent
引數開啟,如:
$ java -javaagent:/path/to/spring-instrument.jar -jar app.jar
在測試時發現,Spring AOP提供的這一Jar包對普通的類是有效果的,但對於使用@Entity
修飾的類就沒有作用了。因此,我們改用AspectJ提供的Jar包(可到Maven中央倉庫下載):
$ java -javaagent:/path/to/aspectjweaver.jar -jar app.jar
對於Spring Boot應用程式,可以在Maven命令中加入以下引數:
$ mvn spring-boot:run -Drun.agent=/path/to/aspectjweaver.jar
此外,在使用AspectJ作為LTW的提供方後,會影響到Spring的事務管理,因此需要在應用程式配置中加入:
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
AnnotationBeanConfigurerAspect
到這裡我們已經通過簡單配置完成了領域模型的依賴注入,這背後都是Spring中的AnnotationBeanConfigurerAspect
在做工作。我們不妨瀏覽一下精簡後的原始碼:
public aspect AnnotationBeanConfigurerAspect implements BeanFactoryAware {
private BeanConfigurerSupport beanConfigurerSupport = new BeanConfigurerSupport();
public void setBeanFactory(BeanFactory beanFactory) {
this.beanConfigurerSupport.setBeanFactory(beanFactory);
}
public void configureBean(Object bean) {
this.beanConfigurerSupport.configureBean(bean);
}
public pointcut inConfigurableBean() : @this(Configurable);
declare parents: @Configurable * implements ConfigurableObject;
public pointcut beanConstruction(Object bean) :
initialization(ConfigurableObject+.new(..)) && this(bean);
after(Object bean) returning :
beanConstruction(bean) && inConfigurableBean() {
configureBean(bean);
}
}
.aj
檔案是AspectJ定義的語言,增加了pointcut、after等關鍵字,用來定義切點、通知等;inConfigurationBean
切點用於匹配使用Configurable
修飾的型別;declare parents
將這些型別宣告為ConfigurableObject
介面,從而匹配beanConstruction
切點;ConfigurableObject+.new(..)
表示匹配該型別所有的建構函式;after
定義一個通知,表示物件建立完成後執行configureBean
方法;- 該方法會呼叫
BeanConfigurerSupport
來對新例項進行依賴注入。
其它方案
- 將依賴作為引數傳入。比如上文中的
render
方法可以定義為render(ObjectMapper mapper)
。 - 將
ApplicationContext
作為某個類的靜態成員,領域模型通過這個引用來獲取依賴。 - 編寫一個工廠方法,所有新建物件都要通過這個方法生成,進行依賴注入。
- 如果領域模型大多從資料庫獲得,並且JPA的提供方是Hibernate,則可以使用它的攔截器功能進行依賴注入。