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 сделать то, что от него требуется.

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


  1. Borz
    15.09.2015 13:40

    Вот этот проект умеет компилировать Java8 не только для Android. Посмотрите на его реализацию — может будет какая подмога.


    1. vedenin1980
      15.09.2015 17:15

      Интересно, проект указан как open-source с некоторой платной поддержкой, но при этом я так и не смог найти на этом сайте где взять исходники. Вы не знаете исходники вообще реально найти?


      1. Borz
        15.09.2015 17:42
        +1

        в документации есть ссылка на исходники плагина, а там есть ссылка на исходники порта


  1. ragesteel
    15.09.2015 14:39
    +2

    Странно что до сих пор не набежали любители Gradle'а и рассказали что в ихней системе все эти проблемы уже решены. ;)


    1. Borz
      15.09.2015 15:04

      OFF: комментом выше как раз дал ссылку на Gradle-проект ;)


    1. burjui
      15.09.2015 15:38

      Не все, к сожалению. В целом всё работает, но иногда случается крайне неприятная вещь: во время работы приложения на Android код, использующий лямбды, может внезапно упасть из-за отсутствия какого-нибудь класса с названием в духе $$блаблабла$Lambda1. Решается полной пересборкой проекта, но проблема в том, что код падает только при обращении к лямбде, то есть клиенты будут недовольны, а мы об этом узнаем только из крэшей в Developer Console. Воистину, DEX — неисчерпаемый источник проблем, да и вообще Java на мобиле.


      1. Dimezis
        15.09.2015 16:48
        +1

        Эта проблема решается в Gradle конфиге, там можно отключить инкрементальную сборку для ретролямбды:

        retrolambda {
        incremental false
        }

        У меня после этого не было подобных эксепшенов.


        1. burjui
          15.09.2015 17:11

          Спасибо. А на скорости сборки сильно отразилось?


          1. Dimezis
            15.09.2015 17:14

            У меня вообще не отразилось. Проект как собирался полторы минуты, так и собирается :)


  1. fogone
    16.09.2015 10:20
    +2

    Котлин компилируется в байткод шестой джавы, имеет лямбды, неплохой инструментарий упрощающий разработку под андроид и совсем скорый первый релиз


    1. artemgapchenko
      16.09.2015 13:30
      +1

      А есть хоть какая-то информация о том, когда релиз? Просто первый раз о скором релизе я услышал где-то в районе M10-M11, которые были с полгода назад примерно. Сами разработчики ничего конкретного о релизе не говорят, ограничиваясь словами что «как только, так сразу».


      1. fogone
        16.09.2015 16:06

        Ко сожалению, я не инсайдер и не в курсе, когда будет релиз, но по состоянию проекта и телодвижениям, которые совершают разработчики, похоже, что они готовят первый релиз. К тому же несколько раз слышал о том, что в планах выпустить этой осенью. Но я готов еще подождать, чтобы первый релиз был хорошо подготовлен, ведь после релиза поменять язык будет уже сильно сложнее, так что пусть позже, но лучше продумано.