суббота, 9 апреля 2011 г.

Интерпретация и компиляция Java-кода в Java-программе

Давайте представим, что вам понадобилось в своей Java-программе выполнять некоторый код, порождаемый пользователем, в некотором выстроенном контексте. Причем, хотелось бы, чтобы из пользовательского кода были доступны некоторые классы и объекты, определенные в исходном приложении.
В 6-ой версии платформы Java появилась технология Java Scripting, которая реализована в пакете javax.script. Используя её и какой-либо из реализованных движков (а может быть вы напишите свой движок), можно получить то что мы хотим - Java-программу, исполняющую некоторый код пользователя, который становится известен лишь в runtime. Типично, используют Java Scripting и движок JavaScript (ссылка), более того JavaScript движок Rhino от Mozilla входит в JDK 6. Нас же будет интересовать Java-движок.

Получение и подключение Java-движка к существующему проекту
К сожалению, Java-движок не входит в JDK 6 (интересно, почему ?), поэтому перед использованием API javax.script придется найти соответствующий jar и подключить его к проекту.

Если вы не используете Maven
, то скорей начните делать это.
Идем сюда, забираем себе директорию svn / trunk / engines / java и при помощи ant собираем java-engine.jar. Осталось лишь подключить java-engine.jar как библиотеку к вашему проекту.

Если вы используете Maven
Поискав немного, я нашел то что искал в репозитории http://repo.fusesource.com/maven2/. Попутно я ещё нашел очень полезный сервис http://www.mvnbrowser.com, позволяющий осуществлять поиск по наиболее популярным открытым Maven репозиториям.

Простой пример
import javax.script.*;

public class Main {

    public static void main(String[] args)
            throws ScriptException {
        ScriptEngineManager manager =
                            new ScriptEngineManager();
        ScriptEngine engine =
                            manager.getEngineByName("java");
        if (engine == null) {
            System.err.println(
                   "Engine error (unknown engine)!");
        } else {
            engine.put(ScriptEngine.FILENAME,
                   "TestApp.java");
            engine.eval("public class TestApp {"
                    + "public static void main(String[] a)"
                    + "{System.out.println(\"hello!\");}"
                    + "}");

        }
    }
}
Что здесь происходит ?
  1. Создаем экземпляр ScriptEngineManager, который позволяет находить конкретный движок (строчки 7-8);
  2. Находим движок java (строчки 9-10);
  3. Так как в Java принято, что имя класса должно совпадать с именем файла, где он располагается, необходимо добавить в контекст движка имя файла TestApp.java (строчки 15-16);
  4. Выполняем скрипт - описание класса TestApp с единственным статическим методом main, что опять же в Java означает, что виртуальная машина вызовет main при исполнении этого кода (строчки 17-20).
Так происходит интерпретация кода, а как быть, если вам необходимо часто исполнять один и тот же кусок кода ? Было бы неплохо, если бы программу можно было скомпилировать в байт-код, что снизило бы затраты на последующую интерпретацию текста.
В Java Scripting для этих целей предусмотрен интерфейс Compilable с функционалом для компиляции скрипта и его исполнения. Этот интерфейс не обязательно реализуется движком. Нам повезло, используемый Java-движок реализует этот интерфейс, поэтому далее следует версия примера, данного выше, в которой происходит компиляция и выполнение получившегося скомпилированного кода.
import javax.script.*;

public class Main {

    public static void main(String[] args)
            throws ScriptException {
        ScriptEngineManager manager =
                            new ScriptEngineManager();
        ScriptEngine engine =
                            manager.getEngineByName("java");
        if (engine == null) {
            System.err.println(
                   "Engine error (unknown engine)!");
        }
        else {
            engine.put(ScriptEngine.FILENAME,
                   "TestApp.java");
            if (engine instanceof Compilable) {
                Compilable comp = (Compilable) engine;
                CompiledScript cs = comp.compile(
                        "public class TestApp {"
                    + "public static void main(String[] a)"
                    + "{System.out.println(\"hello!\");}"
                    + "}");
                cs.eval();
            } else {
                System.err.println(
                        "Engine error (not compilable)!");
            }
        }
    }
}

Чуть более сложный пример
Уметь просто исполнять программу в программе конечно полезно, но, видимо, чаще всего необходимо будет оперировать данными внешнего приложения во вложенном или наоборот, получать результат работы скрипта во внешнем приложении. Для этих целей мы можем изменить контекст исполнения вложенной программы, связав в нем какой-либо объект с Java-объект внешней программы. На самом деле мы уже делали это выше, добавляя имя файла в контекст движка
Далее я покажу, как можно получить доступ к классу внешнего приложения из вложенного и получить доступ к классу вложенного приложения из внешнего.
package testjavaembedded;

import javax.script.*;

public class Main {

    public static class Foo {
        public String a = "hello";
    }

    public static interface MyI {
        public String m();
    }

    public static void main(String[] args)
            throws ScriptException,
                   InstantiationException,
                   IllegalAccessException {
        ScriptEngineManager manager =
                            new ScriptEngineManager();
        ScriptEngine engine =
                            manager.getEngineByName("java");
        if (engine == null) {
            System.err.println(
                   "Engine error (unknown engine)!");
        } else {
            engine.put(ScriptEngine.FILENAME,
                    "TestApp.java");
            engine.getContext().setAttribute("parentLoader",
              Thread.currentThread().getContextClassLoader(),
              ScriptContext.ENGINE_SCOPE);

            Class clazz = (Class) engine.eval(
                    "import testjavaembedded.Main.Foo;"
                    + "import testjavaembedded.Main.MyI;"
                    + "public class TestApp implements MyI {"
                    + "public String m() {return \"world!\";}"
                    + "public static void main(String[] a)"
                    + "{"
                    + "Foo f = new Foo();"
                   + "System.out.println(f.a);}"
                    + "}");

            Object obj = clazz.newInstance();
            MyI my = (MyI) obj;
            System.out.println(my.m());
        }
    }
}
Что здесь происходит ? То же, что и в прошлом примере, за исключением того, что теперь главный класс, приложения, которое исполняется Java-движком, реализует интерфейс testjavaembedded.Main.MyI. Этот интерфейс определен во внешнем приложении. Также, существенно, что теперь в методе main вложенного приложения мы создаем объект класса, определенного во внешнем приложении, и, соответственно, теперь можем вызывать его методы, обращаться к его атрибутам (f.a).
В прошлом примере это было не важно, но здесь надо заметить, что eval Java-движка вернет тот самый главный класс, который определяется во вложенном приложении (TestApp). Понятно, что мы можем создать объект этого класса и, соответственно, вызывать его методы (строчки 44-46).
Заработал этот пример не сразу. У меня были такие же проблемы, как и здесь. Оказывается для загрузки классов Java-движок использует другой ClassLoader, а не тот же, которым был загружен класс, в методе которого создается этот движок. Решение здесь такое - надо передать в Java-движок правильный ClassLoader и все заработает (строчки 29-31).

Безопасность
Отдельного инструмента для безопасного контролируемого выполнения в Java Scripting нет, но есть общий механизм Java Security, который позволяет на уровне виртуальной машины контролировать исполнение кода. Это довольно обширная тема, здесь я попытаюсь лишь показать как можно использовать инструменты Java Security для безопасного выполнения кода Java-движком. Тем кого будут интересовать детали, предлагаю посетить пару ресурсов в ссылках.
Для того чтобы активировать механизм Java Security, необходимо добавить к параметрам запускаемой виртуальной машины следующие ключи: -Djava.security.manager -Djava.security.policy=security.policy. Где security.policy - файл со следующим содержанием:
grant {
    permission java.security.AllPermission;
};
это означает, что корневому приложению (внешняя Java-программа) будут предоставлены все права (мы ему доверяем). По умолчанию, при активировании Java Security виртуальная машина исполняет код без каких либо прав.
Идея безопасного выполнения скрипта состоит в том, чтобы в контексте корневого приложения, с правами на выполнение любого кода, выстроить новый контекст (getSecureContext), в котором уже нельзя будет исполнять любой код. Далее в этом контексте и исполняется (AccessController.doPrivileged) вложенная потенциально опасная Java-программа.
import java.io.FilePermission;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.Permissions;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.util.PropertyPermission;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.script.*;

public class Main {

    public static AccessControlContext getSecureContext() {
        Permissions perms = new Permissions();
        perms.add(new RuntimePermission(
                "accessDeclaredMembers"));
        perms.add(new RuntimePermission(
                "accessClassInPackage.sun.misc"));
        perms.add(new RuntimePermission(
                "createClassLoader"));
        perms.add(new PropertyPermission(
                "com.sun.script.java.sourcepath", "read"));
        perms.add(new PropertyPermission(
                "com.sun.script.java.classpath", "read"));
        perms.add(new PropertyPermission(
                "java.class.path", "read"));
        perms.add(new PropertyPermission(
                "java.endorsed.dirs", "read"));
        perms.add(new PropertyPermission(
                "sun.boot.class.path", "read"));
        perms.add(new PropertyPermission(
                "java.ext.dirs", "read"));
        perms.add(new PropertyPermission(
                "nonBatchMode", "read"));
        perms.add(new PropertyPermission(
                "com.sun.script.java.mainClass", "read"));
        perms.add(new FilePermission(
                "TestApp.java", "read"));
        perms.add(new FilePermission(
                "/C:/Program Files/Java/jdk1.6.0_24/-",
                "read"));
        perms.add(new FilePermission(
                "/C:/windows/Sun/Java/-", "read"));
        perms.add(new FilePermission(
       "/C:/Users/entend/scripting/trunk/engines/java/build/-",
       "read"));
        perms.add(new FilePermission(
       "/C:/Users/entend/NetBeansProjects/TestJavaEmbedded/-",
       "read"));
        perms.add(new FilePermission(
                "TestApp", "read"));
        ProtectionDomain domain = new ProtectionDomain(
                new CodeSource(null,
                (java.security.cert.Certificate[]) null),
                perms);
        return new AccessControlContext(
                new ProtectionDomain[]{domain});
    }

    public static void main(String[] args)
            throws ScriptException {

        ScriptEngineManager manager =
                new ScriptEngineManager();

        final ScriptEngine engine =
                manager.getEngineByName("java");
        if (engine == null) {
            System.err.println(
                    "Engine error (unknown engine)!");
        } else {
            ScriptContext newContext =
                    new SimpleScriptContext();
            final Bindings engineScope =
                    newContext.getBindings(
                       ScriptContext.ENGINE_SCOPE);
            engineScope.put(ScriptEngine.FILENAME,
                    "TestApp.java");
            
            AccessController.doPrivileged(
                new PrivilegedAction() {
                public Object run() {
                    try {
                        engine.eval(
                         "public class TestApp {"
                        + "public static void main(String[] a)"
                        + "{System.exit(0);}"
                        + "}", engineScope);
                    } catch (ScriptException ex) {
                        System.err.println(ex.getMessage());
                    }
                    return null;
                }
            }, getSecureContext());
        }
    }
}
Почему такой уродливый контекст, с непереносимо определенными правами (строчки 17-51) ? Я просто пытался запустить код, который ничего не делает, все эти права, как ни странно нужны именно для этого, а потом поленился выяснить, как можно было проще задать эти права. Здесь, кстати, есть тонкий момент, возможно используемый Java-движок не является сам по себе безопасным, и именно поэтому необходима такая куча прав для исполнения самого безобидного кода. А вложенная программа будет запущена как раз с теми же правами, что и eval, поэтому надо тщательно выбирать движок.
Если вы попытаетесь теперь выполнить полученную программу с ключом -Djava.security.debug=access, то получите следующую ошибку:
...
access: access denied (java.lang.RuntimePermission exitVM.0)
java.lang.reflect.InvocationTargetException
Нельзя завершать работу виртуальной машины, без имеющихся на то прав.
Ссылки

1 комментарий:

  1. Пару вопросов, которые у меня возникли:
    1. Чем решение с использованием Java Scripting и Java-движка лучше/хуже чем вручную загружать классы своим ClassLoader http://piarmedia.ru/?page_id=9 ?
    2. Возможно ли использовать решение со своим ClassLoader в приложении Java EE ?

    ОтветитьУдалить