Так сложилось, что всю кодовую базу для своих проектов я держу на Maven из-за того, что:
- так сложилось исторически, не смотря на всю громоздкость pom.xml;
- есть возможность настраивать сборку в одном месте для модулей любого уровня вложенности;
- есть возможность использовать единый инструмент для сборки всей “вселенной” модулей.
Библиотеки общего назначения из этой кодовой базы написаны и подключаются к другим модулям таким образом, что их можно использовать как и в Java SE-проектах, так и в GWT или Android. Но ввиду того, что у Android плохо с Java 8, эти библиотеки и дальше остаются на Java 6 или 7, как и сами приложения из кодовой базы на Android. Тем не менее, после успешной работы с лямбдами в GWT, появилось желание мигрировать всю свою кодовую базу на Java 8. Скомпилировать и установить в локальный репозиторий свои библиотеки не составляет большого труда:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
После установки библиотек в локальный репозиторий можно, в принципе, собирать само приложение. Но в процессе “dex”-ирования возникнет следующая ошибка:
[INFO] UNEXPECTED TOP-LEVEL EXCEPTION:
[INFO] com.android.dx.cf.iface.ParseException: bad class file magic (cafebabe) or version (0034.0000)
[INFO] at com.android.dx.cf.direct.DirectClassFile.parse0(DirectClassFile.java:472)
[INFO] at com.android.dx.cf.direct.DirectClassFile.parse(DirectClassFile.java:406)
[INFO] at com.android.dx.cf.direct.DirectClassFile.parseToInterfacesIfNecessary(DirectClassFile.java:388)
[INFO] at com.android.dx.cf.direct.DirectClassFile.getMagic(DirectClassFile.java:251)
[INFO] at com.android.dx.command.dexer.Main.processClass(Main.java:665)
[INFO] at com.android.dx.command.dexer.Main.processFileBytes(Main.java:634)
[INFO] at com.android.dx.command.dexer.Main.access$600(Main.java:78)
[INFO] at com.android.dx.command.dexer.Main$1.processFileBytes(Main.java:572)
[INFO] at com.android.dx.cf.direct.ClassPathOpener.processArchive(ClassPathOpener.java:284)
[INFO] at com.android.dx.cf.direct.ClassPathOpener.processOne(ClassPathOpener.java:166)
[INFO] at com.android.dx.cf.direct.ClassPathOpener.process(ClassPathOpener.java:144)
[INFO] at com.android.dx.command.dexer.Main.processOne(Main.java:596)
[INFO] at com.android.dx.command.dexer.Main.processAllFiles(Main.java:498)
[INFO] at com.android.dx.command.dexer.Main.runMonoDex(Main.java:264)
[INFO] at com.android.dx.command.dexer.Main.run(Main.java:230)
[INFO] at com.android.dx.command.dexer.Main.main(Main.java:199)
[INFO] at com.android.dx.command.Main.main(Main.java:103)
[INFO] ...while parsing foo/bar/FooBar.class
Эта ошибка означает, что
dx
не может обработать класс-файлы, сгенерированные компилятором Java 8. Поэтому подключаем Retrolambda, что, по идее, должно исправить ситуацию:<plugin>
<groupId>net.orfjackal.retrolambda</groupId>
<artifactId>retrolambda-maven-plugin</artifactId>
<version>2.0.6</version>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>process-main</goal>
<goal>process-test</goal>
</goals>
</execution>
</executions>
<configuration>
<defaultMethods>true</defaultMethods>
<target>1.6</target>
</configuration>
</plugin>
К сожалению,
foo/bar/FooBar.class
принадлежит библиотеке и ошибка не устраняется. retrolambda-maven-plugin
не может справиться с задачей инструментирования библиотек приложения в принципе, так как может обработать класс-файлы только для текущего модуля (инача для этого нужно было бы обработать класс-файлы прямо в репозитории). То есть приложение не может использовать Java 8 библиотеки, но может использовать Java 8 код только в текущем модуле. Это можно решить так:- распаковать все Java 8 зависимости в директорию, где можно провести “даунгрейд” байткода;
- обработать байткод текущего модуля одновременно с байткодом распакованных зависимостей;
- собрать DEX-файл и APK-файл с исключением модулей, которые уже находятся в обработанном состоянии.
Текущая реализация
android-maven-plugin
запускает dx
с указанием всех зависимостей, что ещё более усугубляет инструментирование зависимостей на Java 8. Вот что примерно запускает android-maven-plugin
:$JAVA_HOME/jre/bin/java
-Xmx1024M
-jar "$ANDROID_HOME/sdk/build-tools/android-4.4/lib/dx.jar"
--dex
--output=$BUILD_DIRECTORY/classes.dex
$BUILD_DIRECTORY/classes
$M2_REPO/foo1-java8/bar1/0.1-SNAPSHOT/bar1-0.1-SNAPSHOT.jar
$M2_REPO/foo2-java8/bar2/0.1-SNAPSHOT/bar2-0.1-SNAPSHOT.jar
$M2_REPO/foo3-java8/bar3/0.1-SNAPSHOT/bar3-0.1-SNAPSHOT.jar
Здесь все три Java 8 библиотеки отправляются на обработку
dx
. В самом плагине не существует возможности управлять фильтром зависимостей, которые нужно передать в dx
. Почему важно иметь возможность управлять таким фильтром? Можно предположить, что некоторые зависимости уже находятся в более удобном для обработки, чем репозиторий артефактов, месте. Например, в ${project.build.directory}/classes
. Именно здесь и можно обработать Java 8 зависимости с помощью retrolambda-maven-plugin
.Для Maven существует плагин, которым можно распаковать зависимости в нужную директорию, что позволит обработать нужные зависимости нужным образом. Например:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<includeGroupIds>foo1-java8,foo2-java8,foo3-java8</includeGroupIds>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
Я добавил в форк
android-maven-plugin
поддержку нескольких опций для управления фильтром зависимостей. Среди них — фильтрация и включение (excludes
и includes
) по идентификатору группы, идентификатору артефакта и версии. Идентификаторы артефактов и их версии можно не указывать. Все элементы, идентифицирующие артефакт или группу артефактов, должны быть разделены двоеточием. Тем не менее, попробовать Java 8 и Java 8-замисимости в Android-приложении можно, хотя запрос на слияние в родительский репозиторий пока не принят. Для этого сначала нужно собрать сам форк плагина:# Хеш коммита последней синхронизации с upstream оригинального плагина:
PLUGIN_REVISION=a79e45bc0721bfea97ec139311fe31d959851476
# Клонируем форк:
git clone https://github.com/lyubomyr-shaydariv/android-maven-plugin.git
# Убеждаемся в том, что используем проверенный коммит:
cd android-maven-plugin
git checkout $PLUGIN_REVISION
# Собираем плагин:
mvn clean package -Dmaven.test.skip=true
# Переходим в target, где будем готовиться к установке форка в Maven-репозиторий:
cd target
cp android-maven-plugin-4.3.1-SNAPSHOT.jar android-maven-plugin-4.3.1-SNAPSHOT-$PLUGIN_COMMIT.jar
# Исправляем pom.xml:
cp ../pom.xml pom-$PLUGIN_COMMIT.xml
sed -i "s/<version>4.3.1-SNAPSHOT<\\/version>/<version>4.3.1-SNAPSHOT-$PLUGIN_COMMIT<\\/version>/g" pom-$PLUGIN_COMMIT.xml
# Обновляем дескриптор плагина:
unzip android-maven-plugin-4.3.1-SNAPSHOT-$PLUGIN_COMMIT.jar META-INF/maven/plugin.xml
sed -i "s/<version>4.3.1-SNAPSHOT<\\/version>/<version>4.3.1-SNAPSHOT-$PLUGIN_COMMIT<\\/version>/g" META-INF/maven/plugin.xml
zip android-maven-plugin-4.3.1-SNAPSHOT-$PLUGIN_COMMIT.jar META-INF/maven/plugin.xml
# Устанавливаем, собственно, плагин:
mvn org.apache.maven.plugins:maven-install-plugin:2.5.2:install-file -DpomFile=pom-$PLUGIN_COMMIT.xml -Dfile=android-maven-plugin-4.3.1-SNAPSHOT-$PLUGIN_COMMIT.jar
После всего этого можно настроить
pom.xml
своего приложения:<!-- Включаем поддержку Java 8 для текущего модуля -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<!-- Распаковываем классы из зависимостей на Java 8 в текущую директорию сборки -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<!-- Нужно указать только Java 8 зависимости -->
<includeGroupIds>foo1-java8,foo2-java8.foo3-java8</includeGroupIds>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<!-- Преобразуем байткод -->
<plugin>
<groupId>net.orfjackal.retrolambda</groupId>
<artifactId>retrolambda-maven-plugin</artifactId>
<version>2.0.6</version>
<executions>
<execution>
<phase>process-classes</phase>
<goals>
<goal>process-main</goal>
<goal>process-test</goal>
</goals>
</execution>
</executions>
<configuration>
<defaultMethods>true</defaultMethods>
<target>1.6</target>
</configuration>
</plugin>
<!-- DEX-ируем все не Java 8 зависимости (к тому моменту в target/classes уже находятся библиотеки, которые уже понятны для dx) и упаковываем всё в APK -->
<plugin>
<groupId>com.simpligility.maven.plugins</groupId>
<artifactId>android-maven-plugin</artifactId>
<version>4.3.1-SNAPSHOT-a79e45bc0721bfea97ec139311fe31d959851476</version>
<executions>
<execution>
<phase>package</phase>
</execution>
</executions>
<configuration>
<androidManifestFile>${project.basedir}/src/main/android/AndroidManifest.xml</androidManifestFile>
<assetsDirectory>${project.basedir}/src/main/android/assets</assetsDirectory>
<resourceDirectory>${project.basedir}/src/main/android/res</resourceDirectory>
<sdk>
<platform>19</platform>
</sdk>
<undeployBeforeDeploy>true</undeployBeforeDeploy>
<proguard>
<skip>true</skip>
<config>${project.basedir}/proguard.conf</config>
</proguard>
<excludes>
<exclude>foo1-java8</exclude>
<exclude>foo2-java8</exclude>
<exclude>foo3-java8</exclude>
</excludes>
</configuration>
<extensions>true</extensions>
<dependencies>
<dependency>
<groupId>net.sf.proguard</groupId>
<artifactId>proguard-base</artifactId>
<version>5.2.1</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</plugin>
Вот, собственно, и всё. Следует отметить, что такой подход подразумевает использование только языковых средств Java 8, а не стандартных библиотек типа Stream API. Хочу также подчеркнуть, что используя данную методику можно не только подружить Android с приложениями и их зависимостями, написанными на Java 8, но и обрабатывать байт-код сторонних зависимостей как заблагорассудится. Не могу сказать, что мне полностью нравится это решение с точки зрения элегантности.
Возможно, в других системах сборки проектов всё значительно проще. Я даже не знаю, может ли это быть проще в самом Maven, и не является ли вся эта поделка частью велосипедостроения, но, тем не менее, мне было интересно заставить Maven сделать то, что от него требуется.
Комментарии (12)
ragesteel
15.09.2015 14:39+2Странно что до сих пор не набежали любители Gradle'а и рассказали что в ихней системе все эти проблемы уже решены. ;)
burjui
15.09.2015 15:38Не все, к сожалению. В целом всё работает, но иногда случается крайне неприятная вещь: во время работы приложения на Android код, использующий лямбды, может внезапно упасть из-за отсутствия какого-нибудь класса с названием в духе $$блаблабла$Lambda1. Решается полной пересборкой проекта, но проблема в том, что код падает только при обращении к лямбде, то есть клиенты будут недовольны, а мы об этом узнаем только из крэшей в Developer Console. Воистину, DEX — неисчерпаемый источник проблем, да и вообще Java на мобиле.
Dimezis
15.09.2015 16:48+1Эта проблема решается в Gradle конфиге, там можно отключить инкрементальную сборку для ретролямбды:
retrolambda {
incremental false
}
У меня после этого не было подобных эксепшенов.
fogone
16.09.2015 10:20+2Котлин компилируется в байткод шестой джавы, имеет лямбды, неплохой инструментарий упрощающий разработку под андроид и совсем скорый первый релиз
artemgapchenko
16.09.2015 13:30+1А есть хоть какая-то информация о том, когда релиз? Просто первый раз о скором релизе я услышал где-то в районе M10-M11, которые были с полгода назад примерно. Сами разработчики ничего конкретного о релизе не говорят, ограничиваясь словами что «как только, так сразу».
fogone
16.09.2015 16:06Ко сожалению, я не инсайдер и не в курсе, когда будет релиз, но по состоянию проекта и телодвижениям, которые совершают разработчики, похоже, что они готовят первый релиз. К тому же несколько раз слышал о том, что в планах выпустить этой осенью. Но я готов еще подождать, чтобы первый релиз был хорошо подготовлен, ведь после релиза поменять язык будет уже сильно сложнее, так что пусть позже, но лучше продумано.
Borz
Вот этот проект умеет компилировать Java8 не только для Android. Посмотрите на его реализацию — может будет какая подмога.
vedenin1980
Интересно, проект указан как open-source с некоторой платной поддержкой, но при этом я так и не смог найти на этом сайте где взять исходники. Вы не знаете исходники вообще реально найти?
Borz
в документации есть ссылка на исходники плагина, а там есть ссылка на исходники порта