Статьи

Динамическое внедрение кода Java

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

(Прежде чем мы начнем, вам может быть интересно, с какой стати кто-то может захотеть это сделать. Очевидный пример — нечто вроде механизма правил. Механизм правил хотел бы предложить пользователям возможность добавлять или изменять правила без необходимости перезапуска система. Вы можете сделать это, внедрив сценарии DSL в качестве правил, которые будут вызываться вашим механизмом правил. Настоящая проблема такого подхода заключается в том, что сценарии DSL необходимо интерпретировать, что делает их чрезвычайно медленными для выполнения. затем можно скомпилировать и запустить так же, как любой другой код в вашей программе будет на порядок эффективнее.

В Chronicle мы используем эту идею в основе нашего нового микросекундного контейнера микро-услуг / алгоритма).

Библиотека, которую мы собираемся использовать, — это библиотека Java-Runtime-Compiler с открытым исходным кодом.

Как вы увидите из приведенного ниже кода, библиотека чрезвычайно проста в использовании — на самом деле она занимает всего пару строк. Создайте CachedCompiler и затем вызовите loadFromJava. (См. Документацию здесь для фактического самого простого случая использования.)

Программа, перечисленная ниже, делает следующее:

  1. Создает поток, который вызывает вычисления для стратегии каждую секунду. Входные данные для Стратегии 10 и 20.
  2. Загружает стратегию, которая складывает два числа вместе
  3. Ждет 3с
  4. Загружает стратегию, которая вычитает одно число из другого

Это полный список кодов:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package test;
 
import net.openhft.compiler.CachedCompiler;
 
/**
 * Loads the addingStrategy and then after 3s replaces it with the
 * subtractingStrategy.
 */
public class DynamicJavaClassLoading {
 
    private final static String className = "test.MyClass";
    private final static String addingStrategy = "package test;\n" +
            "import test.DynamicJavaClassLoading.Strategy;\n" +
            "public class MyClass implements Strategy {\n" +
            "    public int compute(int a, int b) {\n" +
            "        return a+b;\n" +
            "    }\n" +
            "}\n";
 
    private final static String subtractingStrategy = "package test;\n" +
            "import test.DynamicJavaClassLoading.Strategy;\n" +
            "public class MyClass implements Strategy {\n" +
            "    public int compute(int a, int b) {\n" +
            "        return a-b;\n" +
            "    }\n" +
            "}\n";
     
    public static void main(String[] args) throws Exception {
        StrategyProxy strategy = new StrategyProxy();
 
        //Thread calling the strategy once a second
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println(strategy.compute(10,20));
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
 
        {
            ClassLoader cl = new ClassLoader() {
            };
            CachedCompiler cc = new CachedCompiler(null, null);
            Class aClass = cc.loadFromJava(cl, className, addingStrategy);
 
            Strategy runner = (Strategy) aClass.newInstance();
            strategy.setStratgey(runner);
        }
 
        Thread.sleep(3000);
 
        {
            ClassLoader cl = new ClassLoader() {
            };
            CachedCompiler cc = new CachedCompiler(null, null);
            Class aClass = cc.loadFromJava(cl, className, subtractingStrategy);
 
            Strategy runner = (Strategy) aClass.newInstance();
            strategy.setStratgey(runner);
        }
    }
 
    public interface Strategy{
        int compute(int a, int b);
    }
 
    public static class StrategyProxy implements Strategy{
        private volatile Strategy underlying;
 
        public void setStratgey(Strategy underlying){
            this.underlying = underlying;
        }
 
        public int compute(int a, int b){
            Strategy underlying = this.underlying;
            return underlying == null ? Integer.MIN_VALUE : underlying.compute(a, b);
        }
    }
}

Это вывод (комментарии синим цветом):

01
02
03
04
05
06
07
08
09
10
11
12
13
The strategy has not been loaded yet. underlying in the StrategyProxy is null so Integer.MIN_VALUE is returned
-2 1 4 7 4 8 3 6 4 8
The adding strategy has been loaded 10+20=30
30
30
30
After 3s the subtracting strategy is loaded. It replaces the adding strategy. 10-20=-10
-10
-10
-10
-10
 
-10

Обратите внимание, что в коде мы создавали новый ClassLoader и новый CachedCompiler каждый раз, когда загружали стратегию. Причина этого заключается в том, что ClassLoader может иметь только один экземпляр определенного класса, загруженный за один раз.

Если бы вы использовали только эту библиотеку для загрузки нового кода, вы бы сделали это следующим образом, не создав ClassLoader (т.е. используя ClassLoader по умолчанию) и не используя CachedCompiler.

1
Class aClass = CompilerUtils.CACHED_COMPILER.loadFromJava(className, javaCode);
Ссылка: Динамическое внедрение кода Java от нашего партнера по JCG Дэниела Шая из блога Rational Java .