AOP:面向切面编程。
什么是切面?
在开发过程中经常会遇到这样的逻辑,在点击不同的按钮跳转的时候都需要判断是否已经登录,在数据库增删改之前都要先进行备份操作。这种共同的逻辑就可以进行切面。
下面分别用Proxy
和AspectJ
实现这种切面操作。
一、使用Proxy
Proxy是Java里的类,不需要引入任何第三方库,以数据库操作为例,实现增删改之前先进行备份的操作。
如果不用面向切面的思想,肯定是进行一个方法抽取,然后每次操作之前先调用。类似这样的代码:
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.insert:
db.save();
db.insert();
break;
case R.id.delete:
db.save();
db.delete();
break;
case R.id.update:
db.save();
db.update();
break;
}
}
使用切面之后,这样的抽取都会显得冗余。
开撸!
- 先定义一个数据库操作的接口:
public interface DBOperation {
void insert();
void delete();
void update();
void save();
}
- 在Activity里实现这个接口:
public class DBActivity extends AppCompatActivity implements DBOperation, View.OnClickListener {
...
@Override
public void insert() {
Log.i("haha", "数据库操作>>>增");
}
@Override
public void delete() {
Log.i("haha", "数据库操作>>>删");
}
@Override
public void update() {
Log.i("haha", "数据库操作>>>改");
}
@Override
public void save() {
Log.i("haha", "数据库操作>>>备份");
}
...
}
- 如果在Activity里操作时直接调用
this.insert()
this.delete()
this.update()
肯定是没有进行备份操作的,这里就需要用到Proxy
这个类。
DBOperation dbOperation = (DBOperation) Proxy.newProxyInstance(
DBOperation.class.getClassLoader(),
new Class[]{DBOperation.class},
new DBHandler(this)
);
这里的DBHandler
就是一个拦截器,在这个拦截器里就可以做一些统一的处理:
class DBHandler implements InvocationHandler{
private DBOperation dbOperation;
DBHandler(DBOperation dbOperation){
this.dbOperation = dbOperation;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
if (dbOperation != null) {
//先进行查询操作
dbOperation.save();
}
return method.invoke(dbOperation, args);
}
}
然后再调用dbOperation.insert()
dbOperation.delete()
dbOperation.update()
的时候,就会先调用拦截器中的代码:
2019-07-10 15:56:37.983 26593-26593/com.yu.aopdemo I/haha: 数据库操作>>>备份
2019-07-10 15:56:37.983 26593-26593/com.yu.aopdemo I/haha: 数据库操作>>>增
2019-07-10 15:56:40.769 26593-26593/com.yu.aopdemo I/haha: 数据库操作>>>备份
2019-07-10 15:56:40.769 26593-26593/com.yu.aopdemo I/haha: 数据库操作>>>删
2019-07-10 15:56:42.018 26593-26593/com.yu.aopdemo I/haha: 数据库操作>>>备份
2019-07-10 15:56:42.018 26593-26593/com.yu.aopdemo I/haha: 数据库操作>>>改
二、使用AspectJ
AspectJ在Java开发中会经常使用到,这里用AspectJ在Android实现检测登录和事件统计的功能。
- 首先是引入AspectJ库
在Project的build.gradle
中添加:
buildscript {
...
dependencies {
...
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
}
在app的build.gradle
中添加:
...
buildscript { // 编译时用Aspect专门的编译器,不再使用传统的javac
...
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
}
dependencies {
...
implementation 'org.aspectj:aspectjrt:1.8.13'
}
// 添加Groovy支持
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->
if (!variant.buildType.isDebuggable()) {
log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
return;
}
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = ["-showWeaveInfo",
"-1.8",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
log.debug "ajc args: " + Arrays.toString(args)
MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler);
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:
log.warn message.message, message.thrown
break;
case IMessage.INFO:
log.info message.message, message.thrown
break;
case IMessage.DEBUG:
log.debug message.message, message.thrown
break;
}
}
}
}
Sync一下,添加完成。
这里需要注意的是:
如果使用新版本的AS和gradle会有警告,但是可以正常使用
如果使用3.2.1的AS和4.6的gradle,正常使用,无警告
如果使用3.0.1的AS和4.4的gradle,需要配置r17的NDK环境
- 定义一个登录检测注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginCheck {
}
- 定义一个切面:
@Aspect
public class LoginCheckAspect {
// 1、应用中用到了哪些注解,放到当前的切入点进行处理(找到需要处理的切入点)
// execution,以方法执行时作为切点,触发Aspect类
// * *(..)) 表示可以处理LoginCheck这个类所有的方法
@Pointcut("execution(@com.yu.aopdemo.annotation.LoginCheck * *(..))")
public void pointcut() {}
// 2、对切入点如何处理
@Around("pointcut()")
public Object jointPoint(ProceedingJoinPoint joinPoint) throws Throwable {
Context context = (Context) joinPoint.getThis();
boolean isLogin = context.getSharedPreferences("sp", context.MODE_PRIVATE).getBoolean("login", false);
if (isLogin){
Log.i("haha", "LoginCheck>>>已登录");
return joinPoint.proceed();
}else {
Log.i("haha", "LoginCheck>>>未登录");
Toast.makeText(context, "请先登录", Toast.LENGTH_SHORT).show();
context.startActivity(new Intent(context, LoginActivity.class));
return null;
}
}
}
定义切面有两个要素:1、找到切入点,这里是LoginCheck注解。 2、对切入点如何处理,处理操作有Around、Before、After。
- 在Activity中使用,只要在跳转操作上加上LoginCheck注解就可以:
/**
* 跳转到我的购物车
* @param view
*/
@LoginCheck
public void myShoppingCar(View view) {
Log.i("haha", "点击按钮>>>我的购物车");
Toast.makeText(this, "假装已经跳转到我的购物车", Toast.LENGTH_SHORT).show();
}
/**
* 跳转到我的收藏
* @param view
*/
@LoginCheck
public void myCollection(View view) {
Log.i("haha", "点击按钮>>>我的收藏");
Toast.makeText(this, "假装已经跳转到我的收藏", Toast.LENGTH_SHORT).show();
}
- 用户行为检测也是一样的实现过程,不贴了。