Как можно совместить некоторые достоинства динамических языков со строгой типизацией в обычном Java коде?
Самое интересное на хабре, обычно происходит в комментариях к статье. Вот и в этот раз в комментариях к «Модуляризация в JavaSE без OSGI и Jigsaw» началось обсуждение что работа через reflection в java перечеркивает многие плюсы библиотеки mvn-classloader. В Groovy же с этой библиотекой работать просто и удобно:
Попробуем это исправить с помощью java-as-script.jar, исходный код проекта доступен на github.
Стандарт JSR 199 — java Compiler API, существует довольно давно. Интерфейсы API присутствуют в java пакетах javax.tools.*. Но чтобы компилировать java код из памяти в память и потом запустить его, надо изрядно написать кода и побить в бубен. Реализация компилятора не идет в составе JRE и tools.jar нет в maven репозитариях.
Хотелось бы что-нибудь готовое, не велосипедить каждый раз и коллега подсказал проект Janino. Сам janino содержит свой компилятор подмножества java и хорошо подходит лишь для вычисления выраженией. Есть org.codehaus.janino:commons-compiler-jdk который использует JSR 199, но вот только сильно зависит от oracle/openjdk tools.jar. Доработав этот проект, java-as-script включает в себя eclipse java compiler и доработанный под него commons-compiler-jdk. Он самодостаточен и позволяет компилировать и загружать код java 8 даже в JRE.
В Groovy есть удобный механизм Grape, который позволяет добавить любые зависимости из maven репозитариев в ваш скрипт.
Если объеденить компилятор java и mvn-classloader, то в Java программу можно добавить зависимости с помощью комментариев в исходном коде.
Приведу рабочий пример. Для запуска кода сигнализации на RaspberryPi достаточно лишь одного java файла. Запустить пример можно из командной строки:
Чтобы избавиться от reflection в коде, нужно в зависимостях скрипта указать API к которому делается cast классов из mvn-classloader загрузчика и указать в качестве parent загрузчика загрузчик который грузил класс скрипта.
Внедрить запуск скрипта в существующую программу на JVM довольно просто:
Если же вам нужен загрузчик классов, то достаточно вызвать
Для быстрой отладки и генерации pom.xml и java исходников, можно запустить программу с параметром -DgenerateMavenProjectAndExit=true
В текущей директории создастся pom файл для maven и все необходимые директории с исходным кодом. Это позволяет разрабатывать скрипт в привычной вам IDE со всеми ее возможностями по работе с кодом на java и его отладки.
java-as-script загружает исходный код программы по какому-либо из сотни протоколов java.net.URL, разрешает зависимости скрипта на java, указанные как комментарии //dependency:mvn:/, компилирует исходный код с этими зависимостями, загружает класс и запускает его main метод. При этом можно подключиться с помощью remote debugger и отлаживать скрипт как обычную программу на java.
Самое интересное на хабре, обычно происходит в комментариях к статье. Вот и в этот раз в комментариях к «Модуляризация в JavaSE без OSGI и Jigsaw» началось обсуждение что работа через reflection в java перечеркивает многие плюсы библиотеки mvn-classloader. В Groovy же с этой библиотекой работать просто и удобно:
def hawtIoConsole = MavenClassLoader.usingCentralRepo().forMavenCoordinates('io.hawt:hawtio-app:2.0.0').loadClass('io.hawt.app.App')
Thread.currentThread().setContextClassLoader(hawtIoConsole.getClassLoader())
hawtIoConsole.main('--port','10090')
Попробуем это исправить с помощью java-as-script.jar, исходный код проекта доступен на github.
Динамическая компиляция
Стандарт JSR 199 — java Compiler API, существует довольно давно. Интерфейсы API присутствуют в java пакетах javax.tools.*. Но чтобы компилировать java код из памяти в память и потом запустить его, надо изрядно написать кода и побить в бубен. Реализация компилятора не идет в составе JRE и tools.jar нет в maven репозитариях.
Хотелось бы что-нибудь готовое, не велосипедить каждый раз и коллега подсказал проект Janino. Сам janino содержит свой компилятор подмножества java и хорошо подходит лишь для вычисления выраженией. Есть org.codehaus.janino:commons-compiler-jdk который использует JSR 199, но вот только сильно зависит от oracle/openjdk tools.jar. Доработав этот проект, java-as-script включает в себя eclipse java compiler и доработанный под него commons-compiler-jdk. Он самодостаточен и позволяет компилировать и загружать код java 8 даже в JRE.
Динамическое разрешение зависимостей
В Groovy есть удобный механизм Grape, который позволяет добавить любые зависимости из maven репозитариев в ваш скрипт.
Если объеденить компилятор java и mvn-classloader, то в Java программу можно добавить зависимости с помощью комментариев в исходном коде.
//dependency:mvn:/org.slf4j:slf4j-simple:1.6.6
//dependency:mvn:/org.apache.camel:camel-core:2.18.0
Приведу рабочий пример. Для запуска кода сигнализации на RaspberryPi достаточно лишь одного java файла. Запустить пример можно из командной строки:
java -Dlogin=...YOUR_EMAIL...@mail.ru -Dpassword=******* -jar java-as-script-1.1.jar https://raw.githubusercontent.com/igor-suhorukov/alarm-system/master/src/main/java/com/github/igorsuhorukov/alarmsys/AlarmSystem.java
package com.github.igorsuhorukov.alarmsys;
//dependency:mvn:/com.github.igor-suhorukov:mvn-classloader:1.8
//dependency:mvn:/org.apache.camel:camel-core:2.18.0
//dependency:mvn:/org.apache.camel:camel-mail:2.18.0
//dependency:mvn:/io.rhiot:camel-webcam:0.1.4
//dependency:mvn:/io.rhiot:camel-pi4j:0.1.4
//dependency:mvn:/org.slf4j:slf4j-simple:1.6.6
import com.github.igorsuhorukov.smreed.dropship.MavenClassLoader;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
import org.apache.camel.Processor;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.impl.DefaultAttachment;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.management.event.CamelContextStartedEvent;
import org.apache.camel.management.event.CamelContextStoppedEvent;
import org.apache.camel.support.EventNotifierSupport;
import javax.mail.util.ByteArrayDataSource;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.EventObject;
class AlarmSystem {
public static void main(String[] args) throws Exception{
String login = System.getProperty("login");
String password = System.getProperty("password");
DefaultCamelContext camelContext = new DefaultCamelContext();
camelContext.setName("Alarm system");
Endpoint mailEndpoint = camelContext.getEndpoint(String.format("smtps://smtp.mail.ru:465?username=%s&password=%s&contentType=text/html&debugMode=true", login, password));
camelContext.addRoutes(new RouteBuilder() {
@Override
public void configure() throws Exception {
from("pi4j-gpio://3?mode=DIGITAL_INPUT&pullResistance=PULL_DOWN").routeId("GPIO read")
.choice()
.when(header("CamelPi4jPinState").isEqualTo("LOW"))
.to("controlbus:route?routeId=RaspberryPI Alarm&action=resume")
.otherwise()
.to("controlbus:route?routeId=RaspberryPI Alarm&action=suspend");
from("timer://capture_image?delay=200&period=5000")
.routeId("RaspberryPI Alarm")
.to("webcam:spycam?resolution=HD720")
.setHeader("to").constant(login)
.setHeader("from").constant(login)
.setHeader("subject").constant("alarm image")
.process(new Processor() {
@Override
public void process(Exchange it) throws Exception {
DefaultAttachment attachment = new DefaultAttachment(new ByteArrayDataSource(it.getIn().getBody(byte[].class), "image/jpeg"));
it.getIn().setBody(String.format("<html><head></head><body><img src=\"cid:alarm-image.jpeg\" /> %s</body></html>", new Date()));
attachment.addHeader("Content-ID", "<alarm-image.jpeg>");
it.getIn().addAttachmentObject("alarm-image.jpeg", attachment);
//set CL to avoid javax.activation.UnsupportedDataTypeException: no object DCH for MIME type multipart/mixed
Thread.currentThread().setContextClassLoader( getClass().getClassLoader() );
}
}).to(mailEndpoint);
}
});
registerLifecycleActions(camelContext, mailEndpoint, login);
camelContext.start();
}
}
Чтобы избавиться от reflection в коде, нужно в зависимостях скрипта указать API к которому делается cast классов из mvn-classloader загрузчика и указать в качестве parent загрузчика загрузчик который грузил класс скрипта.
Внедрить запуск скрипта в существующую программу на JVM довольно просто:
org.github.suhorukov.java.as.script.ScriptRunner#runScript(String scriptText, String[] scriptArgs)
Если же вам нужен загрузчик классов, то достаточно вызвать
org.github.suhorukov.java.as.script.JavaCompiler#compileScript(java.lang.String scriptText)
и после этого работать с классами из скрипта в вашей программе.Для быстрой отладки и генерации pom.xml и java исходников, можно запустить программу с параметром -DgenerateMavenProjectAndExit=true
В текущей директории создастся pom файл для maven и все необходимые директории с исходным кодом. Это позволяет разрабатывать скрипт в привычной вам IDE со всеми ее возможностями по работе с кодом на java и его отладки.
java-as-script загружает исходный код программы по какому-либо из сотни протоколов java.net.URL, разрешает зависимости скрипта на java, указанные как комментарии //dependency:mvn:/, компилирует исходный код с этими зависимостями, загружает класс и запускает его main метод. При этом можно подключиться с помощью remote debugger и отлаживать скрипт как обычную программу на java.
Поделиться с друзьями
officeMouse
Статья супер, в хорошем смысле этого слова. Вот только один вопрос нафига? В многообразии языков под любую задачу можно найти элегантное решение и не городить костыли!
igor_suhorukov
Действительно, можно использовать groovy, kotlin, можно scala… Но не всегда можно вводить новый язык программирования в проект из за требования на гомогенность технологий в проекте. Но при этом хочется использовать фичу аналогичную Grape.
История такая же как с GWT — я ходил по собеседованиям и не ожидал что так много проектов в Москве до сих пор его используют… Я его в последний раз использовал года 4 назад и думал что мало где он остался. Казалось бы зачем GWT, когда есть typescript, scala.js. Так вот для бизнеса важно легко найти обычного разработчика, а не очень дорогого ниндзю десятка технологий. Бизнес платит деньги на разработку и требует снижать стоимость разработки, поддержки и поиска специалистов. Ведь Мартина Одерски заманить на обычный проект будет не легко! А java разработчиков целая очередь…
Вторая и главная причина почему разработал java-as-script — моя лень. Для публикации примеров к статьям на java мне не нужно 100 файлов, десяток директорий и скрипт сборки. Что для groovy, что для java теперь всего один файл! AlarmSystem.groovy, AlarmSystem.java запускается и то и то с помощью java -jar из командной строки.