Практически Groovy: Подмешайте немного Groovy в приложения Java (исходники)

Эндрю Гловер

Если вы уже читали статьи из этой серии, вы могли увидеть, что существует множество интересных способов использования 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.

И снова музыка в Groovy

Я люблю музыку. В действительности моя коллекция компакт-дисков соревнуется по размерам с библиотекой книг по компьютерам. На протяжении многих лет я переписывал музыку на разные компьютеры и, по ходу дела, запутал свою коллекцию 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

Внедрение Groovy в код Java

Несмотря на то, что решение, работающее в командной строке, лёгкое и удобное, оно не универсально. Если бы мне было интересно перейти на более высокий уровень сложности, я мог бы импортировать мою 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

Вы думаете, что уже всё узнали? Оказывается, есть ещё несколько способов позабавиться с 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();
 }
}

Механизм сценариев Groovy

Объект 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

Последним, но весьма важным, способом является среда сценариев 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, и, кроме того, я отлично позабавился, разбираясь со всеми возможностями!


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=9442