Book Image

Hands-On Cloud-Native Microservices with Jakarta EE

By : Luigi Fugaro, Mauro Vocale
Book Image

Hands-On Cloud-Native Microservices with Jakarta EE

By: Luigi Fugaro, Mauro Vocale

Overview of this book

Businesses today are evolving rapidly, and developers now face the challenge of building applications that are resilient, flexible, and native to the cloud. To achieve this, you'll need to be aware of the environment, tools, and resources that you're coding against. The book will begin by introducing you to cloud-native architecture and simplifying the major concepts. You'll learn to build microservices in Jakarta EE using MicroProfile with Thorntail and Narayana LRA. You'll then delve into cloud-native application x-rays, understanding the MicroProfile specification and the implementation/testing of microservices. As you progress further, you'll focus on continuous integration and continuous delivery, in addition to learning how to dockerize your services. You'll also cover concepts and techniques relating to security, monitoring, and troubleshooting problems that might occur with applications after you've written them. By the end of this book, you will be equipped with the skills you need to build highly resilient applications using cloud-native microservice architecture.
Table of Contents (14 chapters)

OpenJDK 9 and 10 – key features for cloud environments

Java SE 9 and 10 introduce some important new features and improvements.

A detailed analysis of the new features is out of the scope of this book. We will look at the main innovations that have brought added value to the use of Java within cloud platforms and microservices.

JDK 9

Let's start with Java SE 9, the revolutionary element entailing the modularization of the JDK reached through the Jigsaw project (http://openjdk.java.net/projects/jigsaw/).

The main goals of this new feature are as follows:

  • Make Java SE more flexible and scalable
  • Improve security and maintainability
  • Make it easier to construct, maintain, deploy, and upgrade large applications
  • Enable improved performance

Two of the main criticisms made by the community toward the Java SE platform were the slowness in the release of the newer versions, as described previously, and the size of the JDK both in terms of space occupation and class size (approximately 4,240 classes in JDK 8).

In cloud environments and devices related to Internet of Things (IoT) architectures, these aspects represented an important limit.

But, it was not easy to find a solution that would allow overcoming these limits without abandoning one of the cornerstones of Java SE—the backward compatibility between versions.

It was, therefore, difficult to eliminate obsolete classes or remove classes designed for internal use but used intensively, especially by the framework, via Java reflection. The most famous example is the one put up by Sun Microsystems, which is related to the com.sun.misc.Unsafe class. It specified that: sun.* packages are not part of the supported, public Java interface. If you want to see the actual release, you can visit this link: https://www.oracle.com/technetwork/java/faq-sun-packages-142232.html

Through the Jigsaw project, it was not only possible to modularize the JDK in its core code but also to provide a tool for the realization of applications able to significantly decouple the interfaces of exposure of its services with respect to the actual implementation.

For JDK core code, it is possible to encapsulate JDK APIs, as described in JEP 260 (http://openjdk.java.net/jeps/260), using the following approach:

  • Encapsulate, by default, all internal APIs that are considered non-critical.
  • Encapsulate all internal APIs, that are considered critical, for which exist, in JDK 8, supported replacements.
  • Do not encapsulate critical internal APIs, but implement the following steps:
    • Deprecate them in JDK 9
    • Define a plan to remove these APIs in JDK 10
    • Implement a workaround solution via a command-line parameter
    • Remove, from the JDK distribution, a limited number of supported Java Community Process (JCP) standard APIs
    • Remove the extension mechanisms and the endorsed standards override
  • Java EE modules, due to the Jigsaw project, are not resolved by default.

In this way, you can easily obtain a small bootable Java runtime that contains only the features, in terms of classes and interfaces that you really need, avoiding the presence of useless code and that can have only negative side effects in terms of footprint and space allocation.

You could easily analyze this using the following command:

$ java -listmods

As mentioned before, both the JDK and the application can benefit from modular development. Decoupling the components present in the applications is essential in microservice architectures, which need a very agile software life cycle to reduce time to market.

Using a modularity approach, you could easily achieve the following:

  • Loose coupling between components
  • Clear contracts and dependencies between components
  • Hidden implementation using strong encapsulation

The main element in your implementation is the module.

Developers can organize their code into a modular structure, within which are declared dependencies inside their respective module definition files.

The properties of a module are defined into a file named module-info.java that contains the following attributes:

  • The module name
  • The module's packages that you want to make available publicly
  • The dependencies, direct or transitive, that the module depends on
  • The list of the services that the module consumes
  • All possible implementation of the service that the module provides

The following are the main keywords used to set the main features of a module through the module-info.java file:

  • module: The module definition file starts with this keyword followed by its name and definition.
  • provides ... with ...: The provides keyword is used to indicate that the module provides implementations for a defined service interface. The service interface is expressed using the with keyword.
  • requires: This keyword is used to indicate the dependencies of the modules. A module name has to be specified after this keyword and the list of dependencies are set through multiple required directives.
  • transitive: This keyword is set after the requires keyword; with this feature, you are declaring that any module that depends on the module defining requires transitive <modulename> gets an implicit dependence on the <modulename>.
  • uses: This keyword is used to indicate the service interface that this module is using; a type name, complete with fully qualified class or interface name, has to be specified after this keyword.
  • opens: This keyword is used to indicate the packages that are accessible only at runtime; you can also use them for introspection, using Reflection APIs. This is quite important for libraries and frameworks that use reflection APIs in order to be as abstract as possible; the opens directive can also be set at module level—in this case, all packages of the module are accessible at runtime.
  • exports: This keyword is used to indicate the packages of the module that are publicly available; a package name has to be specified after this keyword.

But the two approaches, Java Jigsaw module and Open Service Gateway Initiative (OSGi), have some differences.

OSGi's adoption is largely due to its support for dynamic component control. In this case, plugins or components are loaded dynamically and then activated, deactivated, and even updated or removed as needed. Presently, this dynamic module life cycle is not available with Java modules.

Additionally, compared with Java modules, OSGi supports improved versioning. Other OSGi advantages are related to isolation; for example, bundle changes require only the direct dependencies to be recompiled, whereas a Java module's entire layer, along with all child layers, need to be recompiled if just one module changes.

The downside is that OSGi bundles still suffer from class path issues, such as runtime exceptions for missing dependencies, or arbitrary class loading for packages with the same name.

Additionally, OSGi requires a class loader per module, which can affect some libraries that expect only a single class loader. Java modules don't allow split packages which is considered a big improvement in Java overall, and don't have similar class loader requirements or restrictions. One big advantage Java modules have over OSGi is compiler support.

I think we can get the most out of the modularization of the application components in a microservice architecture, combining the best of both technologies. The overall strategy is to use Java modules to modularize libraries (either imported or exported) and the JVM itself, and use OSGi on top to handle application modularity and dynamic life cycle control.

JDK 10

JDK 10 introduced some new features. As mentioned earlier, we will concentrate on the most important features related to cloud environments and microservice architecture.

The first one is Docker awareness, which is supported for Linux only. With this feature, you can extract container-specific information about the number of CPUs (automatically) and allocated memory (automatically).

The following new JVM configuration parameters have been introduced:

  • -XX:UseContainerSupport: The JVM has been updated in order to be aware that it is running in a Docker container. In this way it will extract container specific configuration information and it will not query the operating system. The more important information that it will be extract is the total memory that have been allocated to the container and the amount of CPUs. The value of CPUs available to the Java process is calculated from any specified CPU shares, CPU quotas or CPU sets.
  • -XX:ActiveProcessorCount: This value overrides any other logic of CPU detection implemented automatically by the JVM.
  • -XX:InitialRAMPercentage/-XX:MaxRAMPercentage/-XX:MinRAMPercentage: This parameters allows users that run the JVM into Docker containers to have more control over the amount of system memory that will be used for the Java Heap allocation.

A great performance improvement is achieved with the Parallel Full GC for G1, as described in JEP 307 (http://openjdk.java.net/jeps/307). With this feature, full GC occurs on parallel threads with great benefits, such as the following:

  • Low latency
  • High throughput
  • No/fewer stop-of-world (STW) pauses
  • Improved G1 worst-case latencies

In order to obtain better performance, it has also introduced the application class-data sharing (CDS) with JEP 310 (http://openjdk.java.net/jeps/310). This feature reduces resource footprint when multiple JVMs are running on the same physical machine, and improves the startup time of applications.

Furthermore, processes and applications can share common class metadata (class data), from a shared archive (the CDS archive); prior to Java SE 10, the use of CDS had been restricted to the bootstrap class loader only.

Another important element in an environment such as the cloud that must be as responsive as possible is the Thread-Local Handshakes feature, defined in JEP 312 (http://openjdk.java.net/jeps/312). It's a new way to execute a callback on threads that reduces the impact of acquiring a stack trace sample, for example, for profiling operations. It makes it possible to cheap-stop individual threads, reduce stops of STW pauses, and give better GC performance. With the Thread-Local Handshakes feature, it is possible to stop single threads and not just all threads or none—in this way, you don't need to make a global JVM safe point, giving a great performance improvement.

Last but not least, JDK 10 makes it possible to perform heap allocation on alternative devices, as described in JEP 316 (http://openjdk.java.net/jeps/316).

This great feature, realized with the contribution of Intel, allows the JVM to allocate the heap needed to store Java objects on a different memory device specified by the user, for example, a Non-Volatile Dual In-line Memory Module (NVDIMM).

This aspect could be extremely important in a multi-JVM environment, where you will instruct the processes with lower priority to use the NVDIMM memory while instructing the higher priority processes to use dynamic random access memory (DRAM).

I have described only a small set of the new features released in JDK 9 and 10 that demonstrate the great effort done to improve the Java language and make it a good choice for microservice implementation in cloud environments.

In September 2018, JDK 11 was released with some new great features, such as Epsilon—a no-op garbage collector.

Do you still have doubts about the use of Java for new microservice architectures?