⬆️ ⬇️

Java 9 tutorial for those who have to work with legacy code

Good evening, colleagues. Exactly one month ago we received a contract for the translation of the book " Modern Java " from the publishing house Manning, which should be one of our most notable new products in the coming year. The problem of "Modern" and "Legacy" in Java is so acute that the need for such a book is quite overdue. The scale of the disaster and how to solve problems in Java 9 are briefly described in the article by Wayne Citrin, the translation of which we want to offer you today.



Every few years, with the release of a new version of Java, speakers at JavaOne begin to savor new language constructs and APIs, praise their merits. And zealous developers in the meantime can not wait to introduce new features. Such a picture is far from reality - it does not take into account at all that most programmers are busy supporting and refining existing applications , rather than writing new applications from scratch.



Most applications — especially commercial ones — must be backward compatible with earlier versions of Java, which do not support all of these new super-duper features. Finally, the majority of customers and end users, especially in the segment of large enterprises, are wary of a radical update of the Java platform, preferring to wait until it gets stronger.



Therefore, as soon as the developer is going to try a new opportunity, he faces problems. Would you use default interface methods in your code? Maybe - if you're lucky, and your application does not need to interact with Java 7 or lower. Want to use the java.util.concurrent.ThreadLocalRandom class to generate pseudo-random numbers in a multi-threaded application? It will not work if your application should work simultaneously on Java 6, 7, 8 or 9.

')

With the release of the new release, developers who are engaged in supporting the inherited code feel like children who are forced to stare at the window of a pastry shop. They are not allowed inside, therefore their destiny is disappointment and frustration.



So, is there anything in the new release of Java 9 for programmers involved in supporting legacy code? Something that could make their lives easier? Fortunately, yes.



What had to be done with the support of the legacy code, the appearance of Java 9



Of course, you can cram the capabilities of the new platform into legacy applications , in which you need to maintain backward compatibility. In particular, there is always the opportunity to take advantage of the new APIs. However, it may turn out a bit ugly.



For example, you can use late binding if you want to access the new API when your application also needs to work with older versions of Java that do not support this API. Suppose you need to use the java.util.stream.LongStream class, introduced in Java 8, and you want to use the anyMatch(LongPredicate) method of this class, but your application must be compatible with Java 7. You can create an auxiliary class like this:



 public classLongStreamHelper { private static Class longStreamClass; private static Class longPredicateClass; private static Method anyMatchMethod; static { try { longStreamClass = Class.forName("java.util.stream.LongStream"); longPredicateClass = Class.forName("java.util.function.LongPredicate"); anyMatchMethod = longStreamClass.getMethod("anyMatch", longPredicateClass): } catch (ClassNotFoundException e) { longStreamClass = null; longPredicateClass = null; anyMatchMethod = null } catch (NoSuchMethodException e) { longStreamClass = null; longPredicateClass = null; anyMatchMethod = null; } public static boolean anyMatch(Object theLongStream, Object thePredicate) throws NotImplementedException { if (longStreamClass == null) throw new NotImplementedException(); try { Boolean result = (Boolean) anyMatchMethod.invoke(theLongStream, thePredicate); return result.booleanValue(); } catch (Throwable e) { // lots of potential exceptions to handle. Let's simplify. throw new NotImplementedException(); } } } 


There are ways to simplify this operation, or make it more general, or more effective - you get the idea.



Instead of calling theLongStream.anyMatch(thePredicate) , as you would in Java 8, you can call LongStreamHelper.anyMatch(theLongStream, thePredicate) in any version of Java. If you are dealing with Java 8, it will work, but if with Java 7, the program will throw a NotImplementedException exception.



Why is it ugly? Because the code may become overly complicated if you need to access a set of APIs (in fact, even now, with a single API, this is already inconvenient). In addition, this practice is not type safe, since the code cannot directly mention LongStream or LongPredicate . Finally, this practice is much less efficient, due to the costs of reflection, and also because of the additional try-catch blocks. Consequently, although it is possible to do so, it is not too interesting, and is fraught with inadvertent errors.



Yes, you can access the new API, and your code still maintains backward compatibility, but with new language constructs you will not succeed. For example, suppose we need to use lambda expressions in code that must remain backward compatible and work in Java 7. You are not lucky. The Java compiler does not allow specifying the source version above target. So, if you set the level of compliance with source code 1.8 (i.e., Java 8), and the target level of compliance is 1.7 (Java 7), the compiler will not allow you to do this.



JAR-files of different versions will help you.



Relatively recently, another great opportunity to use the latest features of Java, while allowing applications to work with older versions of Java, where such applications were not supported, appeared. In Java 9, this feature is provided for both new APIs and new Java language constructs: talking about multi-variance JAR files .



Different JAR files are almost the same as the good old JAR files, but with one important caveat: the new JAR files have a kind of “niche” where you can write classes that use the latest features of Java 9. If you work with Java 9, then The JVM will find this “niche”, use the classes from it and ignore the classes of the same name from the main part of the JAR file.



However, when working with Java 8 or lower, the JVM is not aware of the existence of this “niche”. It ignores it and uses classes from the main part of the JAR file. With the release of Java 10, a new, similar “niche” will appear for classes using the most current features of Java 10 and so on.



In JEP 238 , the Java revision clause, which describes the JAR files, is a simple example. Suppose we have a JAR file with four classes running in Java 8 or lower:



 JAR root - A.class - B.class - C.class - D.class 


Now imagine that after Java 9 comes out, we rewrite classes A and B so that they can use the new features specific to Java 9. Then Java 10 comes out, and we rewrite class A again so that it can use the new Java 10 features. , the application should still work fine with Java 8. The new JAR file of different versions looks like this:



 JAR root - A.class - B.class - C.class - D.class - META-INF Versions - 9 - A.class - B.class - 10 - A.class 


The JAR file has not only acquired a new structure; now in its manifest it is indicated that this file is of different versions.



When you run this Java 8 JVM JAR file, it ignores the \META-INF\Versions section, because it does not even know about it and does not search for it. Only original classes A, B, C and D are used.



When running under Java 9, the classes in \META-INF\Versions\9 are used, and they are used instead of the original classes A and B, but the classes in \META-INF\Versions\10 ignored.



When running under Java 10, both \META-INF\Versions branches are used; in particular, version A of Java 10, version B of Java 9, and the default versions of C and D.



So, if you need a new ProcessBuilder API from Java 9 in your application, but you need to ensure that the application continues to work under Java 8, just write new versions of your classes using ProcessBuilder into the \META-INF\Versions\9 section of the JAR file , and leave the old classes in the main part of the archive used by default. This is the easiest way to use new Java 9 features without sacrificing backward compatibility.



In Java 9 JDK, there is a version of the jar.exe tool that supports the creation of multi-version JAR files. Other non-JDK tools also provide this support.



Java 9: ​​modules, modules everywhere



The Java 9 module system (also known as Project Jigsaw) is undoubtedly the biggest change in Java 9. One of the goals of modularization is to strengthen the Java encapsulation mechanism so the developer can specify which APIs are provided to other components and can be calculated that the JVM will impose encapsulation. With modularization, encapsulation is stronger than with the use of public/protected/private access modifiers for classes or class members.



The second goal of modularization is to indicate which modules other modules need to work and, before launching the application, make sure in advance that all the necessary modules are in place. In this sense, modules are stronger than the traditional classpath mechanism, since the classpath paths are not checked in advance, and errors are possible due to the lack of necessary classes. Thus, an incorrect classpath can be detected already when the application has time to work long enough, or after it has been launched many times.

The whole system of modules is large and complex, and its detailed discussion is beyond the scope of this article (Here is a good, detailed explanation ). Here I will focus on those aspects of modularization that help the developer with the support of legacy applications.



Modularization is a good thing, and the developer should try to break the new code into modules whenever possible, even if the rest of the application is (yet) not modularized. Fortunately, this is easy to do thanks to the specification for working with modules.



First, the JAR file becomes modularized (and turns into a module) when the module-info.class file (compiled from module-info.java) appears at the root of the JAR file. module-info.java contains metadata, in particular, the name of the module whose packages are exported (that is, visible from the outside), which modules this module requires and some other information.



The information in module-info.class is visible only in cases where the JVM is looking for it — that is, the system treats modularized JAR files exactly as usual if it works with older versions of Java (it is assumed that the code was compiled to work with an older version of Java Strictly speaking, it requires a little pohimichit, and still specify Java 9 as the target version of module-info.class, but this is real).



Thus, you should be able to run modularized JAR files with Java 8 and below, provided that in other respects they are also compatible with earlier versions of Java. Also note that the module-info.class can, with reservations, be placed in versioned areas of different JAR-files .



In Java 9, there is a classpath as well as a module path. and a module path. Classpath works as usual. If you put a modularized JAR file in the classpath, it is wasted like any other JAR file. That is, if you modularized the JAR file, and your application is not yet ready to treat it as a module, you can put it in the classpath, it will work as always. Your inherited code should handle it quite successfully.



Also note that the collection of all JAR files in the classpath is considered part of the only unnamed module. Such a module is considered the most common, however, it exports all the information to other modules, and can refer to any other modules. Thus, if you do not have a modularized Java application, but there are some old libraries that are not yet modularized (and probably never will be), you can simply put all these libraries in the classpath and the whole system will work fine.



Java 9 has a module path that works along with the classpath. When using modules from this path, the JVM can check (both at compile time and at run time) whether all the necessary modules are in place, and report an error if there are not enough modules. All JAR files in the classpath, as members of a nameless module, are available to modules listed in a modular path — and vice versa.



It is easy to transfer the JAR file from the classpath to the module path - and take full advantage of modularization. First, you can add the module-info.class file to the JAR file, and then put the modular JAR file in the module path. Such a new module will still be able to access all the remaining JAR files in the JAR classpath, since they are included in an unnamed module and remain in access.



It is also possible that you do not want to modularize the JAR file, or that the JAR file does not belong to you, but to someone else, so you cannot modularize it yourself. In this case, the JAR file can still be put in the path of the modules; it will become an automatic module.



An automatic module is considered a module, even if it does not have a module-info.class . This module is named after the JAR file in which it is contained, and other modules can explicitly request it. It automatically exports all of its publicly available APIs and reads (that is, requires) all other named modules, as well as unnamed modules.



Thus, an unmodularized JAR file from the classpath can be turned into a module without doing anything at all. Inherited JAR files are automatically converted into modules, they simply lack some information that would allow to determine if all the necessary modules are in place, or to determine what is missing.



Not every non-modularized JAR file can be moved to the module path and turned into an automatic module. There is a rule: a package can be part of just one named module . That is, if the package is in more than one JAR file, then only one JAR file with this package in composition can be turned into an automatic module. The rest can remain in the classpath and become part of a nameless module.



At first glance, the mechanism described here seems complicated, but in practice it is very simple. In fact, in this case, the only thing is that you can leave the old JAR-files in the classpath or move them to the module path. You can break them into modules or not. And when your old JAR files are modularized, you can leave them in the classpath or move them to the modules path.



In most cases, everything should simply work as before. Your inherited JAR files should take root in a new modular system. The more you modularize the code, the more dependency information you need to check, and the missing modules and APIs will be detected at much earlier stages of development and may save you from large chunks of work.



Java 9 "does it yourself": Modular JDK and Jlink



One of the problems with legacy Java applications is that the end user may not work with a suitable Java environment. One way to ensure that a Java application is operational is to provide a runtime environment with the application. Java allows you to create private (re-distributed) JREs that can be distributed within an application. Here 's how to create a private JRE. As a rule, the hierarchy of the JRE files that is installed along with the JDK is taken, the necessary files are saved, and optional files are saved with the functionality that your application may need.



The process is a bit troublesome: it is necessary to maintain the hierarchy of installation files, but be careful, so you don’t miss a single file, not a single directory. In itself, this does not hurt, however, I still want to get rid of all the excess, because these files take up space. Yes, it is easy to give in and make such a mistake.



So why not reassign this work to the JDK?



In Java 9, you can create a self-contained environment added to the application — and in this environment you will have everything you need to run the application. You no longer have to worry about the wrong Java environment on the user's computer, you do not have to worry about the wrong JRE you have built yourself.



The key resource for creating such self-sufficient executable images is a modular system. Now it is possible to modularize not only your own code, but also Java 9 JDK itself. Now the Java class library is a collection of modules, and the JDK tools also consist of modules. The module system requires you to specify the base class modules that are needed in your code, and you specify the necessary JDK elements.



To put it all together, Java 9 has a special tool called jlink . By running jlink, you get a hierarchy of files — exactly the ones you need to run your application, no more, no less. Such a set will be much smaller than the standard JRE, moreover, it will be platform-specific (that is, chosen for a specific operating system and machine). Therefore, if you want to create such executable images for other platforms, you will need to run jlink in the context of the installation on each specific platform for which you need such an image.



Also note that if you run jlink with an application in which nothing is modularized, the tool simply does not have the necessary information to compress the JRE, so there is nothing left for jlink except to pack the whole JRE. Even in this case, you will be a little more comfortable: jlink will pack the JRE for you, so you need not worry about how to copy the file hierarchy correctly.



With jlink, it becomes easy to pack an application and all that is needed to run it — and you need not worry about doing something wrong. The tool will pack only that part of the execution environment that is required for the operation of the application. That is, a legacy Java application is guaranteed to receive an environment in which it will be operational.



Meeting old and new



, Java- , , . Java 9, , API , ( ) , , Java.



Java 9: -, , , Java.



JAR- Java 9 JAR-, Java . , Java 9, Java 8 – .



Java, , JAR- , . , , « » .



JDK jlink, , . , Java – .



Java, Java 9 , – , , Java.

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



All Articles