I want to share solutions to one problem that I had to face, plus researching this issue in the context of Java 9.
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:
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.
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).
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!
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 :
- Invoke findLoadedClass (String) to check if the class has already been loaded.
- Invoke the class loader. If the parent is null, the class loader is built-in.
- 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}
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.
SearchClient
),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.
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).
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):
module-info.class
in the root of the jar-archive (module or modularized jar),class-path
.Now this information will be enough to apply it on our project:
elasticsearch.shaded
modules.Searcher
class will manually load es-v * modules into separate child layers with its class loader using the ServiceLoader API.Is everything so simple? ...
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.
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.
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:
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> -->
<excludes>
right behind the commented <includes>
node: <excludes> <exclude>org.codehaus.groovy:groovy-all</exclude> <exclude>org.slf4j:*</exclude> <exclude>log4j:*</exclude> </excludes>
<!--<minimizeJar>true</minimizeJar>-->
<configuration>
node: <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> </transformers>
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>
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.
$ mvn versions:set -DnewVersion=2.4.5
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
wasorg.elasticsearch.common
com.carrotsearch.hppc
wasorg.elasticsearch.common.hppc
jsr166e
wasorg.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>
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
.
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:
$ mvn clean package -pl org.elasticsearch:parent,org.elasticsearch:elasticsearch -DskipTests=true
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:
elasticsearch.client.core
module, since there is an interface in itSearchClient
interface.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 .
, /. :
PS Java 9!
Source: https://habr.com/ru/post/339026/
All Articles