Итак, приступим пожалуй. Я люблю Scala не только за то, что она позволяет писать в два раза меньше кода, чем на Java. Понятного и выразительного кода. Помимо этого на Scala можно делать вещи вообще недоступные разработчикам на Java: generic’и высшего порядка, узнавать типы generic’ов в runtime при помощи манифестов.
Одна из таких вещей, о которой пойдёт речь — это компиляцияScala-кода во время исполнения программы. Это может быть нужно, когда хочется выполнить приходящий из удалённого источника код или написать самомодифицируемую программу, аналог функции eval в JS.
Важнейшее отличие Scala от Java в том, что её компилятор — не native программа, как javac, а поставляется в видеjar-файла , то есть является простой библиотекой, которую никто не мешает вызвать когда угодно, чтобы из исходных кодов получить байткод JVM.
Единственный недостаток этого метода — Scala Compiler API имеет не очень удобный интерфейс для простых задач: когда нужно просто взять исходник и получить из него пачку скомпилированных классов, по которым уже через Java Reflection API можно будет создавать экземпляры. Именно поэтому я решился однажды создать для компиляции простую обёртку, о которой и будет рассказ. Код для неё я собирал по крупицам со Stack Overflow и тематических сайтов.
Интерфейс для компиляции кода Scala предоставляется классом scala.tools.nsc.Global. Для вызова компиляции необходимо создать экземпляр этого класса, потом экземпляр вложенного класса Run и запустить метод compileSources:
Тут всё достаточно просто, но для компиляции нужны настройки (settings), reporter, который будет собирать ошибки компиляции и собственно исходники (sources).
Настройки имеют тип scala.tools.nsc.Settings, в настройках для моих нужд хватило двух параметров: usejavacp, определяющего, что компилятору надо использовать текущий classpath Java (это полезно, поскольку исходные коды, которые вы хотите скомпилировать могут содержать ссылки на внешние классы из окружения), и второго параметра outputDirs. Дело в том, что компилятор, который находится в классе Global, умеет складывать скомпилированные классы только в виде файлов на диске. Мне не хотелось привязываться к диску, так что я нашёл обходное решение, достаточно популярное, кстати — использовать виртуальную директорию в памяти.
В итоге мой код для настроек выглядит так:
Дальше надо настроить reporter. Я использовал scala.tools.nsc.reporters.StoreReporter:
Он сохраняет все проблемы с компиляцией внутри себя, и ошибки можно будет проанализировать так уже после того, как компиляция закончится.
И последнее, что надо сделать — подготовить исходники. Всё не так просто и компилятор принимает список из экземпляров scala.tools.nsc.util.SourceFile, в то время как у нас, как вы помните, есть только строчка с исходным кодом. Спасибо короткому синтаксису Scala, преобразование делается просто:
Мы создаёт один файл, куда помещаем исходный код.
Теперь у нас всё готово, мы можем вызвать компилятор и первое, что надо сделать — проверить: а как же скомпилировались наши исходники:
Для простоты, в случае наличия ошибок (а не предупреждений) я кидаю исключение о том, что компиляция неуспешна, вот это исключение:
Если всё пошло хорошо, значит мы уже имеет собранные классы, осталось ихкак-то загрузить из той самой виртуальной папки. Используем встроенный класс scala.tools.nsc.interpreter.AbstractFileClassLoader:
AbstractFileClassLoader будет создаваться новый при каждой новой компиляции, чтобы, если мы захотим перекомпилироватькакой-нибудь класс, он успешно загрузился во второй раз, не конфликтуя с предыдущей своей инкарнацией.
После создания class loader’а, нужно пройти по файлам виртуальной папки и подгрузить классы из файлов внутри неё. В Scala один файл с исходниками может содержать несколько классов, не обязательно вложенных внутрь друг друга, при компиляции в байткод JVM такие несколько классов будут разложены в несколько файлов, причём вложенные классы будут лежать в отдельных классах с именем ClassName$InnerClassName.class. Я использовал этот код для компиляции реализаций одного интерфейса, так, что я всегда знал чего ожидать от полученных классов. Кстати поэтому вложенные классы, которые норовили встать в один ряд с основными мне сильно мешались, при загрузке я их пропускаю при наличии в названии знака $:
Для загрузки классов нужно получать их полные названия (fully qualified name), включающие структуру пакетов. Для воссоздания этого названия я использовал манипуляцию с путём в папке.
Ну что же, теперь наши классы загружены и конструкция выше вернёт список этих классов.
На этом всё. Вот так простая вроде бы, хотя и нетривиальная задача, смогла быть выполнена только написанием страницы кода.
Полный текст получившегося класса:
Кстати, если заинтересуетесь форматом комментариев — это Circumflex Docco, отличная штука для наглядной документации.
Напоследок, неверно, необходимо сказать, что Scala быстро развивается и иногда разработчики меняют API. Данный код успешно опробован на версии2.9.0.1 , должен работать на всех 2.8.x и 2.9.x .
Одна из таких вещей, о которой пойдёт речь — это компиляция
Важнейшее отличие Scala от Java в том, что её компилятор — не native программа, как javac, а поставляется в виде
Единственный недостаток этого метода — Scala Compiler API имеет не очень удобный интерфейс для простых задач: когда нужно просто взять исходник и получить из него пачку скомпилированных классов, по которым уже через Java Reflection API можно будет создавать экземпляры. Именно поэтому я решился однажды создать для компиляции простую обёртку, о которой и будет рассказ. Код для неё я собирал по крупицам со Stack Overflow и тематических сайтов.
Интерфейс для компиляции кода Scala предоставляется классом scala.tools.nsc.Global. Для вызова компиляции необходимо создать экземпляр этого класса, потом экземпляр вложенного класса Run и запустить метод compileSources:
val compiler = new Global(settings, reporter)<br/>
new compiler.Run()compileSources(sources)
Тут всё достаточно просто, но для компиляции нужны настройки (settings), reporter, который будет собирать ошибки компиляции и собственно исходники (sources).
Настройки имеют тип scala.tools.nsc.Settings, в настройках для моих нужд хватило двух параметров: usejavacp, определяющего, что компилятору надо использовать текущий classpath Java (это полезно, поскольку исходные коды, которые вы хотите скомпилировать могут содержать ссылки на внешние классы из окружения), и второго параметра outputDirs. Дело в том, что компилятор, который находится в классе Global, умеет складывать скомпилированные классы только в виде файлов на диске. Мне не хотелось привязываться к диску, так что я нашёл обходное решение, достаточно популярное, кстати — использовать виртуальную директорию в памяти.
В итоге мой код для настроек выглядит так:
val settings = new Settings<br/>
<br/>
settings.usejavacp.value = true<br/>
<br/>
val directory = new VirtualDirectory("(memory)", None)<br/>
settings.outputDirs.setSingleOutput(directory)
Дальше надо настроить reporter. Я использовал scala.tools.nsc.reporters.StoreReporter:
val reporter = new StoreReporter()
Он сохраняет все проблемы с компиляцией внутри себя, и ошибки можно будет проанализировать так уже после того, как компиляция закончится.
И последнее, что надо сделать — подготовить исходники. Всё не так просто и компилятор принимает список из экземпляров scala.tools.nsc.util.SourceFile, в то время как у нас, как вы помните, есть только строчка с исходным кодом. Спасибо короткому синтаксису Scala, преобразование делается просто:
val sources = List(new BatchSourceFile("<source>", source))
Мы создаёт один файл, куда помещаем исходный код.
Теперь у нас всё готово, мы можем вызвать компилятор и первое, что надо сделать — проверить: а как же скомпилировались наши исходники:
if (reporter.hasErrors) {<br/>
throw new CompilationFailedException(source,<br/>
reporter.infos.map(info => (info.pos.line, info.msg)))<br/>
}
Для простоты, в случае наличия ошибок (а не предупреждений) я кидаю исключение о том, что компиляция неуспешна, вот это исключение:
class CompilationFailedException(val programme: String, val messages: Iterable[(Int, String)])<br/>
extends Exception(messages.map(message => message._1 + ". " + message._2).mkString("n"))
Если всё пошло хорошо, значит мы уже имеет собранные классы, осталось их
val classLoader = new AbstractFileClassLoader(directory, this.getClass.getClassLoader())
AbstractFileClassLoader будет создаваться новый при каждой новой компиляции, чтобы, если мы захотим перекомпилировать
После создания class loader’а, нужно пройти по файлам виртуальной папки и подгрузить классы из файлов внутри неё. В Scala один файл с исходниками может содержать несколько классов, не обязательно вложенных внутрь друг друга, при компиляции в байткод JVM такие несколько классов будут разложены в несколько файлов, причём вложенные классы будут лежать в отдельных классах с именем ClassName$InnerClassName.class. Я использовал этот код для компиляции реализаций одного интерфейса, так, что я всегда знал чего ожидать от полученных классов. Кстати поэтому вложенные классы, которые норовили встать в один ряд с основными мне сильно мешались, при загрузке я их пропускаю при наличии в названии знака $:
for (classFile <- directory; if (!classFile.name.contains('$'))) yield {<br/>
<br/>
val path = classFile.path<br/>
val fullQualifiedName = path.substring(path.indexOf('/')+1, path.lastIndexOf('.')).replace("/", ".")<br/>
<br/>
classLoader.loadClass(fullQualifiedName)<br/>
}
Для загрузки классов нужно получать их полные названия (fully qualified name), включающие структуру пакетов. Для воссоздания этого названия я использовал манипуляцию с путём в папке.
Ну что же, теперь наши классы загружены и конструкция выше вернёт список этих классов.
На этом всё. Вот так простая вроде бы, хотя и нетривиальная задача, смогла быть выполнена только написанием страницы кода.
Полный текст получившегося класса:
/*!# Compiler<br/>
<br/>
This class is a wrapper over Scala Compiler API<br/>
which has simple interface just accepting the source code string.<br/>
<br/>
Compiles the source code assuming that it is a .scala source file content.<br/>
It used a classpath of the environment that called the `Compiler` class.<br/>
*/<br/>
<br/>
import tools.nsc.{Global, Settings}<br/>
import tools.nsc.io._<br/>
import tools.nsc.reporters.StoreReporter<br/>
import tools.nsc.interpreter.AbstractFileClassLoader<br/>
import tools.nsc.util._<br/>
<br/>
class Compiler {<br/>
<br/>
def compile(source: String): Iterable[Class[_]] = {<br/>
<br/>
// prepare the code you want to compile<br/>
val sources = List(new BatchSourceFile("<source>", source))<br/>
<br/>
// Setting the compiler settings<br/>
val settings = new Settings<br/>
<br/>
/*! Take classpath from currently running scala environment. */<br/>
settings.usejavacp.value = true<br/>
<br/>
/*! Save class files for compiled classes into a virtual directory in memory. */<br/>
val directory = new VirtualDirectory("(memory)", None)<br/>
settings.outputDirs.setSingleOutput(directory)<br/>
<br/>
val reporter = new StoreReporter()<br/>
val compiler = new Global(settings, reporter)<br/>
new compiler.Run()compileSources(sources)<br/>
<br/>
/*! After the compilation if errors occured, `CompilationFailedException`<br/>
is being thrown with a detailed message. */<br/>
if (reporter.hasErrors) {<br/>
throw new CompilationFailedException(source,<br/>
reporter.infos.map(info => (info.pos.line, info.msg)))<br/>
}<br/>
<br/>
/*! Each time new `AbstractFileClassLoader` is created for loading classes<br/>
it gives an opportunity to treat same name classes loading well.<br/>
*/<br/>
// Loading new compiled classes<br/>
val classLoader = new AbstractFileClassLoader(directory, this.getClass.getClassLoader())<br/>
<br/>
/*! When classes are loading inner classes are being skipped. */<br/>
for (classFile <- directory; if (!classFile.name.contains('$'))) yield {<br/>
<br/>
/*! Each file name is being constructed from a path in the virtual directory. */<br/>
val path = classFile.path<br/>
val fullQualifiedName = path.substring(path.indexOf('/')+1,path.lastIndexOf('.')).replace("/",".")<br/>
<br/>
Console.println(fullQualifiedName)<br/>
<br/>
/*! Loaded classes are collecting into a returning collection with `yield`. */<br/>
classLoader.loadClass(fullQualifiedName)<br/>
}<br/>
}<br/>
}<br/>
<br/>
/*!### Compilation exception<br/>
<br/>
Compilation exception is defined this way.<br/>
It contains program was compiling and error positions with messages<br/>
of what went wrong during compilation.<br/>
*/<br/>
class CompilationFailedException(val programme: String,<br/>
val messages: Iterable[(Int, String)])<br/>
extends Exception(messages.map(message => message._1 + ". " + message._2).mkString("n"))<br/>
Кстати, если заинтересуетесь форматом комментариев — это Circumflex Docco, отличная штука для наглядной документации.
Напоследок, неверно, необходимо сказать, что Scala быстро развивается и иногда разработчики меняют API. Данный код успешно опробован на версии