Небольшой подробный рассказ о динамическом прокси JDK

Java

При обращении в SpringAOP все будут убеждены в этой волшебной функции и захотят узнать загадку и то, как реализован нижний слой. Итак, через поисковик все узнают странное существительное:Динамический прокси, и постепенно узнал, что есть много способов реализовать динамический прокси, напримерДинамический прокси JDK,Cglibи т.п. Сегодня я просто скажуДинамический прокси JDK.

Простое применение динамического прокси JDK

Начнем с самого простого примера:

Сначала нам нужно определить интерфейс:

public interface UserService {
    void query();
}

Затем реализуйте этот интерфейс:

public class UserServiceImpl implements UserService {
    public void query() {
        System.out.println("查询用户信息");
    }
}

Чтобы определить класс, вам нужно реализовать InvocationHandler:

public class MyInvocationHandler implements InvocationHandler {

    Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("进入了invoke");
        method.invoke(target);
        System.out.println("执行了invoke");
        return null;
    }
}

Затем есть основной метод:

public class Main {
    public static void main(String[] args) {
        MyInvocationHandler myInvocationHandler = new MyInvocationHandler(new UserServiceImpl());
        Object o = Proxy.newProxyInstance(Main.class.getClassLoader(),
                new Class[]{UserService.class}
                , myInvocationHandler);

        ((UserService)o).query();
    }
}

бегать:

image.png

Видно, что все нормально, расширенная логика успешно выполняется, целевой метод тоже выполняется.

три сомнения

Хотя это простейший пример, когда я был новичком, у всех наверняка были такие же сомнения, как и у меня: во-первых, я не знаю, зачем нужен входящий интерфейс, а во-вторых, я не знаю, почему динамический прокси JDK может только проксировать интерфейс. В-третьих, не знаю роль загрузчика классов. Кроме того, код более сложный.

Эти три сомнения беспокоили меня долгое время, пока я не проследил за блогом, не создал сам кастрированную версию динамического прокси JDK и просто не посмотрел код и исходный код, окончательно сгенерированный JDK.

Напишите кастрированную версию динамического прокси JDK

Давайте сначала проанализируем метод вызова в классе MyInvocationHandler.Метод имеет три параметра.Первый параметр — это прокси-класс, второй параметр — это метод, а третий параметр — параметр, необходимый для выполнения метода. Внутри метода реализованы две логики: одна — логика расширения, а другая — выполнение целевого метода. Мы не можем отделаться от мысли, что если мы можем автоматически сгенерировать класс и вызвать метод вызова в MyInvocationHandler, мы можем реализовать динамический прокси.

Как смел человек, как продуктивна земля, это действительно смелая и безумная идея, но ее можно реализовать, в основном, за следующие шаги:

  1. Код для объединения прокси-классов
  2. выходной файл .java
  3. Скомпилируйте файлы .java в файлы .class
  4. Загрузите файл .class
  5. Создать и вернуть объект прокси-класса

Для удобства, независимо от возвращаемого значения и параметров, я написал кастрированную версию класса MockInvocationHandler на основе существующего MyInvocationHandler:

public class MockInvocationHandler {

    private Object targetObject;

    public MockInvocationHandler(Object targetObject) {
        this.targetObject = targetObject;

    }

    public void invoke(Method targetMethod) {
        try {
            System.out.println("进入了invoke");
            targetMethod.invoke(targetObject, null);
            System.out.println("结束了invoke");
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

Чтобы вызвать метод вызова в MockInvocationHandler, сгенерированный прокси-класс может выглядеть следующим образом:

public class $Proxy implements 需要代理的接口{
	 MockInvocationHandler h;
	 public $Proxy (MockInvocationHandler h ) {this.h = h; }
	 public void query(){
	  try{ 
		//method=需要的执行方法
		 this.h.invoke(method);
		}catch(Exception ex){}
	}
}

Ну и следующий шаг - физическая работа, вставляем код напрямую:

public class MockProxy {

    final static String ENTER = "\n";
    final static String TAB = "\t";

    public static Object newProxyInstance(Class interfaceClass,MockInvocationHandler h) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("package com.codebear;");
        stringBuilder.append(ENTER);
        stringBuilder.append("import java.lang.reflect.*;");
        stringBuilder.append(ENTER);
        stringBuilder.append("public class $Proxy implements " + interfaceClass.getName() + "{");
        stringBuilder.append(ENTER);
        stringBuilder.append(TAB);
        stringBuilder.append(" MockInvocationHandler h;");
        stringBuilder.append(ENTER);
        stringBuilder.append(TAB);
        stringBuilder.append(" public $Proxy (MockInvocationHandler h ) {this.h = h; }");
        stringBuilder.append(ENTER);
        stringBuilder.append(TAB);
        for (Method method : interfaceClass.getMethods()) {
            stringBuilder.append(" public void " + method.getName() + "(){");
            stringBuilder.append(ENTER);
            stringBuilder.append(TAB);
            stringBuilder.append("  try{ ");
            stringBuilder.append(ENTER);
            stringBuilder.append(TAB);
            stringBuilder.append(TAB);
            stringBuilder.append(" Method method = " + interfaceClass.getName() + ".class.getMethod(\"" + method.getName() + "\");");
            stringBuilder.append(ENTER);
            stringBuilder.append(TAB);
            stringBuilder.append(TAB);
            stringBuilder.append(" this.h.invoke(method);");
            stringBuilder.append(ENTER);
            stringBuilder.append(TAB);
            stringBuilder.append(TAB);
            stringBuilder.append("}catch(Exception ex){}");
            stringBuilder.append(ENTER);
            stringBuilder.append(TAB);
            stringBuilder.append("}");
            stringBuilder.append(ENTER);
            stringBuilder.append("}");
        }
        String content = stringBuilder.toString();

        try {
            String filePath = "D:\\com\\codebear\\$Proxy.java";
            File file = new File(filePath);

            File fileParent = file.getParentFile();
            if (!fileParent.exists()) {
                fileParent.mkdirs();
            }

            FileWriter fileWriter = new FileWriter(file);
            fileWriter.write(content);
            fileWriter.flush();
            fileWriter.close();

            JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
            StandardJavaFileManager fileManager = compiler.getStandardFileManager
                    (null, null, null);
            Iterable iterable = fileManager.getJavaFileObjects(filePath);
            JavaCompiler.CompilationTask task = compiler.getTask
                    (null, fileManager, null, null, null, iterable);
            task.call();
            fileManager.close();

            URLClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:D:\\\\")});
            Class<?> clazz = classLoader.loadClass("com.codebear.$Proxy");
            Constructor<?> constructor = clazz.getConstructor(MockInvocationHandler.class);
            return constructor.newInstance(h);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }
}

Затем проверьте это:

public class Main {
    public static void main(String[] args) {
        MockInvocationHandler mockInvocationHandler=new MockInvocationHandler(new UserServiceImpl());
        UserService userService = (UserService)MockProxy.
                newProxyInstance(UserService.class, mockInvocationHandler);
        userService.query();
    }
}

результат операции:

image.png

Ну, без учета производительности, ремонтопригодности и безопасности наша кастрированная версия динамического прокси завершена. Сложность кода невелика, но это испытание на размышления и терпение.

Простой анализ исходного кода JDK

Исходный код основан на JDK1.8.

 public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();
        //安全验证
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * 得到代理类
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            final Constructor<?> cons = cl.getConstructor(constructorParams);//获得构造方法
            final InvocationHandler ih = h;
            //如果构造器器不是公共的,需要修改访问权限,使其可以访问
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            return cons.newInstance(new Object[]{h});//通过构造方法,创建对象,传入InvocationHandler 对象
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }

Просто взгляните на исходный код, мы можем сразу же обратить внимание на метод getProxyClass0, это то, о чем нам нужно позаботиться, мы нажимаем:

  private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
        //当接口大于65535报错
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }

        return proxyClassCache.get(loader, interfaces);
    }

Этот метод ничего не может сказать, но через последний proxyClassCache.get легко узнать, что динамический прокси JDK использует кеш.Метод, на который мы должны обратить внимание, находится в get, и продолжайте щелкать:

public V get(K key, P parameter) {
        Objects.requireNonNull(parameter);

        expungeStaleEntries();
        //通过上游方法,可以知道key是类加载器,这里是通过类加载器可以获得第一层key
       Object cacheKey = CacheKey.valueOf(key, refQueue);
        
       //我们查看map的定义,可以看到map变量是一个两层的ConcurrentMap
       ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);//通过第一层key尝试获取数据
       //如果valuesMap 为空,就新建一个ConcurrentHashMap,
       //key就是生成出来的cacheKey,并把这个新建的ConcurrentHashMap推到map
       if (valuesMap == null) {
            ConcurrentMap<Object, Supplier<V>> oldValuesMap
                = map.putIfAbsent(cacheKey,
                                  valuesMap = new ConcurrentHashMap<>());
            if (oldValuesMap != null) {
                valuesMap = oldValuesMap;
            }
        }

        //通过上游方法可以知道key是类加载器,parameter是类本身,这里是通过类加载器和类本身获得第二层key
        Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
        Supplier<V> supplier = valuesMap.get(subKey);
        Factory factory = null;

        while (true) {
            if (supplier != null) {
                //如果有缓存,直接调用get方法后返回,当没有缓存,会继续执行后面的代码,
                //由于while (true),会第二次跑到这里,再get返回出去,
                //其中get方法调用的是WeakCahce中的静态内部类Factory的get方法
                V value = supplier.get();
                if (value != null) {
                    return value;
                }
            }
            //当factory为空,会创建Factory对象
            if (factory == null) {
                factory = new Factory(key, parameter, subKey, valuesMap);
            }

            if (supplier == null) {
                supplier = valuesMap.putIfAbsent(subKey, factory);
                if (supplier == null) {
                    //当没有代理类缓存的时候,会运行到这里,把Factory的对象赋值给supplier ,
                    //进行下一次循环,supplier就不为空了,可以调用get方法返回出去了,
                    //这个Factory位于WeakCahce类中,是一个静态内部类
                    supplier = factory;
                }
            } else {
                if (valuesMap.replace(subKey, supplier, factory)) {
                    supplier = factory;
                } else {
                    supplier = valuesMap.get(subKey);
                }
            }
        }
    }

Код здесь сложнее, проще говоря:

  • Динамический прокси-сервер JDK использует два слоя карты для кэширования, первый слой — это загрузчик классов, второй слой — это загрузчик классов + сам
  • Когда есть кеш, вызовите get и return напрямую. В противном случае продолжайте выполнять следующий код, чтобы присвоить значение поставщику. Из-за while (true) он запустится здесь во второй раз, а затем вызовет get() для возврата . Ядро лежит в supplier.get(), который вызывает get() статического внутреннего класса Factory в WeakCahce, который является методом для получения прокси-класса.

Давайте посмотрим на метод supplier.get():

 value = Objects.requireNonNull(valueFactory.apply(key, parameter));

В этом предложении кроется ядро, но что такое valueFactory? Мы можем посмотреть на его определение:

 private final BiFunction<K, P, V> valueFactory;

Давайте взглянем на его конструктор WeakCahce:

 public WeakCache(BiFunction<K, P, ?> subKeyFactory,
                     BiFunction<K, P, V> valueFactory) {
        this.subKeyFactory = Objects.requireNonNull(subKeyFactory);
        this.valueFactory = Objects.requireNonNull(valueFactory);
    }

Мы наверняка где-то вызывали этот конструктор, и в классе Proxy есть такое определение:

 private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
        proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

Знакомы ли вы с этим proxyClassCache?Да, он используется в методе getProxyClass0.Здесь создается объект WeakCache и вызывается конструктор с двумя параметрами.Вторым параметром является объект ProxyClassFactory, который соответствует WeakCache.Второй параметр BiFunction valueFactory, а затем присваивает значение окончательной фабрике значений, valueFactory.apply, поэтому в конечном итоге будет вызван метод применения в ProxyClassFactory. Ключ:

 byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);//生成代理类的二进制数组
            try {
                //内部是native标记的方法,是用C或者C++实现的,这里不深究
                //方法内部就是通过类加载器和上面生成的代理类的二进制数组等数据,经过处理,成为Class
                return defineClass0(loader, proxyName,
                                    proxyClassFile, 0, proxyClassFile.length);
            } catch (ClassFormatError e) {
                throw new IllegalArgumentException(e.toString());
            }

Метод generateProxyClass внутренне генерирует бинарный массив прокси-класса. Как он генерируется? Вы можете нажать и посмотреть сами. Мы не будем здесь продолжать, потому что наша цель — найти метод generateProxyClass, а затем написать метод самостоятельно чтобы перейти к Execute generateProxyClass, вывести возвращенный byte[] в файл .class и использовать функцию декомпиляции idea, чтобы увидеть, как выглядит окончательный сгенерированный прокси-класс:

 byte[] $proxies = ProxyGenerator.generateProxyClass("$Proxy", new Class[]{UserService.class});
        File file=new File("D:\\$Proxy.class");
        FileOutputStream outputStream = null;
        try {
            outputStream = new FileOutputStream(file);
            try {
                outputStream.write($proxies);
            } catch (IOException e) {
                e.printStackTrace();
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

Запустив его, мы обнаружили, что файл $Proxy.class появился на диске D. Мы перетащили его в идею, чтобы увидеть его истинные цвета.Поскольку сгенерированный код все еще относительно длинный, я вставляю сюда только основной код:

//继承了Proxy类
public final class $Proxy extends Proxy implements UserService {
    public $Proxy(InvocationHandler var1) throws  {
        super(var1);
    }
    public final void query() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
}

Вам знаком этот код? Он очень близок к прокси-классу, сгенерированному нашим собственным рукописным динамическим прокси.

разрешить сомнения

Ну, сначала я сам написал кастрированную версию динамического прокси, а потом просто посмотрел исходный код динамического прокси JDK, а также посмотрел класс прокси, сгенерированный динамическим прокси JDK. Таким образом, можно разрешить три вышеупомянутых сомнения:

  1. Что делает загрузчик классов: первое: JDK должен использовать загрузку классов в качестве ключа кеша внутри второго: загрузчик классов требуется для создания класса
  2. Зачем нужен интерфейс: потому что сгенерированный прокси-класс должен реализовать этот интерфейс
  3. Почему динамический прокси JDK может только проксировать интерфейс: потому что сгенерированный класс прокси уже наследует класс Proxy, а Java наследуется одиночно, поэтому нет возможности наследовать другой класс.

Некоторые блоги могут говорить о разнице между динамическим прокси cglib и JDK, cglib дополняет прокси, манипулируя байт-кодом, на самом делеДинамический прокси JDK также манипулирует байт-кодом.

После такого анализа я считаю, что у всех появилось новое понимание динамического прокси JDK.