Pull to refresh

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

Reading time 9 min
Views 10K
После мучений с отладкой сложных 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 можно использовать в своих скриптах.
Tags:
Hubs:
+7
Comments 12
Comments Comments 12

Articles