Android如何避免定制化需求影响到主逻辑-面向切面编程(AOP)

背景描述

在Android开发中,往往需要处理很多的定制化需求,代码中会充满if...else...这样的分支代码。这样的需求多了,会让业务代码越来越难以维护。有没有什么办法,以一种最小侵入性的形式承载这些定制化需求,既能始终保持主逻辑不变,又能实现定制化需求?
**例:**现Android项目中,有一个支付模块,主逻辑很简单,一个pay方法,传入支付渠道和支付金额即可。但是需要满足其他定制化需求:

  1. 定制化需求1,OPPO支付渠道要求单笔支付金额达到50元,需要先完成实名认证才能进行支付。
  2. 定制化需求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();
    }
```~~删除线格式~~ 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值