Java动态代理运用之实现Xutils注入模块

一.前言
通过上一篇文章的学习,相信我们对于动态代理有了相对深刻的认识,今天我们会通过使用动态代理完成Xutils中的注入。
二.代理对象的功能
通过代理对象我们可以实现的两种功能,一种是功能增强,另一种是方法的拦截,其实功能增强也是通过拦截方法来实现的。因为代理对象和被代理对象实现了相同的接口,所有他们的类型是完全相同的,运用这一点我们可以在需要的时候偷换对象,来完成一些特殊的功能
三.项目分析
我们要完成三个工作,布局注入,视图注入和事件的注入,其中前两个非常简单,重点是第三个工作。我们需要让我们的Activity实现BaseActivity,重写onCreate方法在这个方法中调用 ViewInjectUtil.inject(this);方法,在这个方法中完成所有的注入,在Activity被创建的时候回调用父类的方法,所以回提前完成所有的注入。
看ViewInjectUtil的代码。

public class ViewInjectUtil {
          public static void inject(Activity activity){
              ViewLayoutInject.getInstance().inject(activity);
              ViewInstanceInject.getInstance().inject(activity);
              ViewEventInject.getInstance().inject(activity);
          }
}

在这个方法中分别调用ViewLayoutInject、ViewInstanceInject和ViewEventInject的注入方法,他们分别代表布局注入,视图注入和事件注入,他们继承自BaseInject抽象类中,类中只有一个方法inject(Activity t)方法是抽取出来的通用方法。
(1).布局注入

public  void inject(Activity t) {
        // TODO Auto-generated method stub
        Class calzz=t.getClass();
        Annotation annotation = calzz.getAnnotation(LayoutInject.class);
        if (annotation==null) {
            return;
        }
        LayoutInject layoutInject=(LayoutInject) annotation;
        int layoutVlaue=layoutInject.layout();
        Method method=null;
        try {
            method=calzz.getDeclaredMethod(METHOD_NAME, new Class[]{int.class});
            method.invoke(t, layoutVlaue);
        }catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

布局注入的代码,来看布局注入的注解类

@Retention(value =RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE })
public @interface LayoutInject {
           int layout();
}

这两段代码都非常简单,首先从Activity的实例中反向获取字节码对象,从字节码对象中获取对应的作用在类上的LayoutInject注解,然后从注解中获取值,这便是我们要注入的布局id。然后从字节码中获取setContentView方法的Method对象,调用invoke方法,传入作用对象和布局参数即可。
(2).视图注入

public void inject(Activity activity) {
        // TODO Auto-generated method stub
        Class clazz=(Class) activity.getClass();
        Field[] fields=clazz.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            Field field=fields[i];
            Annotation annotation=field.getAnnotation(ViewInject.class);
            if (annotation!=null) {
               ViewInject viewInjectAnnotation=(ViewInject) annotation;
               int viewId=viewInjectAnnotation.value();
            try {
                Method findMethod=clazz.getMethod(METHOD_NAME, int.class);
                Object view=findMethod.invoke(activity, viewId);
                field.setAccessible(true);
                field.set(activity, view);
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            }
        }
    }

视图注解类

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject {
      int value();
}

视图注入的思路和布局注入基本相似,通过Activity实例反向获取对应的字节码对象,然后拿到所有的Field对象,然后获得作用到Field对象上的注解,如果该注解是ViewInject类型的则获取注解中的值,这便是我们要注入的控件的id,之后获取findViewById方法的Method对象,调用invoke方法获取返回的对象,这就是我们要查找的view对象。最后,调用fieid对象的set方法设置对应的值。
(3).事件的注入
控件的事件类型有setOnClickListener,setOnLoneClickListener类型,对应的接口是OnClickListener和OnLongClickListener,对应的回调方法是onClick和onLongClick方法。通过观察我们可以发现,注入事件的三要素为setXXX方法、接口、onXXX方法,当我们确定了这三个要素便确定了一个点击事件,所以我们可以提取这三个要素作为一个注解。

@Target(ElementType.ANNOTATION_TYPE)
@Retention(value =RetentionPolicy.RUNTIME)
public @interface BaseEvent {
     //onClick,onLongClick
     String callBackName();
     //setOnClickListener setOnLongClickListener
     String viewMethodName();
     //OnClickListener.class
     Class eventInterface();
}
上面是我们定义的BaseEvent注解,分别包含了事件的三个要素,对于一种事件我们需要定义一个对应的注解,并且我们需要把基本注解信息作用在对应的注解上,这里我们使用注解的注解。
@Target(value = { ElementType.METHOD})
@Retention(value =RetentionPolicy.RUNTIME)
@BaseEvent(eventInterface =OnClickListener.class, 
callBackName = "onClick", viewMethodName = "setOnClickListener")
public @interface ClickEvent {
      int value();
}
这是我们定义的onClick注解,在这个注解上我们使用BaseEvent指定事件的三要素,这样非常方便扩展,当我们需要定义OnLongClick注解时,只需要改变相应的参数即可,而没必要把这些参数全部放到Activity中去指定,那样会导致大量的重复。
    @ClickEvent(value =R.id.button)
    public void method1(View view){
        Log.d("ViewEventInject", "事件注入成功");
    }

事件注入的目的是对于在方法上作用ClickEvent注解,当点击button时,将调用method1方法,我们首先来看看通常的方法是如何做的。

button.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                // TODO Auto-generated method stub

            }
        });
当我们点击按钮时,会首先调用我们设置的实现OnClickListener接口的对象是否为null,如果不为null,则会调用对应的OnClick方法,并传递View对象作为参数。在这里我们只要能监听到onClick方法被调用然后通过Activity的字节码对象获取method1方法的Method对象,然后调用invoke方法传入View对象即可。
第一种方式我们可以自己维护一个OnClickListener接口的实现类,然后获取到注入方法上的注解,获取对应的View对象,获取view对象的字节码对象,获取setOnClickListener方法的Method对象,调用该方法传入我们维护的OnClickListener接口的实现类,然后在把id和对应的注入的Method对象放入到一个Map中,在onClick方法中我们便可以调用Method方法,从而完成了方法的注入。
第二种方式,第一种方式虽然可行但是如果事件类型比较多,会产生大量的接口实现类,这和我们上篇文章中的案例相似,所以我们可以使用动态代理来实现。 如果使用动态代理那么被代理类又是谁呢,需要为每一个事件类型创建一个对应的被代理类吗。其实是不需要的,经过上篇文章的分析可以知道,使用JDK动态代理机制创建代理类是不依赖于被代理类,它依赖的是被代理类所实现的接口,甚至在调用被代理类的方法时所使用的Method对象也是接口中的Method对象。其次由于代理类实现类同样的接口,所以代理类可以作为setOnClickListener方法的参数。在这里并不需要被代理类,我们只需要监听到onClick方法被调用即可。
首先来看EventInvocationHandler的代码
public class EventInvocationHandler implements InvocationHandler {
    //要切入的方法,在Activity中,所以对象是Activity
    private Method method;
    private Object receiver;
    public EventInvocationHandler(Method method,Object receiver) {
        // TODO Auto-generated constructor stub
        this.method=method;
        this.receiver=receiver;
    }
    //当调用代理监听对象的OnClick方法时,会调用到这个方法,做方法拦截
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        // TODO Auto-generated method stub
        //插入方法
        this.method.invoke(receiver, args);
        return proxy;
    }

}
 这里的代码非常简单,invoke方法中只有一行代码,this.method.invoke(receiver, args)这行代码其实调用的是Activity中的method1方法,当事件触发,我们传入的代理类的onClick方法会被调用,由我们上篇中的伪代码可以知道,此时会封装参数调用incoke方法,当我们拦截到对应的方法的时候,就可以替换成自己定义的方法了。
 来看ViewEventInject方法的实现
public void inject(Activity activity) {
        // TODO Auto-generated method stub
        Class clazz=(Class) activity.getClass();
        Method[] methods = clazz.getDeclaredMethods();
        for (int i = 0; i < methods.length; i++) {
            Method method=methods[i];
            Annotation[] annotations = method.getAnnotations();
            for (int j = 0; j < annotations.length; j++) {
                Annotation annotation=annotations[j];

                Class annotationClazz=annotation.annotationType();
                //判断该方法上时候作用了ClickEvent注解
                if (!method.isAnnotationPresent(ClickEvent.class)) {
                    //不是ClickEvent 或者OnClickEvent 不处理

                    continue;
                }
                ClickEvent clickEvent=(ClickEvent) annotation;
                int viewId=clickEvent.value();

                BaseEvent baseEvent=(BaseEvent) annotationClazz.getAnnotation(BaseEvent.class);
               //取出事件的三要素
               Class eventClazz=baseEvent.eventInterface();
               String eventMethodName=baseEvent.callBackName();
               String viewMethodName = baseEvent.viewMethodName();

               View view=activity.findViewById(viewId);
               //生成代理对象,事件监听对象的代理对象
               EventInvocationHandler eventInvocationHandler=new EventInvocationHandler(method,activity);
               Object proxy=Proxy.newProxyInstance(eventClazz.getClassLoader(), new Class[]{eventClazz}, eventInvocationHandler);
               Method setMethod;
            try {
                setMethod = view.getClass().getMethod(viewMethodName,eventClazz);
                setMethod.invoke(view, proxy);
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            }
        }
    }
 代码思路,拿到Activity的字节码,获取所有的方法,获得方法上有ClickEvent注解作用的方法,获取ClickEvent注解中的值,这是我们要注入事件的控件的id,获取注解上的注解即事件三要素。通过id获取View对象,调用Proxy.newProxyInstance(eventClazz.getClassLoader(), new Class[]{eventClazz}, eventInvocationHandler);方法。类加载器是三要素中的接口字节码的类加载器,接口字节码也是注解三要素中取出的,然后是EventInvocationHandler对象。接下来是绑定监听的方法,从view的字节码中获取对应的setXXX方法,这个方法名称也是三要素中的,然后获取Method对象,调用invoke方法传入view对象作为目标对象,参数是我们生成的代理对象,

到这里所有的注入工作就已经完成了。
四.总结
经过这几篇文章的学习,使我们对Java中的反射、泛型和注解以及动态d代理模式有了相对深刻的理解,有需要今天代码的同学可以下载我们的代码来学习,有说错的地方也可以指出共同学习。
代码下载

你可能感兴趣的:(Java)