背景描述
在Android开发中,往往需要处理很多的定制化需求,代码中会充满if...else...
这样的分支代码。这样的需求多了,会让业务代码越来越难以维护。有没有什么办法,以一种最小侵入性的形式承载这些定制化需求,既能始终保持主逻辑不变,又能实现定制化需求?
例:现Android项目中,有一个支付模块,主逻辑很简单,一个pay
方法,传入支付渠道和支付金额即可。但是需要满足其他定制化需求:
- 定制化需求1,OPPO支付渠道要求单笔支付金额达到50元,需要先完成实名认证才能进行支付。
- 定制化需求2,支付操作,需要进行数据上报。
/**
* 支付接口
*
* @param payChannel 支付渠道
* @param money 支付金额
*/
private void pay(String payChannel,int money){
// 定制化需求1:OPPO渠道要求,单笔支付金额达到50元,需要先完成实名认证才能进行支付
if ("OPPO".equals(payChannel) && money >= 50){
Log.i(TAG, "pay: 跳转到实名认证界面");
...
return;
}
// 定制化需求2:支付操作事件,进行数据上报
DataReport.uploadPayEvent(payChannel, money);
Log.i(TAG, "pay: 进行标准的支付");
...
}
笔者所在的部门,是手游公司的内部SDK开发部门,SDK为各款手游提供了登录、支付、公共组件等功能,7年下来,原本简单的主逻辑代码中,加入了近百个定制化需求,非常臃肿且维护成本越来越高。不堪重负,正在紧张地重构。那有没有什么办法从技术上破解这个难题,让重构后的SDK不再受其困扰?接下来将提到破解难题的利器,面向切面编程。
简介
面向切面编程(aspect-oriented programming,AOP),是一种程序设计范型,该泛型以一种称为切面的语言构造为基础,用来描述分散在对象、类或函数中的横切关注点。简单理解,一个Java method的运行,把它拆解成运行前
、运行时
、运行后
,对每个环节能有修改能力,则可以更灵活地控制这个Java method的运行逻辑。
本篇重点将其作为各种定制化需求的载体,同时也可以用作日志埋点、登录状态管理日志记录,性能统计,安全控制,事务处理,异常处理等场景。
AspectJ
AspectJ全称为Eclipse AspectJ,是Eclipse开发的面向Java™编程语言的面向切面框架,它兼容Java平台,易于学习和使用。在JavaWeb基于Spring框架开发的项目中有广泛使用,
在本文中,将选用AspectJ作为Java面向切面编程的开发库,该库可以集成进Android Studio项目中。
快速上手
基于背景描述中支付模块的2个定制化需求,通过AOP实现对主逻辑代码的最小侵入性。
配置
1. 在project的build.gradle中配置classpath
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.0-rc03'
// 配置classpath
classpath 'org.aspectj:aspectjtools:1.8.6'
}
}
2. 在app的build.gradle中配置编织脚本
apply plugin: 'com.android.application'
// 编织脚本 start
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.9'
classpath 'org.aspectj:aspectjweaver:1.8.9'
}
}
repositories {
mavenCentral()
}
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;
}
}
}
}
// 编织脚本 end
3. 在其他用到AOP的module的build.gradle中配置编织脚本
apply plugin: 'com.android.library'
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
android.libraryVariants.all { variant ->
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
//下面的1.8是指我们兼容的jdk的版本
String[] args = ["-showWeaveInfo",
"-1.8",
"-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-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;
}
}
}
}
5. 引用库
implementation 'org.aspectj:aspectjrt:1.8.9'
实现
1. 先定义特定的运行时注解
/**
* OPPO特殊处理注解
*
* @author Divin on 2018/11/23
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OppoPayAnnotation {
}
/**
* 支付事件数据上报注解
*
* @author Divin on 2018/11/26
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UploadPayEventAnnotation {
}
2. 用注解修饰主逻辑方法
用多个注解修饰同一个方法时,会按顺序从上到下进行执行。
/**
* 支付接口
*
* @param payChannel 支付渠道
* @param money 支付金额
*/
@OppoPayAnnotation
@UploadPayEventAnnotation
private void pay(String payChannel, int money) {
Log.i(TAG, "pay: 进行标准的支付");
Toast.makeText(this, "进行标准的支付", Toast.LENGTH_LONG).show();
}
3. 编写定制化需求切面代码
AspectJ框架,最简单的是使用@Aspect类注解来修饰切面类,使用@Pointcut方法注解来找到切入点,"execution(@com.acronym.aoplib.pay.OppoPayAnnotation * *(..))"
表示找到所有类中使用OppoPayAnnotation修饰的方法。最后使用@Around方法注解来编写切入代码,"executionPayAnnotation()"
即切入点。在pay方法中,每一行的用途已在在注释中标明。
/**
* 支付切入代码
*
* @author Divin on 2018/11/23
*/
@Aspect
public class PayAnnotationAspectJ {
private static final String TAG = "d5g-" + "PayAnnotationAspect";
/**
* 找到切入点
*/
@Pointcut("execution(@com.acronym.aoplib.pay.OppoPayAnnotation * *(..))")
public void executionPayAnnotation() {
}
/**
* 定制化需求1,OPPO支付渠道要求单笔支付金额达到50元,需要先完成实名认证才能进行支付
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("executionPayAnnotation()")
public Object pay(ProceedingJoinPoint joinPoint) throws Throwable {
Log.i(TAG, "pay: 定制化需求1,OPPO支付渠道要求单笔支付金额达到50元,需要先完成实名认证才能进行支付");
// 获取被切入的方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
OppoPayAnnotation payAnnotation = signature.getMethod().getAnnotation(OppoPayAnnotation.class);
if (payAnnotation != null) {
// 解析出所有参数
Object[] args = joinPoint.getArgs();
String payChannel = (String) args[0];
int money = (int) args[1];
// OPPO渠道,支付金额达到50元
if ("OPPO".equals(payChannel) && money >= 50) {
Log.i(TAG, "pay: 弹出实名认证界面");
// 拦截原方法的执行
return null;
}
}
// 继续原方法的执行
return joinPoint.proceed();
}
}
/**
* 支付事件的数据上报切入代码
*
* @author Divin on 2018/11/23
*/
@Aspect
public class UploadPayEventAnnotationAspectJ {
private static final String TAG = "d5g-" + "UploadPayEventAnnotationAspectJ";
/**
* 找到切入点
*/
@Pointcut("execution(@com.acronym.aoplib.upload.UploadPayEventAnnotation * *(..))")
public void executionUploadPayEventAnnotation() {
}
/**
* 定制化需求2,支付操作,需要进行数据上报
*
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("executionUploadPayEventAnnotation()")
public Object uploadPayEvent(ProceedingJoinPoint joinPoint) throws Throwable {
Log.i(TAG, "定制化需求2,支付操作,需要进行数据上报");
// 获取被切入的方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
OppoPayAnnotation payAnnotation = signature.getMethod().getAnnotation(OppoPayAnnotation.class);
if (payAnnotation != null) {
// 解析出所有参数
Object[] args = joinPoint.getArgs();
String payChannel = (String) args[0];
int money = (int) args[1];
// 数据上报
DataReport.uploadPayEvent(payChannel, money);
}
// 继续原方法的执行
return joinPoint.proceed();
}
}
总结
通过AOP的形式管理定制化需求,可以让主逻辑代码始终保持下面的效果,对于支撑多个业务的SDK里,还是比较难得的。同时,这些AOP的类,可以单独抽成jar包形式,按需进行引用,不加载不影响主逻辑运行。仅建议用作定制化需求的代码载体,不建议用作热加载等场景。
很晚了,常用API、原理讲解再补上。
/**
* 支付接口
*
* @param payChannel 支付渠道
* @param money 支付金额
*/
@OppoPayAnnotation
@UploadPayEventAnnotation
private void pay(String payChannel, int money) {
Log.i(TAG, "pay: 进行标准的支付");
Toast.makeText(this, "进行标准的支付", Toast.LENGTH_LONG).show();
}
网友评论