“AOP” 切面编程“如同切水果一样简单”

AOP是什么东东?

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统 一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的 一种衍生范型。
(利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率)

AOP实现方式

可以使用以下4种方式,完成不同阶段的切面编程

  • APT : 编译期,静态织入

  • AspectJ:编译期,静态织入

  • ASM:编译期和运行期,可以动态也可以静态织入

  • Xposed:运行期,动态织入

    Aop思想可以说成插桩,在类的编译期间中干一些东西,下图看一个图就明白了,主要关注一下AspectJ插入时机:
    image.png
1. APT

介绍

APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。简单来说就是在编译期,通过注解生成.java文件。

用过ButterKnife、Dagger、EventBus等注解框架的同学就能感受到,利用这些框架可以少些很多代码,只要写一些注解就可以了。
其实,他们不过是通过注解,生成了一些代码。通过对APT的学习,你就会发现,他们很强~~~


image.png

实现

  • apt-annotation:自定义注解,存放@BindView
  • apt-processor:注解处理器,根据apt-annotation中的注解,在编译期生成xxxActivity_ViewBinding.java代码
  • apt-library:工具类,调用xxxActivity_ViewBinding.java中的方法,实现View的绑定。
    关系如图所示


    image.png
2. AspectJ

介绍

AspectJ意思就是Java的Aspect,Java的AOP。它其实不是一个新的语言,它在代码的编译期间扫描目标程序,根据切点(PointCut)匹配,将开发者编写的Aspect程序编织(Weave)到目标程序的.class文件中,对目标程序作了重构(重构单位是JoinPoint),目的就是建立目标程序与Aspect程序的连接(获得执行的对象、方法、参数等上下文信息),从而达到AOP的目的,它的核心就是ajc(编译器)\weaver(织入)。

基本概念

  • ajc(编译器):
    基于Java编译器之上的,它是用来编译.aj文件,aspectj在Java编译器的基础上增加了一些它自己的关键字和方法。因此,ajc也可以编译Java代码。

  • weaver(织入器):
    为了在java编译器上使用AspectJ而不依赖于Ajc编译器,aspectJ 5出现了@AspectJ,使用注释的方式编写AspectJ代码,可以在任何Java编译器上使用。

  • Aspect(切面):
    实现了cross�cutting功能,是针对切面的模块。最常见的是logging模块、方法执行耗时模块,这样,程序按功能被分为好几层,如果按传统的继承的话,商业模型继承日志模块的话需要插入修改的地方太多,而通过创建一个切面就可以使用AOP来实现相同的功能了,我们可以针对不同的需求做出不同的切面。

  • JoinPoint(连接点):
    连接点是切面插入应用程序的地方,该点能被方法调用,而且也会被抛出意外。连接点是应用程序提供给切面插入的地方,可以添加新的方法。比如:我们的切点可以认为是findInfo(String)方法。
    AspectJ将面向对象的程序执行流程看成是JoinPoint的执行链,每一个JoinPoint是一个单独的闭包,在执行的时候将上下文环境赋予闭包执行方法体逻辑。

  • PointCut(切点):
    切点的声明决定需要切割的JoinPoint的集合,就结果上来说,它是JoinPoint的一个实际子集合。
    pointcut可以控制你把哪些advice应用于jointpoint(连接点)上去,通常通过正则表达式来进行匹配应用,决定了那个jointpoint(连接点)会获得通知。分为call、execution、target、this、within等关键字,含义下面会附图。

实现

说了一大堆概念,感觉还是不知道在说些什么,我们这个AspectJ到底能做啥?我们现在就举个例子:我们在程序中是不是很多功能需要申请相应的权限才能使用,每次我们要调用此方法的时候是不是需要先看权限是否获取,没获取需要先获取了再执行,这里就会出现每个地方都需要做判断,是不是很麻烦,如果我们可以做到在需要申请权限的方法之前统一获取权限,判断只需要写一次统一处理,代码也好管理,修改也容易,那不是一劳永逸,好,我们的AspectJ就帮我们完成了这个工作。

实现流程

  1. 自定义注解:我们需要自定义一个注解标记需要获取权限的方法,这个注解定义一个数组,你需要申请的权限集合和请求码(为了让回调做不同操作设置)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {

    /**
     * 请求权限
     */
    String[] value();

    /**
     * 请求码
     */
    int requestCode();
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionDenied {
    /**
     * 请求码
     */
    int requestCode();
}


  1. 方法添加注解标记JoinPoint(连接点):
    @Permission(value = "android.permission.READ_EXTERNAL_STORAGE",requestCode = 1)
    public void getSd() {
        //去读取存储卡里面的内容
    }

    @PermissionFailed(requestCode = 1)
    private void requestPermissionFailed() {
        Toast.makeText(this, "用户拒绝了权限", Toast.LENGTH_SHORT).show();
    }

    @PermissionDenied(requestCode = 1)
    private void requestPermissionDenied() {
        Toast.makeText(this, "权限申请失败,不再询问", Toast.LENGTH_SHORT).show();
        //开发者可以根据自己的需求看是否需要跳转到设置页面去
        PermissionUtil.startAndroidSettings(this);
    }

  1. 获取所有连接点实现代码织入:获取所有的 JoinPoint连接点然后实现 PointCut(切点)的代码织入
@Aspect
public class PermissionAspect {
    /**
     * 声明切入点是所有的Permission注解标记了的方法
     * @param permissions
     */
    @Pointcut("execution(@com.maniu.mn_vip_aspectj.permission.annotation.Permission * *(..)) && @annotation(permissions)")
    public void getPermission(Permission permissions){}

    /**
     * 根据切入点 织入代码
     * @param proceedingJoinPoint
     * @param permissions
     * @throws Throwable
     */
    @Around("getPermission(permissions)")
    public void getPermissionPoint(final ProceedingJoinPoint proceedingJoinPoint, Permission permissions) throws Throwable {
        //获取到上下文
        Context context = null;
        //获取到注解所在的类对象
        final Object aThis = proceedingJoinPoint.getThis();
        //判断aThis是否是Context的子类  如果是 就进行赋值
        if(aThis instanceof Context){
            context = (Context) aThis;
        }else if(aThis instanceof Fragment){
            context = ((Fragment) aThis).getActivity();
        }
        //获取到要申请的权限
        if(context==null || permissions == null || permissions.value()==null || permissions.value().length==0){
            return;
        }
        //获取到注解携带的权限数据
        String[] value = permissions.value();
        //由于权限回调的方法  只有Activity才有  因此 在这里我们创建一个透明的Activity来进行权限申请
        ApplyPermissionActivity.launchActivity(context,value,permissions.requestCode(),
                new PermissionRequestCallback() {
            @Override
            public void permissionSuccess() {
                Log.e("-------->","权限申请结果:成功");
                //权限获取成功 继续执行
                try {
                    proceedingJoinPoint.proceed();
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                }
            }

            @Override
            public void permissionCanceled() {
                // 权限申请被拒绝怎么办
                //反射执行permissionCanceled()方法
                PermissionUtil.invokeAnnotation(aThis,PermissionFailed.class);

            }

            @Override
            public void permissionDenied() {
                //权限申请被拒绝并且永久不提示怎么办
                Log.e("-------->","权限申请结果:被永久拒绝");
                //反射执行permissionDenied()方法
                PermissionUtil.invokeAnnotation(aThis,PermissionDenied.class);
            }
        });
    }
}

补充知识
在@Pointcut后面的括号中会有一个匹配连接点的表达式,这里给大家说下匹配的规则如下:
@Pointcut(execution(<><><><>) )
“修饰符”和“异常”可以省略。

  1. 修饰符(public ,private,注解(需要指定自定注释的绝对路径))
  2. 返回类型(使用*代表匹配所有返回类型)
  3. 包.类.方法:(支持模糊匹配)
  4. 参数 :方法传入的参数类型 (可以多个参数匹配,使用空格隔开)

支持三种通配符:

  • 匹配任意字符,只匹配一个元素
  • 匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 联合使用
  • 表示按照类型匹配指定类的所有类,必须跟在类名后面,如 ,表示继承该类的所有子类包括本身

支持逻辑运算符:

  • &&:与操作符。相当于切点的交集运算。xml配置文件中使用切点表达式,&是特殊字符,所以需要转义字符&;来表示。

  • :或操作符。相当于切点的并集运算。

  • :非操作符,相当于切点的反集运算

不同搭配请看如下示范:

execution(public * *(..)):任意public方法
execution(* set*(..)):方法名以set开头的任意方法
execution(* com.xyz.service.AccountService.*(..)):AccountService 接口下的任意方法 
execution(* com.xyz.service..(..)):service包下的任意方法
execution(* com.xyz.service...(..)):service包或子包下的任意方法
execution(public void MyClass.myMethod(String)) :MyClass 类的myMethod方法,方法public访问权限,void返回值,形参只有一个并为String类型
execution(void MyClass.myMethod(..)):MyClass 类的myMethod方法,任意访问权限,返回值void,任意形参
execution(* MyClass.myMethod(..)):MyClass 类的myMethod方法,任意返回值,任意形参
execution(* MyClass.myMethod*(..)):MyClass 类的以myMethod开头的方法,任意返回值,任意形参
execution(* MyClass.myMethod*(String,..)):MyClass 类的以myMethod开头的方法,任意返回值,第一个形参类型是String
execution(* *.myMethod(..)):任意类下myMethod方法 execution(MyClass.new()):任意MyClass类的无参构造器
execution(MyClass.new(..)):任意MyClass类的任意有参构造器 execution(MyClass+.new(..)):任意MyClass或其子类构造器
execution(public * com.mycompany..*.*(..)):com.mycompany包下任意子包的所有类的所有public 方法

//Spring AOP只能切方法(也就是任意连接点中的方法),AspectJ可以切任意成员(任意连接点,包括类/对象初始化块,field,方法,构造器)
within(com.xyz.service.*):service包下任意连接点
within(com.xyz.service..*):service包或子包下任意连接点
this(com.xyz.service.AccountService):AccountService接口的代理实现里的任意连接点
target(com.xyz.service.AccountService):目标对象实现了AccountService接口的任意连接点
args(java.io.Serializable):只有一个参数且参数在运行时是Serializable类型的任意连接点
@target(org.springframework.transaction.annotation.Transactional):目标对象有一个@Transactional注解任意连接点
@within(org.springframework.transaction.annotation.Transactional):目标对象的声明类型有一个@Transactional注解任意连接点
@annotation(org.springframework.transaction.annotation.Transactional):执行方法有一个@Transactional注解的任意连接点
@args(com.xyz.security.Classified):只有一个参数且参数在运行时参数有@Classified注解的任意连接点
bean(tradeService):Spring bean 名称是tradeService的任意连接点
bean(*Service):Spring bean的名称以Service结尾的任意连接点
//表达式可以使用|| && !进行组合  在XML下就是 or and not
execution(* com.xyz.myapp.service..(..)) and this(service)//xml
execution(* com.xyz.myapp.service..(..)) && this(service)//java

你可能感兴趣的:(“AOP” 切面编程“如同切水果一样简单”)