Статьи

История модульности Java

Итак, у вас есть растущее Java-приложение с хорошим набором функций. К сожалению, добавление новых функций со временем усложняется, и вещи начинают ломаться в неожиданных местах. Скорее всего, ваши приложения не так модульны, как могли бы быть. Расслабься, это не (просто) твоя вина. В отделе модульности, как известно, не хватает простой Java. Но так быть не должно.

Модульность приводит к созданию более обслуживаемых, компонуемых и расширяемых систем. Когда вы четко разграничили границы модулей и четко заключили контракты между модулями, жизнь наладилась. Функциональность можно тестировать изолированно, а принцип «разделяй и властвуй» можно применять на уровне кода и команды. Это ускоряет разработку не только в первый год работы системы, но и на протяжении всего ее жизненного цикла.

От архитектуры к программному обеспечению

Так как же вы дошли до этого? Мы найдем конкретное решение позже, но сначала я хочу выделить время, чтобы четко определить проблему. Модульность играет большую роль на многих уровнях абстракций. На уровне системной архитектуры у нас есть Сервис-ориентированная архитектура. Если все сделано правильно, SOA означает открытые и открытые интерфейсы (в основном веб-сервисы) между слабосвязанными подсистемами, которые скрывают свои внутренние детали. Эти подсистемы, возможно, даже работают в совершенно разных технологических стеках и легко заменяются в индивидуальном порядке.

Однако при построении отдельных сервисов или подсистем в Java монолитный подход часто неотразим. Собственная среда выполнения Java, к сожалению, rt.jar является ярким примером . Конечно, вы можете разделить свое монолитное приложение на три обязательных уровня, но это далеко от истинной модульности . Просто спросите себя, что нужно сделать, чтобы поменять нижний уровень вашего приложения на совершенно другую реализацию. Зачастую это будет распространяться по всему приложению. Теперь попробуйте подумать о том, как бы вы сделали это, не прерывая работу других частей вашего приложения, при горячей замене во время выполнения. Потому что почему это должно быть возможно в контексте SOA, а не внутри наших приложений?

Истинная модульность

Так что же это за истинная модульность, о которой я говорил в предыдущем абзаце? Позвольте мне указать некоторые определяющие характеристики настоящего модуля :

  • модуль является независимой единицей развертывания (может использоваться повторно в любом контексте),
  • у него стабильная, подтвержденная идентификация (например, имя и версия),
  • это устанавливает требования (зависимости),
  • он рекламирует возможности для использования другими модулями, скрывая детали реализации.

В идеале, эти модули живут в облегченной среде выполнения, которая соответствует требованиям и возможностям модулей для нас в соответствии с нашим желаемым составом. Короче говоря, мы хотим, чтобы наше приложение использовало модули для получения хороших частей сервис-ориентированной архитектуры в меньшем масштабе. И не только на доске, но и во время кодирования и запуска приложения. Мы хотим, чтобы наша физическая реализация следовала нашему логическому дизайну на всем протяжении производства. Более того, состав модулей не должен быть статичным: приложения должны быть гибкими и расширяемыми без простоев или полного повторного развертывания.

Объекты: настоящие модули?

Как насчет самого нижнего уровня структурной абстракции в Java: классы и объекты. Разве ОО не обеспечивает идентичность, скрытие информации и слабую связь через интерфейсы в Java? Да, в некоторой степени. Однако идентичность объекта эфемерна, а интерфейсы не версионны. Классы, безусловно, не являются независимой единицей развертывания в Java. На практике занятия, как правило, слишком знакомы друг с другом. Публичный означает видимый буквально каждому другому классу на пути к классам JVM. Что, вероятно, не то, что вы хотите для чего-то, кроме действительно общедоступных интерфейсов. Что еще хуже, модификаторы видимости Java слабо применяются (подумайте над отражением) во время выполнения.

Повторное использование классов вне их исходного контекста затруднительно, когда никто не навязывает вам стоимость их неявных внешних зависимостей. Я практически слышу слова «Внедрение зависимостей» и «Инверсия контроля», которые мелькают у тебя в голове. Да, эти принципы помогают сделать зависимости класса явными. К сожалению, их архетипические реализации в Java по-прежнему оставляют ваше приложение в виде большого шара объектов, статически связанных между собой во время выполнения большим шаром конфигурации. Я настоятельно рекомендую прочитать «Архитектура приложений Java: шаблоны модульности», если вы хотите узнать больше о шаблонах модульного проектирования. Но вы также обнаружите, что применение этих шаблонов в Java без дополнительного применения модульности во время выполнения ведет к трудной борьбе.

Пакеты: настоящие модули?

Но тогда какова единица модульности в Java между приложениями и объектами? Вы могли бы утверждать, что пакеты должны быть этим. Сочетание имен пакетов, операторов импорта и модификаторов видимости (например, public / protected / private) создает иллюзию присутствия по крайней мере некоторых характеристик истинных модулей. К сожалению, пакеты являются чисто косметической конструкцией, обеспечивающей пространство имен для классов. Даже их кажущаяся иерархия на самом деле иллюзия .

Так что да, во что бы то ни стало, используйте пакеты, чтобы структурировать вашу кодовую базу в виде логических блоков. Только не рассчитывайте на пакеты, чтобы улучшить модульность за пределами красивых имен. Чтобы быть справедливым, есть инструменты, которые могут помочь в обеспечении зависимостей пакетов посредством статической проверки во время разработки. Но это вряд ли удовлетворительное решение.

JAR-файлы: настоящие модули?

Surely the true unit of modularity for Java applications then must be JAR files (Jars). Well, yes and no. Yes, because Jars are the independent units of deployment for Java applications. No, because they fail on the three other characteristics. Jars have a filename, and sometimes a version in the MANIFEST.MF. Neither are part of the runtime model and hence do not form an explicit reified identity. Dependencies on other Jars can’t be declared. You have to make sure any dependencies are on the classpath. Which, by the way, is just a flat collection of classes: gone is the link to the originating Jars. This also explains another big problem: any public class in a Jar is visible to the whole classpath. There is no ‘jar-scope’ modifier to hide implementation details inside a Jar.

All of the above means that Jars are a necessary, but not sufficient mechanism for modular applications. Many people are successfully building systems out of lots of Jars (by applying modular architecture patterns), managing the identities and dependencies with their favorite compile-time dependency management tool. Take for example the Netflix API, which is composed of 500 JARs. Unfortunately, your compile-time and run-time classpath will diverge in unforeseen ways, giving rise to the JAR-hell. Alas, we can do better than this.

OSGi bundles

Clearly, plain Java doesn’t offer enough in terms of modularity. It’s an acknowledged problem and with project Jigsaw there might be a native solution on the way. However, it has missed the Java 8 train (and Java 7 before that), so it will be quite some time before we can use it. If it ever arrives. Enter OSGi: a modular Java platform that’s mature and battle-hardened. It is used by the likes of applicationservers and IDEs as the basis for their extensible architectures.

ОСГи альянс

OSGi adds modularity as a first-class citizen to the JVM by amending Jars and packages with the necessary semantics to achieve all of our stated goals for true modularity. An OSGi bundle (module) is a Jar++. It defines additional fields inside a Jar’s manifest for a (preferably semantic) version, bundle-name and which packages of the bundle should be exported. Exporting a package means you give the package a version, and all public classes of the package are visible to other bundles. All classes in non-exported packages are only visible inside the bundle. OSGi enforces this at runtime as well by having a separate classloader per bundle. A bundle can choose to import packages exported by another bundle, again by defining its imported dependencies in the Jar’s manifest. Of course such an import must define a version (range) to get meaningful dependencies and guide the OSGi bundle resolving process. This way, you can even have multiple versions of package and its classes running simultaneously. A small example of a manifest with some OSGi parameters:

OSGi-связки

And the accompanying manifest for the service bundle:

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: MyService bundle
Bundle-SymbolicName: com.foo.service
Bundle-Version: 1.0.0
Import-Package:  org.apache.commons.logging;version="[1.0.4, 2.0.0)"
Export-Package:  com.foo.service.api;version="1.0.0"

And there you have it: OSGi provides an independently deployable Jar with a stable identity, and the possibility to require or advertise dependencies (ie. versioned packages). Everything else is strictly contained inside bundles. The OSGi runtime takes care of all the gritty details to enforce this strict separation at runtime. It even allows bundles to be added, removed and hot-swapped at run-time!

OSGi services

So, OSGi bundles take care of dependencies defined on the package level, and defines a dynamic lifecycle for bundles containing these packages. Is that all we need to create a SOA-like solution in the small? Almost. There is one more crucial concept before we can really have modular micro-services with OSGi bundles.

With OSGi bundles, you can program to an interface that is exported by a bundle. But, how do you obtain an implementation of this interface? It would be bad to export the implementing class, just so you can instantiate it in consuming bundles. You could use the factory pattern and export the factory as part of the API. But having to write a factory for every interface sounds… boiler-platey. Not good. Fortunately, there’s a solution: OSGi services. OSGi provides a service-registry mechanism, where you can register your implementation under its interface in the service registry. Typically, you register your service when the bundle containing the implementation is started. Other bundles can request an implementation for a given public interface from the service-registry. They will get an implementation from the registry without ever needing to know the underlying implementation class in their code. Dependencies between service consumers and providers are automatically managed by OSGi in much the same way as the package-level dependencies are.

Услуги OSGi

Sounds good, right? There’s one slight bump in the road: using the low-level OSGi service API correctly is hard and verbose, since services can come and go at runtime. This is because bundles that expose services can be started and stopped at-will and even running bundles can decide to start and their services at any time. That’s extremely powerful when you want to build resilient and long-lived applications, but as a developer you have to stand your ground. Lucky for us, many higher-level abstractions have been created to take care of this problem. So, when using OSGi services, use something like Declarative Services or Felix Dependency Manager (which is what we use in our projects) and create and consume micro-services with ease. You can thank me later.

Is it worth it?

I hope you agree with me that having a modular codebase is a worthy goal. And no, you don’t need OSGi to modularize a codebase. Conversely, modular runtimes like OSGi can’t rescue a non-modular codebase. In the end, modularity is an architectural principle that can be applied in almost any environment, given enough willpower. It takes additional effort to create a modular design. It’s only natural to use runtime in which modules and their dependencies are first-class citizens from design-time through run-time to ease the burden.

Is it hard in practice? There definitely is a learning curve, but it’s not as bad as some people make it out to be. Tooling for OSGi-based development has improved tremendously the past few years. Especially bnd and bndtools deserve a mention for this. If you’d like to get a feel for what it means to develop a modular Java application, watch this screencast from my co-worker Paul Bakker. He is also the co-author of an upcoming O’reilly book on this topic that you can preorder here.

Modular architectures and designs are increasingly getting attention. If you want to apply this now in your Java environment I encourage you to follow some of the links in this article and give OSGi a spin. Again, it won’t come from for free. But remember: OSGi isn’t hard, true modularity is.