Компіляції Java програм і дозвіл залежностей в runtime

Як можна поєднати деякі достоїнства динамічних мов зі строгою типізацією в звичайному 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 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 та доопрацьований під нього 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("з").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 завантажувача завантажувач який вантажив клас скрипта.

Для швидкого налагодження і генерації pom.xml і java исходников, можна запустити програму з параметром -DgenerateMavenProjectAndExit=true
У поточній директорії створиться pom файл для maven та всі необхідні директорії з вихідним кодом.

java-as-script завантажує вихідний код програми по якомусь з сотні протоколів java.net.URL, дозволяє залежно скрипта на java, зазначені як коментарі //dependency:mvn:/, компілює вихідний код з цими залежностями, завантажує клас і запускає його main метод. При цьому можна підключитися за допомогою remote debugger і налагоджувати скрипт як звичайну програму на java.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.