Эндрю Гловер
Если вы уже читали статьи из этой серии, вы могли увидеть, что существует множество интересных способов использования Groovy, и одним из основных преимуществ Groovy является продуктивность работы программиста. Код Groovy чаще всего более прост и лёгок в написании, чем код Java, что делает его ещё более ценным дополнением к вашему инструментарию разработки. С другой стороны, как я уже неоднократно отмечал в рамках серии, Groovy не является заменой языка Java и не предназначен для этого. Итак, вопрос состоит в том, можете ли вы внедрить Groovy в практику программирования Java, и полезно ли это, а если полезно - то когда ?
В этом месяце я попытаюсь ответить на этот вопрос. Я начну с того, что вы уже знаете -- как сценарии Groovy компилируются в Java-совместимые файлы класса, после чего углублюсь в подробное описание того, как средства компиляции Groovy (groovyc
) делают это возможным. Понимание того, как работает Groovy -- это первый шаг к использованию его в коде Java
Обратите внимание, что некоторые методики программирования, продемонстрированные в этой статье, лежат в основе инфраструктур Groovlets
и GroovyTestCase
Groovy, которые я рассматривал в прошлом месяце.
В одной из предыдущих статей этой серии, когда я показывал, как проводить модульное тестирование обычных программ Java с помощью Groovy, вы могли заметить одну особенность: я компилировал сценарии Groovy. Действительно, я скомпилировал модульные тесты в обычные файлы .class Java и запустил их в процессе компоновки Maven.
Компиляция такого типа выполняется путем вызова команды groovyc
, которая компилирует сценарии Groovy в старые добрые файлы .class, совместимые с Java. Например, если в сценарии объявляется три класса, вызов groovyc
приведёт к созданию трёх файлов .class. Сами файлы будут соответствовать стандартным правилам Java, согласно которым название файла .class совпадает с названием класса.
Для примера давайте посмотрим на листинг 1, который создаёт простой сценарий, объявляющий несколько классов. Вы можете увидеть, что выводит команда groovyc
:
Листинг 1. Декларация и компиляция класса в Groovy
package com.vanward.groovy
class Person {
fname
lname
age
address
contactNumbers
String toString(){
numstr = new StringBuffer()
if (contactNumbers != null){
contactNumbers.each{
numstr.append(it)
numstr.append(" ")
}
}
"first name: " + fname + " last name: " + lname +
" age: " + age + " address: " + address +
" contact numbers: " + numstr.toString()
}
}
class Address {
street1
street2
city
state
zip
String toString(){
"street1: " + street1 + " street2: " + street2 +
" city: " + city + " state: " + state + " zip: " + zip
}
}
class ContactNumber {
type
number
String toString(){
"Type: " + type + " number: " + number
}
}
nums = [new ContactNumber(type:"cell", number:"555.555.9999"),
new ContactNumber(type:"office", number:"555.555.5598")]
addr = new Address(street1:"89 Main St.", street2:"Apt #2",
city:"Utopia", state:"VA", zip:"34254")
pers = new Person(fname:"Mollie", lname:"Smith", age:34,
address:addr, contactNumbers:nums)
println pers.toString()
|
В листинге 1 я объявил три класса -- Person
, Address
и ContactNumber
. Приведенный ниже код создаёт объекты только что определенных типов и вызывает метод toString()
. Пока всё довольно просто, но давайте посмотрим, что получилось в результате работы groovyc
, приведенном в листинге 2:
Листинг 2. Классы, созданные командой groovyc
aglover@12d21 /cygdrive/c/dev/project/target/classes/com/vanward/groovy
$ ls -ls
total 15
4 -rwxrwxrwx+ 1 aglover user 3317 May 3 21:12 Address.class
3 -rwxrwxrwx+ 1 aglover user 3061 May 3 21:12 BusinessObjects.class
3 -rwxrwxrwx+ 1 aglover user 2815 May 3 21:12 ContactNumber.class
1 -rwxrwxrwx+ 1 aglover user 1003 May 3 21:12
Person$_toString_closure1.class
4 -rwxrwxrwx+ 1 aglover user 4055 May 3 21:12 Person.class
|
Ого, пять файлов .class! Существование файлов Person
, Address
и ContactNumber
понятно, но зачем нужно ещё два?
Выясняется, что Person$_toString_closure1.class
появился в результате наличия замыкания в методе toString()
класса Person
. Фактически он является внутренним классом Person
. А откуда появился файл BusinessObjects.class
?
Если внимательно посмотреть на листинг 1, можно заметить, что код, который я написал в основном теле сценария после объявления этих трех классов, стал файлом .class, имя которого я указал после названия сценария. В этом случае, именем сценария было BusinessObjects.groovy
, поэтому код, не содержащий определения класса, был скомпилирован в файл .class с названием BusinessObjects
.
Декомпиляция этих классов может быть очень интересной. Получившиеся файлы .java будут значительно больше по размеру, благодаря природе скрытого кода Groovy; однако, вы, вероятно, заметили разницу между классами, объявленными в сценарии Groovy (например, Person
) и кодом вне классов (например, код BusinessObjects.class
). Классы, определенные в Groovy, завершаются реализацией GroovyObject
, а код, расположенный за пределами класса, входит в класс, расширяющий Script
.
Например, если вы изучите файл .java, получившийся из BusinessObjects.class, вы увидите, что в нём определяется методы main()
и run()
. Очевидно, в методе run()
содержится код, который я написал для создания новых экземпляров объектов, а метод main()
вызывает метод run()
.
Суть этого вывода состоит в том, что чем лучше вы понимаете Groovy, тем проще будет встроить его в программы Java. "И зачем мне это нужно?" -- спросите вы? Итак, допустим, вы разработали что-то особенное в Groovy; правда, было бы неплохо встроить это также и в вашу программу на Java?
Просто ради наглядности, я сначала попытаюсь создать в Groovy что-либо полезное, после чего я рассмотрю различные способы его внедрения в обычную программу Java.
Я люблю музыку. В действительности моя коллекция компакт-дисков соревнуется по размерам с библиотекой книг по компьютерам. На протяжении многих лет я переписывал музыку на разные компьютеры и, по ходу дела, запутал свою коллекцию MP3 до предела - сегодня у меня множество каталогов, содержащих самую разнообразную музыку.
Недавно я предпринял первые шаги к наведению порядка в моей музыкальной коллекции. Я написал небольшой сценарий Groovy, который циклически проходил по коллекции файлов MP3, содержащихся в папке, и предоставлял мне подробную информацию о каждом файле -- исполнитель, название альбома и т.п. Этот сценарий показан в листинге 3:
Листинг 3. Очень полезный сценарий Groovy
package com.vanward.groovy
import org.farng.mp3.MP3File
import groovy.util.AntBuilder
class Song {
mp3file
Song(String mp3name){
mp3file = new MP3File(mp3name)
}
getTitle(){
mp3file.getID3v1Tag().getTitle()
}
getAlbum(){
mp3file.getID3v1Tag().getAlbum()
}
getArtist(){
mp3file.getID3v1Tag().getArtist()
}
String toString(){
"Artist: " + getArtist() + " Album: " +
getAlbum() + " Song: " + getTitle()
}
static getSongsForDirectory(sdir){
println "sdir is: " + sdir
ant = new AntBuilder()
scanner = ant.fileScanner {
fileset(dir:sdir) {
include(name:"**/*.mp3")
}
}
songs = []
for(f in scanner){
songs << new Song(f.getAbsolutePath())
}
return songs
}
}
songs = Song.getSongsForDirectory(args[0])
songs.each{
println it
}
|
Как вы можете видеть, сценарий очень прост, особенно учитывая его полезность в ситуации, подобной моей. Всё, что мне нужно делать, это передавать название определенной директории, и я получу нужную мне информацию (имя исполнителя, название песни и альбома) для каждого файла MP3 в этой директории.
Теперь давайте посмотрим, что мне нужно сделать, чтобы внедрить этот отличный сценарий в обычную программу Java, которая может организовывать музыку с помощью базы данных или даже проигрывать MP3.
Как я уже говорил выше, первый вариант -- это просто компиляция сценария с помощью groovyc
. В этом случае я ожидаю, что groovyc
создаст как минимум два файла .class -- один для класса Song
и другой для кода сценария, следующего после объявления Song
.
На самом деле, groovyc
создаст пять файлов .class. Это соотносится с тем фактом, что в Songs.groovy
содержится три замыкания, два в методе getSongsForDirectory()
и один в теле сценария, когда я формировал цикл по коллекции Song
и вызывал println
.
Поскольку три файла .class фактически являются внутренними классами Song.class и Songs.class, мне нужно сконцентрироваться только на двух файлах .class. Song.class накладывается напрямую на объявление Song
в сценарии Groovy и реализует GroovyObject
, тогда как Songs.class представляет код сценария, приведенный после того, как я определил Song
, и после этого расширяет Script
.
Теперь у меня есть два варианта внедрения только что скомпилированного кода Groovy в код Java: Я могу запускать код через метод main()
, реализованный в файле класса Songs.class (поскольку он расширяет Script
), также я могу включать Song.class в путь классов и использовать его так же, как и любые другие объекты в коде Java.
Вызов файла Songs.class с помощью команды java
предельно прост, если вы не забываете включить зависимости Groovy и все возможные зависимости сценария Groovy. Самый простой способ включения необходимых классов Groovy состоит во включении в путь классов файлов jar "всё в одном", встраиваемых в Groovy. В моём случае, это файл groovy-all-1.0-beta-10.jar. Для запуска Songs.class мне также нужно не забыть включить используемую мной библиотеку MP3 (jid3lib-0.5.jar>), и поскольку я использую AntBuilder
, мне также нужно включить в путь класса Ant
. В листинге 4 всё сводится вместе:
Листинг 4. Groovy через командную строку Java
c:\dev\projects>java -cp ./target/classes/;c:/dev/tools/groovy/
groovy-all-1.0-beta-10.jar;C:/dev/tools/groovy/ant-1.6.2.jar;
C:/dev/projects-2.0/jid3lib-0.5.jar
com.vanward.groovy.Songs c:\dev09\music\mp3s
Artist: U2 Album: Zooropa Song: Babyface
Artist: James Taylor Album: Greatest Hits Song: Carolina in My Mind
Artist: James Taylor Album: Greatest Hits Song: Fire and Rain
Artist: U2 Album: Zooropa Song: Lemon
Artist: James Taylor Album: Greatest Hits Song: Country Road
Artist: James Taylor Album: Greatest Hits Song: Don't Let Me
Be Lonely Tonight
Artist: U2 Album: Zooropa Song: Some Days Are Better Than Others
Artist: Paul Simon Album: Graceland Song: Under African Skies
Artist: Paul Simon Album: Graceland Song: Homeless
Artist: U2 Album: Zooropa Song: Dirty Day
Artist: Paul Simon Album: Graceland Song: That Was Your Mother
|
Несмотря на то, что решение, работающее в командной строке, лёгкое и удобное, оно не универсально. Если бы мне было интересно перейти на более высокий уровень сложности, я мог бы импортировать мою MP3-утилиту непосредственно в программу Java. В этом случае я смог бы импортировать Song.class и использовать его так же, как и любой другой класс языка Java. Проблемы с путем классов будут такими же, как и раньше: мне нужно не забыть включить файл архива uber-Groovy , Ant
, и файл jid3lib-0.5.jar. В листинге 5 вы можете увидеть, как я импортировал MP3-утилиту Groovy в пример класса Java:
Листинг 5. Встроенный код Groovy
package com.vanward.gembed;
import com.vanward.groovy.Song;
import java.util.Collection;
import java.util.Iterator;
public class SongEmbedGroovy{
public static void main(String args[]) {
Collection coll = (Collection)Song.getSongsForDirectory
("C:\\music\\temp\\mp3s");
for(Iterator it = coll.iterator(); it.hasNext();){
System.out.println(it.next());
}
}
}
|
Вы думаете, что уже всё узнали? Оказывается, есть ещё несколько способов позабавиться с Groovy в Java. Помимо встраивания сценариев Groovy в программы Java с помощью прямой компиляции у меня также есть несколько вариантов встраивания непосредственно сценариев.
Например, я могу с помощью GroovyClassLoader
Groovy осуществить динамическую загрузку сценария Groovy и его исполнения, как показано в листинге 6:
Листинг 6. GroovyClassLoader динамически загружает и исполняет сценарий Groovy
package com.vanward.gembed;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;
import groovy.lang.MetaMethod;
import java.io.File;
public class CLEmbedGroovy{
public static void main(String args[]) throws Throwable{
ClassLoader parent = CLEmbedGroovy.class.getClassLoader();
GroovyClassLoader loader = new GroovyClassLoader(parent);
Class groovyClass = loader.parseClass(
new File("C:\\dev\\groovy-embed\\src\\groovy\ com\\vanward\\groovy\\Songs.groovy"));
GroovyObject groovyObject = (GroovyObject)
groovyClass.newInstance();
Object[] path = {"C:\\music\\temp\\mp3s"};
groovyObject.setProperty("args", path);
Object[] argz = {};
groovyObject.invokeMethod("run", argz);
}
}
|
|
Классы Meta
Если вы один из тех психов, кому нравятся отражения и те замечательные вещи, которые можно с ними сделать, классы Meta Groovy сведут вас с ума. Точно так же, как и в случае отражений, используя эти классы, вы можете узнать много нового о GroovyObject , например, о его методах, и вы можете действительно создавать новые алгоритмы и выполнять их. Это, к слову, и есть сердце Groovy -- и только представьте, как оно работает, когда вы запускаете сценарии! | |
Обратите внимание, что по умолчанию загрузчик класса загружает класс, соответствующий названию сценария -- в данном случае, Songs.class, а не Song.class>. Поскольку мы с вами знаем, что Songs.class расширяет класс Script Groovy, понятно, что моим следующим действием будет выполнение метода run()
.
Вы, наверное, помните, что мой сценарий Groovy также зависел от аргументов, передаваемых во время работы программы. Поэтому мне нужно настроить переменную args
соответствующим образом, так как в данном случае я первым элементом установил название директории.
Альтернатива использованию компилированных классов и динамической загрузке GroovyObject
посредством загрузчиков классов состоит в использовании GroovyScriptEngine
и GroovyShell
для динамического выполнения сценариев Groovy.
Внедрение объекта GroovyShell
в обычные классы Java позволяет вам динамически выполнять сценарии Groovy так же, как это делает загрузчик класса. Кроме того, это даёт вам несколько возможностей запуска сценариев. В листинге 7 показано, как GroovyShell
внедрен в обычный класс Java:
Листинг 7. Внедрение GroovyShell
package com.vanward.gembed;
import java.io.File;
import groovy.lang.GroovyShell;
public class ShellRunEmbedGroovy{
public static void main(String args[]) throws Throwable{
String[] path = {"C:\\music\\temp\\mp3s"};
GroovyShell shell = new GroovyShell();
shell.run(new File("C:\\dev\\groovy-embed\\src\\groovy\ com\\vanward\\groovy\\Songs.groovy"),
path);
}
}
|
Как вы можете увидеть, сценарий Groovy запускается очень просто. Я просто создаю экземпляр GroovyShell
, передаю название сценария и вызываю метод run()
.
Но это не всё. Если хотите, вы можете запросить экземпляр GroovyShell
для типа Script
вашего сценария. Используя тип Script
, вы можете передавать объект Binding
, содержащий все нужные параметры, и вызывать метод run()
, как показано в листинге 8.
Листинг 8. Забавляемся с GroovyShell
package com.vanward.gembed;
import java.io.File;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
public class ShellParseEmbedGroovy{
public static void main(String args[]) throws Throwable{
GroovyShell shell = new GroovyShell();
Script scrpt = shell.parse(
new File("C:\\dev\\groovy-embed\\src\\groovy\ com\\vanward\\groovy\\Songs.groovy"));
Binding binding = new Binding();
Object[] path = {"C:\\music\\temp\\mp3s"};
binding.setVariable("args",path);
scrpt.setBinding(binding);
scrpt.run();
}
}
|
Объект GroovyScriptEngine
работает так же, как GroovyShell
для динамически запускаемых сценариев. Отличие GroovyScriptEngine
состоит в том, что при создании экземпляра вы можете указать ему несколько директорий, после чего вы в любое время можете вызвать несколько сценариев, как показано в листинге 9:
Листинг 9. GroovyScriptEngine в действии
package com.vanward.gembed;
import java.io.File;
import groovy.lang.Binding;
import groovy.util.GroovyScriptEngine;
public class ScriptEngineEmbedGroovy{
public static void main(String args[]) throws Throwable{
String[] paths = {"C:\\dev\\groovy-embed\\src\\groovy\ com\\vanward\\groovy"};
GroovyScriptEngine gse = new GroovyScriptEngine(paths);
Binding binding = new Binding();
Object[] path = {"C:\\music\\temp\\mp3s"};
binding.setVariable("args",path);
gse.run("Songs.groovy", binding);
gse.run("BusinessObjects.groovy", binding);
}
}
|
В листинге 9 я передаю массив, содержащий нужный мне путь, созданному экземпляру GroovyScriptEngine
, создаю старый знакомый объект Binding
и выполняю также знакомый сценарий Songs.groovy
. Просто ради забавы, я запускаю сценарий BusinessObjects.groovy
, который вы, возможно, помните из начала этого обсуждения.
Последним, но весьма важным, способом является среда сценариев Bean Scripting Framework (BSF) проекта Jakarta. Цель BSF - предложить общий API для внедрения любого языка сценариев, в том числе и Groovy, в обычное приложение Java. Этот стандартный, хотя, возможно, чрезмерно общий, подход позволяет вам внедрять сценарии Groovy без всяких усилий.
Помните приведенный выше сценарий BusinessObjects
? В листинге 10 показано, как просто BSF позволяет вам встроить его в обычную программу Java:
Листинг 10. BSF в работе
package com.vanward.gembed;
import org.apache.bsf.BSFManager;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import java.io.File;
import groovy.lang.Binding;
public class BSFEmbedGroovy{
public static void main(String args[]) throws Exception {
String fileName = "C:\\dev\\project\\src\\groovy\ com\\vanward\\groovy\\BusinessObjects.groovy";
//this is required for bsf-2.3.0
//the "groovy" and "gy" are extensions
BSFManager.registerScriptingEngine("groovy",
"org.codehaus.groovy.bsf.GroovyEngine", new
String[] { "groovy" });
BSFManager manager = new BSFManager();
//DefaultGroovyMethods.getText just returns a
//string representation of the contents of the file
manager.exec("groovy", fileName, 0, 0,
DefaultGroovyMethods.getText(new File(fileName)));
}
}
|
Если что и ясно из этой статьи, так это то, что Groovy открывает множество вариантов повторного использования в коде Java. Во всех случаях, от компиляции сценариев Groovy в обычные старые файлы .class Java до динамической загрузки и запуска сценариев, главное, на что нужно обращать внимание - это гибкость и возможность совместной работы. Компиляция сценариев Groovy в обычные файлы .class - это простейший способ использования функций, которые вы встраиваете, тогда как динамическая загрузка сценариев упрощает добавление и изменение алгоритмов работы без затрат времени на компиляцию. (Конечно же, этот вариант работает только в том случае, если интерфейс не изменился.)
Встраивание языков сценариев в обычный код Java не является повседневным явлением, но иногда такие возможности представляются. В приведенных здесь примерах я встраивал простую утилиту поиска по каталогам в приложение Java, которое может быть MP3-плеером или другой утилитой для работы с MP3. Несмотря на то, что я могу переписать мою утилиту поиска файлов MP3 в код Java, мне не нужно этого делать: Groovy великолепно совместим с языком Java, и, кроме того, я отлично позабавился, разбираясь со всеми возможностями!