我們應該對這樣的需求不陌生:App允許使用者以遊客的身份瀏覽,在使用者點選某些操作時,先判斷使用者的登入狀態,如果已登入,則執行對應操作,如果沒登入,則跳轉至登入頁面要求登入。只是這樣倒也不難處理,無非是加個if判斷,來決定是跳登入還是直接執行對應操作。但是產品經理可能又說了,登入成功後不能直接回到原來的頁面就完了,你還要自動繼續之前中斷的操作。稍加思考,也還行,跳頁面的時候把要執行的操作記錄一下,用startActivityForResult跳轉,要求登入模組把結果返回來,然後我們在onActivityForResult中獲取登入結果,如果登入成功了再根據跳轉前的記錄,執行之前中斷的操作。這麼看好像也不存在難度上的問題,只是繁瑣,過於繁瑣,尤其是頁面上有多個需要登入的操作時,在onActivityForResult裡的判斷要寫炸了,這一點我們在下面的例子中可以看到。
假定幾個需求
頁面是上面這樣,我們的app有登入模組(loginmodule)、實名認證模組(authmodule)、vip模組(vipmodule),“點贊”要求使用者登入,並在登入成功後自動點贊;“評論”操作首先要求使用者是登入狀態,其次還要是進行了實名認證狀態;“遮蔽”要求使用者vip等級達到3,如果不到3會跳轉到購買vip頁面,在購買的時候再驗證登入狀態,同樣,購買成功後自動執行遮蔽操作。
一些背景交待
我們的幾種方式都是基於startActivityForResult和onActivityResult來實現的,在各個模組內部有多少個頁面,如何跳轉我們都不關心,我們只關心以下幾點:
- 模組的入口是哪,即我這個startActivityForResult要往哪跳(以loginmodule為例,入口是InputAccountActivity)
- 模組內部怎麼跳轉我不管,但必須在入口Activity這兒給我setResult把結果給我返回來,要不我怎麼拿到結果並執行之前中斷的操作啊
- 注意:模組內部頁面較多時,我們可能傾向於在登入成功時通過singleTask啟動模式返回到入口Activity,並在onNewIntent中setResult和finish,但是不要直接在manifest裡設定入口activity的啟動模式為singleTask,因為這樣onActivityResult就收不到了。
For example, if the activity you are launching uses the singleTask launch mode, it will not run in your task and thus you will immediately receive a cancel result.
基於此原因,可以在manifest中設定為標準模式,在返回入口Activity的時候給跳轉的intent設定flags來達到和singleTask一樣的效果。具體可參考loginmodule的程式碼
val intent = Intent(this,InputAccountActivity::class.java)
intent.putExtra("loginResult",true)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
startActivity(intent)
複製程式碼
下面我們會由繁到簡介紹多種方式來處理這種業務流程,最終的方案只需在方法上加幾個註解就ok了。
方式一
最原始的,使用startActivityForResult跳轉,並在onActivityResult裡處理結果
這是recyclerView子view的點選事件程式碼(用的BRVAH,好東西不必多說)
mAdapter.setOnItemChildClickListener { adapter, view, position ->
when (view.id) {
R.id.tv_like -> doLike(position)
R.id.tv_comment -> doComment(position)
R.id.tv_block -> doBlock(position)
}
}
複製程式碼
下面是各個方法的程式碼以及onActivityResult程式碼
//需要登入才能點贊
private fun doLike(position: Int) {
if (!LoginManager.isLogin) {
action = "like"
clickedPosition = position
val i = Intent(this, InputAccountActivity::class.java)
startActivityForResult(i, REQUEST_CODE_LOGIN)
return
}
val user = mAdapter.getItem(position)
toast("點讚了 ${user?.name}")
}
//需要登入且通過實名認證才能評論
private fun doComment(position: Int) {
if (!LoginManager.isLogin) {
action = "comment"
clickedPosition = position
val i = Intent(this, InputAccountActivity::class.java)
startActivityForResult(i, REQUEST_CODE_LOGIN)
return
}
if (!AuthManager.isAuthed) {
action = "comment"
clickedPosition = position
val i = Intent(this, AuthActivity::class.java)
startActivityForResult(i, REQUEST_CODE_AUTH)
return
}
val user = mAdapter.getItem(position)
toast("評論了 ${user?.name}")
}
//需要達到vip3才能遮蔽其他人
private fun doBlock(position: Int) {
if(VipManager.vipLevel<3){
action = "block"
clickedPosition = position
val i = Intent(this,BuyVipActivity::class.java)
startActivityForResult(i,REQUEST_CODE_VIP)
return
}
val user = mAdapter.getItem(position)
toast("遮蔽了 ${user?.name}")
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE_LOGIN) {
val loginResult = data?.getBooleanExtra("loginResult", false)
if (loginResult == true) {
when (action) {
"like" -> doLike(clickedPosition)
"comment" -> doComment(clickedPosition)
"block" -> doBlock(clickedPosition)
}
}
} else if (requestCode == REQUEST_CODE_AUTH) {
val authResult = data?.getBooleanExtra("authResult",false)
if(authResult == true){
when(action){
"comment" -> doComment(clickedPosition)
}
}
} else if (requestCode == REQUEST_CODE_VIP) {
val vipLevel:Int = data?.getIntExtra("vipLevel",0) ?:0
if(vipLevel>=3){
when(action){
"block" -> doBlock(clickedPosition)
}
}
}
}
}
複製程式碼
以點贊為例,在方法裡先判斷登入狀態,沒登入則記錄以下點選的操作action,以及點選的位置clickedPosition,並startActivityForResult跳至InputAccountActivity,等待在onActivityResult裡處理登入結果。如果是已登入狀態則直接執行點贊操作(這裡用toast代替)。
再來看onActivityResult裡的邏輯,先判斷resultCode,再根據不同的requestCode從data裡取出對應的結果值,如果是true(即登入成功),再根據之前記錄下的action和clickedPosition執行之前中斷的操作。
遮蔽與點贊邏輯類似,至於評論,不過是又加了個實名認證的判斷,不再贅述。
可以看到程式碼幾乎不存在多少難度,主要就是一層又一層的判斷太繁瑣,導致程式碼臃腫。而造成這一切的原因是所有的處理必須在onActivityResult中進行,如果獲取的結果能直接在doLike中拿到就不會這樣了,也就不必再記錄action和clickedPosition了。所以,凶手只有一個,onActivityResult!下面方式二我們會對此進行處理。
方式二
針對方式一的問題,是時候介紹一下我之前寫的一個東西了AvoidOnResult,如果想了解原理可以看這篇文章如何避免使用onActivityResult,以提高程式碼可讀性,如果暫時只想知道怎麼用,我來簡單介紹一下,它主要用來解決startActivityForResult和onActivityResult這種發起呼叫和回撥分離的問題,它的使用方式如下
AvoidOnResult(activity).startForResult(XXXActivity::class.java,object :AvoidOnResult.Callback{
override fun onActivityResult(resultCode: Int, data: Intent?) {
//TODO
}
})
複製程式碼
構造器裡需要傳一個activity例項,startForResult方法傳要跳轉過去的class(或intent),同時,再傳一個AvoidOnResult.Callback,在callback的onActivityResult中就可以處理收到的結果了,而完全不用重寫activity的onActivityResult方法。
這樣我們的點選事件可以寫成這樣了
when (view.id) {
R.id.tv_like -> {
if (LoginManager.isLogin) {
doLike(position)
} else {
AvoidOnResult(activity).startForResult(InputAccountActivity::class.java, object : AvoidOnResult.Callback {
override fun onActivityResult(resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK && data?.getBooleanExtra("loginResult", false) == true) {
doLike(position)
}
}
})
}
}
……
}
複製程式碼
可以看到所有的處理都在點選事件這兒進行了,不需要在onActivityResult統一判斷處理了,也不需要記錄action和clickedPosition。當然我們還可以進一步封裝一下,LoginManager程式碼如下
object LoginManager {
var isLogin = false
fun toLogin(activity:Activity,loginCallback: LoginCallback) {
AvoidOnResult(activity).startForResult(InputAccountActivity::class.java,object :AvoidOnResult.Callback{
override fun onActivityResult(resultCode: Int, data: Intent?) {
if(resultCode == Activity.RESULT_OK && data?.getBooleanExtra("loginResult",false)==true){
loginCallback.onLoginResult(true)
}else{
loginCallback.onLoginResult(false)
}
}
})
}
interface LoginCallback{
fun onLoginResult(loginResult: Boolean)
}
}
複製程式碼
其他兩個模組也類似,最終點選事件是下面這樣的,而doLike、doComment、doBlock中不再進行判斷了,可以看到程式碼整齊多了。
mAdapter.setOnItemChildClickListener { adapter, view, position ->
when (view.id) {
R.id.tv_like -> {
if (LoginManager.isLogin) {
doLike(position)
} else {
LoginManager.toLogin(this, object : LoginManager.LoginCallback {
override fun onLoginResult(loginResult: Boolean) {
if (loginResult) {
doLike(position)
}
}
})
}
}
R.id.tv_comment -> {
if(AuthManager.isAuthed){
doComment(position)
}else{
AuthManager.toAuth2(this,object :AuthManager.AuthCallback{
override fun onAuthResult(authResult: Boolean) {
if(authResult){
doComment(position)
}
}
})
}
}
R.id.tv_block -> {
if (VipManager.vipLevel >= 3) {
doBlock(position)
} else {
VipManager.toBuyVip(this, object : VipManager.VipCallback {
override fun onBuyVip(vipLevel: Int) {
if (vipLevel >= 3) {
doBlock(position)
}
}
})
}
}
}
}
複製程式碼
可能有人會問,doComment只進行了實名認證狀態的判斷,沒判斷登入啊,那是因為在AuthManager的toAuth2方法中處理過登入的檢查了,也就是說要進行實名認證,首先你得登入,否則你都進不了實名認證模組,這個聽起來很合理吧(但後面我們會推翻它……)
//方式2
fun toAuth2(activity: Activity, authCallback: AuthCallback){
if(LoginManager.isLogin){
realToAuth(activity,authCallback)
}else{
LoginManager.toLogin(activity,object :LoginManager.LoginCallback{
override fun onLoginResult(loginResult: Boolean) {
if(loginResult){
realToAuth(activity,authCallback)
}else{
authCallback.onAuthResult(false)
}
}
})
}
}
private fun realToAuth(activity: Activity, authCallback: AuthCallback){
AvoidOnResult(activity).startForResult(AuthActivity::class.java,object :AvoidOnResult.Callback{
override fun onActivityResult(resultCode: Int, data: Intent?) {
if(resultCode == Activity.RESULT_OK && data?.getBooleanExtra("authResult",false) == true){
authCallback.onAuthResult(true)
}else{
authCallback.onAuthResult(false)
}
}
})
}
複製程式碼
其實到這裡,相比方式一的程式碼已經改善很多了。但是,我說但是,作為一名程式猿軟體工程師——這個世界上最接近魔法師的神奇職業之一,怎麼能就此滿足!繼續優化!
方式三
這次要祭出的是AOP(面向切面)了,專案用的是aspectjx,為Android處理過的aspectj。沒聽過的可以看我之前寫的文章
AOP:利用Aspectj注入程式碼,無侵入實現各種功能,比如一個註解請求許可權
如果你還是不想看的話,我簡單介紹一下(我儘量說明白吧)
不同於物件導向,面向切面程式設計是針對滿足某些條件的切面進行統一處理,比方我們現在有一個麵包(物件導向裡的物件),需要把它做成漢堡,所需要的操作就是把它中間切一刀(這就是切面了),然後向切面裡塞入一些肉和菜什麼的。
對應現在的例子呢,所有需要驗證登入的地方就是一個切面,我們要做的就是確定這個切面,然後在這個切面統一處理(用@Around,可以實現方法的攔截或允許繼續執行),判斷登入狀態,已登入就允許執行(joinpint.proceed()),否則就跳轉至登入模組,登入成功再執行。
aspectj還涉及到一些Aspect、Pointcut、Advice等名詞,還是建議瞭解一下 再繼續往下看,我在之後的講述中也會假定各位對此有了一些瞭解。
還是以點贊登入為例,先分析一下,首先我們要找到需要檢查登入的切面,然後在Advice中判斷如果已登入,就允許方法執行,即proceed,這個很容易;如果未登入,則要走登入流程,登入成功再proceed,登入失敗就相當於攔截了,回顧之前的LoginManager,它需要一個Activity引數,只要能拿到activity例項就行,最簡單粗暴的,我不管你方法在哪,我直接取top Activity,即當前resume的activity,關於top Activity的獲取不多說,我是Application中獲取的,Weak是我自己寫的一個委託,你只把它當弱引用就行了。好了,關於activity例項的獲取解決了,那就正式開始吧
class MyApplication: Application() {
companion object {
var topActivity by Weak<Activity>()
}
override fun onCreate() {
super.onCreate()
registerActivityLifecycleCallbacks(object :ActivityLifecycleCallbacks{
override fun onActivityPaused(activity: Activity?) {
}
override fun onActivityResumed(activity: Activity?) {
topActivity = activity
}
override fun onActivityStarted(activity: Activity?) {
}
override fun onActivityDestroyed(activity: Activity?) {
}
override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {
}
override fun onActivityStopped(activity: Activity?) {
}
override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {
}
})
}
}
複製程式碼
首先是需要登入的切面,我是通過註解來確定的,先來個RequireLogin註解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireLogin {
boolean proceed() default true;
}
複製程式碼
可以看到我定義了一個boolean型別的引數proceed(字面義:繼續,尤指打斷後),因為產品的需求中可能會要求某些地方登入完後自動繼續之前打斷的操作,有些地方卻不用,這個引數我們會用來判斷要不要繼續被打斷的操作,即要不要執行joinpoint.proceed()。
繼續,上aspect,首先來個pointcut,所有方法的執行
//所有方法的execution
@Pointcut("execution(* *..*.*(..))")
public void anyExecution() {
}
複製程式碼
針對RequireLogin註解,再來個pointcut
//註解有RequireLogin
@Pointcut("@annotation(requireLogin)")
public void annotatedWithRequireLogin(RequireLogin requireLogin) {
}
複製程式碼
再來是它們兩個pointcut的交集,也就是所有註解有RequireLogin的方法的執行,這就是需要驗證登入的切面了
@Pointcut("anyExecution() && annotatedWithRequireLogin(requireLogin)")
public void requireLoginPointcut(RequireLogin requireLogin) {
}
複製程式碼
怎麼攔截處理呢?上Advice
@Around("requireLoginPointcut(requireLogin)")
public void requireLogin(final ProceedingJoinPoint proceedingJoinPoint, RequireLogin requireLogin) throws Throwable {
final boolean proceed = requireLogin.proceed();
if (LoginManager.INSTANCE.isLogin()) {
proceedingJoinPoint.proceed();
} else {
Activity activity = MyApplication.Companion.getTopActivity();
if (activity != null) {
LoginManager.INSTANCE.toLogin(activity, new LoginManager.LoginCallback() {
@Override
public void onLoginResult(boolean loginResult) {
if (loginResult && proceed) {
try {
proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
});
}
}
}
複製程式碼
proceedingJoinPoint.proceed()就是執行了被攔截的方法,不呼叫這個方法就相當於程式碼被攔截不執行了。邏輯和我們之前說的一樣,先取了註解的引數proceed,然後判斷登入狀態,如果登入了就proceedingJoinPoint.proceed(),否則就先獲取topActivity,走登入流程,在登入結果中判斷,如果登入成功並且註解的引數proceed傳的true,就proceedingJoinPoint.proceed()。
另兩個模組不贅述了,直接看程式碼吧,這時候UserListActivity3的方法是這樣的
//需要登入才能點贊
@RequireLogin(proceed = true)
private fun doLike(position: Int) {
val user = mAdapter.getItem(position)
toast("點讚了 ${user?.name}")
}
//需要登入且通過實名認證才能評論
@RequireAuth(proceed = true)
private fun doComment(position: Int) {
val user = mAdapter.getItem(position)
toast("評論了 ${user?.name}")
}
//需要達到vip3才能遮蔽其他人
@RequireVip(proceed = true,requireLevel = 3)
private fun doBlock(position: Int) {
val user = mAdapter.getItem(position)
toast("遮蔽了 ${user?.name}")
}
複製程式碼
程式碼中沒有一丁點的判斷,doLike需要登入,那就給它加個RequireLogin註解,要求登入完自動執行點贊,那就給proceed引數設為true,否則就false。以後其他地方如果也要求登入了,只需要在對應的方法上也給它加個註解,哪裡登入注哪裡。
還是doComment,明明又要登入又要註冊,可是卻只有一個RequireAuth註解,為什麼,因為AuthManager的toAuth上加了RequireLogin註解啊,即要想實名認證,首先你得登入。
//方式3請新增註解,方式四請注掉下面的註解
@RequireLogin(proceed = true)
fun toAuth(activity: Activity, authCallback: AuthCallback){
AvoidOnResult(activity).startForResult(AuthActivity::class.java,object :AvoidOnResult.Callback{
override fun onActivityResult(resultCode: Int, data: Intent?) {
if(resultCode == Activity.RESULT_OK && data?.getBooleanExtra("authResult",false) == true){
authCallback.onAuthResult(true)
}else{
authCallback.onAuthResult(false)
}
}
})
}
複製程式碼
到這裡程式碼夠精簡了吧,一個註解就能完成之前一大堆的判斷,難道還能再減少?下一步的優化不再是精簡程式碼了,而是增加靈活性。之前兩次提到了實名認證對於登入的依賴,即要想進行實名認證的話,首先得檢查登入。依賴關係大概這樣的 doComment -> toAuth -> toLogin,大概是個鏈式的依賴關係,雖然這個需求聽起來很合理,但是程式碼這麼寫卻缺少靈活性。怎麼說呢?比如現在又有需求了,想遮蔽別人首先你得是vip3,然後還要通過了實名認證,很多人第一時間想到的可能是像實名認證那樣,我在VipManager的toBuyVip方法上再加個RequireAuth註解,然而並不可以,因為要求不登入也可以跳轉到vip購買頁面,在點購買的時候才要求登入,而如果給toBuyVip新增RequireAuth註解之後依賴關係就是這樣的了doBlock -> toBuyVip -> toAuth -> toLogin,這樣在點遮蔽的時候會先走登入註冊流程,導致使用者無法以遊客的身份進入vip購買頁面。當然我們在此不討論需求的合理性,不討論遊客該不該進入vip購買頁面。
說這麼多也不知道表達清楚沒,我就是想說,我們現在存在的問題是各個切面有依賴關係,有耦合,如果讓它們彼此獨立,我們可以自由地組合就好了,比如實名認證模組就只管判斷實名認證的狀態,不管你登沒登入(你沒登入,那實名認證就該是false的狀態)。我們往方法上加註解的時候直接加多個註解,比如doComment,要求兩點,一登入,二認證,那我就註解RequireLogin和RequireAuth;doBlock要求vip3和實名認證,那我就註解RequireVip和RequireAuth,就像搭積木一樣,自由組合。
方式四
這次以doComment為例,既然要做到各個切面獨立,那就先把AuthManager.toAuth方法的RequireLogin註解去掉,讓它不依賴於登入切面,然後我們往doComment上加兩個註解,RequireLogin、RequireAuth,然後執行一下先看下效果,點選評論,發現兩個問題:
- 兩個流程的先後順序有問題,先走了實名認證,然後才走了登入
- 兩個流程走完後發現並沒有彈出toast
首先先說第二個問題,我們的幾種方式本質上都是用的onActivityResult,AvoidOnResult的callback就是在構造器傳入的activity例項的onActivityResult中呼叫的,如果這個activity例項finish掉了,那callback就不會呼叫了。我們可以打個斷點,分別打在AuthAspect和LoginAspect中獲取topActivity的地方,點選評論首先會走到AuthAspect中,這裡我們可以看到獲取到的topActivity是UserListActivity4,是我們期望的結果,沒問題。放開斷點繼續走,走完實名認證的流程後會進入LoginAspect的程式碼,這裡我們發現獲取到的topActivity是AuthActivity,也就是這時雖然已經在UserListActivity4的onActivityResult程式碼中了,但是當前resume狀態的activity卻還是AuthActivity,而我們再通過AuthActivity去startForResult,callback肯定不會執行了,因為它馬上就要finish掉了,關於onActivityResult和onResume的執行順序問題在Activity的onActivityResult的原始碼註釋中其實說的也很明白了,onActivityResult比onResume先執行。
You will receive this call immediately before onResume() when your activity is re-starting.
那也就是說在多個切面的情況下,我們直接獲取resume的Activity是不可行的。那怎麼解決呢?我的方法是先通過joinpoint的this,target,args,看看這些地方有沒有能拿到的activity,如果有,就用這裡拿到的activity,如果沒有再用top Activity,下面的方法只判斷了Activity,其實如果能獲取到Fragment或其他什麼型別的例項,再間接獲取到Activity也可以,這裡圖簡單沒做太多處理。
public class AspectUtils {
public static Activity getActivity(JoinPoint joinPoint){
//先看this
if(joinPoint.getThis() instanceof Activity){
return (Activity) joinPoint.getThis();
}
//target
if(joinPoint.getTarget() instanceof Activity){
return (Activity) joinPoint.getTarget();
}
//args
for(Object arg:joinPoint.getArgs()){
if (arg instanceof Activity){
return (Activity) arg;
}
}
//如果實在找不到,再返回topActivity
return MyApplication.Companion.getTopActivity();
}
}
複製程式碼
第二個問題解決了再來看第一個問題,aspectj織入順序問題,我是在這裡找到方法的https://stackoverflow.com/questions/11850160/aspectj-execution-order-precedence-for-multiple-advice-within-one-aspect
When two pieces of advice defined in the same aspect both need to run at the same join point, the ordering is undefined (since there is no way to retrieve the declaration order via reflection for javac-compiled classes). Consider collapsing such advice methods into one advice method per joinpoint in each aspect class, or refactor the pieces of advice into separate aspect classes - which can be ordered at the aspect level.
也就是說同一個aspect中的多個advice的順序是不確定的,可以考慮把想排序的advice分別放到不同的aspect中,然後對這些aspect排序(用@DeclarePrecedence)
分拆aspect很簡單,不必說。排序再建立個類,如下
@Aspect
@DeclarePrecedence("LoginAspect,VipAspect,AuthAspect")
public class CoordinationAspect {
// empty
}
複製程式碼
評論的時候要求先登入,再實名認證,所以LoginAspect在AuthAspect前面,遮蔽的時候要求首先是vip,然後還要通過了實名認證,所以VipAspect在AuthAspect前面。
這樣之後再執行一下,應該已經沒問題了。
方式四的程式碼如下,相比方式三更靈活,可以自由組合,當然要確定好各個切面的順序
//需要登入才能點贊
@RequireLogin(proceed = true)
private fun doLike(position: Int) {
val user = mAdapter.getItem(position)
toast("點讚了 ${user?.name}")
}
//需要登入且通過實名認證才能評論
@RequireLogin(proceed = true)
@RequireAuth(proceed = true)
private fun doComment(position: Int) {
val user = mAdapter.getItem(position)
toast("評論了 ${user?.name}")
}
//需要達到vip3且通過實名認證才能遮蔽其他人
@RequireVip(proceed = true,requireLevel = 3)
@RequireAuth(proceed = true)
private fun doBlock(position: Int) {
val user = mAdapter.getItem(position)
toast("遮蔽了 ${user?.name}")
}
複製程式碼
存在的問題
- AvoidOnResult,低記憶體極端情況下A跳轉到B,如果回到A之前A被系統回收了,會觸發不了callback,也就是說會無法繼續之前中斷的操作,如果你的應用對此很在意,請慎用。如果你有解決方案,歡迎來討論。
- 如果有不能執行的問題,先clean或者sync一下,不行的話到aspectjx的github看有沒有對應的issue。
結束
demo裡還有個ShowConfirmAspect,是用來在一個方法執行前對使用者進行一些詢問操作,彈個對話方塊,根據使用者反饋決定要不要執行該方法,我在MainActivity的onBackPressed上加了該註解來詢問使用者是否要退出應用,這裡不細說了,只是想告訴大家面向切面有很多應用場景,特別適合進行一些統一的處理,從而避免大量的重複工作,千萬不要侷限於本文所舉的幾個例子中。