📜 ⬆️ ⬇️

How to run a Java application with multiple versions of the same library in 2017

How to run a Java application with multiple versions of the same library in 2017


KDPV, nothing interesting


I want to share solutions to one problem that I had to face, plus researching this issue in the context of Java 9.


disclaimer

The writer of me is still the one (I write for the first time), so throwing delicious tomatoes with reasons given is welcome.
We will immediately agree that the article is not suitable as a guide for:


  • Java 9
  • Elasticsearch
  • Maven

If the last names of the information in the network are complete, then the first one ... with time it will appear, at least there is the necessary information .


Imagine a simple situation: expand the Elasticsearch cluster and load data into it. We are writing an application that searches in this cluster. Since new versions of Elasticsearch are constantly being released, we bring new ones to the cluster. Problems features using rolling upgrade. But here's a bad luck - at some point we changed the format of the stored data (for example, to make the most efficient use of some of the new features) and reindex it is not advisable to do so. Such an option will suit us: we put a new cluster on the same machines - the first cluster with the old data scheme remains in place only for the search, and the incoming data is loaded into the second with the new scheme. Then our search component will need to keep in touch 2 clusters.


Our application uses the Java API to communicate with the cluster, which means that it draws Elasticsearch itself in dependencies. It should be noted that the Rest Client was released along with the 5th version, which relieves us of such problems (as well as the convenient API of Elasticsearch itself), but we will move in time at the time of the 2nd version release.


Let's look at possible solutions using a simple application as an example: searching for a document in 2 clusters of Elasticsearch 1.7 and 2.4. The code is available on github , and repeats the structure of this article (only OSGi is missing).


Let's get down to business. Create a Maven project with the following structure:


+---pom.xml +---core/ | +---pom.xml | +---src/ | +---main/ | | +---java/ | | | +---elasticsearch/ | | | +---client/ | | | +---SearchClient.java | | | +---Searcher.java | | +---resources/ | +---test/ | +---java/ +---es-v1/ | +---pom.xml | +---src/ | +---main/ | | +---java/ | | | +---elasticsearch/ | | | +---client/ | | | +---v1/ | | | +---SearchClientImpl.java | | +---resources/ | +---test/ | +---java/ +---es-v2/ +---pom.xml +---src/ +---main/ | +---java/ | | +---elasticsearch/ | | +---client/ | | +---v2/ | | +---SearchClientImpl.java | +---resources/ +---test/ +---java/ 

It is obvious that in one module it is impossible to connect several versions of one library, therefore the project should be multi-module:



The core module contains the Searcher class, which is the "tester" of our es-v1 and es-v2 modules:


 public class Searcher { public static void main(String[] args) throws Exception { List<SearchClient> clients = Arrays.asList( getClient("1"), getClient("2") ); for (SearchClient client : clients) { System.out.printf("Client for version: %s%n", client.getVersion()); Map doc = client.search("test"); System.out.println("Found doc:"); System.out.println(doc); System.out.println(); } clients.forEach(SearchClient::close); } private static SearchClient getClient(String desiredVersion) throws Exception { return null; // .  } } 

Nothing supernatural: the version of Elasticsearch used by the module is displayed, and a test search is conducted through it - this will be enough for demonstration.


Let's look at one of the implementations, the second one is almost identical:


 public class SearchClientImpl implements SearchClient { private final Settings settings = ImmutableSettings.builder() .put("cluster.name", "es1") .put("node.name", "es1") .build(); private final Client searchClient = new TransportClient(settings) .addTransportAddress(getAddress()); private InetSocketTransportAddress getAddress() { return new InetSocketTransportAddress("127.0.0.1", 9301); } @Override public String getVersion() { return Version.CURRENT.number(); } @Override public Map search(String term) { SearchResponse response = searchClient.prepareSearch("*") .setQuery(QueryBuilders.termQuery("field", term)) .execute() .actionGet(); if (response.getHits().getTotalHits() > 0) { return response.getHits().getAt(0).getSource(); } else { return null; } } @Override public void close() { searchClient.close(); } } 

Everything is also simple: the current version is wired in Elasticsearch, and a search on the field in all indexes ( * ) returns the first found document, if any.


The problem here lies in exactly how to call the SearchClient interface SearchClient in the Searcher#getClient and get the desired result.


Maybe Class.forName?


Even if you are not a Java expert, you have probably heard that ClassLoader is dominant . It will not allow us to make our plans if left at the default, so this solution will not work in the least:


 private static SearchClient getClient(String desiredVersion) throws Exception { String className = String.format("elasticsearch.client.v%s.SearchClientImpl", desiredVersion); return (SearchClient) Class.forName(className).newInstance(); } 

We collect, run and see the result ... quite uncertain, for example, like this:


 Exception in thread "main" java.lang.IncompatibleClassChangeError: Implementing class at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:763) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) at java.net.URLClassLoader.defineClass(URLClassLoader.java:467) at java.net.URLClassLoader.access$100(URLClassLoader.java:73) at java.net.URLClassLoader$1.run(URLClassLoader.java:368) at java.net.URLClassLoader$1.run(URLClassLoader.java:362) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:361) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:348) at elasticsearch.client.Searcher.getClient(Searcher.java:28) at elasticsearch.client.Searcher.main(Searcher.java:10) 

Although I could throw a ClassNotFoundException ... or something else ...


Since the URLClassLoader will find and load the first class with the specified name from the specified set of jar-files and directories, this will not necessarily be the required class. In this case, this error occurs because the elasticsearch-2.4.5.jar library in the class-path elasticsearch-2.4.5.jar goes to elasticsearch-1.7.5.jar , so all classes (which match by name) will be loaded for 2.4.5 . Since our Searcher first tries to load the module for Elasticsearch 1.7.5 ( getClient("1") ), the URLClassLoader will not load those classes at all ...


When the class loader has classes intersecting by name (and therefore file names) classes, this state is called jar hell (or class-path hell).


Own ClassLoader


It becomes obvious that modules and their dependencies need to be spread across different class loaders. Simply create a URLClassLoader for each es-v * module and specify each of its directory with jar-files:


 private static SearchClient getClient(String desiredVersion) throws Exception { String className = String.format("elasticsearch.client.v%s.SearchClientImpl", desiredVersion); Path moduleDependencies = Paths.get("modules", "es-v" + desiredVersion); URL[] jars = Files.list(moduleDependencies) .map(Path::toUri) .map(Searcher::toURL) .toArray(URL[]::new); ClassLoader classLoader = new URLClassLoader(jars); // parent = app's class loader return (SearchClient) classLoader.loadClass(className).newInstance(); } 

We need to build and copy all the modules into the corresponding directories of the modules/es-v*/ , for this we use the maven-dependency-plugin in the es-v1 and es-v2 modules.


We will collect the project:


 mvn package 

And run:


 . 29, 2017 10:37:08  org.elasticsearch.plugins.PluginsService <init> INFO: [es1] loaded [], sites [] . 29, 2017 10:37:12  org.elasticsearch.plugins.PluginsService <init> INFO: [es2] modules [], plugins [], sites [] Client for version: 1.7.5 Found doc: {field=test 1} Client for version: 2.4.5 Found doc: {field=test 2} 

Bingo!


(1.7 will not work under JRE 9)

if you don't patch JvmInfo, which is mentioned later in the reassembly of Elasticsearch 1.7.


A very hardcore case assumes that the core module also uses some utility methods from the Elasticsearch library. Our current solution will no longer work because of the class loading order :


  1. Invoke findLoadedClass (String) to check if the class has already been loaded.
  2. Invoke the class loader. If the parent is null, the class loader is built-in.
  3. Invoke the findClass (String) method to find the class.

That is, in this case the Elasticsearch classes will be loaded from the core , and not es-v * . Looking closely at the boot order, we see a workaround: write your own class loader that violates this order by reversing steps 2 and 3. Such a loader will not only be able to load its es-v * module separately, but also see classes from the core .


Let's write your URLClassLoader, let's call it, for example, ParentLastURLClassLoader:


 public class ParentLastURLClassLoader extends URLClassLoader { ... } 

and override loadClass(String,boolean) by copying code from ClassLoader and removing all unnecessary:


 @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null) { try { if (getParent() != null) { c = getParent().loadClass(name); } } catch (ClassNotFoundException e) { } if (c == null) { c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } } 

Swapping calls to getParent().loadClass(String) and findClass(String) and we get:


 @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null) { try { c = findClass(name); } catch (ClassNotFoundException ignored) { } if (c == null) { c = getParent().loadClass(name); if(c == null) { throw new ClassNotFoundException(name); } } } if (resolve) { resolveClass(c); } return c; } } 

Since our application will manually load the module classes, the loader must throw a ClassNotFoundException if the class is not found anywhere.


The loader is written, now we use it, replacing the URLClassLoader in the getClient(String) method:


 ClassLoader classLoader = new URLClassLoader(jars); 

on ParentLastURLClassLoader :


 ClassLoader classLoader = new ParentLastClassLoader(jars); 

and running the application again, we see the same result:


 . 29, 2017 10:42:41  org.elasticsearch.plugins.PluginsService <init> INFO: [es1] loaded [], sites [] . 29, 2017 10:42:44  org.elasticsearch.plugins.PluginsService <init> INFO: [es2] modules [], plugins [], sites [] Client for version: 1.7.5 Found doc: {field=test 1} Client for version: 2.4.5 Found doc: {field=test 2} 

ServiceLoader API


In Java 6, the java.util.ServiceLoader class was added, which provides a mechanism for loading implementations on an interface / abstract class. This class also solves our problem:


 private static SearchClient getClient(String desiredVersion) throws Exception { Path moduleDependencies = Paths.get("modules", "es-v" + desiredVersion); URL[] jars = Files.list(moduleDependencies) .map(Path::toUri) .map(Searcher::toURL) .toArray(URL[]::new); ServiceLoader<SearchClient> serviceLoader = ServiceLoader.load(SearchClient.class, new URLClassLoader(jars)); return serviceLoader.iterator().next(); } 

Everything is very simple:



In order for the ServiceLoader to see the interface implementations, you need to create files with the full interface name in the META-INF/services directory:


 +---es-v1/ | +---src/ | +---main/ | +---resources/ | +---META-INF/ | +---services/ | +---elasticsearch.client.spi.SearchClient +---es-v2/ +---src/ +---main/ +---resources/ +---META-INF/ +---services/ +---elasticsearch.client.spi.SearchClient 

and write the full names of the classes that implement this interface on each line: in our case, one SearchClientImpl for each module.
For es-v1 , the file will have the line:


 elasticsearch.client.v1.SearchClientImpl 

and for es-v2 :


 elasticsearch.client.v2.SearchClientImpl 

We also use the maven-dependency-plugin to copy es-v * modules/es-v*/ into modules/es-v*/ . Rebuild the project:


 mvn clean package 

And run:


 . 29, 2017 10:50:17  org.elasticsearch.plugins.PluginsService <init> INFO: [es1] loaded [], sites [] . 29, 2017 10:50:20  org.elasticsearch.plugins.PluginsService <init> INFO: [es2] modules [], plugins [], sites [] Client for version: 1.7.5 Found doc: {field=test 1} Client for version: 2.4.5 Found doc: {field=test 2} 

Great, the desired result is again obtained.


For the mentioned hardcore case, you will have to bring the SearchClient interface into a separate spi module and use the following chain of loaders:


 core: bootstrap -> system spi: bootstrap -> spi es-v1: bootstrap -> spi -> es-v1 es-v2: bootstrap -> spi -> es-v2 

those.


  1. create a separate bootloader for spi (throw null in parent - the bootstrap bootloader will be used),
  2. we load im spi (interface SearchClient ),
  3. then create a loader for each es-v * module, which will have a spi loader for the parent,
  4. ...
  5. PROFIT!

OSGi Modules


I’ll confess immediately and honestly - I haven’t had to deal with OSGi frameworks (can it be for the better?). Looking at the beginner mana from Apache Felix and Eclipse Equinox, they are more like containers into which (manually?) They load bundles. Even if there are implementations for embedding, this is too cumbersome for our simple application. If I am wrong, state the opposite point of view in the comments (and in general you want to see what someone uses it and how) .


I did not go into this question, because in Java 9, the modules are now out of the box, which we will now consider.


Native modules in java?


Last week, the 9th version of the platform was released, in which the main innovation was the modularization of runtime and the source code of the platform itself. Just what we need!


Hint: In order to use the modules, you must first download and install JDK 9 , if you have not done so already.


Here, the problem is complicated only by the ability of the used libraries to run under the nine as modules (in fact, I just did not find a way in IntelliJ IDEA to specify the class-path along with the module-path, so then we do everything in context of module-path).


How the modular system works


Before we proceed to modifying the code of our application under a modular system, we first find out how it works (I can be mistaken, since I myself just started to understand this question).


In addition to the modules mentioned, there are still layers containing them. When the application starts, a boot layer is created, into which all the modules specified in the --module-path that the application consists of and their dependencies are java.base (all modules automatically depend on the java.base module). Other layers can be created programmatically.


Each layer has its own class loader (or hierarchy). Like loaders, modular layers can also be built hierarchically. In such a hierarchy, the modules of one layer can see other modules that are in the parent layer.


The modules themselves are isolated from each other and by default their packages and classes in them are not visible to other modules. The module descriptor (it is module-info.java ) allows you to specify which packages each module can open and which modules and their packages they themselves depend on. Additionally, modules can declare that they use some interfaces in their work, and can declare an accessible implementation of these interfaces. This information is used by the ServiceLoader API to load implementations of the intefraces from the modules.


Modules are explicit and automatic (there are more types, but we will limit ourselves to these):



Now this information will be enough to apply it on our project:


  1. In the boot layer, we will have only the core module. If there are es-v * modules in it, the application will not start due to conflicting transitive elasticsearch.shaded modules.
  2. The Searcher class will manually load es-v * modules into separate child layers with its class loader using the ServiceLoader API.

Is everything so simple? ...


Package hell


Modules are not allowed to have intersecting package names (in at least one layer).


For example, there is some library that provides some kind of API in the public class Functions . This library has a class Helpers with batch scopes. Here they are:


 com.foo.bar public class Functions class Helpers 

The second library comes onto the scene, which provides which complements the functionality of the first:


 com.foo.baz public class Additional 

And she needs some functionality from the closed class Helpers . We leave the situation by placing some class in the same package:


 com.foo.baz public class Additional com.foo.bar public class AccessorToHelpers 

Let's congratulate ourselves - we have just created the problem of split packages for ourselves from the point of view of a modular system. What can be done with such libraries? We are offered to leave such libraries in the class-path and reach them from the modules using automatic modules as a bridge. But we are not looking for easy ways, so we use another option: we report all its dependencies to the library and get one single jar-archive (known as fat jar and uber jar), it can be used in the module-path as an automatic module, bypassing the class-path. The problem may be the assembly of such an all-in-one jar.


Instructions for rebuilding Elasticsearch

Elasticsearch actively uses access to package-private methods / fields to access some Lucene functionality. To use it as an automatic module, we will make an uber jar out of it and install it in a local repository under the name elasticsaerch-shaded for further use in our project.


We collect Elasticsearch 1.7


In the first versions, the application project is a single Maven module, so there’s not much problem here: you need to fix pom.xml and some classes if you pom.xml 8th one.


We clone the repository in any directory, check the v1.7.5 tag and start to edit:


  • The project is already using the maven-shade-plugin , so to build uberjar you will need to comment out the inclusion of some packages so that everything will be included:

 <!-- <includes> <include>com.google.guava:guava</include> <include>com.carrotsearch:hppc</include> <include>com.fasterxml.jackson.core:jackson-core</include> ... </includes> --> 

and preferably in the original, without moving:


 <!-- <relocations> <relocation> <pattern>com.google.common</pattern> <shadedPattern>org.elasticsearch.common</shadedPattern> </relocation> <relocation> <pattern>com.carrotsearch.hppc</pattern> <shadedPattern>org.elasticsearch.common.hppc</shadedPattern> </relocation> ... </relocations> --> 

  • We'll have to remove Groovy (breaks the load due to such ambiguity ), as well as loggers (there are no configs for them, JUL will work fine by default), adding <excludes> right behind the commented <includes> node:

 <excludes> <exclude>org.codehaus.groovy:groovy-all</exclude> <exclude>org.slf4j:*</exclude> <exclude>log4j:*</exclude> </excludes> 

  • Turn off the cutting of unused classes - the plugin does not know about the ServiceLoader / Reflection API:

 <!--<minimizeJar>true</minimizeJar>--> 

  • And add the gluing of the service files with the implementation classes for the ServiceLoader API to the plug-in <configuration> node:

 <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> </transformers> 

  • With pom.xml finished, it remains to resolve UnsupportedOperationException , which throws java.lang.management.RuntimeMXBean#getBootClassPath . To do this, find the following string in the JvmInfo class:

 info.bootClassPath = runtimeMXBean.getBootClassPath(); 

and wrap it in the "right":


 if (runtimeMXBean.isBootClassPathSupported()) { info.bootClassPath = runtimeMXBean.getBootClassPath(); } else { info.bootClassPath = ""; } 

This information is used only for statistics.


Done, now you can build the jar:


 $ mvn package 

and after compiling and assembling we get the required elasticsearch-1.7.5.jar in the target directory. Now it needs to be installed in a local repository, for example, under the name elasticsearch-shaded :


 $ mvn install:install-file \ > -Dfile=elasticsearch-1.7.5.jar \ > -DgroupId=org.elasticsearch \ > -DartifactId=elasticsearch-shaded \ > -Dversion=1.7.5 \ > -Dpackaging=jar \ > -DgeneratePom=true 

Now this artifact can be used as an automatic module in our Maven-module es-v1 :


 <dependencies> <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch-shaded</artifactId> <version>1.7.5</version> </dependency> ... </dependencies> 

We collect Elasticsearch 2.4


Roll back local changes and check out the v2.4.5 tag. Starting with version 2, the project is divided into modules. The module we need, issuing elasticsearch-2.4.5.jar is the core module.


  • First of all, remove snapshot, we need a release:

 $ mvn versions:set -DnewVersion=2.4.5 

  • Now we will look for whether the shade plugin is configured where ... and stumble upon such a dock:

Shading and package relocation removed



Elasticsearch used to shade dependencies and to relocate packages. We no longer use shading or relocation.
Your package names:
  • com.google.common was org.elasticsearch.common
  • com.carrotsearch.hppc was org.elasticsearch.common.hppc
  • jsr166e was org.elasticsearch.common.util.concurrent.jsr166e
    ...

We will have to add the shade plugin again to the core module, adding the transformer service files and excluding loggers in the settings:


 <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> </transformers> <artifactSet> <excludes> <exclude>org.slf4j:*</exclude> <exclude>log4j:*</exclude> </excludes> </artifactSet> </configuration> </plugin> 

  • Remove the com.twitter:jsr166e ( com.twitter:jsr166e is used sun.misc.Unsafe , which is not in 9-ke) for the core module:

 <!-- <dependency> <groupId>com.twitter</groupId> <artifactId>jsr166e</artifactId> <version>1.1.0</version> </dependency> --> 

and change the com.twitter.jsr166e com.twitter.jsr166e to java.util.concurrent.atomic .


  • The animal-sniffer-maven-plugin detect the change in the previous step (there is no jsr166e in 7-ke), we remove:

 <!-- <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>animal-sniffer-maven-plugin</artifactId> </plugin> --> 

Done, now we are doing the same assembly and installation steps as for es-v1 , with some differences:


  • change version to 2.4.5,
  • just collect the core module:

 $ mvn clean package -pl org.elasticsearch:parent,org.elasticsearch:elasticsearch -DskipTests=true 

Modules in our project


After we found out that the libraries we use are capable of working in a modular Java system, we will create explicit Java modules from our Maven modules. To do this, we will need to create module-info.java files in each source directory ( src/main/java ) and describe the relationships between the modules in them.


The core module does not depend on any other modules, but only contains the interface that other modules must implement, so the description will look like this:


 //   - elasticsearch.client.core module elasticsearch.client.core { // ,     , //       exports elasticsearch.client.spi; //    ,     SearchClient //    ServiceLoader' uses elasticsearch.client.spi.SearchClient; } 

For modules es-v1 and es-v2 there will be a similar description:



Total we have for es-v1 :


 //   - elasticsearch.client.v1 module elasticsearch.client.v1 { //  core  SearchClient requires elasticsearch.client.core; //   requires elasticsearch.shaded; //   core,        provides elasticsearch.client.spi.SearchClient with elasticsearch.client.v1.SearchClientImpl; } 

For es-v2, almost everything is the same, only version v2 should appear in the names.


Now how to load such modules? The answer to this question is in the description of the ModuleLayer class, which contains a small example of loading a module from a file system. Assuming that es-v * modules are each all in the same modules/es-v*/ directories, you can write something like this:


 private static SearchClient getClient(String desiredVersion) throws Exception { Path modPath = Paths.get("modules", "es-v" + desiredVersion); ModuleFinder moduleFinder = ModuleFinder.of(modPath); ModuleLayer parent = ModuleLayer.boot(); Configuration config = parent.configuration().resolve(moduleFinder, ModuleFinder.of(), Set.of("elasticsearch.client.v" + desiredVersion)); ModuleLayer moduleLayer = parent.defineModulesWithOneLoader(config, Thread.currentThread().getContextClassLoader()); ServiceLoader<SearchClient> serviceLoader = ServiceLoader.load(moduleLayer, SearchClient.class); Optional<SearchClient> searchClient = serviceLoader.findFirst(); if (searchClient.isPresent()) { return searchClient.get(); } throw new Exception("Module 'elasticsearch.client.v" + desiredVersion + "' not found on " + modPath); } 

ModuleLayer#defineModulesWithManyLoaders , es-v* .


, . maven-compiler-plugin , — 3.7.0:


 <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.7.0</version> </plugin> 

Java 9 :


 <properties> <maven.compiler.source>1.9</maven.compiler.source> <maven.compiler.target>1.9</maven.compiler.target> </properties> 

maven-dependency-plugin , :


 $ mvn clean package 

, :


 . 29, 2017 10:59:01  org.elasticsearch.plugins.PluginsService <init> INFO: [es1] loaded [], sites [] . 29, 2017 10:59:04  org.elasticsearch.plugins.PluginsService <init> INFO: [es2] modules [], plugins [], sites [] Client for version: 1.7.5 Found doc: {field=test 1} Client for version: 2.4.5 Found doc: {field=test 2} 

, — , , Java 8 .


the end


, /. :



PS Java 9!


')

Source: https://habr.com/ru/post/339026/


All Articles