AOP面向切面编程--解放你的双手

AOP面向切面编程

假如现在有一个需求,在对数据库进行增删改查的时候,假如执行每个操作之前都要求把数据备份一下。这个时候怎么做比较好呢,难道要在每个方法之前都写一个save()方法吗,如果用到增删改查的地方非常多,这时候就非常麻烦了。

通过java中的动态代理就可以很方便的实现。比如

首先有个操作数据库的类

1
2
3
4
5
6
7
8
9
public interface DBOperation {
int save();

int delete();

int insert();

Object get();
}

定义一个activity,实现数据库操作接口,通过Proxy.newProxyInstance方法创建出DBOperation的代理实现类,这个方法需要一个InvocationHandler参数,

自定义一个InvocationHandler,在其invoke方法中我们就可以在执行每个方法之前和之后做一些自己的操作了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class ProxyActivity extends AppCompatActivity implements DBOperation{
private final static String TAG = "myTag >>> ";
DBOperation db;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_proxy);

db = (DBOperation) Proxy.newProxyInstance(DBOperation.class.getClassLoader()
,new Class[]{DBOperation.class},new DBHandler(this));
}

public void action(View view) {
db.delete();
}

class DBHandler implements InvocationHandler{
DBOperation db;

public DBHandler(DBOperation db) {
this.db = db;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (db != null) {
Log.e("TAG","before");
save();
Log.e("TAG","after");
return method.invoke(db,args);
}
return null;
}
}

@Override
public int save() {
Log.e(TAG, "保存数据");
return 0;
}

@Override
public int delete() {
Log.e(TAG, "删除数据");
return 0;
}

@Override
public int insert() {
return 0;
}

@Override
public Object get() {
return null;
}
}

上面的代码点击执行action方法,执行结果如果下

1
2
3
4
2019-07-02 22:49:55.296 7516-7516/com.chs.architecturetest E/TAG: before
2019-07-02 22:49:55.296 7516-7516/com.chs.architecturetest E/myTag >>>: 保存数据
2019-07-02 22:49:55.296 7516-7516/com.chs.architecturetest E/TAG: after
2019-07-02 22:49:55.297 7516-7516/com.chs.architecturetest E/myTag >>>: 删除数据

在项目开发中,我们经常会遇到这样的需求

  1. 统计用户的点击行为
  2. 在进入某些页面之前先判断是否登录,如果没登录就去登录页面

我们不可能去每个方法中都写相关的统计代码,如果类很多的情况下会麻烦死还容易出错,如果使用动态代理也是比较麻烦的,这时候我们可以使用AspectJ。

AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。

下面使用它来解决前面的两个问题

首先配置AspectJ

app下的build.gralde中添加依赖

1
implementation 'org.aspectj:aspectjrt:1.8.13'

工程的build.gralde中添加classpath AspectJ还需要添加maven的依赖

1
2
3
4
5
6
7
8
9
10
dependencies {
classpath 'com.android.tools.build:gradle:3.4.1'
classpath 'org.aspectj:aspectjtools:1.8.10'
classpath 'org.aspectj:aspectjweaver:1.8.10'
}

buildscript {
repositories {
mavenCentral()
}

最后app下的build.gralde中添加AspectJ的编译代码,在dependencies同级添加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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;
}
}
}
}

OK配置完毕下面开始解决第一个行为统计的问题

首先定义一个注解ClickBehavior,运行时注解,作用在方法上,并且有一个参数代表需要统计的行为的名称

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ClickBehavior {
String value();
}

然后定义一个切面类 ClickBehaviorAspectJ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Aspect//定义切面类
public class ClickBehaviorAspectJ {
private final static String TAG = "myTag >>> ";
//execution 定义切入点
//* *(..)) 通配符 可以处理所有ClickBehavior注解的方法
@Pointcut("execution(@com.chs.architecturetest.annotation.ClickBehaviorAspectJ * *(..))")
public void methodPointCut() {}


//对切入点方法应该如何处理 环绕通知 切入点之前和之后需要做的的事情
@Around("methodPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//获取签名方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取方法名
String methodName = signature.getName();
//获取class名
String className = signature.getDeclaringType().getSimpleName();
//获取需要统计的value值
String funName = signature.getMethod().getAnnotation(com.chs.architecturetest.annotation.ClickBehavior.class).value();
//当前时间
long begin = System.currentTimeMillis();
Log.e(TAG,"ClickBehaviorAspectJ Method Before");
Object proceed = joinPoint.proceed();
Log.e(TAG,"ClickBehaviorAspectJ Method End");
//执行时间
long duration = System.currentTimeMillis() - begin;

Log.e(TAG, String.format("统计了:%s功能,在%s类的%s方法,用时%d ms",
funName, className, methodName, duration));
return proceed;
}
}

这里面有几个注解,一般用前三个就能完成

  • @Aspect 代表这是一个切面类
  • @Pointcut 设置需要切入的方法,这里设置的所有的有ClickBehavior注解的方法。我们也可以指定某一个类下的所有方法("execution(com.chs.architecturetest.MainActivity *(..))"),或者整个工程中的所有方法("execution(* *(..))") //execution(<修饰符模式>? <返回类型模式> <方法名模式>(<参数模式>) <异常模式>?)
  • @Around 对切入点方法应该如何处理 环绕通知 切入点之前和之后需要做的的事情
  • @Before(“methodPointCut()”) 切入之前执行
  • @After(“methodPointCut()”)切入之后执行
  • @AfterReturning(value = “methodPointCut()”, returning = “returnValue”) 返回通知,切点方法返回结果之后执行
  • @AfterThrowing(value = “methodPointCut()”, throwing = “throwable”) 异常通知,切点抛出异常时执行

在Activity中整3个按钮分别为登录,VIP,账户,并设置点击方法。给这几个点击方法设置行为点击注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ClickBehavior("VIP页面")
public void goToVip(View view) {
Log.e(TAG,"去VIP页面");
startActivity(new Intent(this,OtherActivity.class));
}
@ClickBehavior("账户页面")
public void goToZh(View view) {
Log.e(TAG,"去账户页面");
startActivity(new Intent(this,OtherActivity.class));
}

@ClickBehavior("登录页面")
public void goToLogin(View view) {
Log.e(TAG,"去登录页面");
}

OK完成到这里行为统计就完成了,执行带@ClickBehavior注解的方法都会执行统计的代码, 比如点击登录按钮打印日志

1
2
3
4
2019-07-02 23:23:48.639 8640-8640/com.chs.architecturetest E/myTag >>>: ClickBehaviorAspectJ Method Before
2019-07-02 23:23:48.639 8640-8640/com.chs.architecturetest E/myTag >>>: 去登录页面
2019-07-02 23:23:48.639 8640-8640/com.chs.architecturetest E/myTag >>>: ClickBehaviorAspectJ Method End
2019-07-02 23:23:48.640 8640-8640/com.chs.architecturetest E/myTag >>>: 统计了:登录页面功能,在ProxyActivity类的goToLogin方法,用时0 ms

检查登录的功能

首先写一个注解ClickBehavior。它不需要有值

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LoginBehavior {
}

定义登录的AspectJ类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Aspect//定义切面类
public class LoginAspectJ {
private final static String TAG = "myTag >>> ";
//execution 定义切入点
//* *(..)) 通配符 可以处理所有ClickBehavior注解的方法
@Pointcut("execution(@com.chs.architecturetest.annotation.LoginBehavior * *(..))")
public void methodPointCut() {}


//对切入点方法应该如何处理 环绕通知 切入点之前和之后需要做的的事情
@Around("methodPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
//是否登录真实项目中去sharedprefrence中去那
Context context = (Context) joinPoint.getThis();
if(false){
Log.e(TAG, "检测到已登录!");
return joinPoint.proceed();
}else {
Log.e(TAG, "检测到没有登录!");
context.startActivity(new Intent(context,LoginActivity.class));
return null;
}
}
}

在around方法中就可以执行判断是否登录的逻辑了,真实项目中一般都是从SharedPreferences中拿到数据判断是否登录。

最后给需要判断登录状态的地方添加@LoginBehavior注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@LoginBehavior
@ClickBehavior("VIP页面")
public void goToVip(View view) {
Log.e(TAG,"去VIP页面");
startActivity(new Intent(this,OtherActivity.class));
}
@LoginBehavior
@ClickBehavior("账户页面")
public void goToZh(View view) {
Log.e(TAG,"去账户页面");
startActivity(new Intent(this,OtherActivity.class));
}

@ClickBehavior("登录页面")
public void goToLogin(View view) {
Log.e(TAG,"去登录页面");
}

比如这里将前面代码if判断中直接改为false,点击去VIP页面的按钮测试结果如下,会跳转到到登录页面

1
2
3
4
2019-07-02 23:26:00.057 8640-8640/com.chs.architecturetest E/myTag >>>: ClickBehaviorAspectJ Method Before
2019-07-02 23:26:00.058 8640-8640/com.chs.architecturetest E/myTag >>>: 检测到没有登录!
2019-07-02 23:26:00.067 8640-8640/com.chs.architecturetest E/myTag >>>: ClickBehaviorAspectJ Method End
2019-07-02 23:26:00.068 8640-8640/com.chs.architecturetest E/myTag >>>: 统计了:VIP页面功能,在ProxyActivity类的goToVip方法,用时10 ms

OK完成啦

# 架构

コメント

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×