Уличная магия в скриптах или что связывает 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,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 и минимально необходимым набором зависимостей. И Ivy провайдер с его проблемами больше не нужен. Теперь буду быстрее переходить с языка MVEL на Groovy. А вам желаю удачных экспериментов с Grape и новой степени свободы и удобства в программировании на Groovy.
Мы разобрали что Groovy, Ivy и Maven связывает часть языка груви Grape — технология для динамического подключения зависимостей и узнали как Grape можно использовать в проекте.