Java代理簡述

碼猿手發表於2021-04-24

  1.什麼是代理?

  對類或物件(目標物件)進行增強功能,最終形成一個新的代理物件,(Spring Framework中)當應用呼叫該物件(目標物件)的方法時,實際呼叫的是代理物件增強後的方法,比如對功能方法login實現日誌記錄,可以通過代理實現;

  PS:目標物件--被增強的物件;代理物件--增強後的物件;

  2.為什麼需要代理?

  一些類裡面的方法有相同的程式碼或類中有相同的功能,可以將這些相同抽取出來形成一個公共的方法或功能,但Java有兩個重要的原則:單一職責(對類來說的,即一個類應該只負責一項職責)和開閉原則(開放擴充套件,修改關閉),如果每個類的每個功能都呼叫了公共功能,就破壞了單一職責,如下圖;如果這個類是別人已經寫好的,你動了這個程式碼,同時也破壞了開閉原則(同時改動程式碼很麻煩,裡面可能涉及其他很多的呼叫,可能帶出無數的bug,改程式碼比新開發功能還難/(ㄒoㄒ)/~~);

  由於有上面的問題存在,使用代理來實現是最好的解決辦法;

  3.Java實現代理有哪些?

  (1)靜態代理:通過對目標方法進行繼承或聚合(介面)實現;(會產生類爆炸,因此在不確定的情況下,儘量不要使用靜態代理,避免產生類爆炸)

    1)繼承:代理物件繼承目標物件,重寫需要增強的方法;

//業務類(目標物件)
public class UserServiceImpl {
    public void query(){
        System.out.println("業務操作查詢資料庫....");
    }
}
//日誌功能類
public class Log {
    public static void info(){
        System.out.println("日誌功能");
    }
}
//繼承實現代理(代理物件)
public class UserServiceLogImpl extends UserServiceImpl {
    public void query(){
        Log.info();
        super.query();
    }
}

    從上面程式碼可以看出,每種增強方法會產生一個代理類,如果現在增強方法有日誌和許可權,單個方法增強那需要兩個代理類(日誌代理類和許可權代理類),如果代理類要同時擁有日誌和許可權功能,那又會產生一個代理類,同時由於順序的不同,可能會產生多個類,比如先日誌後許可權是一個代理類,先許可權後日志又是另外一個代理類。

    由此可以看出代理使用繼承的缺陷:產生的代理類過多(產生類爆炸),非常複雜,維護難;

    2)聚合:代理物件和目標物件都實現同一介面,使用裝飾者模式,提供一個代理類構造方法(代理物件當中要包含目標物件),引數是介面,重寫目標方法;

//介面
public interface UserDao {
    void query();
}
//介面實現類:目標物件
public class UserDaoImpl implements  UserDao {
    @Override
    public void query() {
        System.out.println("query......");
    }
}
//日誌功能類
public class Log {
    public static void info(){
        System.out.println("日誌功能");
    }
}
//聚合實現代理:同樣實現介面,使用裝飾者模式;(代理物件)
public class UserDaoLogProxy implements UserDao {
    UserDao userDao;
    public UserDaoLogProxy(UserDao userDao){//代理物件包含目標物件
        this.userDao = userDao;
    }
    @Override
    public void query() {
        Log.info();
        userDao.query();
    }
}
//測試
public static void main(String[] args) {
    UserDaoImpl target = new UserDaoImpl();
    UserDaoLogProxy proxy = new UserDaoLogProxy(target);
    proxy.query();
}

    聚合由於利用了面向介面程式設計的特性,產生的代理類相對繼承要少一點(雖然也是會產生類爆炸,假設有多個Dao,每個Dao產生一個代理類,所以還是會產生類爆炸),如下案例:

//時間記錄功能
public class Timer {
    public static void timer(){
        System.out.println("時間記錄功能");
    }
}
//代理類:時間功能+業務
public class UserDaoTimerProxy implements UserDao {

    UserDao userDao;
    public UserDaoTimerProxy(UserDao userDao){
        this.userDao = userDao;
    }
    @Override
    public void query() {
        Timer.timer();
        userDao.query();
    }
}
//測試
public static void main(String[] args) {
    //timer+query
    UserDao target = new UserDaoTimerProxy(new UserDaoImpl());
    //log+timer+query
    UserDao proxy = new UserDaoLogProxy(target);
    proxy.query();
}
public static void main(String[] args) {
    //log+query
    UserDao target = new UserDaoLogProxy(new UserDaoImpl());
    //timer+log+query
    UserDao proxy = new UserDaoTimerProxy(target);
    proxy.query();
}

  PS:

    裝飾和代理的區別:代理不需要指定目標物件,可以對任何物件進行代理;裝飾需要指定目標物件,所以需要構造方法引數或set方法指定目標物件;

    幾個io的Buffer流(BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter)就是使用了裝飾模式的靜態代理;

public class BufferedReader extends Reader {
    private Reader in;
    ........
    public BufferedReader(Reader in, int sz) {
        super(in);
        if (sz <= 0)
            throw new IllegalArgumentException("Buffer size <= 0");
        this.in = in;
        cb = new char[sz];
        nextChar = nChars = 0;
    }

    public BufferedReader(Reader in) {
        this(in, defaultCharBufferSize);
    }
}

  (2)動態代理:Java有JDK動態代理和CGLIB代理;(Spring Framework通過Spring AOP實現代理,底層還是使用JDK代理和CGLIB代理來實現)

    模擬動態代理:不需要手動建立類檔案(因為一旦手動建立類檔案,會產生類爆炸),通過介面反射生成一個類檔案,然後呼叫第三方的編譯技術,動態編譯這個產生的類檔案成class檔案,然後利用URLclassLoader把這個動態編譯的類載入到jvm中,然後通過反射把這個類例項化。(通過字串產生一個物件實現代理);

    PS:Java檔案 -> class檔案 -> byte位元組(JVM)-> class物件(類物件)-> new(物件);

    所以步驟如下:

      1)程式碼實現一個內容(完整的Java檔案內容,包含包名、變數、構造方法、增強後的方法等),使其通過IO產生一個Java檔案;
      2)通過第三方編譯技術產生一個class檔案;
      3)載入class檔案利用反射例項化一個代理物件出來;
public class ProxyUtil {
    /**
     *  content --->string
     *     |
     *     |生成
     *     v
     *  .java   <-----通過io產生
     *  .class  <-----Java檔案程式設計產生
     *
     *  .new    <-----class檔案反射產生例項物件
     * @return
     */
    public static Object newInstance(Object target){
        Object proxy=null;
        //根據物件獲取介面
        Class targetInf =target.getClass().getInterfaces()[0];
        //獲取介面的所有方法
        //getMethods(),該方法是獲取本類以及父類或者父介面中所有的公共方法(public修飾符修飾的)
        //getDeclaredMethods(),該方法是獲取本類中的所有方法,包括私有的(private、protected、預設以及public)的方法
        Method[] declaredMethods = targetInf.getDeclaredMethods();
        String line="\n";
        String tab ="\t";
        //介面名稱
        String targetInfName = targetInf.getSimpleName();
        String content ="";
        //包位置
        String packageContent = "package com;"+line;
        //介面
        String importContent = "import "+targetInf.getName()+";"+line;
        String clazzFirstLineContent = "public class $Proxy implements "+targetInfName+"{"+line;
        //屬性
        String filedContent  =tab+"private "+targetInfName+" target;"+line;
        //構造方法
        String constructorContent =tab+"public $Proxy ("+targetInfName+" target){" +line
                +tab+tab+"this.target =target;"
                +line+tab+"}"+line;
        //方法
        String methodContent = "";
        for(Method method:declaredMethods){
            //返回值
            String returnTypeName = method.getReturnType().getSimpleName();
            //方法名
            String methodName = method.getName();
            // Sting.class String.class 引數型別
            Class<?>[] parameterTypes = method.getParameterTypes();
            String argsContent = "";
            String paramsContent="";
            int flag = 0;
            for(Class args :parameterTypes){
                //String,引數型別
                String simpleName = args.getSimpleName();
                //String p0,Sting p1,
                argsContent+=simpleName+" p"+flag+",";
                paramsContent+="p"+flag+",";
                flag++;
            }
            if (argsContent.length()>0){
                argsContent=argsContent.substring(0,argsContent.lastIndexOf(",")-1);
                paramsContent=paramsContent.substring(0,paramsContent.lastIndexOf(",")-1);
            }
            methodContent+=tab+"public "+returnTypeName+" "+methodName+"("+argsContent+") {"+line
                    //增強方法先寫死
                    +tab+tab+"System.out.println(\"log\");"+line;
            if(returnTypeName.equals("void")){
                methodContent+=tab+tab+"target."+methodName+"("+paramsContent+");"+line
                        +tab+"}"+line;
            }else{
                methodContent+=tab+tab+"return target."+methodName+"("+paramsContent+");"+line
                        +tab+"}"+line;
            }
        }
        content+=packageContent+importContent+clazzFirstLineContent+filedContent
                +constructorContent+methodContent+"}";
        //生成Java檔案
        File file = new File("D:\\com\\$Proxy.java");
        try{
            if(!file.exists()){
                file.createNewFile();
            }
            //建立
            FileWriter wr = new FileWriter(file);
            wr.write(content);
            wr.flush();
            wr.close();
            //編譯Java檔案
            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

            StandardJavaFileManager fileMgr = compiler.getStandardFileManager(null, null, null);
            Iterable units = fileMgr.getJavaFileObjects(file);

            JavaCompiler.CompilationTask t = compiler.getTask(null, fileMgr, null, null, null, units);
            t.call();
            fileMgr.close();
            //new --> 反射
            URL[] urls = new URL[]{new URL("file:D:\\\\")};
            //載入外部檔案
            URLClassLoader classLoader = new URLClassLoader(urls);
            Class<?> proxyClass = classLoader.loadClass("com.$Proxy");
            Constructor constructor = proxyClass.getConstructor(targetInf);
            //構造方法建立例項
            proxy = constructor.newInstance(target);
            //clazz.newInstance();//根據預設構造方法建立物件
            //Class.forName()
        }catch (Exception e){
            e.printStackTrace();
        }
        return proxy;
    }
}

  自定義代理的缺點:生成Java檔案;動態編譯檔案;需要一個URLClassLoader;涉及到了IO操作,軟體的最終效能體現到了IO操作,即IO操作影響到軟體的效能;

案例:

  1)介面:

public interface UserDao {
     void query();
     void query(String name);
     String getName(String id);
}

  2)實現類:

public class UserService implements UserDao {
    @Override
    public void query() {
        System.out.println("query");
    }

    @Override
    public void query(String name) {
        System.out.println(name);
    }

    @Override
    public String getName(String id) {
        System.out.println("id:"+id);
        return "李四";
    }
}

  3)測試:

public static void main(String[] args) {
    UserDao dao = (UserDao) ProxyUtil.newInstance(new UserService());
    dao.query();
    dao.query("張三");
}
--------結果--------
log
query
log
張三

          

package com;
import com.hrh.dynamicProxy.dao.UserDao;
public class $Proxy implements UserDao{//自定義動態代理生成的檔案
    private UserDao target;
    public $Proxy (UserDao target){
        this.target =target;
    }
    public String getName(String p) {
        System.out.println("log");
        return target.getName(p);
    }
    public void query(String p) {
        System.out.println("log");
        target.query(p);
    }
    public void query() {
        System.out.println("log");
        target.query();
    }
}

  JDK動態代理:基於反射實現,通過介面反射得到位元組碼,然後將位元組碼轉成class,通過一個native(JVM實現)方法來執行;

  案例實現:實現 InvocationHandler 介面重寫 invoke 方法,其中包含一個物件變數和提供一個包含物件的構造方法;

public class MyInvocationHandler implements InvocationHandler {
    Object target;//目標物件
    public MyInvocationHandler(Object target){
        this.target=target;
    }
    /**
     * @param proxy 代理物件
     * @param method 目標物件的目標方法
     * @param args    目標方法的引數
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("log");
        return method.invoke(target,args);
    }
}
public class MyInvocationHandlerTest {
    public static void main(String[] args) {
        //引數: 當前類的classLoader(保證MyInvocationHandlerTest當前類可用)
        //         介面陣列:通過介面反射得到介面裡面的方法,對介面裡面的所有方法都進行代理
        //         實現的InvocationHandler:引數是目標物件
        UserDao jdkproxy = (UserDao) Proxy.newProxyInstance(MyInvocationHandlerTest.class.getClassLoader(),
                new Class[]{UserDao.class},new MyInvocationHandler(new UserService()));
        jdkproxy.query("query");
        //-----結果------
        //log
        //query
    }
}

  下面檢視底層JDK生成的代理類class:

        byte[] bytes = ProxyGenerator.generateProxyClass("$Proxy18", new Class[]{UserDao.class});

        try {
            FileOutputStream fileOutputStream = new FileOutputStream("xxx本地路徑\\$Proxy18.class");
            fileOutputStream.write(bytes);
            fileOutputStream.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }

  或在程式碼的最前面新增下面程式碼:

System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "xxx本地路徑");

  代理類class反編譯後的內容:下面的內容驗證了JDK動態代理為什麼基於聚合(介面)來的,而不能基於繼承來的?因為JDK動態代理底層已經繼承了Proxy,而Java是單繼承,不支援多繼承,所以以介面來實現;

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import com.hrh.dao.UserDao;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy18 extends Proxy implements UserDao {
    private static Method m1;
    private static Method m4;
    private static Method m5;
    private static Method m2;
    private static Method m0;
    private static Method m3;

    public $Proxy18(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final void query(String var1) throws  {
        try {
            super.h.invoke(this, m4, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final void query() throws  {
        try {
            super.h.invoke(this, m5, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String getName(String var1) throws  {
        try {
            return (String)super.h.invoke(this, m3, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m4 = Class.forName("com.hrh.dao.UserDao").getMethod("query", Class.forName("java.lang.String"));
            m5 = Class.forName("com.hrh.dao.UserDao").getMethod("query");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            m3 = Class.forName("com.hrh.dao.UserDao").getMethod("getName", Class.forName("java.lang.String"));
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

  CGLIB代理:藉助asm(一個操作位元組碼的框架)實現代理操作;CGLIB基於繼承來的(前文有CGLIB代理類class的反編譯可以看出);

public class UserService {
    public void query(){
        System.out.println("query");
    }

    public static void main(String[] args) {
        // 通過CGLIB動態代理獲取代理物件的過程
        Enhancer enhancer = new Enhancer();
        // 設定enhancer物件的父類
        enhancer.setSuperclass(UserService.class);
        // 設定enhancer的回撥物件
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                System.out.println("before method run...");
                Object result = proxy.invokeSuper(obj, args);
                System.out.println("after method run...");
                return result;
            }
        });
        //建立代理物件
        UserService bean = (UserService) enhancer.create();
        bean.query();
    }
}
//-----------結果------
before method run...
query
after method run...

  PS:如果只是對專案中一個類進行代理,可以使用靜態代理,如果是多個則使用動態代理;

  關於代理的其他相關知識介紹可參考前文:Spring(11) - Introductions進行類擴充套件方法Spring筆記(3) - SpringAOP基礎詳解和原始碼探究