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

от автора

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

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

Чтобы не быть голословным и были понятны возможности 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> 

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

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

ссылка на оригинал статьи http://habrahabr.ru/post/270145/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *