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

Original author: Wayne Citrin
  • Transfer
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? Perhaps - if you are lucky, and your application does not need to interact with Java 7 or lower. Want to use a class java.util.concurrent.ThreadLocalRandomto 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 a new release, developers who are engaged in supporting legacy code feel like children who have to stare at a pastry shop window. 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 legacy-code is 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 little 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 want to use a classjava.util.stream.LongStream, which appeared in Java 8, and you want to apply a method of anyMatch(LongPredicate)this class, but the application must be compatible with Java 7. You can create an auxiliary class, like this:

public classLongStreamHelper {
     privatestatic Class longStreamClass;
     privatestatic Class longPredicateClass;
     privatestatic 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;
          }
          publicstaticbooleananyMatch(Object theLongStream, Object thePredicate)throws NotImplementedException {
               if (longStreamClass == null) thrownew NotImplementedException();
               try {
                    Boolean result 
                         = (Boolean) anyMatchMethod.invoke(theLongStream, thePredicate);
                    return result.booleanValue();
               } catch (Throwable e) { // lots of potential exceptions to handle. Let’s simplify.thrownew 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 an exception NotImplementedException.

Why is it ugly? Because the code can be 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 LongStreamor LongPredicate. Finally, this practice is much less effective, due to the costs associated with reflection, as well as due to additional blocks.try-catch. Consequently, although it is possible to do so, it is not too interesting, and is fraught with inadvertent errors.

Yes, you can refer to the new API, and your code at the same time 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), then 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 are not supported. 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: a new “niche” appeared in new JAR files where you can write classes that use the latest features of Java 9. If you are working with Java 9, then The JVM will find this “niche”, use the classes from it and ignore the classes with 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 JAR file in Java 8 JVM, it ignores the partition \META-INF\Versionsbecause it does not even know about it and does not search for it. Only the original classes A, B, C and D are used.

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

When running under Java 10, both branches are used \META-INF\Versions; 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 \META-INF\Versions\9new versions of your classes using ProcessBuilder in the JAR file section, and leave the old classes in the main part default archive. 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, All Over Modules

System of Java 9 Modules(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 that the developer can specify which APIs are provided to other components and can expect the JVM to impose encapsulation. With modularization, encapsulation is stronger than with access modifiers public/protected/privatefor 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 (not yet) 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.javacontains metadata, in particular, the name of the module, the packages of which are exported (ie, they become visible from the outside), which modules this module requires and some other information.

Information inmodule-info.classis only visible when the JVM searches 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 takes a little , and still specify Java 9 as the target version of module-info.class, but this is realistic).

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 files module-info.classcan, with reservations, be placed in versioned areas of different JAR-files .

In Java 9, there is both a classpath and 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 a file module-info.classto a JAR file, and then put the modularized 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 there is no file in it 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 the 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 the optional files with the functionality that your application may need.

The process is a bit troublesome: it is necessary to maintain the hierarchy of installation files, and 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 unnecessary, 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 don’t have to worry about the wrong JRE compiled by you yourself.

The key resource for creating such self-sufficient executable images- this 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 system of modules requires you to specify the modules of the base classes that are necessary in your code, and in doing so 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 start it — and you need not worry about doing something wrong. The tool will pack only that part of the runtime 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

One of the problems that arise with the support of an inherited Java application is that you are deprived of all the advantages that appear when a new version is released. In Java 9, as in previous versions, a whole bunch of great new APIs and language features appeared, but developers (taught by bitter experience) can assume that they simply cannot use them without violating backward compatibility with earlier versions of Java.

Java 9 designers should be honored: they seem to have taken this into account and worked well to provide these new features and to those developers who have to support older versions of Java.

Different JAR files allow developers to apply new Java 9 features and bring them to a separate part of the JAR file, where earlier versions of Java will not notice them. Thus, it is easy for a developer to write code for Java 9, leave the old code for Java 8 and below and give the runtime a choice of which classes it can run.

Thanks to Java modules, it is easier for developers to check dependencies, it is enough to write all new JAR files in a modular form, and leave the old code unmodularized. The system is very gentle, it is focused on a gradual migration and almost always supports work with legacy code that “has never heard of” a modular system.

Thanks to the modular JDK and jlink, you can easily create executable images and guaranteed to provide the application with such a runtime environment that has everything you need to work. Previously, such a process was fraught with many errors, but modern Java toolkit allows you to automate it - and everything just works.

Unlike previous Java releases, you can freely use all the new features in Java 9, and if you are dealing with an old application and have to guarantee its support, then you can meet the needs of all your users, regardless of whether they are eager to upgrade to actual java version.

Also popular now: