AOP,也就是面向方面編程或者說面向面編程宴合,是一種很重要的思想卦洽。在企業(yè)級系統(tǒng)中經(jīng)常需要打印日志、事務(wù)管理這樣針對某一方面的需求蚤霞,但是傳統(tǒng)的面向?qū)ο缶幊虩o法很好的滿足這些需求昧绣。因此催生了面向切面編程這樣的思想。面向切面編程贪绘,通過動態(tài)代理這樣的功能兔簇,向要執(zhí)行的方法添加鉤子边酒,能夠在不改動原方法的情況下坯认,動態(tài)添加新功能。所以在現(xiàn)代系統(tǒng)中算是一項必需的功能了陋气。Spring框架也很好的支持了AOP。
AOP的幾個術(shù)語如下议慰,詳細(xì)的使用方法會在具體使用的時候說明别凹。
- 切面(Aspect),官方的抽象定義為“一個關(guān)注點的模塊化拍霜,這個關(guān)注點可能會橫切多個對象”,上面所說的打印日志吠裆、事務(wù)管理這樣的需求,就是切面抠蚣。
- 連接點(JoinPoint)祝旷,程序執(zhí)行過程中的某一行為。比如說我們計劃在某個方法執(zhí)行的時候打印日志嘶窄,那么這個方法就是連接點怀跛。
- 通知(Advice),切面對于某個連接點產(chǎn)生的動作就是通知柄冲。比如說我們上面計劃在某個方法執(zhí)行的時候打印日志吻谋,那么打印日志這件事情就是通知。通知按照執(zhí)行時機可以分為前置通知现横、后置通知等五種通知阁最。
- 切入點(Pointcut),可以簡單地理解為正則表達(dá)式之類的東西。我們想要在哪些方法上應(yīng)用打印日志的通知,就需要一個切入點來匹配感帅。
- 目標(biāo)對象(Target Object)实苞,被切面通知的對象就是目標(biāo)對象。
環(huán)境配置
Spring核心的依賴注入功能不需要AOP等其他組件的支持即可使用。不過反過來AOP卻需要依賴注入的支持猪半。因此我們需要添加比較多的依賴。以下是Gradle的依賴配置,為了運行后面的Hibernate例子句葵,需要Hibernate等幾個額外的包轻专。
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
compile group: 'org.springframework', name: 'spring-core', version: springVersion
compile group: 'org.springframework', name: 'spring-context', version: springVersion
compile group: 'org.springframework', name: 'spring-aop', version: springVersion
compile group: 'org.springframework', name: 'spring-test', version: springVersion
compile group: 'org.springframework', name: 'spring-orm', version: springVersion
compile group: 'org.projectlombok', name: 'lombok', version: '1.16.12'
compile group: 'org.hibernate', name: 'hibernate-core', version: '5.2.6.Final'
compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.40'
compile group: 'org.apache.commons', name: 'commons-dbcp2', version: '2.1.1'
}
定義服務(wù)
要使用AOP嫂侍,我們首先需要確定要把AOP用在什么地方诡挂。這里我定義了一個小服務(wù)抄课,執(zhí)行幾個方法沦童。這幾個方法列舉了最常用的幾種使用情景氏豌,無參方法、有參方法芜繁、有返回值方法榔袋。
public class MyService {
public void doSomething() {
System.out.println("做一些事情...");
}
public void printSomething(String msg) {
System.out.println("信息室:" + msg);
}
public int calculateSomething(int a, int b) {
return a + b;
}
public void throwSomething() {
throw new RuntimeException("一個異常");
}
public void longWork() {
int N = 10000;
int sum = 0;
for (int i = 0; i < N; ++i) {
sum += i;
}
}
}
然后將這個服務(wù)注冊為Spring Bean罕容。
<bean id="myService" class="yitian.learn.aop.MyService"/>
XML方式配置AOP
定義切面
我們在這里定義一個切面,這個切面包含幾個方法柱蟀,將會在我們的服務(wù)執(zhí)行前伶椿、執(zhí)行后輸出信息看彼,追蹤服務(wù)的參數(shù)廊佩、返回值和異常信息等囚聚。在編程實踐中,一個切面一般是一個類标锄,其中包含若干方法將會代理到服務(wù)方法上顽铸。
public class MyAspect {
public void before() {
System.out.println("在方法之前");
}
public void after() {
System.out.println("在方法之后");
}
public void printDataFlow(int input1, int input2, int output) {
System.out.println(
String.format("程序輸入是:%d,%d,輸出是:%d", input1, input2, output));
}
public void afterThrow(Exception e) {
System.out.println("方法拋出了" + e);
}
}
定義好日志切面之后料皇,我們同樣需要將其配置為一個Bean谓松。
<bean id="myAspect" class="yitian.learn.aop.MyAspect"/>
要將某個Bean配置為切面還需要一步,也就是在XML配置文件中beans根節(jié)點添加如下一行践剂,引用AOP的相關(guān)規(guī)則鬼譬。
xmlns:aop="http://www.springframework.org/schema/aop"
然后在配置文件中添加如下一節(jié)。將Bean聲明為切面逊脯。所有的AOP相關(guān)配置优质,都只能編寫在<aop:config>
節(jié)點中,而且順序必須按照切入點军洼、通知和切面的順序聲明巩螃。
<aop:config>
<aop:aspect id="logAspect" ref="logAspect">
</aop:aspect>
</aop:config>
定義切入點
切入點可以理解為正則表達(dá)式,簡單地說匕争,切入點和目標(biāo)方法之間的關(guān)系就像正則表達(dá)式和要匹配的字符串的關(guān)系一樣避乏。切入點定義了一個模式,可以匹配一個或多個目標(biāo)方法甘桑。Spring的切入點表達(dá)式使用的是AspectJ的切入點表達(dá)式語法淑际,詳細(xì)信息可以參考Spring AspectJ文檔。Spring沒有支持所有的AspectJ語法扇住,只支持了一部分春缕。
Spring AOP支持以下幾種指示符:
- execute,匹配指定方法執(zhí)行的連接點艘蹋,這是我們最常用的一種锄贼。
- within,匹配指定類型內(nèi)的連接點女阀。
- this宅荤,匹配bean引用(AOP代理)是指定類型的連接點。
- target浸策,匹配目標(biāo)對象(被代理的對象)是指定類型的連接點冯键。
- args,匹配方法參數(shù)是指定類型的連接點庸汗。
- @target惫确,匹配目標(biāo)對象的類被指定注解標(biāo)記的連接點。
- @args,匹配方法參數(shù)標(biāo)記有指定注解的連接點改化。
- @within掩蛤,匹配被指定注解標(biāo)記的類型的連接點。
- @annotation陈肛,匹配執(zhí)行方法含有指定注解的連接點揍鸟。
- bean,Spring AOP特有的句旱,匹配指定id或名稱的Spring Bean的連接點阳藻。
在指示符后面,需要一組括號谈撒,括號內(nèi)容是方法的匹配稚配,語法如下:
指示符(返回類型 包名.類名.方法名(參數(shù)列表) )
下面這個切入點表示的是當(dāng)yitian.learn.aop.MyService
類下的返回任意值的任意名稱和任意個參數(shù)的方法執(zhí)行時。這樣這個切入點代表的就是MyService
類的所有方法港华。id屬性指定切入點標(biāo)識符,expression指定切入點表達(dá)式午衰。切入點既可以定義在切面內(nèi)部立宜,也可以定義在切面外。如果定義在切面外臊岸,就可以被多個切面所共享橙数。但是必須定義在所有切面之前,順序上面已經(jīng)說了帅戒。
這里使用到了兩個通配符灯帮。星號*
代表單個的任意類型和名稱,兩個點..
表示任意多個名稱或參數(shù)逻住。此外還有一個通配符+
钟哥,用在某個類型之后,表示該類型的子類或者實現(xiàn)了該接口的某個類瞎访。
<aop:pointcut id="myService"
expression="execution(* yitian.learn.aop.MyService.*(..))"/>
再來幾個例子腻贰。匹配任意公有方法。
execution(public * *(..))
匹配com.xyz.someapp.trading
及其子包下所有方法執(zhí)行扒秸。
within(com.xyz.someapp.trading..*)
匹配以set開頭的所有方法執(zhí)行播演。
execution(* set*(..))
匹配com.xyz.service包下的任意類的任意方法。
execution(* com.xyz.service.*.*(..))
匹配任何實現(xiàn)了com.xyz.service.AccountService
接口目標(biāo)對象的切入點伴奥。
target(com.xyz.service.AccountService)
切入點還可以疊加写烤,使用&&
、||
拾徙、!
表示切入點的與或非洲炊。由于在XML配置文件中存在字符轉(zhuǎn)義現(xiàn)象,所以在XML配置中還可以使用and
、or
选浑、not
來替代上面的關(guān)系運算符蓝厌。
定義通知
切面對于某個連接點所執(zhí)行的動作就是通知。通知有以下幾種:
- 前置通知(before)古徒,在目標(biāo)方法執(zhí)行前執(zhí)行拓提、
- 返回后通知(after-returning),在目標(biāo)方法正常返回之后執(zhí)行隧膘。
- 異常后通知(after-throwing)代态,在目標(biāo)方法拋出異常之后執(zhí)行。
- 后置通知(after)疹吃,在目標(biāo)方法結(jié)束(包括正常返回和拋出異常)之后執(zhí)行蹦疑。
- 環(huán)繞通知(around),將目標(biāo)方法包裹到切面方法中執(zhí)行萨驶。
通知將切面和目標(biāo)方法之間聯(lián)系起來歉摧。pointcut-ref
屬性指定命名切入點的引用,如果不想使用命名切入點也可以使用pointcut
指定切入點表達(dá)式腔呜;method
指定切面中當(dāng)連接點執(zhí)行時所執(zhí)行的方法叁温。通知需要定義在切面之中。下面定義了前置通知和后置通知核畴。其他通知的定義類似膝但,寫在上面通知的括號中了。
<aop:aspect id="aspect" ref="myAspect">
<aop:before method="before" pointcut-ref="something"/>
<aop:after method="after" pointcut-ref="something"/>
</aop:aspect>
這樣定義之后谤草,每當(dāng)連接點執(zhí)行的時候跟束,通知隨之執(zhí)行。如果AOP的功能僅僅是這樣的話顯然沒什么作用丑孩。在通知中冀宴,我們還可以獲取目標(biāo)方法的參數(shù)和返回值。下面定義了一個通知温学,切入點是當(dāng)calculateSomething方法執(zhí)行的時候胸竞;返回值使用returning
屬性指明肌括;參數(shù)在切入點表達(dá)式中使用args
指明樊零;最后指定了這幾個參數(shù)在切面方法中的順序刃鳄。這樣,連接的參數(shù)和返回值就可以正確的綁定到切面方法上了箩帚。
<aop:after-returning method="printDataFlow"
pointcut="execution(int yitian.learn.aop.MyService.calculateSomething(int,int)) and args(input1,input2)"
returning="output"
arg-names="input1,input2,output"/>
如果要獲取方法拋出的異常真友,需要throwing
屬性,這樣切面方法就可以順利獲取到異常對象了紧帕。
<aop:after-throwing method="afterThrow"
pointcut="execution(* yitian..MyService.throwSomething())"
throwing="e"/>
最后來說說環(huán)繞通知盔然。相比而言環(huán)繞通知應(yīng)該是最復(fù)雜的通知了桅打。連接點會被包裹在環(huán)繞通知方法內(nèi)執(zhí)行。如何來處理連接點的執(zhí)行和返回值呢愈案?這需要環(huán)繞通知的方法具有一些特征:
- 必須有一個
org.aspectj.lang.ProceedingJoinPoint
類型的參數(shù)作為方法的第一個參數(shù)挺尾,否則無法執(zhí)行方法。 - 環(huán)繞通知方法最好有返回值站绪,如果沒有返回值遭铺,連接點方法的返回值將會丟失。
下面我們在MyAspect
類中新建一個方法恢准,用于測試連接點方法執(zhí)行時間魂挂,因為只是測試執(zhí)行時間,因此這里沒有為方法添加返回值馁筐。
public void around(ProceedingJoinPoint pjp) {
StopWatch watch = new StopWatch();
watch.start();
try {
pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
watch.stop();
System.out.println(watch.shortSummary());
}
然后涂召,我們定義一個環(huán)繞通知。
<aop:around method="around"
pointcut="execution(void yitian..MyService.longWork())"/>
這樣的話敏沉,在執(zhí)行l(wèi)ongWork方法的時候就會自動包裹在around方法中執(zhí)行果正。環(huán)繞通知主要用于事務(wù)處理等必須包裹的情形當(dāng)中。使用前面幾種通知可以實現(xiàn)功能的話就不要使用環(huán)繞通知盟迟。
定義引入
引入(Introduction)是AOP的一項功能秋泳,可以在不改變源代碼的情況下,動態(tài)的讓某個對象實現(xiàn)某個接口队萤。
首先我們需要一個接口和一個默認(rèn)實現(xiàn)。
public interface Service {
void doService();
}
public class ServiceImpl implements Service {
@Override
public void doService() {
System.out.println("實現(xiàn)了Service接口");
}
}
然后在<aop:aspect>
中添加如下一節(jié)矫钓。<aop:declare-parents>
來指定一個引入要尔。types-matching
屬性指定要匹配的類;implement-interface
屬性指定要實現(xiàn)的接口新娜;default-impl
屬性指定該接口的默認(rèn)實現(xiàn)赵辕。
<aop:declare-parents types-matching="yitian.learn.aop.MyService"
implement-interface="yitian.learn.aop.Service"
default-impl="yitian.learn.aop.ServiceImpl"/>
然后我們就可以將MyService
轉(zhuǎn)換成Service
接口了。
Service s = context.getBean("myService", Service.class);
s.doService();
@AspectJ配置
前面用的是XML方式配置的AOP概龄,由于Spring AOP的很多概念和類直接來自于AspectJ開源項目还惠。當(dāng)然也支持AspectJ形式的注解配置。要啟用AspectJ注解形式的配置私杜,需要在Java配置類上添加@EnableAspectJAutoProxy注解蚕键。
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
如果使用XML配置Spring而使用注解配置Spring AOP,需要在配置文件中添加下面一行衰粹。
<aop:aspectj-autoproxy/>
定義切面
定義切面很簡單锣光,在切面類上應(yīng)用@Aspect即可。
@Aspect
public class MyAspect {
...
}
定義切入點
定義切入點需要在切面類中定義一個空方法铝耻,方法名會作為切入點的名稱誊爹,切入點表達(dá)式使用注解聲明蹬刷。這里這個方法的作用就是充當(dāng)一個占位符,所以方法體為空频丘,這個方法返回類型必須是void办成。
@Pointcut(value = "execution(* yitian..MyService.doSomething())")
private void something() {
}
定義通知
定義通知和配置XML文件類似。這里不說了搂漠。直接上代碼迂卢。
@Aspect
public class MyAspect {
//定義切入點
@Pointcut("execution(* yitian..MyService.doSomething())")
private void something() {
}
//定義通知
@Before("something()")
public void before() {
System.out.println("在方法之前");
}
@After("something()")
public void after() {
System.out.println("在方法之后");
}
@AfterReturning(pointcut = "execution(* yitian..MyService.calculateSomething(..)) && args(input1,input2)",
returning = "output", argNames = "input1,input2,output")
public void printDataFlow(int input1, int input2, int output) {
System.out.println(
String.format("程序輸入是:%d,%d,輸出是:%d", input1, input2, output));
}
@AfterThrowing(pointcut = "execution(* yitian..MyService.throwSomething())",
throwing = "e")
public void afterThrow(Exception e) {
System.out.println("方法拋出了" + e);
}
@Around("execution(* yitian..MyService.longWork())")
public void around(ProceedingJoinPoint pjp) {
System.out.println("開始計時");
StopWatch watch = new StopWatch();
watch.start();
try {
pjp.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
watch.stop();
System.out.println(watch.shortSummary());
}
}
可以看到使用注解配置的優(yōu)勢就是配置文件和切面類在一起状答,閱讀方便冷守。如果使用XML配置的話,要查看切面應(yīng)用了什么方法惊科,需要同時查看XML和Java代碼拍摇,比較麻煩。
此外通知還有一個順序的問題馆截,在前面沒有說明充活。如果有兩個切面的相同通知(比如都是前置通知)要應(yīng)用到某個連接點上,我們就可以定義它們之間的順序蜡娶。有兩種方法混卵,第一種是讓通知所在的切面類實現(xiàn)org.springframework.core.Ordered
接口,這個接口有一個getValue()
方法窖张,我們可以實現(xiàn)這個方法來確定順序幕随。第二種方法就是在切面類上應(yīng)用Order
注解,并給定一個值宿接。不論用哪種方法赘淮,值較小的通知會先執(zhí)行。同一切面中的通知睦霎,執(zhí)行順序是未定義的梢卸,也就是不確定的,我們無法指定它們的執(zhí)行順序副女。
定義引入
在切面類中定義一個接口類型的字段蛤高,然后應(yīng)用DeclareParents
注解并定義要引入的類和該接口的默認(rèn)實現(xiàn)。
//定義引入
@DeclareParents(value = "yitian..MyService", defaultImpl = ServiceImpl.class)
private Service service;
理解Spring AOP
Spring AOP是一個基于代理實現(xiàn)的框架碑幅,因此有一些事情需要我們注意戴陡。舉個例子,我們定義如下一個類沟涨。
public class SimplePojo {
public void foo() {
System.out.println("調(diào)用了foo");
bar();
}
public void bar() {
System.out.println("調(diào)用了bar");
}
}
然后定義一個切面和兩個通知猜欺,在目標(biāo)方法之后執(zhí)行。
@Aspect
public class PojoAspect {
@AfterReturning(pointcut = "execution(* yitian..SimplePojo.foo())")
public void afterFoo() {
System.out.println("代理了foo");
}
@AfterReturning(pointcut = "execution(* yitian..SimplePojo.bar())")
public void afterBar() {
System.out.println("代理了bar");
}
}
然后我們運行一下foo方法拷窜,看看會出現(xiàn)什么情況开皿。
@Test
public void testProxy() {
ApplicationContext context = new AnnotationConfigApplicationContext(AOPConfig.class);
SimplePojo pojo = context.getBean("simplePojo", SimplePojo.class);
pojo.foo();
}
結(jié)果如下:
調(diào)用了foo
調(diào)用了bar
代理了foo
我們注意到一個事實涧黄,在foo方法中調(diào)用bar方法并沒有相應(yīng)的通知執(zhí)行。由于Spring AOP是一個基于代理的框架赋荆,因此我們從ApplicationContext中獲取到的Bean其實是一個代理笋妥,因此foo方法會執(zhí)行相應(yīng)的通知。但是窄潭,foo方法調(diào)用自己類中的bar方法春宣,使用的是this引用,沒有經(jīng)過代理嫉你,因此無法觸發(fā)AOP的通知執(zhí)行月帝。這一點需要注意。如果我們希望編寫一個目標(biāo)類型幽污,讓其能夠使用Spring AOP嚷辅,那么盡量不要出現(xiàn)調(diào)用自己類中的方法的情況。由于AspectJ不是基于代理的框架距误,因此如果你使用AspectJ簸搞,就不會出現(xiàn)上面的問題。
小例子
我們來使用環(huán)繞通知配置一下Hibernate的事務(wù)管理准潭。
首先需要定義一個實體類趁俊。
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@NaturalId
private String username;
@Column(nullable = false)
private String password;
@Column
private String nickname;
@Column
private LocalDate birthday;
}
然后添加一個用戶服務(wù),向數(shù)據(jù)庫中添加用戶刑然。這里為了使用環(huán)繞通知來進行事務(wù)管理寺擂,故意將Session寫在參數(shù)中,方便環(huán)繞通知獲取Session泼掠。
public class UserService {
public void add(Session session, User user) {
session.save(user);
}
}
然后我們需要一個切面和一個環(huán)繞通知怔软,環(huán)繞通知將連接點的代碼用事務(wù)處理語句環(huán)繞。
@Aspect
public class TransactionAspect {
@Pointcut("execution(* yitian..UserService.add(..))&&args(session,user)")
private void addUser(Session session, User user) {
}
@Around(value = "addUser(session,user)", argNames = "pjp,session,user")
public void manageTransaction(ProceedingJoinPoint pjp, Session session, User user) {
Transaction transaction = session.beginTransaction();
try {
pjp.proceed(new Object[]{session, user});
transaction.commit();
} catch (Throwable e) {
transaction.rollback();
}
}
}
當(dāng)然上面這幾個類應(yīng)該注冊為Spring Bean武鲁。
@Configuration
@EnableAspectJAutoProxy
public class HibernateConfig {
@Autowired
private SessionFactory sessionFactory;
@Bean
public SessionFactory sessionFactory() {
final StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.configure()
.build();
try {
SessionFactory sessionFactory = new MetadataSources(registry).buildMetadata().buildSessionFactory();
return sessionFactory;
} catch (Exception e) {
StandardServiceRegistryBuilder.destroy(registry);
throw new RuntimeException(e);
}
}
@Bean
public Session session() {
return sessionFactory.openSession();
}
@Bean
public UserService userService() {
return new UserService();
}
}
最后來測試一下爽雄,我們運行測試方法蝠检,然后查看一下數(shù)據(jù)庫沐鼠,看是否成功插入了。
@ContextConfiguration(classes = {HibernateConfig.class})
@RunWith(SpringRunner.class)
public class HibernateTest {
@Autowired
private UserService userService;
@Autowired
private Session session;
@Test
public void testTransactionAspect() {
User user = new User();
user.setUsername("yitian");
user.setPassword("123456");
user.setNickname("易天");
user.setBirthday(LocalDate.now());
userService.add(session, user);
}
}
參考資料
https://my.oschina.net/sniperLi/blog/491854
http://blog.csdn.net/wangpeng047/article/details/8556800
項目代碼
項目在csdn代碼庫中叹谁,見下饲梭。
https://code.csdn.net/u011054333/spring-core-sample/tree/master