Уличная магия в скриптах или что связывает Groovy, Ivy и Maven?

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

3f0ecd0dbafe402caa35b2a1a88ca7d3.jpg

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

Про это рассказывал и показывал на примерах в цикле публикаций на хабре

В публикации вы найдете работающее решение для 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
8572f479e1db410a99d1c084db5b0111.png

Итак, мы можем теперь использовать зависимости из 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>

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

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

© Habrahabr.ru