您现在的位置是:首页 > 文章详情

组件化框架设计之AOP&IOC(四)

日期:2019-11-13点击:342

阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680

本篇文章将从以下两个方面来介绍组件化框架设计:

  • 【AOP(面向切面编程)、Filter(过虑器)、Interceptor(拦截器)】
  • 【Android IOC注入框架】

一、AOP(面向切面编程)、Filter(过虑器)、Interceptor(拦截器)

1.1 AOP(面向切面编程)

面向切面编程(AOP是Aspect Oriented Program的首字母缩写) ,我们知道,面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。

但是人们也发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。

也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。从技术上来说,AOP基本上是通过代理机制实现的。 AOP在编程历史上可以说是里程碑式的,对OOP编程是一种十分有益的补充。

AOP不一定都像Spring AOP那样,是在运行时生成代理对象来织入的,还可以在编译期、类加载期织入,比如AspectJ。AOP像OOP一样,只是一种编程范式,AOP并没有规定说,实现AOP协议的代码,要用什么方式去实现。

什么是代理模式?
就是再生成一个代理类,去代理UserController的saveUser()方法,代码大概就长这样:

class UserControllerProxy { private UserController userController; public void saveUser() { checkAuth(); userController.saveUser(); } }

代理分为静态代理和动态代理。
静态代理,顾名思义,就是你自己写代理对象。
动态代理,则是在运行期,生成一个代理对象。Spring AOP就是基于动态代理的,但是不是所有AOP的实现都是在运行时进行织入的,因为这样效率太低了,而且只能针对方法进行AOP,无法针对构造函数、字段进行AOP。我完全可以在编译成class时就织入啊,比如AspectJ,当然AspectJ还提供了后编译器织入和类加载期织入。
1.1.1 AOP概念

方面(Aspect):一个关注点的模块化,这个关注点实现可能另外横切多个对象。事务管理是J2EE应用中一个很好的横切关注点例子。方面用Spring的Advisor或拦截器实现。

连接点(Joinpoint):程序执行过程中明确的点,如方法的调用或特定的异常被抛出。

通知(Advice):在特定的连接点,AOP框架执行的动作。各种类型的通知包括“around”、“before”和“throws”通知。通知类型将在下面讨论。许多AOP框架包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器链。

切入点(Pointcut):指定一个通知将被引发的一系列连接点的集合。AOP框架必须允许开发者指定切入点,例如,使用正则表达式。

引入(Introduction):添加方法或字段到被通知的类。Spring允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现IsModified接口,来简化缓存。

目标对象(Target Object):包含连接点的对象,也被称作被通知或被代理对象。

AOP代理(AOP Proxy):AOP框架创建的对象,包含通知。在Spring中,AOP代理可以是JDK动态代理或CGLIB代理。

编织(Weaving):组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

1.1.2 通知类型

Around通知:包围一个连接点的通知,如方法调用。这是最强大的通知。Aroud通知在方法调用前后完成自定义的行为,它们负责选择继续执行连接点或通过返回它们自己的返回值或抛出异常来短路执行。

Before通知:在一个连接点之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)。

Throws通知:在方法抛出异常时执行的通知。Spring提供强制类型的Throws通知,因此你可以书写代码捕获感兴趣的异常(和它的子类),不需要从Throwable或Exception强制类型转换。

After returning通知:在连接点正常完成后执行的通知,例如,一个方法正常返回,没有抛出异常。

Around通知是最通用的通知类型。大部分基于拦截的AOP框架(如Nanning和Jboss 4)只提供Around通知。

如同AspectJ,Spring提供所有类型的通知,我们推荐你使用最为合适的通知类型来实现需要的行为。例如,如果只是需要用一个方法的返回值来更新缓存,你最好实现一个after returning通知,而不是around通知,虽然around通知也能完成同样的事情。使用最合适的通知类型使编程模型变得简单,并能减少潜在错误。例如,你不需要调用在around通知中所需使用的MethodInvocation的proceed()方法,因此就调用失败。

切入点的概念是AOP的关键,它使AOP区别于其他使用拦截的技术。切入点使通知独立于OO的层次选定目标。例如,提供声明式事务管理的around通知可以被应用到跨越多个对象的一组方法上。 因此切入点构成了AOP的结构要素。

1.2 Filter(过虑器)

Filter技术是Servlet2.3新增加的功能,Servlet2.3是sun公司于2000年10月发布的,它的开发者包括许多个人和公司团体,充分体现了sun公司所倡导的代码开放性原则。在众多参与者的共同努力下,Servlet2.3比以往功能都强大了很多,而且性能也有了提高。那么Filter有什么功能呢?Filter可以用来设置字符集、控制权限、控制转向等等。简单点说:就是定义在访问某对象之前和访问之后要做的事情。

1.3Interceptor(拦截器)

1,拦截器的概念
java里的拦截器是动态拦截Action调用的对象,它提供了一种机制可以使开发者在一个Action执行的前后执行一段代码,也可以在一个Action
执行前阻止其执行,同时也提供了一种可以提取Action中可重用部分代码的方式。在AOP中,拦截器用于在某个方法或者字段被访问之前,进行拦截
然后再之前或者之后加入某些操作。目前,我们需要掌握的主要是Spring的拦截器,Struts2的拦截器不用深究,知道即可。

2,拦截器的原理
大部分时候,拦截器方法都是通过代理的方式来调用的。Struts2的拦截器实现相对简单。当请求到达Struts2的ServletDispatcher时,Struts2
会查找配置文件,并根据配置实例化相对的拦截器对象,然后串成一个列表(List),最后一个一个的调用列表中的拦截器。Struts2的拦截器是可
插拔的,拦截器是AOP的一个实现。Struts2拦截器栈就是将拦截器按一定的顺序连接成一条链。在访问被拦截的方法或者字段时,Struts2拦截器链
中的拦截器就会按照之前定义的顺序进行调用。

3,自定义拦截器的步骤

第一步:自定义一个实现了Interceptor接口的类,或者继承抽象类AbstractInterceptor。 第二步:在配置文件中注册定义的拦截器。 第三步:在需要使用Action中引用上述定义的拦截器,为了方便也可以将拦截器定义为默认的拦截器,这样在不加特殊说明的情况下,所有的

Action都被这个拦截器拦截。

4,过滤器与拦截器的区别

过滤器可以简单的理解为“取你所想取”,过滤器关注的是web请求;拦截器可以简单的理解为“拒你所想拒”,拦截器关注的是方法调用,比如拦截

敏感词汇。
4.1,拦截器是基于java反射机制来实现的,而过滤器是基于函数回调来实现的。(有人说,拦截器是基于动态代理来实现的)
4.2,拦截器不依赖servlet容器,过滤器依赖于servlet容器。
4.3,拦截器只对Action起作用,过滤器可以对所有请求起作用。
4.4,拦截器可以访问Action上下文和值栈中的对象,过滤器不能。
4.5,在Action的生命周期中,拦截器可以多次调用,而过滤器只能在容器初始化时调用一次。

5,Spring拦截器
5.1,抽象类HandlerInterceptorAdapter

我们如果在项目中使用了Spring框架,那么,我们可以直接继承HandlerInterceptorAdapter.java这个抽象类,来实现我们自己的拦截器。 

Spring框架,对java的拦截器概念进行了包装,这一点和Struts2很类似。HandlerInterceptorAdapter继承了抽象接口HandlerInterceptor。

二、Android IOC注入框架

2.1 什么是IOC注入框架

ButterKnife大家都应该使用过,对于view的注入减少了大量篇幅的findViewById操作,而注解注入的方式也显得更加优雅。这里介绍一下我的IOC简单注入框架,项目地址移步这里

2.2 IOC使用简单介绍

添加依赖

项目根目录下的build.gradle文件添加如下内容

allprojects { repositories { ... maven { url "https://raw.githubusercontent.com/demoless/ioc/master/repo" } } } 

然后在app模块的build.gradle文件添加如下内容

implementation 'com.demoless:ioc:1.0.0' 

这里我贴出demo的调用示例代码看看如何使用:

要实现这样一套IOC框架我们还要先注册一下,看BaseActiivty的代码:

可以看到相比于传统的Activity的写法,IOC注入框架颇具诱惑,下面我就带大家了解一下我的IOC实现思路。

如何实现IOC

IOC是一套注解注入框架,所以主要是通过Java的反射与注解来实现的,这里就不介绍了,不了解的可以看看这篇文章

布局注入

@ContentView(R.layout.activity_main)

首先创建ContentView这个注解

创建过程跟创建类的过程是一样的,只需要将Kind选择为Annotation即可:

注解的编写

package com.demo.iocinject.ioc; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * Create by Zhf on 2019/7/13 **/ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface ContentView { int value(); } 

我们可以看到在这个注解的上面还有两个注解,这是两个元注解,首先@Target(ElementType.TYPE)代表这个ContentView注解作用在类上面,然后@Retention(RetentionPolicy.RUNTIME)表示注解在运行时执行,因为一个类只会有一个布局文件,所以这里value方法的返回值为int,而不是数组。

Java实现注解的执行逻辑

定义好了这个注解之后,我们就要考虑如何将ContentView里传入的布局文件设置给Activity,我们知道传统的activity是通过在onCreate方法里的setContentView来将布局文件设置给activity的,那我们也只需要通过反射将传入注解的布局再传入setContentView并且让它自动执行不就可以实现了嘛,思路好像没错,我们来实现以下:

//布局注入 private static void injectLayout(Activity activity) { Class<? extends Activity> clazz = activity.getClass(); //获取类之上的注解 ContentView contentView = clazz.getAnnotation(ContentView.class); if (contentView != null){ //获取注解的返回值 int layoutId = contentView.value(); //第一种方法 //activity.setContentView(layoutId); //第二种方法 try { Method setContentView = clazz.getMethod("setContentView", int.class); setContentView.invoke(activity,layoutId); } catch (Exception e) { e.printStackTrace(); } } } 

这段代码首先通过activity.getClass()拿到这个Class对象,再通过clazz.getAnnotation(ContentView.class)获取类上的注解,拿到注解之后自然要获取他的返回值,所以再调用他的value方法,这样我们就拿到了对应的布局文件,最好要完成的是传入setContentView这个布局方法并执行。这里我给出了两种方法,第一种很简单直接调用activity的setContentView方法,第二种通过反射拿到setContentView这个Method对象,在调用invoke方法自动执行。这样一个简易布局注入就实现了。

控件注入

@InjectView

注解文件

与ContentView一样这里就不在赘述了:

Java执行逻辑

//控件的注入 private static void injectViews(Activity activity) { Class<? extends Activity> clazz = activity.getClass(); //获取类的全部属性 Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { //获取属性上的注解 InjectView injectView = field.getAnnotation(InjectView.class); if (injectView != null){ //获取注解的值 int viewId = injectView.value(); //View view = activity.findViewById(viewId); try { //获取findViewById方法 Method findViewById = clazz.getMethod("findViewById", int.class); //执行findViewById方法 Object view = findViewById.invoke(activity, viewId); //设置访问权限 private field.setAccessible(true); //为属性赋值 field.set(activity,view); } catch (Exception e) { e.printStackTrace(); } } } } 

这段代码也跟布局注入的实现很相似,这是回去类的全部属性的时候,需要调用的是clazz.getDeclaredFields()方法,如果用getFields方法程序会崩溃,这个很容易想到,因为在父类和子类中可能会有相同命名的一个控件,就行这样:private Button mButton;,所以就造成崩溃,这只能获取类自身的属性;第二个需要注意的地方是,通常我们声明一个控件,都是用的private关键字,所以这里还需要设置一下访问权限,调用 field.setAccessible(true),这样对稀有属性进行操作;另一个与布局注入实现不同的是,setContentView方法没有返回值,而findViewById则相反,所以我们需要为属性(这里就是一些View)赋值,调用的是field.set(activity,view)。

事件的注入

@InjectEvent

Android事件监听规律

事件的注入相比之前的布局和控件注入,难度和复杂度大大提高了。通过对Android中的事件监听代码的观察,我们得出如下三部曲:

  • setListener
  • new Listener
  • doCallback

    就像View的点击事件和长按时间监听那样,首先setListener:View.setOnClickListener(),然后new 一个Listener传入,View.setOnClickListener(new OnClickListener(View v){}),最后执行回调方法: onClick(View v){...} 

定义事件监听规律的注解

@EventBase

通过上述规律总结,我们要先定义这个注解:

/** * Create by Zhf on 2019/7/13 **/ @Target(ElementType.ANNOTATION_TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface EventBase { //setListener Class<?> listenerType(); //new View.OnxxxListener String listenerSetter(); //回调 最终执行方法 String callBackListener(); } 

我们看到这个注解是放在注解类之上的,那么这个注解怎么使用呢,就以View的长按事件监听为例:

/** * Create by Zhf on 2019/7/13 **/ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @EventBase(listenerSetter = "setOnLongClickListener", listenerType = View.OnLongClickListener.class, callBackListener = "onLongClick") public @interface OnLongClick { int[] value(); } 

在这个注解上面调用了刚才定义的EventBase注解,根据传入的值大家似乎就什么都看明白了吧,没错这里传入了View.OnLongClickListener事件监听三部曲,因为在一个类中可能不止一个控件会设置长按事件监听,所以这里的返回值是数组。

事件注入的逻辑

 //事件的注入 private static void injectEvents(Activity activity) { Class<? extends Activity> clazz = activity.getClass(); //获取一个类的所有方法 Method[] methods = clazz.getDeclaredMethods(); //遍历所有方法 for (Method method : methods) { Annotation[] annotations = method.getAnnotations(); //遍历所有注解 for (Annotation annotation : annotations) { Class<? extends Annotation> annotationType = annotation.annotationType(); if (annotationType != null) { EventBase eventBase = annotationType.getAnnotation(EventBase.class); if (eventBase != null) { String listenerSetter = eventBase.listenerSetter(); Class<?> listenerType = eventBase.listenerType(); String callBackListener = eventBase.callBackListener(); try { Method valueMethod = annotationType.getDeclaredMethod("value"); int[] viewIds = (int[]) valueMethod.invoke(annotation); //设置private权限可见 method.setAccessible(true); //AOP切面 ListenerInvocationHandler handler = new ListenerInvocationHandler(activity); handler.addMethods(callBackListener, method); //代理模式 Object listener = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, handler); for (int viewId : viewIds) { View view = activity.findViewById(viewId); Method setter = view.getClass().getMethod(listenerSetter, listenerType); setter.invoke(view, listener); } } catch (Exception e) { e.printStackTrace(); } } } } } } 

这里其他的不多介绍了,主要不同的就是这里使用了动态代理和AOP切面技术:

import android.util.Log; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.util.HashMap; /** * Create by Zhf on 2019/7/13 **/ public class ListenerInvocationHandler implements InvocationHandler { private final static long QUICK_EVENT_TIME_SPAN = 300; private long lastClickTime; private Object target;//需要拦截的对象 private HashMap<String, Method> map = new HashMap<>(); public ListenerInvocationHandler(Object target){ this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (target != null){ String methodName = method.getName(); method = map.get(methodName); long timeSpan = System.currentTimeMillis() - lastClickTime; if (timeSpan < QUICK_EVENT_TIME_SPAN){ Log.e("点击阻塞,防止误点", String.valueOf(timeSpan)); return null; } lastClickTime = System.currentTimeMillis(); if (method != null){ if (method.getGenericParameterTypes().length == 0) return method.invoke(target); return method.invoke(target,args); } } return null; } public void addMethods(String methodName, Method method){ map.put(methodName, method); } } 

这个类实现了InvocationHandler接口,可以实现点击事件不传参数以及点击阻塞,防误点,具体的逻辑比较简单,可以看看代码以及注释。
参考https://www.jianshu.com/p/aa84f0370079
https://blog.csdn.net/testcs_dn/article/details/80225584
阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680

原文链接:https://yq.aliyun.com/articles/727006
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章