當下Java後端的SpringBoot微服務框架大火,緣由離不開註解的使用,其簡單易配置的註解方式使得更多的社區爲其編寫適用於SpringBoot的框架,也就是註解逐漸取代了傳統的xml配置方式。那麼註解在Android中也一樣的獲得了昇華,著名的框架有ButterKnife、 Dagger二、Retrofit等等。今天帶來一款Android中比較實用的註解框架AopArms,其用法簡單,裏面編寫了Android開發中經常使用的一套註解,如日誌、攔截(登陸)、異步處理、緩存、SP、延遲操做、定時任務、重試機制、try-catch安全機制、過濾頻繁點擊等,後續還會有更多更強大的註解功能加入。android
AOP(Aspect-Oriented Programming,面向切面編程),可謂是OOP(Object-Oriented Programing,面向對象編程)的補充和完善。AOP技術利用一種稱爲「橫切」的技術,剖解開封裝的對象內部,並將那些影響了多個類的公共行爲封裝到一個可重用模塊,並將其名爲「Aspect」,即方面。所謂「方面」,簡單地說,就是將那些與業務無關,卻爲業務模塊所共同調用的邏輯或責任封裝起來,便於減小系統的重複代碼,下降 模塊間的耦合度,並有利於將來的可操做性和可維護性。Android相關AOP經常使用的方法有 JNI HOOK 和 靜態織入,本文以靜態織入的方式之一AspectJ來進行講解使用。(在編譯期織入,切面直接以字節碼的形式編譯到目標字節碼文件中,這要求使用特殊的 Java 編譯器。)git
apply plugin: 'android-aspectjx'
dependencies {
...
compile 'org.aspectj:aspectjrt:1.8.9'
}
複製代碼
項目跟目錄的gradle腳本中加入github
buildscript {
repositories {
mavenCentral()
}
dependencies {
//此處推薦該庫,由滬江出品,可免去配置各類複雜任務
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
}
}
複製代碼
@Aspect:聲明切面,標記類
@Pointcut(切點表達式):定義切點,標記方法
@Before(切點表達式):前置通知,切點以前執行
@Around(切點表達式):環繞通知,切點先後執行
@After(切點表達式):後置通知,切點以後執行
@AfterReturning(切點表達式):返回通知,切點方法返回結果以後執行
@AfterThrowing(切點表達式):異常通知,切點拋出異常時執行
複製代碼
切點表達式的示例:編程
如:execution(@com.xxx.aop.TimeLog *.(..))
切點表達式的組成:
execution(@註解 訪問權限 返回值的類型 包名.函數名(參數))
複製代碼
下面簡單列舉兩個簡單例子:json
@Before("execution(@com.xxx.aop.activity * *(..))")
public void before(JoinPoint point) {
Log.i(TAG, "method excute before...");
}
匹配activity包下的全部方法,在方法執行前輸出日誌
複製代碼
@Around("execution(@cn.com.xxx.Async * *(..))")
public void doAsyncMethod(ProceedingJoinPoint joinPoint) {
Log.i(TAG, "method excute before...");
joinPoint.proceed();
Log.i(TAG, "method excute after...");
}
匹配帶有Async註解的方法,在方法執行先後分別輸出日誌
複製代碼
@Around("execution(@cn.com.xxx.Async * *(..)) && @annotation(async)")
public void doAsyncMethod(ProceedingJoinPoint joinPoint, Async async) {
Log.i(TAG, "value>>>>>"+async.value());
<!--to do somethings-->
joinPoint.proceed();
<!--to do somethings-->
}
//注意:@annotation(xxx)必須與下面的的參數值對應
匹配帶有Async註解的方法,並獲取註解中的值,去作相應處理。
複製代碼
一、定義註解Cache以下:後端
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Cache {
String key(); //緩存的key
int expiry() default -1; // 過時時間,單位是秒
}
複製代碼
二、編寫Aspect實現緩存
@Aspect
public class CacheAspect {
private static final String POINTCUT_METHOD = "execution(@cn.com.xxx.annotation.Cache * *(..))";
@Pointcut(POINTCUT_METHOD)
public void onCacheMethod() {
}
@Around("onCacheMethod() && @annotation(cache)")
public Object doCacheMethod(ProceedingJoinPoint joinPoint, Cache cache) throws Throwable {
//獲取註解中的key
String key = cache.key();
//獲取註解中的過時時間
int expiry = cache.expiry();
//執行當前註解的方法(放行)
Object result = joinPoint.proceed();
//方法執行後進行緩存(緩存對象必須是方法返回值)
ArmsCache aCache = ArmsCache.get(AopArms.getContext());
if (expiry>0) {
aCache.put(key,(Serializable)result,expiry);
} else {
aCache.put(key,(Serializable)result);
}
return result;
}
}
複製代碼
此處引用ACache該項目中的緩存實現,僅一個Acache文件,我的以爲仍是比較好用的,固然你也能夠本身去實現。 三、測試安全
public static void main(String[] args) {
initData();
getUser();
}
//緩存數據
@Cache(key = "userList")
private ArrayList<User> initData() {
ArrayList<User> list = new ArrayList<>();
for (int i=0; i<5; i++){
User user = new User();
user.setName("艾神一不當心:"+i);
user.setPassword("密碼:"+i);
list.add(user);
}
return list;
}
//獲取緩存
private void getUser() {
ArrayList<User> users = ArmsCache.get(this).getAsList("userList", User.class);
Log.e(TAG, "getUser: "+users);
}
複製代碼
一、定義註解CacheEvict以下:bash
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CacheEvict {
//須要移除的key
String key();
// 緩存的清除是否在方法以前執行, 默認表明緩存清除操做是在方法執行以後執行;若是出現異常緩存就不會清除
boolean beforeInvocation() default false;
//是否清空全部緩存
boolean allEntries() default false;
}
複製代碼
二、編寫Aspect實現app
@Aspect
public class CacheEvictAspect {
private static final String POINTCUT_METHOD = "execution(@cn.com.xxx.annotation.CacheEvict * *(..))";
//切點位置,全部的CacheEvict處
@Pointcut(POINTCUT_METHOD)
public void onCacheEvictMethod() {
}
//環繞處理,並拿到CacheEvict註解值
@Around("onCacheEvictMethod() && @annotation(cacheEvict)")
public Object doCacheEvictMethod(ProceedingJoinPoint joinPoint, CacheEvict cacheEvict) throws Throwable {
String key = cacheEvict.key();
boolean beforeInvocation = cacheEvict.beforeInvocation();
boolean allEntries = cacheEvict.allEntries();
ArmsCache aCache = ArmsCache.get(AopArms.getContext());
Object result = null;
if (allEntries){
//若是是所有清空,則key不須要有值
if (!TextUtils.isEmpty(key))
throw new IllegalArgumentException("Key cannot have value when cleaning all caches");
aCache.clear();
}
if (beforeInvocation){
//方法執行前,移除緩存
aCache.remove(key);
result = joinPoint.proceed();
}else {
//方法執行後,移除緩存,若是出現異常緩存就不會清除(推薦)
result = joinPoint.proceed();
aCache.remove(key);
}
return result;
}
}
複製代碼
三、測試
public static void main(String[] args) {
removeUser();
getUser();
}
//移除緩存數據
@CacheEvict(key = "userList")
private void removeUser() {
Log.e(TAG, "removeUser: >>>>");
}
//獲取緩存
private void getUser() {
ArrayList<User> users = ArmsCache.get(this).getAsList("userList", User.class);
Log.e(TAG, "getUser: "+users);
}
複製代碼
能夠看到執行結果:
一、在主工程中添加依賴
//引入aspectjx插件
apply plugin: 'android-aspectjx'
dependencies {
...
implementation 'cn.com.superLei:aop-arms:1.0.3'
}
複製代碼
二、項目跟目錄的gradle腳本中加入
buildscript {
repositories {
mavenCentral()
}
dependencies {
//該庫基於滬江aspect插件庫
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
}
}
複製代碼
三、在Application中初始化
AopArms.init(this);
複製代碼
一、緩存篇(可緩存任意類型)
一、插入緩存
/**
* key:緩存的鍵
* expiry:緩存過時時間,單位s
* @return 緩存的值
*/
@Cache(key = "userList", expiry = 60 * 60 * 24)
private ArrayList<User> initData() {
ArrayList<User> list = new ArrayList<>();
for (int i=0; i<5; i++){
User user = new User();
user.setName("艾神一不當心:"+i);
user.setPassword("密碼:"+i);
list.add(user);
}
return list;
}
二、獲取緩存
private ArrayList<User> getUser() {
return ArmsCache.get(this).getAsList("userList", User.class);
}
三、移除緩存
/**
* key:緩存的鍵
* beforeInvocation:緩存的清除是否在方法以前執行, 若是出現異常緩存就不會清除 默認false
* allEntries:是否清空全部緩存(與key互斥) 默認false
*/
@CacheEvict(key = "userList", beforeInvocation = true, allEntries = false)
public void removeUser() {
Log.e(TAG, "removeUser: >>>>");
}
複製代碼
二、SharedPreferences篇(可保存對象)
一、保存key到sp
@Prefs(key = "article")
private Article initArticle() {
Article article = new Article();
article.author = "jerry";
article.title = "hello android";
article.createDate = "2019-05-31";
article.content = "this is a test demo";
return article;
}
二、從sp中移除key
/**
* key:sp的鍵
* allEntries:是否清空全部存儲(與key互斥) 默認false
*/
@PrefsEvict(key = "article", allEntries = false)
public void removeArticle() {
Log.e(TAG, "removeArticle: >>>>");
}
三、經過key從sp中獲取value
public void getArticle() {
Article article = ArmsPreference.get(this, "article", null);
Log.e(TAG, "getArticle: "+article);
}
複製代碼
三、異步篇
@Async
public void asyn() {
Log.e(TAG, "useAync: "+Thread.currentThread().getName());
}
複製代碼
四、try-catch安全機制篇
//自動幫你try-catch 容許你定義回調方法
@Safe(callBack = "throwMethod")
public void safe() {
String str = null;
str.toString();
}
//自定義回調方法(注意要和callBack的值保持一致)
private void throwMethod(Throwable throwable){
Log.e(TAG, "throwMethod: >>>>>"+throwable.toString());
}
複製代碼
五、重試機制篇
/**
* @param count 重試次數
* @param delay 每次重試的間隔
* @param asyn 是否異步執行
* @param retryCallback 自定義重試結果回調
* @return 當前方法是否執行成功
*/
@Retry(count = 3, delay = 1000, asyn = true, retryCallback = "retryCallback")
public boolean retry() {
Log.e(TAG, "retryDo: >>>>>>"+Thread.currentThread().getName());
return false;
}
private void retryCallback(boolean result){
Log.e(TAG, "retryCallback: >>>>"+result);
}
複製代碼
六、定時任務篇
/**
* @param interval 初始化延遲
* @param interval 時間間隔
* @param timeUnit 時間單位
* @param count 執行次數
* @param taskExpiredCallback 定時任務到期回調
*/
@Scheduled(interval = 1000L, count = 10, taskExpiredCallback = "taskExpiredCallback")
public void scheduled() {
Log.e(TAG, "scheduled: >>>>");
}
private void taskExpiredCallback(){
Log.e(TAG, "taskExpiredCallback: >>>>");
}
複製代碼
七、延遲任務篇
//開啓延遲任務(10s後執行該方法)
@Delay(key = "test", delay = 10000L)
public void delay() {
Log.e(TAG, "delay: >>>>>");
}
//移除延遲任務
@DelayAway(key = "test")
public void cancelDelay() {
Log.e(TAG, "cancelDelay: >>>>");
}
複製代碼
八、過濾頻繁點擊
//value默認500ms
@SingleClick(value = 2000L)
private void onclick(){
Log.e(TAG, "onclick: >>>>");
}
複製代碼
九、攔截篇(如登陸)
一、在須要進行攔截的方法添加註解
@Intercept("login_intercept")
public void loginIntercept() {
Log.e(TAG, "intercept: 已登錄>>>>");
}
二、(建議,統一處理)在Application中進行進行監聽攔截回調
public class MyApplication extends Application {
private static final String TAG = "MyApplication";
private static MyApplication mApplication;
@Override
public void onCreate() {
super.onCreate();
mApplication = this;
AopArms.init(this);
AopArms.setInterceptor(new Interceptor() {
@Override
public boolean intercept(String key, String methodName) throws Throwable {
Log.e(TAG, "intercept methodName:>>>>>"+methodName);
if ("login_intercept".equals(key)){
String userId = ArmsPreference.get(mApplication, "userId", "");
if (TextUtils.isEmpty(userId)){
Toast.makeText(mApplication, "您尚未登陸", Toast.LENGTH_SHORT).show();
return true;//表明攔截
}
}
return false;//放行
}
});
}
}
複製代碼
十、動態受權篇
一、開啓請求權限
/**
* @param value 權限值
* @param rationale 拒絕後的下一次提示(開啓後,拒絕後,下一次會先提示該權限申請提示語)
* @param requestCode 權限請求碼標識
*/
@Permission(value = {Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, rationale = "爲了更好的體驗,請打開相關權限")
public void permission(View view) {
Log.e(TAG, "permission: 權限已打開");
}
二、請求拒絕註解回調
@PermissionDenied
public void permissionDenied(int requestCode, List<String> denyList){
Log.e(TAG, "permissionDenied: "+requestCode);
Log.e(TAG, "permissionDenied>>>: "+denyList.toString());
}
三、請求拒絕且不在提示註解回調
@PermissionNoAskDenied
public void permissionNoAskDenied(int requestCode, List<String> denyNoAskList){
Log.e(TAG, "permissionNoAskDenied: "+requestCode);
Log.e(TAG, "permissionNoAskDenied>>>: "+denyNoAskList.toString());
//前往設置頁面打開權限
AopPermissionUtils.showGoSetting(this, "爲了更好的體驗,建議前往設置頁面打開權限");
}
複製代碼
十一、測試方法耗時篇
@TimeLog
public void getUser(View view) {
ArrayList<User> users = ArmsCache.get(this).getAsList("userList", User.class);
Log.e(TAG, "getUser: " + users);
}
複製代碼
以上是庫的一些經常使用的基本用法,後續會添加更多的註解來簡化Android開發,歡迎前來issues來提問或者提出你認爲所須要的更多註解需求。
GitHub地址:AopArms