WHY
如果說OOP(面向對象的程序設計)的主要思想是將問題歸分到相應的對象(類)中去實現,再把相關類模塊化使得模塊內聚、模塊間低耦。正是因為高度的模塊化使得每個模塊處理類似需求的時候,很容易造成冗余,分散的情況
那么AOP面向切面編程(Aspect-Oriented Programming)就是把涉及到眾多模塊的某一類問題進行統一管理,因為AOP是方法論,你可以用任何的方式按照這個思想去實現。
那么問題來了:怎么樣不需要顯示的去修改原來的業務邏輯,最少的改動插入代碼,去優雅的實現AOP切面。
WHO
AOP從實現原理上可以分為運行時AOP和編譯時AOP,對于Android來講運行時AOP的實現主要是hook某些關鍵方法,編譯時AOP主要是在Apk打包過程中對java,class,dex文件進行掃描更改。
Android主流的aop 框架和工具類庫:
- AspectJ: 一個 JavaTM 語言的面向切面編程的無縫擴展
- Javassist for Android: 用于字節碼操作的知名 java 類庫 Javassist 的 Android 平臺移植版。
- DexMaker: Dalvik 虛擬機上,在編譯期或者運行時生成代碼的 Java API。
- ASMDEX: 一個類似 ASM 的字節碼操作庫,運行在Android平臺,操作Dex字節碼。
- APT: 通過Android-apt的Gradle插件(官方),以及jre原生代碼在編譯器生成Java文件
本文主要探究一下通過預編譯方式和運行期動態代理在Android中的AOP實現方式,包括***APT,AspectJ ***
其實APT,AspectJ,Javassist正好對應的是Android不通編譯期,進行代碼或者字節碼操作的,因為實現的方式,解決的問題也有不同
其實通俗來說,
APT就是單獨用一個Java的module來生成app內的java代碼
AspectJ就是提供了很多橫切關注點來在編譯class文件的時候動態來修改你的java的代碼
HOW
APT
代表框架:DataBinding,Dagger2, ButterKnife, EventBus3 、AndroidAnnotation等
注解處理器 Java5 中叫APT(Annotation Processing Tool),在Java6開始,規范化為 Pluggable Annotation Processing。Apt應該是這其中我們最常見到的了,難度也最低。定義編譯期的注解,再通過繼承Proccesor實現代碼生成邏輯,實現了編譯期生成代碼的邏輯。
Talk is cheap. Show me your code .
下面我來用APT實現一個簡單的全局路由來演示一下APT的工作原理
-
1.配置
使用官方gradle插件(如果之前有使用框架用到了android-apt插件的最好去掉,原作者不再維護了,而且兩個插件兼容會有問題)
主app的module的gradle配置,工程依賴apt入口
dependencies {
annotationProcessor project(':apt')
compile project(':apt-lib')
}
創建一個apt-lib的module用來提供路由需要的注解
gradle配置
apply plugin: 'java'
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
// 解決build警告:編碼GBK的不可映射字符
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
全局路由注解類
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface Rounter {
String value();
}
路由參數注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Extra {
String value();
}
創建一個apt的module用來負責編譯期生成java代碼
gradle配置
apply plugin: 'java'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
dependencies {
compile 'com.google.auto.service:auto-service:1.0-rc2'
compile 'com.squareup:javapoet:1.7.0'
compile project(':lib')
}
// 解決build警告:編碼GBK的不可映射字符
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
com.google.auto.service:auto-service:1.0-rc2
谷歌提供的Java 生成源代碼庫
com.squareup:javapoet:1.7.0
提供了各種 API 讓你用各種姿勢去生成 Java 代碼文件
- 2.AnnotationProcessor
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)//java版本支持
@SupportedAnnotationTypes({//標注注解處理器支持的注解類型
"com.app.annotation.apt.Rounter"})
public class AnnotationProcessor extends AbstractProcessor {
public Filer mFiler; //文件相關的輔助類
public Elements mElements; //元素相關的輔助類
public Messager mMessager; //日志相關的輔助類
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
mFiler = processingEnv.getFiler();
mElements = processingEnv.getElementUtils();
mMessager = processingEnv.getMessager();
new RouterProcessor().process(roundEnv, this);
return true;
}
}
@AutoService(Processor.class)
指定了AnnotationProcessor
類為Processor
的入口process方法相當于java的main()方法
動態生成路由類TRouter
的RouterProcessor
類
public class RouterProcessor implements IProcessor {
@Override
public void process(RoundEnvironment roundEnv, AnnotationProcessor mAbstractProcessor) {
String CLASS_NAME = "TRouter";
TypeSpec.Builder tb = classBuilder(CLASS_NAME).addModifiers(PUBLIC, FINAL).addJavadoc("@ 全局路由器 此類由apt自動生成");
FieldSpec extraField = FieldSpec.builder(ParameterizedTypeName.get(DataMap.class), "mCurActivityExtra")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.build();
tb.addField(extraField);
MethodSpec.Builder methodBuilder1 = MethodSpec.methodBuilder("go")
.addJavadoc("@此方法由apt自動生成")
.addModifiers(PUBLIC, STATIC)
.addParameter(String.class, "name").addParameter(DataMap.class, "extra")
.addParameter(ClassName.get("android.view", "View"), "view");
MethodSpec.Builder methodBuilder2 = MethodSpec.methodBuilder("bind")
.addJavadoc("@此方法由apt自動生成")
.addModifiers(PUBLIC, STATIC)
.addParameter(ClassName.get("android.app", "Activity"), "mContext");
List<ClassName> mList = new ArrayList<>();
CodeBlock.Builder blockBuilderGo = CodeBlock.builder();
CodeBlock.Builder blockBuilderBind = CodeBlock.builder();
ClassName appClassName = ClassName.get("com.wingjay.jianshi.global", "ActivityStackManager");
blockBuilderGo.addStatement("mCurActivityExtra=extra");
blockBuilderGo.addStatement("Activity mContext=$T.getInstance().getTopActivity()", appClassName);
blockBuilderGo.beginControlFlow(" switch (name)");//括號開始
blockBuilderBind.add(" if (mCurActivityExtra == null) return;\n");
blockBuilderBind.beginControlFlow(" switch (mContext.getClass().getSimpleName())");//括號開始
List<RouterActivityModel> mRouterActivityModels = new ArrayList<>();
try {
for (TypeElement element : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(Rounter.class))) {
ClassName currentType = ClassName.get(element);
if (mList.contains(currentType)) continue;
mList.add(currentType);
RouterActivityModel mRouterActivityModel = new RouterActivityModel();
mRouterActivityModel.setElement(element);
mRouterActivityModel.setActionName(element.getAnnotation(Rounter.class).value());
List<Element> mExtraElements = new ArrayList<>();
List<String> mExtraElementKeys = new ArrayList<>();
for (Element childElement : element.getEnclosedElements()) {
SceneTransition mSceneTransitionAnnotation = childElement.getAnnotation(SceneTransition.class);
if (mSceneTransitionAnnotation != null) {
mRouterActivityModel.setSceneTransitionElementName(mSceneTransitionAnnotation.value());
mRouterActivityModel.setSceneTransitionElement(childElement);
}
Extra mExtraAnnotation = childElement.getAnnotation(Extra.class);
if (mExtraAnnotation != null) {
mExtraElementKeys.add(mExtraAnnotation.value());
mExtraElements.add(childElement);
}
}
mRouterActivityModel.setExtraElementKeys(mExtraElementKeys);
mRouterActivityModel.setExtraElements(mExtraElements);
boolean isNeedBind = (mExtraElementKeys != null && mExtraElementKeys.size() > 0
|| mRouterActivityModel.getSceneTransitionElement() != null);
mRouterActivityModel.setNeedBind(isNeedBind);
mRouterActivityModels.add(mRouterActivityModel);
}
ClassName mActivityCompatName = ClassName.get("android.support.v4.app", "ActivityCompat");
ClassName mIntentClassName = ClassName.get("android.content", "Intent");
ClassName mActivityOptionsCompatName = ClassName.get("android.support.v4.app", "ActivityOptionsCompat");
for (RouterActivityModel item : mRouterActivityModels) {
blockBuilderGo.add("case $S: \n", item.getActionName());//1
if (item.isNeedBind())
blockBuilderBind.add("case $S: \n", item.getElement().getSimpleName());//1
if (item.getExtraElements() != null && item.getExtraElements().size() > 0) {
for (int i = 0; i < item.getExtraElements().size(); i++) {
Element mFiled = item.getExtraElements().get(i);
blockBuilderBind.add("(($T)mContext)." +//1
"$L" +//2
"= ($T) " +//3
"mCurActivityExtra.get(" +//4
"$S);\n",//5
item.getElement(),//1
mFiled,//2
mFiled,//3
item.getExtraElementKeys().get(i)//5
);//5
}
}
if (item.getSceneTransitionElement() != null) {
blockBuilderGo.add("$L.startActivity(mContext," +//2
"\nnew $L(mContext," +//3
"\n$L.class)," +//4
"\n$T.makeSceneTransitionAnimation(" +//5
"\nmContext,view," +//6
"\n$S).toBundle());", //7
mActivityCompatName,//2
mIntentClassName,//3
item.getElement(),//4
mActivityOptionsCompatName,//5
item.getSceneTransitionElementName());//6
blockBuilderBind.add(
"$T.setTransitionName(" +//2
"(($T)mContext).mViewBinding." +//3
"$L, " +//4
"$S);\n",//5
ClassName.get("android.support.v4.view", "ViewCompat"),//2
item.getElement(),//3
item.getSceneTransitionElement(),//4
item.getSceneTransitionElementName());//5
} else {
blockBuilderGo.add("mContext.startActivity(" +//2
"\nnew $L(mContext," +//3
"\n$L.class));", //7
mIntentClassName,//3
item.getElement()//4
);
}
blockBuilderGo.addStatement("\nbreak");//1
if (item.isNeedBind()) blockBuilderBind.addStatement("break");//1
}
blockBuilderGo.addStatement("default: break");
blockBuilderGo.endControlFlow();
methodBuilder1.addCode(blockBuilderGo.build());
blockBuilderBind.addStatement("default: break");
blockBuilderBind.endControlFlow();
methodBuilder2.addCode(blockBuilderBind.build());
tb.addMethod(methodBuilder1.build());
tb.addMethod(methodBuilder2.build());
//增加go(action)和go(action,extra):兩個重載方法
tb.addMethod(MethodSpec.methodBuilder("go")
.addJavadoc("@此方法由apt自動生成")
.addModifiers(PUBLIC, STATIC)
.addParameter(String.class, "name")
.addParameter(DataMap.class, "extra")
.addCode("go(name,extra,null);\n").build());
tb.addMethod(MethodSpec.methodBuilder("go")
.addJavadoc("@此方法由apt自動生成")
.addModifiers(PUBLIC, STATIC)
.addParameter(String.class, "name")
.addCode("go(name,null,null);\n").build());
tb.addMethod(MethodSpec.methodBuilder("getMap")
.returns(DataMap.class)
.addJavadoc("@此方法由apt自動生成")
.addModifiers(PUBLIC, STATIC)
.addCode("return new DataMap();\n").build());
JavaFile javaFile = JavaFile.builder(AptConstants.PackageName, tb.build()).build();// 生成源代碼
javaFile.writeTo(mAbstractProcessor.mFiler);// 在 app module/build/generated/source/apt 生成一份源代碼
} catch (FilerException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
看結尾javaFile.writeTo(mAbstractProcessor.mFiler)
,
OK這個類就是使用了com.squareup:javapoet提供的一些很方便寫.java文件的類和方法來生成最終的TRounter.java
-
3.使用路由
下面就可以在原來的項目中使用路由了
@Rounter(RounterConstants.MAIN)
public class MainActivity extends BaseActivity {
...
}
@Rounter(RounterConstants.EDIT)
public class EditActivity extends BaseActivity {
@Extra(DataMap.DATA_HEAD)
public String diaryUUID;
...
}
路由跳轉
TRouter.go(RounterConstants.MAIN);
TRouter.go(RounterConstants.EDIT, TRouter.getMap().addHead(diary.getUuid()));
那么生成的這個TRounter在哪呢,可以在\build\generated\source\apt\debug中相應的包名下查看apt生成的代碼,如下
/**
* @ 全局路由器 此類由apt自動生成 */
public final class TRouter {
public static DataMap mCurActivityExtra;
/**
* @此方法由apt自動生成 */
public static void go(String name, DataMap extra, View view) {
mCurActivityExtra=extra;
Activity mContext=ActivityStackManager.getInstance().getTopActivity();
switch (name) {
case "rounter_diary":
mContext.startActivity(
new android.content.Intent(mContext,
com.wingjay.jianshi.ui.DiaryListActivity.class));
break;
case "rounter_edit":
mContext.startActivity(
new android.content.Intent(mContext,
com.wingjay.jianshi.ui.EditActivity.class));
break;
case "rounter_main":
mContext.startActivity(
new android.content.Intent(mContext,
com.wingjay.jianshi.ui.MainActivity.class));
break;
case "rounter_setting":
mContext.startActivity(
new android.content.Intent(mContext,
com.wingjay.jianshi.ui.SettingActivity.class));
break;
case "rounter_signup":
mContext.startActivity(
new android.content.Intent(mContext,
com.wingjay.jianshi.ui.SignupActivity.class));
break;
case "rounter_view":
mContext.startActivity(
new android.content.Intent(mContext,
com.wingjay.jianshi.ui.ViewActivity.class));
break;
default: break;
}
}
/**
* @此方法由apt自動生成 */
public static void bind(Activity mContext) {
if (mCurActivityExtra == null) return;
switch (mContext.getClass().getSimpleName()) {
case "EditActivity":
((EditActivity)mContext).diaryUUID= (String) mCurActivityExtra.get("data_head");
break;
case "ViewActivity":
((ViewActivity)mContext).diaryUuid= (String) mCurActivityExtra.get("data_head");
break;
default: break;
}
}
/**
* @此方法由apt自動生成 */
public static void go(String name, DataMap extra) {
go(name,extra,null);
}
/**
* @此方法由apt自動生成 */
public static void go(String name) {
go(name,null,null);
}
/**
* @此方法由apt自動生成 */
public static DataMap getMap() {
return new DataMap();
}
}
好了如上就簡單可以用APT實現一個簡單的全局路由了,如果不是開發框架的團隊的話,我認為APT在搭建團隊工程的框架方面還是有很大的利用場景的,比方說綁定model和View和present,比方說生成一些約定的接口或者方法
End of APT
AspectJ
代表框架:我就只知道JakeWharton/hugo了 = =||
如果說APT還很難體現AOP的概念的話,那么AspectJ就是先行者特別為AOP開發的一套語言支持了。它是一種幾乎和Java完全一樣的語言,或者說AspectJ應該就是一種擴展Java。當然,除了使用AspectJ特殊的語言外,AspectJ還支持原生的Java,只要加上對應的AspectJ注解就好。
AspectJ現在托管于Eclipse項目中
|【這是官方網站】
|【這是參考文檔】
|【這是接口文檔】
主要術語
- Cross-cutting concerns(橫切關注點): 盡管面向對象模型中大多數類會實現單一特定的功能,但通常也會開放一些通用的附屬功能給其他類。例如,我們希望在數據訪問層中的類中添加日志,同時也希望當UI層中一個線程進入或者退出調用一個方法時添加日志。盡管每個類都有一個區別于其他類的主要功能,但在代碼里,仍然經常需要添加一些相同的附屬功能。
- Advice(通知): 注入到class文件中的代碼。典型的 Advice 類型有 before、after 和 around,分別表示在目標方法執行之前、執行后和完全替代目標方法執行的代碼。 除了在方法中注入代碼,也可能會對代碼做其他修改,比如在一個class中增加字段或者接口。
- Joint point(連接點): 程序中可能作為代碼注入目標的特定的點,例如一個方法調用或者方法入口。
- Pointcut(切入點): 告訴代碼注入工具,在何處注入一段特定代碼的表達式。例如,在哪些 joint points 應用一個特定的 Advice。切入點可以選擇唯一一個,比如執行某一個方法,也可以有多個選擇,比如,標記了一個定義成@DebguTrace 的自定義注解的所有方法。
- Aspect(切面): Pointcut 和 Advice 的組合看做切面。例如,我們在應用中通過定義一個 pointcut 和給定恰當的advice,添加一個日志切面。
-
Weaving(織入): 注入代碼(advices)到目標位置(joint points)的過程。
整體原理就如下圖:
30689-55846998f4f5b4ce.png - 其實我的理解就是AspectJ提供了各種各樣的切入點(關注點/代碼插入點),在生成class文件的時候可以動態替換你原來的代碼或者插入你想要的代碼
舉幾個栗子
- 性能監控切片
- 用戶登錄切片
- 防止頻繁點擊
- 用戶行為路徑
環境配置
項目的Gradle
dependencies {
classpath 'com.android.tools.build:gradle:0.12.+'
classpath 'org.aspectj:aspectjtools:1.8.1'
}
app的Gradle
import com.app.plugin.AspectjPlugin
dependencies {
compile 'org.aspectj:aspectjrt:1.8.9'
}
性能監控切片
性能監控的注解
@Retention(RetentionPolicy.CLASS)//這個注解周期聲明在 class 文件上
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})//可以注解構造函數和方法
public @interface MonitorLog {
}
切片實現類,如果看不懂的可以去上面看下語法
@Aspect
public class MonitorAspect {
@Pointcut("execution(@com.zzj.jianshi.aspect.annotation.MonitorLog * *(..))")//方法切入點
public void methodAnnotated() {
}
@Pointcut("execution(@com.zzj.jianshi.aspect.annotation.MonitorLog *.new(..))")//構造器切入點
public void constructorAnnotated() {
}
@Around("methodAnnotated() || constructorAnnotated()")//在連接點進行方法替換
public Object aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
if (!isMonitorOpened()) {//后臺下發監控開關
return joinPoint.proceed();
}
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className = methodSignature.getDeclaringType().getSimpleName();
String methodName = methodSignature.getName();
long startTime = System.nanoTime();
Object result = joinPoint.proceed();//執行原方法
StringBuilder keyBuilder = new StringBuilder();
keyBuilder.append(methodName + ":");
for (Object obj : joinPoint.getArgs()) {
if (obj instanceof String) keyBuilder.append((String) obj);
else if (obj instanceof Class) keyBuilder.append(((Class) obj).getSimpleName());
}
String key = keyBuilder.toString();
Timber.e("MonitorAspect --->: " + className + "." + key + joinPoint.getArgs().toString() +
" --->:" + "[" + (TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) + "ms]");// 記錄時間差
return result;
}
}
業務代碼中,在耗時操作或者關鍵方法中添加注解,如下:
@Override
@MonitorLog
protected void onStart() {
可以看下日志輸出
06-08 15:49:02.457 729-729/? E/MonitorAspect: MonitorAspect --->: JianShiApplication.initLog:[Ljava.lang.Object;@c567935 --->:[2ms]
06-08 15:49:02.465 729-729/? E/MonitorAspect: MonitorAspect --->: JianShiApplication.onCreate:[Ljava.lang.Object;@3a1d85d --->:[53ms]
06-08 15:49:03.684 729-729/com.wingjay.android.jianshi E/MonitorAspect: MonitorAspect --->: MainActivity.onCreate:[Ljava.lang.Object;@64c8c14 --->:[369ms]
06-08 15:49:03.816 729-729/com.wingjay.android.jianshi E/MonitorAspect: MonitorAspect --->: MainActivity.onStart:[Ljava.lang.Object;@970c60a --->:[130ms]
06-08 15:50:20.288 729-729/com.wingjay.android.jianshi E/MonitorAspect: MonitorAspect --->: MainActivity.onStart:[Ljava.lang.Object;@efb02be --->:[2ms]
用戶登錄切片
@Aspect
public class CheckLoginAspect {
@Pointcut("execution(@com.app.annotation.aspect.CheckLogin * *(..))")//方法切入點
public void methodAnnotated() {
}
@Around("methodAnnotated()")//在連接點進行方法替換
public void aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
if (null == SpUtil.getUser()) {
Snackbar.make(App.getAppContext().getCurActivity().getWindow().getDecorView(), "請先登錄!", Snackbar.LENGTH_LONG)
.setAction("登錄", new View.OnClickListener() {
@Override
public void onClick(View view) {
TRouter.go(C.LOGIN);//跳轉到登錄頁面
}
}).show();
return;
}
joinPoint.proceed();//執行原方法
}
}
防止頻繁點擊
@Aspect
public class SingleClickAspect {
static int TIME_TAG = R.id.click_time;
public static final int MIN_CLICK_DELAY_TIME = 600;
@Pointcut("execution(@com.app.annotation.aspect.SingleClick * *(..))")//方法切入點
public void methodAnnotated() {
}
@Around("methodAnnotated()")//在連接點進行方法替換
public void aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
View view = null;
for (Object arg : joinPoint.getArgs())
if (arg instanceof View) view = (View) arg;
if (view != null) {
Object tag = view.getTag(TIME_TAG);
long lastClickTime = ((tag != null) ? (long) tag : 0);
LogUtils.showLog("SingleClickAspect", "lastClickTime:" + lastClickTime);
long currentTime = Calendar.getInstance().getTimeInMillis();
if (currentTime - lastClickTime > MIN_CLICK_DELAY_TIME) {//過濾掉600毫秒內的連續點擊
view.setTag(TIME_TAG, currentTime);
LogUtils.showLog("SingleClickAspect", "currentTime:" + currentTime);
joinPoint.proceed();//執行原方法
}
}
}
}
OK,用戶行為路徑什么的我就沒有實現了,是用來裝逼的
那么問題來了,說好的AspectJ執行之后修改的class代碼去哪里了?
我們來看\build\intermediates\classes下面對應的class的原來代碼變了么?
源代碼
@Override
protected void onPause() {
super.onPause();
Timber.e("befor testAspect() ");
testAspect();
Timber.e("after testAspect() ");
}
@MonitorLog
public void testAspect() {
Timber.e("testAspect() inn start");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Timber.e("testAspect() inn end");
}
\build\intermediates\classes下面
protected void onPause() {
super.onPause();
Timber.e("testAspect() befor", new Object[0]);
this.testAspect();
Timber.e("testAspect() after", new Object[0]);
}
@MonitorLog
public void testAspect() {
JoinPoint var2 = Factory.makeJP(ajc$tjp_2, this, this);
MonitorAspect var10000 = MonitorAspect.aspectOf();
Object[] var3 = new Object[]{this, var2};
var10000.aroundJoinPoint((new MainActivity$AjcClosure5(var3)).linkClosureAndJoinPoint(69648));
}
為什么要加日志?沒錯我就是想看一下AspectJ切片對原邏輯性能是否有影響,我相信這個對于加入項目的選型可能是致命的,下面來看我實際測試的日志
06-08 16:23:33.895 31713-31713/ E/MainActivity: befor testAspect()
06-08 16:23:33.895 31713-31713/ E/MonitorAspect: MonitorAspect start
06-08 16:23:33.895 31713-31713/ E/MainActivity: testAspect() inn start
06-08 16:23:33.997 31713-31713/ E/MainActivity: testAspect() inn end
06-08 16:23:33.997 31713-31713/ E/MonitorAspect: MonitorAspect --->: MainActivity.testAspect:[Ljava.lang.Object;@7f6e61b --->:[101ms]
06-08 16:23:33.998 31713-31713/ E/MonitorAspect: MonitorAspect end
06-08 16:23:33.998 31713-31713/ E/MainActivity: after testAspect()
這就說明在編譯器做的代理性能還是非常高的
可以說AspectJ是面向切面編程最好的切入點了