面向切面编程在android中的应用
Aspect Oriented Programming in android project
- Aspect Oriented Programming in android project
背景
在我们实际的Android项目开发中经常碰到的一个问题就是Token过期的处理问题。
客户端的很多接口都要依赖Token,对Token过期处理不当就必定会为项目埋下很多坑,所以这就促使我们必需要找找到一个有效可行的方式来处理Token过期问题。
但是在使用新方式来处理Token过期问题时我们又想在不改动一丁点儿(或者改动尽可能小)项目已有源代码的情况下(以免对项目做大换血导致意外bug的滋生)来植入我们对Token的处理(或是其他)逻辑代码。那这应该怎么办呢?有没有办法呢?
————办法还是有的
下面我们就来介绍下在实际项目中是怎么和如何以及使用什么方法来有效的对Token过期进行处理的。
在进入Iseema实际项目的介绍前,我们有必要先来简单认识下AOP(Aspect Oriented Programming)和AspectJ是什么以及在如何在一个Android项目中使用AOP。
什么是AOP?(来自网络,一搜一大把)
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
从上面的定义可以看出,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低。这正好符合我们在不改动现有代码以低耦合的方式向原有代码中织入代码的目的。
AOP中的一些概念 (来自网络,一搜一大把)
1、横切关注点
对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点
2、切面(aspect)
类是对物体特征的抽象,切面就是对横切关注点的抽象
3、连接点(joinpoint)
被拦截到的点,指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
4、切入点(pointcut)
对连接点进行拦截的定义
5、通知(advice)
所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类
6、目标对象
代理的目标对象
7、织入(weave)
将切面应用到目标对象并导致代理对象创建的过程
8、引入(introduction)
在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
什么是AspectJ?
简单来说就是AOP编程范式在JVM上的一种具体实现。
想要更多了解?
http://www.eclipse.org/aspectj/doc/released/progguide/starting-aspectj.html这个逼装的好,而且还加了些细节在里面
如何在Android中使用AspectJ?
如何在Android项目中使用呢?根据各种搜索和研究发现最终发现,我们只需要按照以下几个步骤(主要参考了 Jake Wharton的Hugo Library开源项目的实现)就可以很轻松的将AspectJ集成到我们的Android项目中。这里主使用AspectJ版本是1.8.13,其他的版本应该也遵循同样的步骤。
项目根目录下的build.gradle
文件中配置aspectjtools
在下面的dependencies block中加入一句classpath 'org.aspectj:aspectjtools:1.8.13'
即可添加对aspectjtools的依赖。aspectjtools是在Android上使用AspectJ的插件,主要用于编辑字节码在字节码中植入通知
buildscript {
repositories {
jcenter()
google()
}
dependencies {
......
// 引入aspectjtools
classpath 'org.aspectj:aspectjtools:1.8.13'
......
}
}
allprojects {
repositories {
jcenter()
google()
}
app model下面的build.gradle
文件中配置aspectjrt
1)、在下面的dependencies block中加入一句:compile 'org.aspectj:aspectjrt:1.8.13'
添加对aspectjrt依赖的依赖,aspectjrt是AspectJ的运行时环境,所必须要引入。
2)、在西面的android block和dependencies block之间添加如下代码:
android.applicationVariants.all { variant ->
JavaCompile javaCompile = variant.javaCompile
javaCompile << {
String[] args = ["-showWeaveInfo",
"-1.8",
"-XnoInline",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-log", "weave.log",
"-bootclasspath", android.bootClasspath.join(File.pathSeparator)
]
MessageHandler handler = new MessageHandler(true)
new Main().run(args, handler)
def log = project.logger
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break
case IMessage.WARNING:
case IMessage.INFO:
log.info message.message, message.thrown
break
case IMessage.DEBUG:
log.debug message.message, message.thrown
break
}
}
}
app model下的build.gradle
与AspecJ配置相关的完整配置
注意下面倒入的包,别倒错了
import groovy.xml.XmlUtil
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
apply plugin: 'com.android.application'
android {
......
......
}
// 对编译后的字节码做处理,添加面向切面(AOP)的编程支持
android.applicationVariants.all { variant ->
JavaCompile javaCompile = variant.javaCompile
javaCompile << {
String[] args = ["-showWeaveInfo",
"-1.8",
"-XnoInline",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-log", "weave.log",
"-bootclasspath", android.bootClasspath.join(File.pathSeparator)
]
MessageHandler handler = new MessageHandler(true)
new Main().run(args, handler)
def log = project.logger
for (IMessage message : handler.getMessages(null, true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break
case IMessage.WARNING:
case IMessage.INFO:
log.info message.message, message.thrown
break
case IMessage.DEBUG:
log.debug message.message, message.thrown
break
}
}
}
dependencies {
......
// 添加aspectjrt的依赖
compile 'org.aspectj:aspectjrt:1.8.13'
......
}
到此,一个Android项目已经集成了AspectJ。
在进入Iseema实际项目前,让我们先来看一个更简单的具体使用AspectJ的例子,以便验证我们是否成功集成了以及简单了解下AspectJ的使用。
举个栗子
假设我们目前有一个应用App类,需要调用业务类OrderManager
完成一些业务,OrderManager
包含了与订操作单相关API,这些API都要求用户登陆后才有权限访问。OrderManager
类如下:
/**
* @author AveryZhong.
*/
public class OrderManger {
private static final String TAG = OrderManger.class.getSimpleName();
// 创建订单
public String createOrder(String productName) {
return "Create " + productName + " order success";
}
// 查询订单
public String queryOrder(String productName) {
return "Query " + productName + " order success";
}
// 删除订单
public String deleteOrder(String productName) {
return "Delete " + productName + " order success";
}
}
在没有使用AspectJ的情况下App
类画风通常是这样的:
/**
* @author AveryZhong.
*/
public class App {
private static final String TAG = App.class.getSimpleName();
// 注入OrderManger
@Inject
private OrderManger mOrderManger;
// 处理订单的创建
public void processOrderCreation() {
if (!isLogined()) { // 用户未登陆,则跳转到登陆页面
jumpToLoginPage();
} else { // 用户已登陆,则可进行订单的创建操作
String result = mOrderManger.createOrder("productName");
Log.d(TAG, "processOrderCreation: " + result);
}
}
// 处理订单的删除
public void processOrderDeletion() {
if (!isLogined()) { // 用户未登陆,则跳转到登陆页面
jumpToLoginPage();
} else { // 用户已登陆,则可进行订单的删除操作
String result = mOrderManger.deleteOrder("productName");
Log.d(TAG, "processOrderDeletion: " + result);
}
}
// 查询订单
public void queryOrder() {
if (!isLogined()) { // 用户未登陆,则跳转到登陆页面
jumpToLoginPage();
} else { // 用户已登陆,则可进行订单的查询操作
String result = mOrderManger.queryOrder("productName");
Log.d(TAG, "queryOrder: " + result);
}
}
// 此处省略好多方法......
}
观察上面的App
类会发现,在调用每个OrderManager
类方法的时候都需要做用户是否登陆的判断,因为这些操作必须在用户登陆后才有权限操作。这样的判断充斥在整个App
类中,是登陆业务仅仅的耦合在了订单业务中。
在使用AspectJ的情况下App
类画风通常是这样的:
/**
* @author AveryZhong.
*/
public class App {
private static final String TAG = App.class.getSimpleName();
// 注入OrderManger
@Inject
private OrderManger mOrderManger;
// 处理订单的创建
public void processOrderCreation() {
String result = mOrderManger.createOrder("productName");
Log.d(TAG, "processOrderCreation: " + result);
}
// 处理订单的删除
public void processOrderDeletion() {
String result = mOrderManger.deleteOrder("productName");
Log.d(TAG, "processOrderDeletion: " + result);
}
// 查询订单
public void queryOrder() {
String result = mOrderManger.queryOrder("productName");
Log.d(TAG, "queryOrder: " + result);
}
// 此处省略好多方法......
}
观察上面的App
类会发现,整个类干净很多了,现在只一心一意的处理自己本职工作业务,再也不用去处理不属于自己本职的任务(判断登陆状态,跳转登陆界面)了。那这是怎么做到的呢?下面就轮到我们的AspectJ粉墨登场了。
此时《Can You Feel It》音乐的高潮部分开始响起♪♬♪♬♪♬......
登陆状态校验切面LoginVerificationAspect
类如下:
/**
* @author AveryZhong.
*/
@Aspect
public class LoginVerificationAspect {
private static final String TAG = LoginVerificationAspect.class.getSimpleName();
// 定义切点,选择OrderManger类中返回值为任何类型,参数为任何类型的所有方法
private static final String METHOD_POINTCUT
= "execution(* com.avery.zhong.aspectjdemo.OrderManger.*(..))";
@Around(METHOD_POINTCUT)
public Object longiVerify(ProceedingJoinPoint joinPoint) throws Throwable {
final Object[] args = joinPoint.getArgs();
if (isLogined()) { // 用户已登陆,则直接调用目标方法
return joinPoint.proceed(args);
} else { // 用户未登陆,则跳转到登陆界面并阻止目标方法的执行
jumpToLoginPage();
return null;
}
}
// 此处省略好多方法......
}
上面的切面拦截OrderManger类中每个方法的调用,在OrderManger类的方法被调用时检测登陆状态:如果用户以登陆则直接调用目标方法(即OrderManger此刻被拦截的方法),上面代码中的joinPoint.proceed(args)
语句就表示调用目前方法;如果用户还未登陆则跳转到登陆界面并阻止目标方法的执行。
至此,应该能够大概了解怎么通过使用AspectJ来对横切多个模块的公共逻辑提取到一个单独的类(切面)中来对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
Android项目中使用AspectJ步骤总结
1.在项目根目录下的
build.gradle
文件dependencies block中加入一句classpath 'org.aspectj:aspectjtools:1.8.13'
的依赖;
2.在app model下面的build.gradle
文件中的dependencies block中加入一句:compile 'org.aspectj:aspectjrt:1.8.13'
添加对aspectjrt依赖的依赖;
3.在app model下面的build.gradle
文件中的android block和dependencies block之间添加如下代码:
android.applicationVariants.all { variant -> JavaCompile javaCompile = variant.javaCompile javaCompile << { String[] args = ["-showWeaveInfo", "-1.8", "-XnoInline", "-inpath", javaCompile.destinationDir.toString(), "-aspectpath", javaCompile.classpath.asPath, "-d", javaCompile.destinationDir.toString(), "-classpath", javaCompile.classpath.asPath, "-log", "weave.log", "-bootclasspath", android.bootClasspath.join(File.pathSeparator) ] MessageHandler handler = new MessageHandler(true) new Main().run(args, handler) def log = project.logger for (IMessage message : handler.getMessages(null, true)) { switch (message.getKind()) { case IMessage.ABORT: case IMessage.ERROR: case IMessage.FAIL: log.error message.message, message.thrown break case IMessage.WARNING: case IMessage.INFO: log.info message.message, message.thrown break case IMessage.DEBUG: log.debug message.message, message.thrown break } } }
4.编写切面类将横切多个模块的公共逻辑提取到切面类中进行统一和集中处理。
下面再来简单看看Iseema项目中使用AspectJ来处理了什么问题。
实际项目使用AspectJ来有效的处理Token过期问题
下面的CloudApi中的一些接口是依赖Token的
public class CloudApi extends ICDApi {
@Override
public void userTokenRefresh(String requestId, Context context, ICallback<UserToken> callback) {
super.userTokenRefresh(requestId, context, callback);
}
@Override
public void productPurchase(String requestId, Context context, ICallback<Purchase> callback, String contentId, String productId, PurchaseAction action, int fee) {
super.productPurchase(requestId, context, callback, contentId, productId, action, fee);
}
@Override
public void contentAuthentication(String requestId, Context context, ICallback<Authentication> callback, String contentId) {
super.contentAuthentication(requestId, context, callback, contentId);
}
@Override
public void contentAuthentication(String requestId, Context context, ICallback<Authentications> callback, List<String> contentList) {
super.contentAuthentication(requestId, context, callback, contentList);
}
@Override
public void getProductPurchaseList(String requestId, Context context, ICallback<ProductOrders> callback, int pageSize, int begin) {
super.getProductPurchaseList(requestId, context, callback, pageSize, begin);
}
@Override
public void getProductPurchasedList(String requestId, Context context, ICallback<Purchased> callback, PurchasedListReq purchasedListReq) {
super.getProductPurchasedList(requestId, context, callback, purchasedListReq);
}
@Override
public void getUserInfo(String requestId, Context context, ICallback<UserInfos> callback) {
super.getUserInfo(requestId, context, callback);
}
@Override
public void balancePayment(String requestId, Context context, ICallback<Purchase> callback, String orderId) {
super.balancePayment(requestId, context, callback, orderId);
}
@Override
public void getPaymentOrder(String requestId, Context context, ICallback<PaymentOrders> callback, PaymentOrderReq paymentOrderReq) {
super.getPaymentOrder(requestId, context, callback, paymentOrderReq);
}
@Override
public void getUserProfile(String requestId, Context context, ICallback<UserProfile> callback) {
super.getUserProfile(requestId, context, callback);
}
@Override
public void getProductPurchaseList(String requestId, Context context, ICallback<ProductOrders> callback, int pageSize, int begin, int status) {
super.getProductPurchaseList(requestId, context, callback, pageSize, begin, status);
}
@Override
public void getProductContents(String requestId, Context context, String productId, int pageSize, int begin, ICallback<ProductContent> callback) {
super.getProductContents(requestId, context, productId, pageSize, begin, callback);
}
}
上面这些API都是要依赖于一个有效的token。那如何保证在调用这些API时确保token新鲜度呢?可以用那些方法来处理呢?
以下以内容鉴权接口为例contentAuthentication
列举出几种可能会出现的处理方式。
版本1-简单粗暴方式
void contentAuthentication(final String requestId, final Context context, final String contentId, final ICallback<Authentication> callback) {
mCloudApi.userTokenRefresh(requestId, context, new ICallback<UserToken>() {
@Override
public void errorCause(StatusCode errorCode) {
mCloudApi.contentAuthentication(requestId, context, callback, contentId);
}
@Override
public void handleData(UserToken data) {
mCloudApi.contentAuthentication(requestId, context, callback, contentId);
}
});
}
上面方式使用一个代理层封装目标接口,每次调用鉴权接口时都需要先调用刷新Token的接口,Token刷新返回后在调用鉴权接口,无视Token有个有效期,在有效期内时不需要再次请求刷新Token的;
弊端:1.效率低;2.鉴权逻辑也token刷新逻辑紧耦合在一起;3.而外增加了一层代理层。
版本2-缓存Token方式
void contentAuthentication(final String requestId, final Context context, final String contentId, final ICallback<Authentication> callback) {
if (isTokenNotExpired()) { // 本地缓存的token未过期,则直接调用目标API
mCloudApi.contentAuthentication(requestId, context, callback, contentId);
} else { // 本地缓存的token已经过期,则直先调用刷新token的API,token返回后
// 再调用目标API
mDataOperateProxy.userTokenRefresh(requestId, context, new ICallback<UserToken>() {
@Override
public void errorCause(StatusCode errorCode) {
mCloudApi.contentAuthentication(requestId, context, callback, contentId);
}
@Override
public void handleData(UserToken data) {
// 刷新保本地保存的token,在token为过期期间,其他以来toekn的接口可直接从缓存读取
mCloudApi.contentAuthentication(requestId, context, callback, contentId);
}
});
}
}
上面方式使用一个代理层封装目标接口,充分利用了Token的有效期,请求Token刷新后把新Token缓存在本地,后续接口在该Token有效期内被调用时无需调用Token刷新接口,只有在本地Token过期后才会在调用鉴权接口时请求Token刷新。
弊端:1.鉴权逻辑也token刷新逻辑紧耦合在一起;3.而外增加了一层代理层。
版本3-使用AspectJ的方式
@Aspect
public class TokenAspect {
private static final String TAG = TokenAspect.class.getSimpleName();
private static final String METHOD_POINTCUT
= "execution(* com.fonsview.iseema.launcher.api.CloudApi.*(..))"
+ " && !execution(* com.fonsview.iseema.launcher.api.CloudApi.getInstance(..))";
@Around(METHOD_POINTCUT)
public void refreshTokenIfNeeded(final ProceedingJoinPoint joinPoint)
throws Throwable {
final Signature signature = joinPoint.getSignature();
Log.i(TAG, "refreshTokenIfNeeded: methodName="
+ signature.getDeclaringTypeName() + "#" + signature.getName());
final Object[] args = joinPoint.getArgs();
if (args != null) {
if (BuildConfig.DEBUG) {
printArgs(args);
}
if (isTokenNotExpired()) { // Token没过期,直接调用目标方法
joinPoint.proceed(args);
} else { // Token过期,先刷新token后再执行目标方法的调用
CloudAPi.getInstance().userTokenRefresh("RefreshToken",
LauncherApplication.getInstance(),
new ICallback<UserToken>() {
@Override
public void handleData(UserToken data) {
Log.d(TAG, "refreshToken() request success");
// 获取新Token
String token = data.getUserToken();
// 获取Token的有效期
String validityDuration = data.getExpiration();
// 刷新本地Token缓存
Cache.getInstance().updateUserToken(token, validityDuration)
// 调用目标方法
joinPoint.proceed(args);
}
@Override
public void errorCause(StatusCode errorCode) {
Log.e(TAG, "refreshToken() errorCode = " + errorCode);
// 调用目标方法
joinPoint.proceed(args);
}
});
}
}
private void printArgs(Object[] args) {
......
}
private boolean isTokenNotExpired() {
.......
return isTokenNotExpired;
}
}
上面的方式定义了一个切面类,拦截了com.fonsview.iseema.launcher.api.CloudApi
类中除了getInstance()
方法以外的所有方法,用private static final String METHOD_POINTCUT
进行定义拦截的方法。如果Token未过期直接调用目标方法(即
= "execution(* com.fonsview.iseema.launcher.api.CloudApi.*(..))"
+ " && !execution(* com.fonsview.iseema.launcher.api.CloudApi.getInstance(..))";CloudAp
中此刻被拦截的方法),上面代码中的joinPoint.proceed(args)
语句就表示调用目前方法;如果Token未过期则调用Token刷新接口,待Token返回后刷新本地缓存的Toekn在调用目标方法。
优点:1.另外无需再增加一次代理层; 2.API客户端还是使用原始的com.fonsview.iseema.launcher.api.CloudApi
类。
对比以上三种方式,我感觉还是使用AspectJ方式最简单和有效
一些用AspectJ处理的常见问题
1、日志记录
2、权限检查
3、数据库事务管理
4、留给你来说