Обзор Java App Bundlers

Итак, в прошлый раз я писал об инструменте для сборки приложений JavaFXPackager. Там было 2 каких-то способа собрать приложение, но ни один из них не мог быть удобно вызван просто из кода. Но мы же труЪ Java-программисты. И вот для таких труЪ-программистов с версии 8u20 и был создан в JDK специальный API в JavaFXPackager, который позволяет просто вот так взять и собрать бандл из ваших бинарников. Одна проблема — этот API незадокументирован. Но не беда, разберёмся.Источники информацииПервичным источником будет, естественно, JavaDoc, который, впрочем, тоже не доступен публично. Поэтому мы соберём его сами из исходников. Не заставляя это делать читателей, я просто выложу его: ginz.bitbucket.org/fxpackager-javadoc. Конечно, нелишним будет иметь у себя исходный код, так как документирован API плохо.Введение Какой класс заставляет в первую очередь обратить на себя внимание? Видимо, интерфейс Bundler с вот такой спецификацией: // action methods File execute (Map params, File outputParentDir); boolean validate (Map params);

// information methods Collection> getBundleParameters (); String getBundleType (); String getDescription (); String getID (); String getName (); Методы явно делятся на 2 вида: методы, производящие действия с уже данными параметрами и методы, дающие информацию об этом бандлере.

execute принимает Map параметров и выходную директорию в качестве параметров и создаёт в данной директории бандл в каком-то ожидаемом формате. Но ожидать правильного исполнения мы можем только если validate проходит без исключений, а их он может кинуть 2 вида: UnsupportedPlatformException и ConfigException, говорящие названия которых подсказывают, что в случае выкидывания первого вы просто используете бандлер, который не поддерживается вашей платформой. Второй же выкидывается, если у вас что-то не так с переданными параметрами.

А что вообще можно передавать в качестве параметров в эти методы? Большая часть ключей в этих методах находится во всяких статических экземплярах класса BundlerParamInfo, большая часть из которых (общие кроссплатформенные параметры) находятся в классе StandardBundlerParam, а более специфичные (платформенно-зависимые), как правило, находятся в самих классах-реализациях Bundler’а: LinuxAppBundler, WinExeBundler и так далее. Хотя не совсем очевидно, каким образом этот класс вообще связан с тем, что мы кладём в params, который передаём execute. На самом деле оказывается, что id, получаемый getID (), является ключом в этом Map’е, а значение должно быть типа, которым параметризован BundlerParamInfo.

А как получить экземпляры бандлеров? Самый простой метод получения всех возможных «предустановленных» Bundler’ов через статический метод createBundlersInstance интерфейса Bundlers (Warning, HOT: Java 8): public static List getSuitableBundlers () { return Bundlers.createBundlersInstance () .getBundlers () .stream () .filter (bundler → { try { bundler.validate (Collections.emptyMap ()); } catch (UnsupportedPlatformException ex) { return false; } catch (ConfigException ignored) { } return true; }).collect (Collectors.toList ()); } Таким образом, мы не только получили все «предустановленные» бандлеры, но и отфильтровали их по платформенному принципу: как правило, невозможно собирать пакеты для одной платформы на другой.

И что, никаких примеров?! А, точно, можно попробовать перестать теоретизировать и нарисовать какой-нибудь пример.Поскольку мне кажется, что статическая типизация всегда лучше, то мы можем обернуть создание ассоциативного массива параметров во что-то типа BunderParamsBuilder:

public class BundlerParamsBuilder { private Map params = new HashMap<>();

public BundlerParamsBuilder setParam (BundlerParamInfo param, T value) { params.put (param.getID (), value); return this; }

public BundlerParamsBuilder unsafeSetParam (String key, Object value) { params.put (key, value); return this; }

public Map build () { return new HashMap<>(params); } } Таким образом, сборка проекта будет заключаться примерно в следующем (подразумевается существование запускающегося /tmp/helloWorld.jar): List bundlers = getSuitableBundlers (); Path directoryWithBundles = Files.createTempDirectory («bundles»); Path jar = Paths.get (»/tmp/helloWorld.jar»);

RelativeFileSet mainJar = new RelativeFileSet (jar.getParent ().toFile (), new HashSet( Arrays.asList (jar.toFile ()) )); Map params = new BundlerParamsBuilder () .setParam (StandardBundlerParam.APP_NAME, «HelloWorld») .setParam (StandardBundlerParam.APP_RESOURCES, mainJar) .build ();

bundlers.forEach (bundler → bundler.execute (params, directoryWithBundles.toFile ()));

System.out.println («Bundles are created in » + directoryWithBundles); System.out.println («Parameters after bundling:» + params); Выхлоп после запуска будет примерно следующий: Bundles are created in /tmp/bundles5791581710818077755 Parameters after bundling: {appVersion=1.0, copyright=Copyright © 2015, stopOnUninstall=true, .mac-jdk.runtime.rules=[Lcom.oracle.tools.packager.JreUtils$Rule;@4c3e4790, mac.app.bundler=Mac Application Image, linux.deb.imageRoot=/tmp/fxbundler6592356981290936843/images/linux-deb.image/helloworld-1.0/opt, buildRoot=/tmp/fxbundler6592356981290936843, mac.bundle-id-signing-prefix=HelloWorld., linux.deb.licenseText=Unknown, linux.deb.maintainer=Unknown , jvmProperties={}, mac.signing-key-user-name=, licenseFile=[], identifier=HelloWorld, linux.rpm.imageDir=/tmp/fxbundler6592356981290936843/images/linux-rpm.image, runtime=RelativeFileSet{basedir:/home/dginzburg/soft/jdk1.8.0_25/jre, files:[]}, shortcutHint=false, mainJar=RelativeFileSet{basedir:/tmp, files:[helloWorld.jar]}, jvmOptions=[], name.fs=HelloWorld, fxPackaging=false, name=HelloWorld, appResources=RelativeFileSet{basedir:/tmp, files:[helloWorld.jar]}, mac.category=Unknown, linux.deb.imageDir=/tmp/fxbundler6592356981290936843/images/linux-deb.image/helloworld-1.0, .mac.default.icns=GenericAppHiDPI.icns, runAtStartup=false, linux.app.bundler=Linux Application Image, mac.signing-key-developer-id-app=null, linux.launcher.url=jar: file:/home/dginzburg/soft/jdk1.8.0_25/lib/ant-javafx.jar!/com/oracle/tools/packager/linux/JavaAppLauncher, description=HelloWorld, configRoot=/tmp/fxbundler6592356981290936843/macosx, preferencesID=HelloWorld, title=HelloWorld, linux.bundleName=helloworld, startOnInstall=false, mac.pkg.packagesRoot=/tmp/fxbundler6592356981290936843/packages, licenseType=Unknown, linux.deb.fullPackageName=helloworld-1.0, mac.CFBundleIdentifier=HelloWorld, serviceHint=false, vendor=Unknown, email=Unknown, applicationCategory=Unknown, mac.app.imageRoot=/tmp/fxbundler6592356981290936843/images/dmg.image, userJvmOptions={}, classpath=, linux.deb.configDir=/tmp/fxbundler6592356981290936843/images/linux-deb.image/helloworld-1.0/DEBIAN, verbose=false, imagesRoot=/tmp/fxbundler6592356981290936843/images, mac.daemon.image=/tmp/fxbundler6592356981290936843/images/pkg.daemon, applicationClass=HelloWorld, .linux.runtime.rules=[Lcom.oracle.tools.packager.JreUtils$Rule;@38cccef, menuHint=true} Ой, откуда у нас всё это тут взялось? Мы вроде клали в params только 2 параметра. Чтобы разобраться в этом, надо посмотреть на сорцы метода BunderParamInfo.fetchFrom, который используется для того, чтобы получить значение параметра из params:

//… if (getDefaultValueFunction () != null) { T result = getDefaultValueFunction ().apply (params); if (result!= null) { params.put (getID (), result); } return result; } //… Ага, если параметр не был найден в params и он может быть получен через другие параметры (за это отвечает функция defaultValueFunction), то значение, полученное таким образом, запихивается в params. Например, мы не указали параметр MAIN_JAR, но MAIN_JAR обязателен для того, чтобы создать запускаемый файл. Посмотрим на то, как определяется MAIN_JAR: в качестве defaultValueFunction видим: params → { extractMainClassInfoFromAppResources (params); return (RelativeFileSet) params.get («mainJar»); } Вот и ответ, откуда там появляется значение mainJar.Полный список параметров Для того, чтобы не искать каждый раз параметры, я сделал табличку, в которую вывел информацию по всем найденным статическим экземплярам BundlerParamInfo.Заключение Описан самый минимум и самые очевидные возможности, так что не стесняйтесь читать JavaDoc и даже код.Статья будет улучшаться и исправляться по запросу комментаторов.

© Habrahabr.ru