Пользователь
0,0
рейтинг
15 сентября 2015 в 12:16

Разработка → Использование библиотек на Java 8 для приложений под Android с помощью Maven из песочницы

Java 8 вышла в начале 2014 года, позволив Java-разработчикам использовать весьма удобные новшества для облегчения программирования тривиальных задач. Среди них — лямбда-выражения, ссылки на методы и конструкторы, реализация интерфейсных методов по умолчанию на уровне языка и JVM, а также использование Stream API на уровне стандартной библиотеки. К сожалению, вялость внедрения таких введений сказывается на поддержке этих средств на других программных платформах, ориентированных на Java. GWT и Android всё ещё не располагают официальной поддержкой хотя бы языковых средств Java 8. Впрочем, весенние SNAPSHOT-версии GWT 2.8.0 уже поддерживали лямбда-выражения. С Android дела обстоят иначе, так как здесь работа лямбда-выражений зависит не только от самого компилятора, но и от среды исполнения. Но с помощью Maven можно относительно просто решить проблему использования Java 8.

Так сложилось, что всю кодовую базу для своих проектов я держу на 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 сделать то, что от него требуется.
@lyubomyr-shaydariv
карма
4,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (12)

  • 0
    Вот этот проект умеет компилировать Java8 не только для Android. Посмотрите на его реализацию — может будет какая подмога.
    • 0
      Интересно, проект указан как open-source с некоторой платной поддержкой, но при этом я так и не смог найти на этом сайте где взять исходники. Вы не знаете исходники вообще реально найти?
      • +1
        в документации есть ссылка на исходники плагина, а там есть ссылка на исходники порта
  • +2
    Странно что до сих пор не набежали любители Gradle'а и рассказали что в ихней системе все эти проблемы уже решены. ;)
    • 0
      OFF: комментом выше как раз дал ссылку на Gradle-проект ;)
    • 0
      Не все, к сожалению. В целом всё работает, но иногда случается крайне неприятная вещь: во время работы приложения на Android код, использующий лямбды, может внезапно упасть из-за отсутствия какого-нибудь класса с названием в духе $$блаблабла$Lambda1. Решается полной пересборкой проекта, но проблема в том, что код падает только при обращении к лямбде, то есть клиенты будут недовольны, а мы об этом узнаем только из крэшей в Developer Console. Воистину, DEX — неисчерпаемый источник проблем, да и вообще Java на мобиле.
      • +1
        Эта проблема решается в Gradle конфиге, там можно отключить инкрементальную сборку для ретролямбды:

        retrolambda {
        incremental false
        }

        У меня после этого не было подобных эксепшенов.
        • 0
          Спасибо. А на скорости сборки сильно отразилось?
          • 0
            У меня вообще не отразилось. Проект как собирался полторы минуты, так и собирается :)
  • +2
    Котлин компилируется в байткод шестой джавы, имеет лямбды, неплохой инструментарий упрощающий разработку под андроид и совсем скорый первый релиз
    • +1
      А есть хоть какая-то информация о том, когда релиз? Просто первый раз о скором релизе я услышал где-то в районе M10-M11, которые были с полгода назад примерно. Сами разработчики ничего конкретного о релизе не говорят, ограничиваясь словами что «как только, так сразу».
      • 0
        Ко сожалению, я не инсайдер и не в курсе, когда будет релиз, но по состоянию проекта и телодвижениям, которые совершают разработчики, похоже, что они готовят первый релиз. К тому же несколько раз слышал о том, что в планах выпустить этой осенью. Но я готов еще подождать, чтобы первый релиз был хорошо подготовлен, ведь после релиза поменять язык будет уже сильно сложнее, так что пусть позже, но лучше продумано.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.