天天看點

JavaEE架構---Spring---AOPAOP    Spring--四種方式實作AOP完整代碼連結

AOP    

面向切面程式設計:Aspect Oriented Programming,可以通過預編譯方式和運作期動态代理實作在不修改源代碼的情況下給程式動态統一添加功能的一種技術。

AOP可以說是OOP(面向對象程式設計)的補充和完善。在OOP設計中有可能導緻代碼的重複不利于子產品的重用性,例如日志功能。日志代碼往往水準地散布在所有對象層次中,而與它所散布到的對象的核心功能關系不大。但是在OOP中這些業務要和核心業務代碼在代碼這一級內建。還有些如安全性、事務等也是如此。能不能把這些與核心業務無關但系統中需要使用的業務(稱為切面)單獨編寫成一個子產品,在主要核心業務代碼中不調用,而是在配置檔案中做些配置,配置核心業務需要使用到得切面部分,在系統編譯時才織入到業務子產品中。

切面(Aspect):簡單的了解就是把那些與核心業務無關的代碼提取出來,進行封裝成一個或幾個子產品用來處理那些附加的功能代碼。(如日志,事務,安全驗證)我們把這個子產品的作用了解為一個切面,其實切面就是我們寫一個類,這個類中的代碼原來是在業務子產品中完成的,現在單獨成一個或幾個類。在業務子產品需要的時候才織入。

連接配接點(Joinpoint):在程式執行過程中某個特定的點,比如某方法調用的時候或者處理異常的時候。 在Spring AOP中,一個連接配接點總是代表一個方法的執行。通過聲明一個JoinPoint類型的參數可以使通知(Advice)的主體部分獲得連接配接點資訊。 

切入點(Pointcut):本質上是一個捕獲連接配接點的結構。在AOP中,可以定義一個pointcut,來捕獲相關方法的調用

織入(Weaving):把切面(aspect)連接配接到其它的應用程式類型或者對象上,并建立一個被通知(advised)的對象。 這些可以在編譯時,類加載時和運作時完成。Spring和其它純Java AOP架構一樣,在運作時完成織入。

通知(Advice):在切面的某個特定的連接配接點(Joinpoint)上執行的動作。通知有各種類型,其中包括“around”、“before”和“after”等通知。 通知的類型将在後面部分進行讨論。許多AOP架構,包括Spring,都是以攔截器做通知模型,并維護一個以連接配接點為中心的攔截器鍊。

通知的類型:

  • 前置通知(Before advice):在某連接配接點(join point)之前執行的通知,但這個通知不能阻止連接配接點前的執行(除非它抛出一個異常)。
  • 傳回後通知(After returning advice):在某連接配接點(join point)正常完成後執行的通知:例如,一個方法沒有抛出任何異常,正常傳回。
  • 抛出異常後通知(After throwing advice):在方法抛出異常退出時執行的通知。
  • 後置通知(After (finally) advice):當某連接配接點退出的時候執行的通知(不論是正常傳回還是異常退出)。
  • 環繞通知(Around Advice):包圍一個連接配接點(join point)的通知,如方法調用。這是最強大的一種通知類型。 環繞通知可以在方法調用前後完成自定義的行為。它也會選擇是否繼續執行連接配接點或直接傳回它們自己的傳回值或抛出異常來結束執行。

AOP中的一個重要等式:

切面=切點+通知   (advisor=pointCut+advice)

  • 切面: 定義的一個攔截事件(動作)
  • 切點: 要攔截哪些(個)類的哪些(個)方法
  • 通知: 定義在方法的前面、後面、環繞、出異常 還是 正常傳回的時候攔

Spring--四種方式實作AOP

方式一:Spring最底層的方式

純java實作AOP---了解實作AOP需要哪些Bean

package cn.hncu.aop.hello.v1;

import java.lang.reflect.Method;

import org.aopalliance.aop.Advice;
import org.springframework.aop.Advisor;
import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.JdkRegexpMethodPointcut;

import cn.hncu.aop.hello.v1.domain.Person;

/*AOP中的一個重要等式: 
 *     切面=切點+通知  
 *     advisor=pointCut+advice
 *     切面: 定義的一個攔截事件(動作)
 *     切點: 要攔截哪些(個)類的哪些(個)方法
 *     通知: 定義在方法的前面、後面、環繞、出異常 還是 正常傳回的時候攔
 */
//存java實作 AOP
public class Demo {
	public static void main(String[] args) {
		//1 被切面的對象(被代理的對象)
		Person person = new Person();
		
		//2.2 切點  
		JdkRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
		//攔截的方法的表達式
		pointcut.setPattern(".*run.*");
		
		//2.3 通知  
		Advice advice = new MethodBeforeAdvice() { 
			//這種是無法進行方法攔截的,隻是在方法執行前執行一段代碼
			@Override
			public void before(Method method, Object[] args, Object target) throws Throwable {
				System.out.println("前面做些事...");
			}
			
		};
		
		// 2.1切面入口  --- 切面=切點+通知  
		Advisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
		
		//建立代理bean工廠
		ProxyFactoryBean factory = new ProxyFactoryBean();
		
		//把切面處理方案添加到工廠中
		factory.addAdvisor(advisor); 
		
		//把需要進行切面的對象放入工廠
		factory.setTarget( person ); 
		
		//拿出代理後的對象
		Person proxyPerson = (Person) factory.getObject(); 
		
		proxyPerson.run(); //執行run
		
		System.out.println( proxyPerson.getClass().getSuperclass() );
		System.out.println( proxyPerson.hashCode() );
		
		Person proxyPerson2 = (Person) factory.getObject();
		System.out.println( proxyPerson2.hashCode() );
		
	}
}
           

使用Spring容器實作AOP

有五個版本具體了解見完整代碼連結,這裡就展示出第四版本的代碼。

JavaEE架構---Spring---AOPAOP    Spring--四種方式實作AOP完整代碼連結

xml配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans  http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context.xsd
	">
	
	<!-- 切面=切點+通知 -->
	<!-- 相比于 t2版本  這裡的切面bean采用了DefaultPointcutAdvisor的兄弟類,使用時更簡便-->
	<bean id="advisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
		<!-- 相比于 t2版本 這裡的 切點 已經融入切面之中,隻需要配置攔截方法的表達式即可-->
		<property name="patterns">
			<list>
				<value>.*run.*</value>
				<value>.*eat.*</value>
			</list>
		</property>
		
		<!-- 通知 -->
		<property name="advice">
			<bean id="advice" class="cn.hncu.aop.hello.v2.AroundAdviceImpl" />
		</property>
		
	</bean>
	
	<bean id="person" class="cn.hncu.aop.hello.v2.domain.Person" />
	
	<bean class="cn.hncu.aop.hello.v2.domain.Student"></bean>
	
	<!-- 自動代理bean -->
	<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"></bean>
	
	
</beans>
           

實作通知的類

package cn.hncu.aop.hello.v2;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

/*
 * 實作org.aopalliance.intercept.MethodInterceptor接口,
 * 即可實作同動态代理那樣,既可以前面攔截,後面攔截,同時可以控制放行
 */
public class AroundAdviceImpl implements MethodInterceptor{

	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		System.out.println("前面攔攔...");
		//控制下面這一句執行就可以控制放行,也就可以實作方法攔截。
		Object returnValue = invocation.proceed(); //放行
		System.out.println("後面攔攔...");
		return returnValue;
	}


}
           

測試代碼

/* DefaultAdvisorAutoProxyCreator類 實作了 BeanPostProcessor接口,
	 * BeanPostProcessor:在每個bean的建立-->初始化過程的前/後-->放入 applicationContext 初始化過程的前/後 起作用的
	 * 在每個 bean 建立後 --> 初始化過程的前/後 都會觸發 BeanPostProcessor中的方法
	 * DefaultAdvisorAutoProxyCreator類會根據 容器中的 Advisor 的規則把每個符合 的bean進行自動代理
	 * 然後把代理的bean放入 applicationContext 容器中
	 */
	@Test 
	public void t4() {
		//使用 t4.xml
		ApplicationContext appCtx = new ClassPathXmlApplicationContext("cn/hncu/aop/hello/v2/t4.xml");
		Person person = (Person) appCtx.getBean(Person.class); 
		person.run();
		person.eat();//配置攔截
		/* 該類中有 切入點 的函數,是以會被 DefaultAdvisorAutoProxyCreator 檢測到
		 * 并且放入applicationContext中是代理後的對象,即獲得到的是 代理後的對象
		 */
		System.out.println( person.getClass() ); 
		
		Student student = appCtx.getBean(Student.class);
		/* 該類中沒有 切入點 的函數,是以不會被 DefaultAdvisorAutoProxyCreator 檢測到
		 */
		System.out.println( student.getClass() ); 
		
		
		((AbstractApplicationContext) appCtx).close();
	}
           

方式二:Spring---引入AspectJ

aspectj技術主要是改進之前定位切點時不夠精确的問題,如aspectj可以使用切點語言定義切點更具體的特征如方法的傳回類型、方法的形參等。

aspectj定義切點時采用的是切點語言,助了解:

    △設定切點: cut.setExpression("execution( 用切點語言寫的切點表達式  )");

    △用切點語言寫的切點表達式---相關知識點如下:

  1) 切點表達式格式:  傳回類型   包名.[子包名.]類名.方法名(參數類型清單)

  2) "."是包名之間  或   包名與類名  或   類名與方法名  之間的間隔符

  3) ".."在包路徑位置代表'任意深的目錄',在參數類型清單位置代表'任意個數與類型的參數'

  4) "*"是作業系統中的通匹符

純Java方式使用AspectJ實作AOP

package cn.hncu.aop.aspectj.v1;

import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.Test;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.aop.support.DefaultPointcutAdvisor;

import cn.hncu.aop.hello.v2.domain.Person;
/*
1. aspectj技術主要是改進之前定位切點時不夠精确的問題,
如aspectj可以使用切點語言定義切點更具體的特征如方法的傳回類型、方法的形參等

2. aspectj定義切點時采用的是切點語言,助了解:
    △設定切點: cut.setExpression("execution( 用切點語言寫的切點表達式  )");
    △用切點語言寫的切點表達式---相關知識點如下:
  1) 切點表達式格式:  傳回類型   包名.[子包名.]類名.方法名(參數類型清單)
  2) "."是包名之間  或   包名與類名  或   類名與方法名  之間的間隔符
  3) ".."在包路徑位置代表'任意深的目錄',在參數類型清單位置代表'任意個數與類型的參數'
  4) "*"是作業系統中的通匹符
 */
public class AspectJDemo {
	
	//示範純java的方式實作AOP
	@Test
	public void t1() {
		//被代理的對象
		Person person = new Person();
		
		/AspectJ加強處///
		//切點
		AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
		
		//攔截 傳回值 為 void 并且是cn/hncu/任意目錄/Person類中的任意r開頭的空參方法
		//pointcut.setExpression( "execution( void cn.hncu..Person.r*() )" );
		
		//攔截 傳回值 為 void 并且是cn/hncu/任意目錄/Person類中的任意r開頭的參數為int類型的方法
		pointcut.setExpression( "execution( void cn.hncu..Person.r*(int) )" );
		//
		
		//通知
		Advice advice = new MethodInterceptor() {
			@Override
			public Object invoke(MethodInvocation invocation) throws Throwable {
				System.out.println("前面攔攔...");
				Object returnValue = invocation.proceed(); //放行
				System.out.println("後面攔攔...");
				return returnValue;
			}
		};
		
		//切面=切點+通知
		Advisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
		
		//建立代理bean工廠
		ProxyFactoryBean factory = new ProxyFactoryBean();
		//添加切面方案
		factory.addAdvisor(advisor);
		//放入目标: 被代理對象
		factory.setTarget( person );
		
		Person person2 = (Person) factory.getObject();
		
		person2.run();
		person2.run(5);
		person2.eat();
		
	}
}
           

使用Spring容器

xml檔案

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans  http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context.xsd
	">
	
	<!-- 使用 AspectJExpressionPointcutAdvisor 切面,該類封裝了切點,隻要給切點表達式即可-->
	<bean class="org.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor">
	
		<!-- 切點表達式 -->
		<property name="expression" value="execution( * cn.hncu..Person.run(..) )" />
		
		<!-- 通知:環繞通知,即前後都攔截 -->
		<property name="advice">
			<bean class="cn.hncu.aop.aspectj.v2.AroundAdviceImpl"/>
		</property>
	</bean>
	
	<bean class="cn.hncu.aop.aspectj.v2.domian.Person" />
	
	<!-- 通過切面自動掃描容器中符合切點表達式的bean,并生産代理bean -->
	<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"></bean>
	
</beans>
           

實作通知的類

package cn.hncu.aop.aspectj.v2;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

/*
 * 實作org.aopalliance.intercept.MethodInterceptor接口,
 * 即可實作同動态代理那樣,既可以前面攔截,後面攔截,同時可以控制放行
 */
public class AroundAdviceImpl implements MethodInterceptor{

	@Override
	public Object invoke(MethodInvocation invocation) throws Throwable {
		System.out.println("前面攔攔...");
		//控制下面這一句執行就可以控制放行,也就可以實作方法攔截。
		Object returnValue = invocation.proceed(); //放行
		System.out.println("後面攔攔...");
		return returnValue;
	}
}
           

測試代碼

package cn.hncu.aop.aspectj.v2;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import cn.hncu.aop.aspectj.v2.domian.Person;

/* 示範使用applicationContext的方式
 */
public class AspectjXmlDemo {
	
	public static void main(String[] args) {
		ApplicationContext appCtx = new ClassPathXmlApplicationContext("cn/hncu/aop/aspectj/v2/applicationContext.xml");
		Person person = appCtx.getBean(Person.class);
		person.run();
		person.eat();
		((AbstractApplicationContext) appCtx).close();
	}
}
           

最後面有完整代碼連結

方式三:AspectJ+注解的方式

xml檔案

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans  http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context.xsd
	http://www.springframework.org/schema/aop  http://www.springframework.org/schema/aop/spring-aop.xsd
	">
	
   	<!-- 1自動代理标簽。為了讓Spring能夠識别基于注釋的切面,必須要加這個aop标簽 -->
  	<aop:aspectj-autoproxy />
	<!-- Spring2.5,Spring3x用下面這個自動代理bean 可以替換上面的自動代理标簽。但是高版本就不行了
	<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"></bean>
	-->
	
	<!-- 2被攔截的Bean -->
	<bean class="cn.hncu.aop.annotation.domain.Person" />
	
	<!-- 3切面Bean -->
	<bean class="cn.hncu.aop.annotation.v2.MyAdvisor" />
	
</beans>
           

切面類

方式一:@Pointcut注解寄托于方法上

package cn.hncu.aop.annotation.v1;

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;

/* 切面=切點+通知:@Aspect = @Pointcut + ( @Before | @After | @Around | @AfterReturning | @AfterThrowing )
 */
@Aspect //被該 注解 注解後的類會被識别為'切面' Advisor
public class MyAdvisor {
	
	//攔截Person類中的所有空參方法
	@Pointcut(value="execution( * cn.hncu..Person.*() )")
	public void pointCut() { //切點注解的函數沒有任何函數功能,該函數隻是用來承載 @Pointcut和辨別它
	}
	//可以有多個切點
	@Pointcut(value="execution( * cn.hncu..Person.*(*) )")
	public void pointCut2() {
		
	}
	
	@Before(value="pointCut()") //通過辨別找到對應的切點進行通知
	public void before() { //函數名可以随便取,沒有實際意義,隻不過規範點友善識别
		System.out.println("...before...");
	}
	@Before(value="pointCut2()") //第二個切點
	public void before2() { 
		System.out.println("...before...");
	}
	
	@After(value="pointCut()") //通過辨別找到對應的切點進行通知
	public void after() { //函數名可以随便取,沒有實際意義,隻不過規範點友善識别
		System.out.println("...after...");
	}
	
	/* 使用@Around 注解時要注意:
	 * 	  被注解的函數一定要有 ProceedingJoinPoint p 作為函數參數, 并且要有一個 Object型 的傳回值。
	 */
	@Around(value="pointCut()")
	public Object around(ProceedingJoinPoint p ) throws Throwable {
		String methdName = p.getSignature().getName(); //可以擷取到被攔截的方法名等資訊
		System.out.println(methdName+">>前面攔攔...");
		Object returnValue = p.proceed(); //放行
		System.out.println(methdName+">>後面攔攔...");
		return returnValue;
	}
	
	/* @AfterReturning 和  @AfterThrowing 是一對'互斥'的注解
	 * @AfterReturning:函數正常傳回後通知
	 * @AfterThrowing:函數抛出異常(沒抓)後通知
	 */
	@AfterReturning(value="pointCut()")
	public void afterReturning() {
		System.out.println("這是調用完成,正常傳回(沒有捕捉到異常)以後");
	}
	
	@AfterThrowing(value="pointCut()") 
	public void afterThrowing() {
		System.out.println("這是調用完成,發現有異常");
	}
	@AfterThrowing(value="pointCut2()") //第二個切點
	public void afterThrowing2() {
		System.out.println("這是調用完成,發現有異常");
	}
}
           

方式二:不需要@Pointcut注解,直接使用切點表達式

package cn.hncu.aop.annotation.v2;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

/* 切面=切點+通知:@Aspect = str + ( @Before | @After | @Around | @AfterReturning | @AfterThrowing )
 */
@Aspect //被該 注解 注解後的類會被識别為'切面' Advisor
public class MyAdvisor {
	//切點語言表達式
	private final String CUT = "execution( * cn.hncu..Person.*( ) )";
	private final String CUT2 = "execution( * cn.hncu..Person.*( * ) )";
	
	
	@Before(value=CUT) 
	public void before() { 
		System.out.println("...before...");
	}
	@Before(value=CUT2) 
	public void before2() { 
		System.out.println("...before...");
	}
	
	@Around(value=CUT)
	public Object around(ProceedingJoinPoint p ) throws Throwable {
		System.out.println("前面攔攔...");
		Object returnValue = p.proceed(); //放行
		System.out.println("後面攔攔...");
		return returnValue;
	}
}
           

測試代碼

package cn.hncu.aop.annotation.v1;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import cn.hncu.aop.annotation.domain.Person;

public class Demo {
	
	public static void main(String[] args) {
		ApplicationContext appCtx = new ClassPathXmlApplicationContext("cn/hncu/aop/annotation/v1/demo.xml");
		Person person = appCtx.getBean( Person.class );
		System.out.println("--------------");
		person.run();
		System.out.println("--------------");
		person.run(5); 
//		person.run(0); //這裡示範 @AfterThrowing 注解
		System.out.println("--------------");
		person.eat();
		System.out.println("--------------");
		
		//關流防止記憶體洩漏
		((AbstractApplicationContext) appCtx).close();
	}
}
           

方式四:AspectJ+label的方式

xml檔案

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:aop="http://www.springframework.org/schema/aop"
	
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans  http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context.xsd
	http://www.springframework.org/schema/aop  http://www.springframework.org/schema/aop/spring-aop.xsd
	">
	
   	<!-- 1自動代理标簽,專用于基于标簽的切面的 -->
  	<aop:config>
  		<aop:aspect ref="hello">
  			<aop:pointcut expression="execution( * cn.hncu..Person.*( ) )" id="pc1"/>
  			<aop:after method="hello" pointcut-ref="pc1"/>
  			<aop:before method="say" pointcut-ref="pc1"/>
  			<aop:around method="around" pointcut-ref="pc1"/>
  		</aop:aspect>
  	</aop:config>
	
	<!-- 2被攔截的Bean -->
	<bean class="cn.hncu.aop.annotation.domain.Person" />
	
	<!-- 3切面Bean -->
	<bean id="hello" class="cn.hncu.aop.label.HelloWorld" />
	
</beans>
           

切面類

package cn.hncu.aop.label;

import org.aspectj.lang.ProceedingJoinPoint;
/* 采用label方式使 java類最多隻依賴 org.aspectj.lang.ProceedingJoinPoint
 * 如果把該類無視掉的話,該類就是一個純粹的POJO(普通的java對象)。
 * 完全看不出來該類是一個切面。
 */
public class HelloWorld {
	
	public void hello() {
		System.out.println("Hello world!");
	}
	
	public void say() {
		System.out.println("say...");
	}
	
	public Object around( ProceedingJoinPoint p ) throws Throwable {
		
		System.out.println("前面攔攔...");
		Object returnValue = p.proceed(); //放行
		System.out.println("後面攔攔...");
		
		return returnValue;
	}
}
           

完整代碼連結

JavaEE架構---Spring---AOPAOP    Spring--四種方式實作AOP完整代碼連結
JavaEE架構---Spring---AOPAOP    Spring--四種方式實作AOP完整代碼連結

繼續閱讀