AOP基礎(chǔ)之動態(tài)代理

AOP即面向切面編程,實現(xiàn)的方式有很多,這篇文章主要介紹一下動態(tài)代理實現(xiàn)AOP的方式。主要從動態(tài)代理的原理進行分析。

1. jdk自帶動態(tài)代理

代理分為靜態(tài)代理和動態(tài)代理,靜態(tài)代理這里就不多說了,直接看動態(tài)代理,下面看個小例子。
假設(shè)有個Login接口和LoginImpl實現(xiàn)類

public interface Login {
   boolean login(String userName,String passwd);
}
public class LoginImpl implements Login{
    @Override
    public boolean login(String userName, String passwd) {
        System.out.println("userName:"+userName+"passwd:"+passwd);
        return true;
    }
}

現(xiàn)在我們對login方法進行動態(tài)代理。動態(tài)代理實現(xiàn)方式有兩種,jdk自帶的動態(tài)代理和cglib方式。jdk自帶的動態(tài)代理方式要求被代理對象必須實現(xiàn)至少一個接口,cglib則沒有這個限制。但是cglib也有其自身的限制,就是被代理對象不能是final修飾的,同時final修飾的方法也是不能被代理的。看到這里,可能有些讀者已經(jīng)明白了其中的道理,其實試想一下,我們?nèi)绻獙崿F(xiàn)對某個對象的代理,就要能拿到被代理對象的方法,大致有以下兩種思路:

  • 通過接口,被代理類和代理類均實現(xiàn)相同的接口,代理類通過接口可以很輕松的拿到被代理類的方法。
  • 繼承的方式,如果代理類繼承了被代理類,那么很明顯,通過子類進行方法增強,可以達到aop的目的,但是final類不能被繼承。

猜測jdk自帶的動態(tài)代理應(yīng)該采用的是思路一,cglib應(yīng)該采用的是思路二,是不是這樣呢,我們?nèi)ヌ剿饕幌麓鸢福葋砜磈dk自帶的動態(tài)代理模式。實現(xiàn)起來很簡單,首先需要實現(xiàn)InvocationHandler接口

public class ProxyHandler implements InvocationHandler{
    private Object target;
    public ProxyHandler(Object obj){
        this.target=obj;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object ret=null;
        System.out.println("before->"+method.getName());
        ret=method.invoke(target, args);
        System.out.println("after->"+method.getName());
        return ret;
    }
}

接著通過Proxy的newProxyInstance方法創(chuàng)建Proxy對象

public class Main {
     public static void main(String[] args)throws Exception 
        {  
            LoginImpl imp=new LoginImpl();
            Login login=(Login)Proxy.newProxyInstance(imp.getClass().getClassLoader(), imp.getClass().getInterfaces(), new ProxyHandler(imp));
            boolean ret=login.login("lanjunjian", "1234");
            System.out.println(ret);
      }
}

結(jié)果如下:可見我們成功的實現(xiàn)了動態(tài)代理。

before->login
userName:lanjunjian    passwd:1234
after->login
true

下面我們看一下動態(tài)代理是怎么實現(xiàn)的。動態(tài)代理其實就涉及到兩個類,InvocationHandler和Proxy。InvocationHandler是個接口,只包含invoke方法,這里沒什么可看的,直接查看下Proxy的newProxyInstance方法。

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        if (h == null) {
            throw new NullPointerException();
        }
        Class cl = getProxyClass(loader, interfaces);
        try {
            Constructor cons = cl.getConstructor(constructorParams);
            return (Object) cons.newInstance(new Object[] { h });
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString());
        } catch (IllegalAccessException e) {
            throw new InternalError(e.toString());
        } catch (InstantiationException e) {
            throw new InternalError(e.toString());
        } catch (InvocationTargetException e) {
            throw new InternalError(e.toString());
        }
    }

看起來很清晰,通過getProxyClass方法生成了代理對象的Class,然后調(diào)用代理對象的只含InvocationHandler的構(gòu)造函數(shù)生成實例。接著看一下getProxyClass方法。

private final static String proxyClassNamePrefix = "$Proxy";
String proxyName = proxyPkg + proxyClassNamePrefix + num;
public static Class<?> getProxyClass(ClassLoader loader,
                                       Class<?>... interfaces)
      throws IllegalArgumentException
  {
      if (interfaces.length > 65535) {
          throw new IllegalArgumentException("interface limit exceeded");
      }
      Class proxyClass = null;
      Set interfaceSet = new HashSet();       // for detecting duplicates
      for (int i = 0; i < interfaces.length; i++) {
          String interfaceName = interfaces[i].getName();
          Class interfaceClass = null;
          try {
              interfaceClass = Class.forName(interfaceName, false, loader);
          } catch (ClassNotFoundException e) {
          }
          if (interfaceClass != interfaces[i]) {
              throw new IllegalArgumentException(
                  interfaces[i] + " is not visible from class loader");
          }
          interfaceSet.add(interfaceClass);
          interfaceNames[i] = interfaceName;
      }
            ...
            ...
          byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                  proxyName, interfaces);
           proxyClass = defineClass0(loader, proxyName,
                      proxyClassFile, 0, proxyClassFile.length);
          proxyClasses.put(proxyClass, null);
          ...
          ...
      return proxyClass;
  }

篇幅原因,省略了部分代碼,代碼很清晰,沒有太多可以解釋的,主要是進行收集接口,然后轉(zhuǎn)交給ProxyGenerator的generateProxyClass生成字節(jié)碼的byte數(shù)組。ProxyGenerator類并不屬于J2SE規(guī)范,代碼位于sun.misc包下。我們大致看一下ProxyGenerator源碼,字節(jié)碼生成的過程是在generateClassFile中完成的。

  private final static boolean saveGeneratedFiles =
        java.security.AccessController.doPrivileged(
            new GetBooleanAction(
                "sun.misc.ProxyGenerator.saveGeneratedFiles")).booleanValue();

    /**
     * Generate a proxy class given a name and a list of proxy interfaces.
     */
    public static byte[] generateProxyClass(final String name,
                                            Class[] interfaces)
    {
        ProxyGenerator gen = new ProxyGenerator(name, interfaces);
        final byte[] classFile = gen.generateClassFile();

        if (saveGeneratedFiles) {
            java.security.AccessController.doPrivileged(
            new java.security.PrivilegedAction() {
                public Object run() {
                    try {
                        FileOutputStream file =
                            new FileOutputStream(dotToSlash(name) + ".class");
                        file.write(classFile);
                        file.close();
                        return null;
                    } catch (IOException e) {
                        throw new InternalError(
                            "I/O exception saving generated file: " + e);
                    }
                }
            });
        }
        return classFile;
    }
 private byte[] generateClassFile() {
        addProxyMethod(hashCodeMethod, Object.class);
        addProxyMethod(equalsMethod, Object.class);
        addProxyMethod(toStringMethod, Object.class);
        for (int i = 0; i < interfaces.length; i++) {
            Method[] methods = interfaces[i].getMethods();
            for (int j = 0; j < methods.length; j++) {
                addProxyMethod(methods[j], interfaces[i]);
            }
        }
        for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
            checkReturnTypes(sigmethods);
        }
        try {
            methods.add(generateConstructor());
            for (List<ProxyMethod> sigmethods : proxyMethods.values()) { 
                for (ProxyMethod pm : sigmethods) {
                    // add static field for method's Method object
                    fields.add(new FieldInfo(pm.methodFieldName,
                        "Ljava/lang/reflect/Method;",
                         ACC_PRIVATE | ACC_STATIC));
                    methods.add(pm.generateMethod());
                }
            }
            methods.add(generateStaticInitializer());
        } catch (IOException e) {
            throw new InternalError("unexpected I/O Exception");
        }

        if (methods.size() > 65535) {
            throw new IllegalArgumentException("method limit exceeded");
        }
        if (fields.size() > 65535) {
            throw new IllegalArgumentException("field limit exceeded");
        }
        cp.getClass(dotToSlash(className));
        cp.getClass(superclassName);
        for (int i = 0; i < interfaces.length; i++) {
            cp.getClass(dotToSlash(interfaces[i].getName()));
        }
        cp.setReadOnly();
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        DataOutputStream dout = new DataOutputStream(bout);
        try {
            dout.writeInt(0xCAFEBABE);
            dout.writeShort(CLASSFILE_MINOR_VERSION);
            dout.writeShort(CLASSFILE_MAJOR_VERSION);
            cp.write(dout);  
            dout.writeShort(ACC_PUBLIC | ACC_FINAL | ACC_SUPER);
            dout.writeShort(cp.getClass(dotToSlash(className)));
            dout.writeShort(cp.getClass(superclassName));
            dout.writeShort(interfaces.length);
                                        // u2 interfaces[interfaces_count];
            for (int i = 0; i < interfaces.length; i++) {
                dout.writeShort(cp.getClass(
                    dotToSlash(interfaces[i].getName())));
            }
            dout.writeShort(fields.size());
            for (FieldInfo f : fields) {
                f.write(dout);
            }
            dout.writeShort(methods.size());
            for (MethodInfo m : methods) {
                m.write(dout);
            }
            dout.writeShort(0); // (no ClassFile attributes for proxy classes)
        } catch (IOException e) {
            throw new InternalError("unexpected I/O Exception");
        }
        return bout.toByteArray();
    }

generateClassFile展示了Proxy類生成的全過程,包括了接口聲明的所有方法,此外還包括Object類中的三個方法hashCode,equals,toString方法。具體的addProxyMethod這里就不分析了,我們可以想辦法拿到動態(tài)代理生成的字節(jié)碼。動態(tài)代理是在運行期進行的,默認是不保存字節(jié)碼文件的,怎么保存字節(jié)碼呢?我們看到上文中g(shù)enerateProxyClass方法會判斷saveGeneratedFiles是否為true。ok,很明顯我們在代理前將這個變量設(shè)置為true即可。我們運行前加入如下代碼,可在工程根目錄下得到包名為com.sun.proxy的Proxy[num].class文件,num一般情況下為0。

public static void saveGeneratedJdkProxyFiles() throws Exception {
            Field field = System.class.getDeclaredField("props");
            field.setAccessible(true);
            Properties props = (Properties) field.get(null);
        props.put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
}

下面我們反編譯一下$Proxy0.class。

public final class $Proxy0 extends Proxy implements Login
{
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;
    public $Proxy0(final InvocationHandler invocationHandler) {
        super(invocationHandler);
    }
    public final boolean equals(final Object o) {
        try {
            return (boolean)super.h.invoke(this, $Proxy0.m1, new Object[] { o });
        }
        catch (Error | RuntimeException t) {
            throw t;
        }
        catch (Throwable t2) {
            throw new UndeclaredThrowableException(t2);
        }
    }
    public final String toString() {
        try {
            return (String)super.h.invoke(this, $Proxy0.m2, null);
        }
        catch (Error | RuntimeException t) {
            throw t;
        }
        catch (Throwable t2) {
            throw new UndeclaredThrowableException(t2);
        }
    }
    public final boolean login(final String s, final String s2) {
        try {
            return (boolean)super.h.invoke(this, $Proxy0.m3, new Object[] { s, s2 });
        }
        catch (Error | RuntimeException t) {
            throw t;
        }
        catch (Throwable t2) {
            throw new UndeclaredThrowableException(t2);
        }
    }
    public final int hashCode() {
        try {
            return (int)super.h.invoke(this, $Proxy0.m0, null);
        }
        catch (Error | RuntimeException t) {
            throw t;
        }
        catch (Throwable t2) {
            throw new UndeclaredThrowableException(t2);
        }
    }
    static {
        try {
            $Proxy0.m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            $Proxy0.m2 = Class.forName("java.lang.Object").getMethod("toString", (Class<?>[])new Class[0]);
            $Proxy0.m3 = Class.forName("com.ljj.Login").getMethod("login", Class.forName("java.lang.String"), Class.forName("java.lang.String"));
            $Proxy0.m0 = Class.forName("java.lang.Object").getMethod("hashCode", (Class<?>[])new Class[0]);
        }
        catch (NoSuchMethodException ex) {
            throw new NoSuchMethodError(ex.getMessage());
        }
        catch (ClassNotFoundException ex2) {
            throw new NoClassDefFoundError(ex2.getMessage());
        }
    }
}

可以看到$Proxy繼承了Proxy類并實現(xiàn)了Login接口,在靜態(tài)代碼塊中加載了所有需要代理的方法,方法的調(diào)用都是通過InvocationHandler的invoke方法轉(zhuǎn)發(fā)的。這里也看出來了由于java的單繼承的限制,jdk自帶的動態(tài)代理是無法代理普通類的,換句話說即使某個類實現(xiàn)了某個接口,但主要不是接口內(nèi)定義的方法都是無法進行代理的。此外,動態(tài)代理后方法的調(diào)用只能通過反射來進行,性能上會有一些開銷。

2. cglib動態(tài)代理

為了彌補jdk自帶動態(tài)代理的限制,出現(xiàn)了cglib,可以實現(xiàn)類的動態(tài)代理,像spring框架就是jdk動態(tài)代理和cglib結(jié)合進行的,jdk動態(tài)代理搞不定的利用cglib進行,cglib引入了ASM庫來進行底層字節(jié)碼生成。這里簡單的介紹一下。
同樣定義一個Login類,不實現(xiàn)任何接口

public class Login {
    public boolean login(String name,String passwd){
        System.out.println("name->:"+name+"passwd->:"+passwd);
        return false;
    }
}

第一步實現(xiàn)一個MethodInterceptor類似于InvocationHandler

public class Hacker implements MethodInterceptor{
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // TODO Auto-generated method stub
        System.out.println("xxxxxxbefore");
        Object ret=proxy.invokeSuper(obj, args);
        System.out.println("xxxxxxafter");
        return ret;
    }
}

第二步直接調(diào)用

 public static void main(String[] args)throws Exception 
        {    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/Users/ljj/Documents/workspace/HookTest"); 
         Test1 test=new Test1();
         Hacker hacker=new Hacker();
         Enhancer enhancer=new Enhancer();
         enhancer.setSuperclass(test.getClass());
         enhancer.setCallback(hacker);
         Test1 proxy=(Test1)enhancer.create();
         proxy.login("lanjunjian", "12346");
        }

由于android中加載的是dex文件,不是class文件,cglib不支持android系統(tǒng),所以cglib的具體實現(xiàn)過程就不詳細說了。具體原理可以參考這篇文章說說cglib動態(tài)代理。簡單理解cglib是采用繼承的方式進行代理。生成的代理類是繼承自被代理類的。

public class Test1$$EnhancerByCGLIB$$d42d7d8c extends Test1 implements Factory

此外,cglib采用了FastClass機制,F(xiàn)astClass就是根據(jù)方法簽名保存了代理類和被代理類的索引信息,然后為每個方法生成一個MethodProxy,proxy中有Invoke和invokeSuper兩個方法,當(dāng)我們調(diào)用invokeSuper時,根據(jù)方法簽名去FastClass可以找到被代理類的方法,然后直接進行調(diào)用。所以cglib和動態(tài)代理很大的區(qū)別是 cglib使用的是直接調(diào)用,jdk是利用的反射。也有人利用dexmaker實現(xiàn)了android上的cglib,項目地址:MethodInterceptProxy。

3.動態(tài)代理在android上的簡單應(yīng)用

代理可以理解為是hook的一種手段,例如插件框架中替換Instrumention,實際上采用的是靜態(tài)代理的方式,但是很多情況下,接口或類可能是hide的,我們無法通過繼承或者接口實現(xiàn)等方式構(gòu)造代理類,這種情況下我們就沒法使用靜態(tài)代理,可以酌情考慮動態(tài)代理。
我認為進行動態(tài)代理最大的難點在于hook的點很難找,主要能找到hook點,一切也好辦。一般情況下,hook點最好是靜態(tài)或者是單例,有些時候很難找到實例對象,而且往往我們都需要借助反射來獲取被代理對象。下面就以發(fā)送通知為例,假設(shè)我要攔截每次發(fā)送通知的內(nèi)容該怎么做呢?

 Intent intent=new Intent();
 Notification build = new Notification.Builder(this)
     .setContentTitle("hook")
     .setContentText("攔截通知")
     .setAutoCancel(true)
     .setSmallIcon(R.mipmap.ic_launcher)
     .setWhen(System.currentTimeMillis())
     .setContentIntent(PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
     .build();
 NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
 manager.notify((int) (System.currentTimeMillis()/1000L), build);

上一段是我們典型的發(fā)送通知的代碼。整個發(fā)送的過程是由NotificationManager來控制的,我們知道通知的發(fā)送是一個跨進程的操作,這里由于篇幅原因,不去詳細談Binder相關(guān)的內(nèi)容,只是為了從主觀上感受下動態(tài)代理在android方面怎么使用。

 public void notifyAsUser(String tag, int id, Notification notification, UserHandle user)
    {
        int[] idOut = new int[1];
        INotificationManager service = getService();
        String pkg = mContext.getPackageName();
        // Fix the notification as best we can.
        Notification.addFieldsFromContext(mContext, notification);
        if (notification.sound != null) {
            notification.sound = notification.sound.getCanonicalUri();
            if (StrictMode.vmFileUriExposureEnabled()) {
                notification.sound.checkFileUriExposed("Notification.sound");
            }
        }
        fixLegacySmallIcon(notification, pkg);
        if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.LOLLIPOP_MR1) {
            if (notification.getSmallIcon() == null) {
                throw new IllegalArgumentException("Invalid notification (no valid small icon): "
                        + notification);
            }
        }
        if (localLOGV) Log.v(TAG, pkg + ": notify(" + id + ", " + notification + ")");
        final Notification copy = Builder.maybeCloneStrippedForDelivery(notification);
        try {
            service.enqueueNotificationWithTag(pkg, mContext.getOpPackageName(), tag, id,
                    copy, idOut, user.getIdentifier());
            if (localLOGV && id != idOut[0]) {
                Log.v(TAG, "notify: id corrupted: sent " + id + ", got back " + idOut[0]);
            }
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

當(dāng)調(diào)用manager.notify時,會調(diào)用到NotificationManager的notifyAsUser方法,可以看到整個發(fā)送流程都是通過INotificationManager接口進行的。一看到接口感覺應(yīng)該可以做點什么,我們進一步看一下getService方法。

private static INotificationManager sService;
    /** @hide */
    static public INotificationManager getService()
    {
        if (sService != null) {
            return sService;
        }
        IBinder b = ServiceManager.getService("notification");
        sService = INotificationManager.Stub.asInterface(b);
        return sService;
    }

首先在ServiceManager通過getService方法獲取到了一個原聲的IBinder對象,然后通過AIDL機制由asInterface方法轉(zhuǎn)換成了本地的代理對象,INotificationManager是一個由AIDL接口生成的本地代理對象,正好sService是一個static變量,我們通過反射獲取到該對象然后替換成我們的Proxy是不是就能實現(xiàn)通知的攔截了呢?想一下我們動態(tài)代理的實現(xiàn)過程,操作一下。我們需要三個要素,接口的Class對象,獲取被代理對象和一個InvocationHandler的子類。。

  • 獲取接口class對象很簡單,反射即可。
Class<?> INotificationManagerClazz = Class.forName("android.app.INotificationManager");
  • 獲取被代理對象,也就是要拿到sService,可以通過反射拿到,我們可以通過發(fā)射sService變量拿到,也可以通過反射調(diào)用getService方法獲取,如果直接發(fā)射sService變量,此時有可能獲取到的為null,所以采用反射getService方法獲取。
 NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
      Method method = notificationManager.getClass().getDeclaredMethod("getService");
      method.setAccessible(true);
      final Object sService = method.invoke(notificationManager);
  • InvocationHandler的子類直接實現(xiàn)一個即可。
    準(zhǔn)備工作做完了,我們直接進行代理的生成,同時要記得用proxy替換原來的sService,所有的工作就完成了。
 Object proxy = Proxy.newProxyInstance(INotificationManagerClazz.getClassLoader(),
          new Class[]{INotificationManagerClazz},new ProxyHandler(sService));
      Field target = notificationManager.getClass().getDeclaredField("sService");
      target.setAccessible(true);
      target.set(notificationManager, proxy);

我們怎么攔截內(nèi)容呢,觀察notifyAsUser方法中會調(diào)用enqueueNotificationWithTag方法,我們只需要攔截這個方法即可。

class ProxyHandler implements InvocationHandler {
    private Object mObject;
    public ProxyHandler(Object mObject) {
      this.mObject = mObject;
    }
    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if (method.getName().equals("enqueueNotificationWithTag")) {
        for (int i = 0; i < args.length; i++) {
          if(args[i] instanceof Notification){
            Notification notification=(Notification)(args[i]);
            String content=notification.extras.getString(Notification.EXTRA_TEXT);
            Log.i("ljj", "invoke: "+content);
          }
        }
        return method.invoke(mObject, args);
      }
      return null;
    }
  }

這里說明一下,通知的內(nèi)容在不同版本里獲取方式不太一樣,這里只是為了直觀的體現(xiàn)動態(tài)代理的作用,沒有進行適配,下面給出完整的hook代碼。

  public  void hookNotificationContent(Context context) {
    try {
      NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
      Method method = notificationManager.getClass().getDeclaredMethod("getService");
      method.setAccessible(true);
      final Object sService = method.invoke(notificationManager);//獲取到Nofificiaton原來的sService對象
      Class<?> INotificationManagerClazz = Class.forName("android.app.INotificationManager");
      Object proxy = Proxy.newProxyInstance(INotificationManagerClazz.getClassLoader(),
          new Class[]{INotificationManagerClazz},new ProxyHandler(sService));
      Field target = notificationManager.getClass().getDeclaredField("sService");
      target.setAccessible(true);
    target.set(notificationManager, proxy);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

最后總結(jié)一下,這篇文章比較基礎(chǔ),主要想搞明白以下三個知識點,希望對大家有所幫助。

  • jdk動態(tài)代理的原理以及為什么只能對接口做代理
  • cglib與jdk動態(tài)代理的區(qū)別
  • 在android的應(yīng)用場景

參考文獻

  1. java動態(tài)代理機制詳解
  2. Android插件化開發(fā)-hook 系統(tǒng)服務(wù)(通過binder修改粘貼板服務(wù)行為)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容