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

    После мучений с отладкой сложных 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,8 МБ, который заменяет собой groovy-all и позволяет пользоваться механизмом Grape в вашем Groovy приложении, используя резолвер зависимостей AetherGrapeEngine.

    Скачиваем из центрального репозитария groovy-grape-aether-2.4.5.1.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.1.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.1</version>
    </dependency>
    


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

    Комментарии 12

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

      Правда, я использую обычные скрипты #!/usr/bin/groovy и у меня всё работает. В чем преимущество запуска через java -jar?
        0
        Да, вы правы про скрипты и удобство.
        Но похоже что ответ содержиться в вашем вопросе — для этого нужен установленный груви, доступный в env path и java.
        Преимущество в подходе из статьи: нужна только установленная в системе java и файл groovy-grape-aether-2.4.5.jar
        0
        Как лютый хак при
        Caused by: java.lang.ClassNotFoundException: org.apache.ivy.Ivy
        можно предварительно натравливать URLClassLoader на ivy jar-ник в maven central, а затем Grape.grab.
          0
          maven central

          Вы имели в виду кеш maven в локальной файловой системе?
          Для этого хака Ivy должен был запрашиваться каким-либо приложением раньше чем вам он понадобиться, чтобы оказаться в этом кеше
            +1
            Нет, натравив прямо на URL джарника в maven central
          0
          Можете пожалуйста готовый пример предложенного решения привести, чтобы запустить то же самое с «чистым» groovy?
            0
            3 Мб оверхеда, да ещё и выковырянных из другого проекта. Вы же, как я понял, всё равно используете maven-указаение зависимостей и его артефакты. Ivy (https://mvnrepository.com/artifact/org.apache.ivy/ivy/2.4.0) занимает всего 900Кб и готов к использованию. В том числе как зависимость проекта, в котором вы собираете единый артефакт.

            На сколько я понимаю, основной смысл использование в скриптах, эта же тема развивается в комментариях, что сложных случаев каких-то зависимостей не нужно. Так зачем тогда на столько усложнять и увеличивать объём артефакта более чем в 3 раза?
              0
              Добрый вечер! Первая аналогия что пришла в голову в связи с вашим аргументом про разницу в размере библиотеки в 3 раза: «Зачем играть в Quake4 когда Doom2 в Nдцать раз меньше и его можно записать на флоппи-диски!?». Странный критерий оценки ПО, когда различается не только объем, но качество и функциональность.

              Если вам интересно, посмотрите на пример AlarmSystem.groovy. Там используется не только разрешение зависимостей с помощью aether, но и динамическая загрузка класса с помощью MavenClassLoader из этой же библиотеки. Функциональные возможности сборки Groovy не ограничиваются этим…
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              Это зависит от конечной цели. Не удобно забивать гвозди отверткой…

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

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое