Using Java 8 Libraries for Android Applications Using Maven

Java 8 came out in early 2014, allowing Java developers to use some very convenient innovations to make programming trivial tasks easier. Among them are lambda expressions, references to methods and constructors, the implementation of default interface methods at the language level and JVM, as well as the use of the Stream APIat the standard library level. Unfortunately, the sluggishness of introducing such introductions affects the support of these tools on other Java-oriented software platforms. GWT and Android still do not have official support for at least the Java 8 language tools. However, the spring SNAPSHOT versions of GWT 2.8.0 already supported lambda expressions. With Android, things are different, since here the work of lambda expressions depends not only on the compiler itself, but also on the runtime. But with the help of Maven, it is relatively easy to solve the problem of using Java 8.

It so happened that I keep the entire code base for my projects on Maven due to the fact that:

  • it happened historically, despite the bulkiness of pom.xml;
  • it is possible to configure the assembly in one place for modules of any level of nesting;
  • it is possible to use a single tool for assembling the entire “universe” of modules.

General purpose libraries from this code base are written and connected to other modules in such a way that they can be used both in Java SE projects and in GWT or Android. But in view of the fact that Android is bad with Java 8, these libraries continue to remain in Java 6 or 7, as well as the applications themselves from the Android code base. Nevertheless, after successfully working with lambdas in GWT, there was a desire to migrate your entire Java 8 code base. Compiling and installing your libraries in a local repository is not a big deal:

org.apache.maven.pluginsmaven-compiler-plugin1.8
		1.8

After installing the libraries in the local repository, you can, in principle, build the application itself. But during the “dex" process, the following error will occur:

[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

This error means that dxit cannot process the class files generated by the Java 8 compiler. Therefore, we connect Retrolambda , which, in theory, should fix the situation:

net.orfjackal.retrolambdaretrolambda-maven-plugin2.0.6process-classesprocess-mainprocess-testtrue1.6

Unfortunately, it foo/bar/FooBar.classbelongs to the library and the error persists. retrolambda-maven-plugincannot handle the task of instrumenting application libraries in principle, since it can only process class files for the current module (otherwise, it would be necessary to process class files directly in the repository). That is, the application cannot use the Java 8 libraries, but can only use the Java 8 code in the current module. This can be solved like this:

  • unpack all Java 8 dependencies into a directory where you can downgrade bytecode;
  • process the bytecode of the current module simultaneously with the bytecode of the unpacked dependencies;
  • compile a DEX file and an APK file with the exception of modules that are already in a processed state.

The current implementation android-maven-pluginlaunches dxwith all the dependencies specified, which further complicates the instrumentation of dependencies in Java 8. Here's what roughly launches 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

Here, all three Java 8 libraries are sent for processing dx. In the plugin itself, there is no way to manage the filter of dependencies that need to be passed to dx. Why is it important to be able to control such a filter? It can be assumed that some dependencies are already in a more convenient place for processing than the artifact repository. For example, in ${project.build.directory}/classes. This is where you can handle Java 8 dependencies with retrolambda-maven-plugin.

There is a plugin for Maven , with which you can unpack dependencies in the desired directory, which will allow you to process the necessary dependencies in the right way. For instance:

org.apache.maven.pluginsmaven-dependency-plugin2.10process-classesunpack-dependenciesruntimefoo1-java8,foo2-java8,foo3-java8${project.build.directory}/classes

I added to the fork android-maven-pluginsupport for several options for managing the dependency filter. Among them - filtering and inclusion ( excludesand includes) on the group ID, the identifier of the artifact and the version. Artifact identifiers and their versions can be omitted. All elements that identify an artifact or group of artifacts must be separated by a colon. Nevertheless, you can try Java 8 and Java 8-dependencies in an Android application, although the merge request to the parent repository has not yet been accepted. To do this, you first need to assemble the fork of the plug-in itself:

# Хеш коммита последней синхронизации с 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/4.3.1-SNAPSHOT<\\/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/4.3.1-SNAPSHOT<\\/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

After all this, you can configure pom.xmlyour application:

org.apache.maven.pluginsmaven-compiler-plugin3.21.8
        1.8org.apache.maven.pluginsmaven-dependency-plugin2.10process-classesunpack-dependenciesruntimefoo1-java8,foo2-java8.foo3-java8${project.build.directory}/classesnet.orfjackal.retrolambdaretrolambda-maven-plugin2.0.6process-classesprocess-mainprocess-testtrue1.6com.simpligility.maven.pluginsandroid-maven-plugin4.3.1-SNAPSHOT-a79e45bc0721bfea97ec139311fe31d959851476package${project.basedir}/src/main/android/AndroidManifest.xml${project.basedir}/src/main/android/assets${project.basedir}/src/main/android/res19truetrue${project.basedir}/proguard.conffoo1-java8foo2-java8foo3-java8truenet.sf.proguardproguard-base5.2.1runtime

That, in fact, is all. It should be noted that this approach implies the use of only Java 8 language tools, and not standard libraries such as Stream API. I also want to emphasize that using this technique, you can not only make friends with Android applications and their dependencies written in Java 8, but also process byte code of third-party dependencies as you like. I can’t say that I completely like this solution in terms of elegance.

Perhaps in other project build systems, things are much simpler. I don’t even know if it could be easier in Maven itself, and whether all this hack is not part of the bicycle industry, but, nevertheless, I was interested in forcing Maven to do what is required of it.

Also popular now: