После мучений с отладкой сложных MVEL скриптов + MavenClassloader, обнаружил, что механизм динамического разрешения зависимостей есть в языке Groovy. К тому же отладка Groovy скриптов возможна и в Idea и в Eclipse.



Вы спросите зачем нужно динамическое разрешение зависимостей? Некоторые вещи проще делать так, а некоторые возможно только так.

В публикации вы найдете работающее решение для Groovy в виде одного jar файла и загрузчик классов из репозитариев maven для Java приложения. Узнаете про особенности работы Grape «из коробки». Чтобы не быть голословным и были понятны возможности Grape

Приведу пример из официального руководства:
@Grapes([
    @Grab(group='org.eclipse.jetty.aggregate', module='jetty-server', version='8.1.7.v20120910'),
    @Grab(group='org.eclipse.jetty.aggregate', module='jetty-servlet', version='8.1.7.v20120910'),
    @Grab(group='javax.servlet', module='javax.servlet-api', version='3.0.1')])

import org.eclipse.jetty.server.Server
import org.eclipse.jetty.servlet.*
import groovy.servlet.*

def runServer(duration) {
    def server = new Server(8080)
    def context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);
    context.resourceBase = "."
    context.addServlet(TemplateServlet, "*.gsp")
    server.start()
    sleep duration
    server.stop()
}

runServer(10000)

Этот скрипт загружает из удаленного репозитария артефакты jetty сервера, добавляет их в classpath скрипта, создает экземпляр класса http сервера, добавляет обработчик gsp страниц (это мощный шаблонный механизм, который есть в самом груви), стартует сервер, ждет 10 секунд и останавливает его. Т.е. на момент написания скрипта не нужны эти зависимости, нужен лишь доступ к репозитариям и при следующем запуске зависимости jetty уже лежат в локальной файловой системе и не надо качать их из сети.

По мне так гениальный механизм, встроенный в сам язык!!!

Для запуска скрипта с jetty сервером нужен лишь groovy и классы ivy провайдера в classpath. Классы рантайм загружает из maven репозитария с помощью ivy.

B дебрях груви, спрятана конфигурация, которая говорит что зависимости нужно сначала искать в локальной файловой системе ${user.home}/.groovy/grapes, потом в ${user.home}/.m2/repository/, ну а затем пытаться найти сначала в jcenter, потом в ibiblio, а на последок поискать в java.net2 репозитариях
Та самая конфигурация
<ivysettings>
  <settings defaultResolver="downloadGrapes"/>
  <resolvers>
    <chain name="downloadGrapes" returnFirst="true">
      <filesystem name="cachedGrapes">
        <ivy pattern="${user.home}/.groovy/grapes/[organisation]/[module]/ivy-[revision].xml"/>
        <artifact pattern="${user.home}/.groovy/grapes/[organisation]/[module]/[type]s/[artifact]-[revision](-[classifier]).[ext]"/>
      </filesystem>
      <ibiblio name="localm2" root="file:${user.home}/.m2/repository/" checkmodified="true" changingPattern=".*" changingMatcher="regexp" m2compatible="true"/>
      <!-- todo add 'endorsed groovy extensions' resolver here -->
      <ibiblio name="jcenter" root="https://jcenter.bintray.com/" m2compatible="true"/>
      <ibiblio name="ibiblio" m2compatible="true"/>
      <ibiblio name="java.net2" root="http://download.java.net/maven/2/" m2compatible="true"/>
    </chain>
  </resolvers>
</ivysettings>



Но есть один нюанс, который препятствует широкому применению Grape — это реализация его механизма разрешения зависимостей на Ivy и отсутствие классов провайдера в одном jar с груви. Вот про что я говорю:
igor@igor-comp:~/dev/projects/groovy-grape-aether$ java -jar /home/igor/.m2/repository/org/codehaus/groovy/groovy-all/2.4.5/groovy-all-2.4.5.jar ~/dev/projects/jetty.groovy
Caught: java.lang.NoClassDefFoundError: org/apache/ivy/Ivy
java.lang.NoClassDefFoundError: org/apache/ivy/Ivy
Caused by: java.lang.ClassNotFoundException: org.apache.ivy.Ivy


Не одну шишку набивали и те, кто пытался использовать Ivy с сложными транзитивными зависимостями, диапазонами версий или snapshot версиями из maven репозитариев.

В исходном тексте Grape.java проекта groovy есть такие строчки
                // by default use GrapeIvy
                //TODO META-INF/services resolver?
                instance = (GrapeEngine) Class.forName("groovy.grape.GrapeIvy").newInstance();


Поиски привели к проекту Spring boot, который под капотом использует Grape, но за счет реализованного на Aether провайдера maven. Aether — это единая библиотека для доступа к репозитариям и публикации артефактов. Она используется в maven, nexus, m2eclipse. Вряд ли Ivy сможет с ней потягаться на одном поле боя. Было бы отлично использовать aether в grape!

GrapeEngineInstaller делает почти то, о чем думали авторы groovy когда писали TODO комментарий — присваивает полю Grape.instance провайдер AetherGrapeEngine вместо захардкоженого в груви GrapeIvy.
public abstract class GrapeEngineInstaller {

	public static void install(GrapeEngine engine) {
		synchronized (Grape.class) {
			try {
				Field field = Grape.class.getDeclaredField("instance");
				field.setAccessible(true);
				field.set(null, engine);

И не важно что в boot реализован «грязный хак» с помощью рефлекшена) Мысль авторов груви «TODO META-INF/services resolver?» тоже не лучшая, особенно при модуляризации приложения и такой резолвер точно будет болью в OSGI окружении.

Для полного счастья мне нужен AetherGrapeEngine без всего boot и классов spring, да еще и со всеми необходимыми для его работы классами Aether.

Это и привело меня к хирургии проекта spring boot и изоляции, объединении AetherGrapeEngine и загрузчиков классов mvn-classloader в отдельный артефакт размером всего 3 МБ. Эти 3 мегабайта, помогут и языку груви и моему проекту AspectJ-Scripting! Я рад поделиться результатами, надеюсь, что проект пригодится и вам.

После объединения mvn-classloader и groovy-all получился артефакт размером 9,7 МБ, который заменяет собой groovy-all и позволяет пользоваться механизмом Grape в вашем Groovy приложении, используя резолвер зависимостей AetherGrapeEngine.

Скачиваем из центрального репозитария groovy-grape-aether-2.4.5.jar. Собран он был на основе проекта groovy-grape-aether.

Инициализируем ssh сервер в груви скрипте carash.groovy:
@Grab(group='org.crashub', module='crash.connectors.ssh', version='1.3.1')
import org.crsh.standalone.Bootstrap
import org.crsh.vfs.FS.Builder
import org.crsh.vfs.spi.url.ClassPathMountFactory

def classLoader = Bootstrap.getClassLoader();

def classpathDriver = new ClassPathMountFactory(classLoader);
def cmdFS = new Builder().register("classpath", classpathDriver).mount("classpath:/crash/commands/").build();
def confFS = new Builder().register("classpath", classpathDriver).mount("classpath:/crash/").build();
def bootstrap = new Bootstrap(classLoader, confFS, cmdFS);

def config = new java.util.Properties();
config.put("crash.ssh.port", "2000");
config.put("crash.ssh.auth_timeout", "300000");
config.put("crash.ssh.idle_timeout", "300000");
config.put("crash.auth", "simple");
config.put("crash.auth.simple.username", "admin");
config.put("crash.auth.simple.password", "admin");

bootstrap.setConfig(config);
bootstrap.bootstrap();

sleep 60000

bootstrap.shutdown();


Запустим этот скрипт на выполнение командой java -jar groovy-grape-aether-2.4.5.jar carash.groovy
И наблюдаем в консоли как скрипт находит в репозитарии зависимость и работает
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginLifeCycle configureProperty
INFO: Configuring property vfs.refresh_period=1 from properties
ноя 05, 2015 1:01:50 AM org.crsh.plugin.ServiceLoaderDiscovery getPlugins
INFO: Loaded plugin Plugin[type=SSHPlugin,interface=SSHPlugin]
ноя 05, 2015 1:01:50 AM org.crsh.plugin.ServiceLoaderDiscovery getPlugins
INFO: Loaded plugin Plugin[type=SSHInlinePlugin,interface=CommandPlugin]
ноя 05, 2015 1:01:50 AM org.crsh.plugin.ServiceLoaderDiscovery getPlugins
INFO: Loaded plugin Plugin[type=KeyAuthenticationPlugin,interface=KeyAuthenticationPlugin]
ноя 05, 2015 1:01:50 AM org.crsh.plugin.ServiceLoaderDiscovery getPlugins
INFO: Loaded plugin Plugin[type=CRaSHShellFactory,interface=ShellFactory]
ноя 05, 2015 1:01:50 AM org.crsh.plugin.ServiceLoaderDiscovery getPlugins
INFO: Loaded plugin Plugin[type=GroovyLanguageProxy,interface=Language]
ноя 05, 2015 1:01:50 AM org.crsh.plugin.ServiceLoaderDiscovery getPlugins
INFO: Loaded plugin Plugin[type=JavaLanguage,interface=Language]
ноя 05, 2015 1:01:50 AM org.crsh.plugin.ServiceLoaderDiscovery getPlugins
INFO: Loaded plugin Plugin[type=ScriptLanguage,interface=Language]
ноя 05, 2015 1:01:50 AM org.crsh.plugin.ServiceLoaderDiscovery getPlugins
INFO: Loaded plugin Plugin[type=JaasAuthenticationPlugin,interface=AuthenticationPlugin]
ноя 05, 2015 1:01:50 AM org.crsh.plugin.ServiceLoaderDiscovery getPlugins
INFO: Loaded plugin Plugin[type=SimpleAuthenticationPlugin,interface=AuthenticationPlugin]
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginLifeCycle configureProperty
INFO: Configuring property ssh.port=2000 from properties
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginLifeCycle configureProperty
INFO: Configuring property ssh.auth_timeout=300000 from properties
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginLifeCycle configureProperty
INFO: Configuring property ssh.idle_timeout=300000 from properties
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginLifeCycle configureProperty
INFO: Configuring property ssh.default_encoding=UTF-8 from properties
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginLifeCycle configureProperty
INFO: Configuring property auth=simple from properties
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginLifeCycle configureProperty
INFO: Configuring property auth.simple.username=admin from properties
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginLifeCycle configureProperty
INFO: Configuring property auth.simple.password=admin from properties
SLF4J: Failed to load class «org.slf4j.impl.StaticLoggerBinder».
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See www.slf4j.org/codes.html#StaticLoggerBinder for further details.
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginManager getPlugins
INFO: Initialized plugin Plugin[type=KeyAuthenticationPlugin,interface=KeyAuthenticationPlugin]
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginManager getPlugins
INFO: Initialized plugin Plugin[type=JaasAuthenticationPlugin,interface=AuthenticationPlugin]
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginManager getPlugins
INFO: Initialized plugin Plugin[type=SimpleAuthenticationPlugin,interface=AuthenticationPlugin]
ноя 05, 2015 1:01:50 AM org.crsh.ssh.SSHPlugin init
INFO: Booting SSHD
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginManager getPlugins
INFO: Initialized plugin Plugin[type=GroovyLanguageProxy,interface=Language]
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginManager getPlugins
INFO: Initialized plugin Plugin[type=JavaLanguage,interface=Language]
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginManager getPlugins
INFO: Initialized plugin Plugin[type=ScriptLanguage,interface=Language]
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginManager getPlugins
INFO: Initialized plugin Plugin[type=CRaSHShellFactory,interface=ShellFactory]
ноя 05, 2015 1:01:50 AM org.crsh.ssh.term.SSHLifeCycle init
INFO: About to start CRaSSHD
ноя 05, 2015 1:01:50 AM org.crsh.ssh.term.SSHLifeCycle init
INFO: CRaSSHD started on port 2000
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginManager getPlugins
INFO: Initialized plugin Plugin[type=SSHPlugin,interface=SSHPlugin]
ноя 05, 2015 1:01:50 AM org.crsh.plugin.PluginManager getPlugins
INFO: Initialized plugin Plugin[type=SSHInlinePlugin,interface=CommandPlugin]
ноя 05, 2015 1:01:56 AM org.crsh.ssh.SSHPlugin destroy
INFO: Shutting down SSHD


В этом можно удостовериться, подключившись к этому серверу: ssh admin@127.0.0.1 -p 2000



Итак, мы можем теперь использовать зависимости из maven репозитариев в наших groovy скриптах. Для этого лишь нужен groovy-all-2.4.5, объединенный с AetherGrapeEngine в артефакте
<dependency>
  <groupId>com.github.igor-suhorukov</groupId>
  <artifactId>groovy-grape-aether</artifactId>
  <version>2.4.5</version>
</dependency>


В этом же артефакте есть загрузчик классов com.github.igorsuhorukov.smreed.dropship.MavenClassLoader для java программы. Так что если невозможно использовать Groovy в проекте, то похожая функциональность с динамической загрузкой классов доступна и в java проекте. Но только для этого все же будет удобнее использовать
<dependency>
  <groupId>com.github.igor-suhorukov</groupId>
  <artifactId>mvn-classloader</artifactId>
  <version>1.1</version>
</dependency>

Приведу пример кода на java для получения класса org.crsh.standalone.Bootstrap из maven репозитария:
URLClassLoader sshServerClassloader= MavenClassLoader.forMavenCoordinates("org.crashub:crash.connectors.ssh:1.3.1");
Class<?> bootstrapClass = sshServerClassloader.loadClass("org.crsh.standalone.Bootstrap");


Подытожим сказанное в статье: Grape — встроенный в груви механизм динамической загрузки зависимостей из maven репозитариев. Мне удалось извлечь из spring boot только часть необходимую для GrapeEngine провайдера, и объединить ее с Aether и минимально необходимым набором зависимостей. Ivy провайдер вместе с его проблемами больше не нужен. Я буду скорее переходить с языка MVEL на Groovy в своем проекте. А вам желаю удачных экспериментов с Grape и новой степени свободы и удобства в программировании на Groovy.

Мы выяснили, что Groovy, Ivy и Maven связывает часть языка груви Grape — технология для динамического подключения зависимостей, и узнали, как Grape можно использовать в своих скриптах.

Комментарии (9)


  1. Scf
    05.11.2015 23:09

    Сам активно использую груви в качестве основного скриптового языка — Grape позволяет по-быстрому и страничку скачать/отпарсить, и данные в базу залить, и по файловой системе пройтись.

    Правда, я использую обычные скрипты #!/usr/bin/groovy и у меня всё работает. В чем преимущество запуска через java -jar?


    1. igor_suhorukov
      06.11.2015 12:09

      Да, вы правы про скрипты и удобство.
      Но похоже что ответ содержиться в вашем вопросе — для этого нужен установленный груви, доступный в env path и java.
      Преимущество в подходе из статьи: нужна только установленная в системе java и файл groovy-grape-aether-2.4.5.jar


  1. SerCe
    06.11.2015 01:43

    Как лютый хак при
    Caused by: java.lang.ClassNotFoundException: org.apache.ivy.Ivy
    можно предварительно натравливать URLClassLoader на ivy jar-ник в maven central, а затем Grape.grab.


    1. igor_suhorukov
      06.11.2015 11:57

      maven central

      Вы имели в виду кеш maven в локальной файловой системе?
      Для этого хака Ivy должен был запрашиваться каким-либо приложением раньше чем вам он понадобиться, чтобы оказаться в этом кеше


      1. SerCe
        06.11.2015 15:21
        +1

        Нет, натравив прямо на URL джарника в maven central


        1. igor_suhorukov
          06.11.2015 22:21

          Интересный хак!)


  1. guai
    06.11.2015 11:34

    Примерно то же самое можно сделать на грэдл. Делаем gradlew run, враппер укачивает сам грэдл, запустит сборочный скрипт, подтягивает плагины, зависимости, всё такое, потом запустит собранное приложение. Хотя это может быть помедленнее, т.к. там сначала настройка, потом, если надо, сборка; настройка на каждом запуске происходит. Но зато с подзависимостями проблем не будет.
    Одноразовые задачи можно в сам сборочный скрипт загнать, там офигенно удобно манипулировать файлами.


    1. igor_suhorukov
      06.11.2015 11:55

      Это зависит от конечной цели. Не удобно забивать гвозди отверткой…

      Если нужен полноценный проект, то Grape возможно не лучший инстурмент для этого. И конечно лучше это делать на gradle. Вы правильно заметили количество действий, что необходимо сделать и еще необходимо написать скрипт сборки. Я не призываю на Grape делать то что делает система сборки проекта)
      Если же нужно написать компактный скрипт или решать задачи, похожие на те задачи которые решает AspectJ-Scripting, то Grape — отличный инструмент


      1. guai
        06.11.2015 12:53
        +1

        Конечно, для такой задачи грэдл — оверкилл и/или задел на будущее.