Once you've organized your project neatly in a module-specific directory, created a module declaration, and written your application, you're ready to build and (later) run your application. To build an application, you need to create a module artifact, which is a two-step process: compilation and packaging.
At compile time, the compiler needs to know where the declaration references the module location, rightj**aThis is a breeze for its own modules, because the compiler knows where the dependencies are located (in the runtime environmentlibs/modulesfile).
Key Points In order to be able to find their own modules, one must use a module path, which is a concept that is parallel to a classpath. As the name suggests, it expects to store modularityjarAnd not ordinaryjar。When the compiler searches for a referenced module, it scans it.
In order to define the module path,j**acA new option has been added:--module-path, or simply that-p(Ideas with.)jvmSame when launching the app. Correspondingly,j**aThe same option was introduced--module-path and -p, they have the same function).
SelectmodsThe directory storage module means two things:
1) Module path containsmodsDirectory;
modsThe catalog contains the packaged artifacts.
Some modules have external dependencies:persistencemodule requiredhibernate(hibernate.jpa), whilerestmodule requiredsparkspark.core)。It is assumed that their workpieces are already modularjar, along with its dependencies, is placedmodsTable of Contents.
If it will be ordinaryjaron the module path, or modularjarWhat happens if you put it on a classpath or even mix and match? What if the dependency isn't yet modular but you want to use it? These are part of the migration to modularity, which will be explained in detail later.
Based on these prerequisites, modules can be compiled and packaged. frommonitor.observerTo start, it has no dependencies and doesn't contain anything new - using an older versionj**ato run it will also get the same result.
monitor.alphaModules have dependencies, so you have to use the module path to tell the compiler that the required artifact can be found at **. Of course, withjarCommand packing is not affected by it.
Most of the other modules are about the same. One exception is:monitor.rest, which has been locatedlibsdirectory, so you need to add thelibsto the module path.
Another exception is:monitor, you need to tell the module system that it has an entry point for the applicationmainFunction.
Figure 2-6 shows the final contents. thesejarThe file is like a plain oldjarWith one exception: each file contains a module descriptormodule-info.classfile, mark it as modularjar
All application modules are compiled and packaged tomodsdirectory and is ready to launch.
All modules are compiled tomodsdirectory, it's finally time to launch the app. As you can see in the following brief explanation, this is where the value of module declaration work lies.
Essentials All you need to do is callj**a, specify the module path, letj**aKnows that from ** you can find the artifacts that the application contains, and tell it which module to launch. The module system takes care of all dependencies, avoiding conflicting or ambiguous versions, and starting with the correct modules.
Of course, no software project really ends (unless the project "dies"), so change is inevitable. For example, if you want to add another oneobserverrealization, so what happens? Typically, you'll take these steps:
1) Develop sub-projects for it.
2) Build it.
3) Use it in an existing **.
That's what you need to do now. For new modules, adding a module declaration will allow it to be integrated into the module system.
Compile and package it like any other module.
Then add it as a dependency to the existing **.
And that's it. If the build includes compilation and packaging, all you need to do is add or modify the module declarations. The same goes for deleting or refactoring modules: in addition to the usual changes, you also need to think a little about how this will affect your module graph and update the corresponding module declarations.
So far so good, hasn't it? Before diving into the details of the module system in the following chapters, take some time to understand the two benefits that the module system promises, and what corner carryover problems can be solved with some of the advanced features.
In the previous article, we discussed the goals of the module system, and talked about two of the most important goals: reliable configuration and strong packaging. Now that you've built something more specific, let's look back at these goals and see how they help people deliver robust and maintainable software.
01.Reliable configuration
If a dependency can't be onmodsWhat happens when you find it? If two dependencies require the same project (for examplelog4jorgu**aWhat will happen to the different versions? What happens if two modules intentionally or unintentionally export two identical types?
In the classpath mechanism, these issues are exposed at runtime, some of which crash the application, while others are more subtle and undetectable, eventually leading to erroneous program behavior.
In modular systems, many unreliable cases like this (especially the ones just mentioned) are detected much earlier. compiler orjvmIt terminates the run and returns a specific message to give people a chance to fix the error.
For example, when the app starts but can't be foundmonitor.statistics, you'll get the following prompt.
Similarly, when there are two in the module pathslf4jversion, startservicemonitorThe application will get the following result.
You'll never accidentally rely on an indirect dependency again. hibernateWill useslf4j, which means that this library is always there when the application starts. But once you start importingslf4j(which does not appear in any module declaration), the compiler will block it and tell you that you are using ** in a module that has no explicit dependencies.
Even if you find a way to bypass the compiler's checks, the module system will do the same checks at startup.
02.Strong encapsulation
Now let's switch from the perspective of the module consumer to the perspective of the module maintainer. Imagine in order to fix onebugOr improve performance, rightmonitor.observer.alphaRefactoring.
After releasing a new version, you find out:monitorSome of them don't work properly, which creates instability in the application. If you change a public ownershipapi, then it's your mistake.
But what if you change the internal implementation details of a type that is marked as unsupported but is still called? It's possible that this type should be public because you want to use it in both packages; It's also possiblemonitorof developers accessed it via reflection. In this case, you can't prevent the user from relying on the implementation.
With the help of a modular system, this is avoided. In fact you have already done it :
Only the types in the exported package are visible, the rest are safe and not accessible even through reflection.
Note: If you have to do so, you do need to drill down into the inside of a module.
AlthoughservicemonitorModularization is going well, but there are still some shortcomings that are worth discussing. There's nothing you can do about it yet, but the advanced features described in Part 3 of this book can help you solve these problems. This section previews these advanced features.
01.Tag indispensable module dependencies
monitor.observer.alphamodules andmonitor.observer.betaThe module declares the rightmonitor.observerdependence. This is justified because they achieve the latter exposureserviceobserverinterface, and returns a module that belongs to the same modulediagnosticdatapointInstance.
This leads to interesting results on any ** that uses an implementation module.
Modules that contain these two lines also need to be dependentmonitor.observer, otherwise it will be inaccessibleserviceobserverType anddiagnosticdatapointType.
If the call has no dependenciesmonitor.observer, entirelymonitor.observer.alphaModules then become meaningless.
It is only available if the call explicitly depends on another module, which is stupid. Luckily, there is a way! Implicit readability (impliedreadability
02.Decouple API implementation and invocation
Think about itmonitor.observerwith its implementation modulemonitor.observer.alphawithmonitor.observer.betawill find some other problems. WhymonitorMust know the implementation?
For now,monitorYou need to instantiate a concrete class, but then only interact with the relevant interface. Relying on an entire module in order to call a constructor seems somewhat redundant.
In fact, at any time, to remove a discarded oneserviceobserverImplement or introduce a new implementation, you have to updatemonitorand recompile, package, and deploy artifacts.
In order to make thatapiThere is a looser coupling between the implementation and the caller, likemonitorSuch a caller does not need to rely on things such as:monitor.observer.alphawithmonitor.observer.betaWith this implementation, the modular system is able to achieve this goal. This issue will be discussed later.
03.Make the export more explicit
Remember that the inclusion was annotated as being only byhibernateDo you use the data transfer object package? How does the persistence module export it?
It doesn't seem quite right – onlyhibernateAccess to these entities is required. But now, other dependenciesmonitor.persistencemodules, such as:monitor, you can also see them. You're exposed to the advanced features of a modular system. Compliant export allows a module to export a package to a number of specific modules, rather than all modules. More on this mechanic later.
04.Make the package only for reflections
Even exporting packages to specific modules is sometimes too complex.
1) You will be based on:apiFor examplej**aPersistence layerapijpa) compiles modules rather than based on specific implementations (e.ghibernate), so the implementation module needs to be carefully mentioned in the compliance export.
2) You can use reflection-based tools (eghibernateorguiceIt's only accessible via reflection at runtime, so why make it accessible at compile time?
3) You'll rely on reflections to private members (hibernateThis is done after the field injection is configured), which is not possible in the export package.
A solution is presented later in the article - the introduction of open modules and open packages. This allows some packages to be available only at runtime. In exchange, it allows reflection for private members, as reflector-based tools would often ask for. There is also the Qualified Open, which is similar to Export, which allows you to open a package to only a few specific modules.
If you have used ithibernateAs a JPA provider, you've probably put a lot of effort into blocking ithibernateof direct dependence. In this case, hardcoding a dependency into a module declaration is never what you want to see. This scenario will be discussed in more detail later.
It's not uncommon for some ** to execute only if there is a dependency in a running application. For example:monitor.statisticsThere may be some ** in the module that uses a funky static class library, and maybe because of license issues whenservicemonitorAt startup, this library doesn't always exist.
Another example is a library with features that only excite users if a third-party dependency exists - such as a testing framework that works with an assertion repository when it exists.
As we discussed earlier, dependencies must be declared in the module declaration. This enforces that dependencies must exist at compile time in order for compilation to succeed. But unfortunatelyrequiresThe keyword implies that the dependency must also be present at startup, otherwisejvmThe application will refuse to run.
It's hardly satisfying. But as expected, the module system leaves a way out for it, which is the optional dependency. It must exist at compile time, but not at runtime. This will be discussed later. After discussing all the advanced features, this will be shown laterservicemonitor, which uses most of the advanced features.
The three steps of defining, building, and running a modular application are described below. They are all important, but the following article is especially important because it explains the basic concepts and fundamentals of the module system.
1) When modularizing an application, module dependencies can be inferred based on type dependencies that cross module boundaries, which makes it very intuitive to create an initial module dependency graph.
2) The directory structure of multi-module projects is related toj**a 9The previous directory structure was similar, so the existing tools and tools could continue to work.
3) Module declaration – in the root directory of the projectmodule-info.j**aFiles are the most noticeable change brought about by the module system at the ** level. It names the modules and declares dependencies and public ownershipapi。Other than that, there is basically no change in the way ** is written.
j**ac、jarwithj**aThe command has been updated to support modules. The most obvious relevant change is the module path (command-line parameter--module-pathor-p)。It has the same status as the classpath, but serves modules.