Erhalten Sie Zugang zu diesem und mehr als 300000 Büchern ab EUR 5,99 monatlich.
Создать надёжное и безопасное приложение гораздо проще, если упаковать код в аккуратные блоки. Система модулей в Java представляет собой языковой стандарт для создания таких блоков. Теперь вы можете контролировать взаимодействия различных JAR и легко обнаруживать недостающие зависимости. Фундаментальные изменения архитектуры затронули ядро Java, начиная с версии 9. Все API ядра распространяются в виде модулей, а для библиотек, фреймворков и приложений аналогичный подход можно считать хорошей практикой и рекомендацией. Вы освоите наилучшие практики модульного проектирования, отладки приложения и его развертывания перед сдачей в продакшен. В этой книге - Архитектура модульного приложения Java. - Создание модулей: от исходников до JAR-файлов. - Миграция на версию Java с модулями. - Избавление от зависимостей и оттачивание API. - Обработка рефлексии и версионирование.
Sie lesen das E-Book in den Legimi-Apps auf:
Seitenzahl: 609
Veröffentlichungsjahr: 2024
Das E-Book (TTS) können Sie hören im Abo „Legimi Premium” in Legimi-Apps auf:
Переводчики С. Черников, С. Черников, С. Черников
Николай Парлог
Система модулей Java. — СПб.: Питер, 2024.
ISBN 978-5-4461-1620-1
© ООО Издательство "Питер", 2024
Разве это не прекрасно, что все происходит по его желанию?1
Посвящается Габи и Майе
1Перевод отрывка из песни No Leaf Clover группы Metallica.
Стремление к модульности не ново. В 1968 году на знаковой конференции НАТО, посвященной программной инженерии, сыгравшей ключевую роль в популяризации видения программных компонентов и самого термина «разработка ПО» (software engineering), Э.Э. Дэвид (E. E. David) изложил подход к разработке больших систем:
Определите подмножества системы, достаточно малые, чтобы ими можно было управлять, затем постройте из них подсистему. Эта стратегия подразумевает создание системы на основе модулей, которые могут быть написаны, протестированы и изменены без учета их взаимодействия друг с другом.
На этой же конференции Х.Р. Джиллетт (H. R. Gillette) рассказал, как модульность стала предпосылкой эволюции систем:
Модульность помогает изолировать функциональные элементы системы. Один модуль можно отладить, улучшить или расширить с минимальным взаимодействием или разрывом системы.
Ничего нового. Просто понадобилось немного практики и несколько языков программирования, прежде чем эта тема была поднята и изучена.
История модульности Java, подобно фрагментам головоломки, разбросана во времени и пространстве и закодирована. Первой и существенной реализацией модулей в Java стал класс. В других языках он являл собой смесь модульности и типов данных, предоставляя типам данных, их свойствам и методам некоторую приватность и связность. Язык Java сделал следующий шаг, превратив класс из артефакта в самостоятельный двоичный компонент.
Увы, если попытаться ответить на вопрос: «Что означает быть достаточно малым, чтобы им можно было управлять?» — класс оказывается не просто, а чересчур малым. Как заметили Маркс и Энгельс в 1848 году:
История всех доселе существующих обществ — это история классовой борьбы.
Манифест Коммунистической партии
За исключением совсем небольших кодовых баз и очень загроможденных классов, класс — не самая большая часть компонентной архитектуры.
История Java также связана с пустыми обещаниями насчет пакета — во всех смыслах данного слова — чего-то, что запечатано и отправлено, целостно, неделимо и готово к использованию. Вот только это переросло в кое-что не имеющее ничего общего с первоначальным замыслом: пространства имен для организации кода в папках — в открытый, но нескромный и обреченный на провал план, запятнанный и отягощенный модными в свое время, но в итоге непрактичными доменными именами.
Затем наступил момент Пандоры — появление мифа. Греческий миф о Пандоре, девушке, приносящей дары, обычно искажают: якобы она открывает ящик, содержимое которого наводит болезни на все человечество. Так вот, это был не ящик, а сосуд. То, что открыла Пандора, — pithos (сосуд), впоследствииневерно переведенный как pyxis (ящик). Как и при написании кода, названия имеют значение.
Появление JAR-файлов стало первым шагом в сторону компонентной модели, но за ним не последовало ничего больше чем архивирование файлов классов. Для борьбы с возникающим JAR-адом многие средства (возможно, самые наглядные — это инструменты сборки и OSGi-контейнеры) расширили JAR-модель и ее манифест, что позволило продвинуться дальше на пути к модульности.
Но все это в прошлом. А что же сейчас? Что ждет нас в будущем?
Ответ прямо перед вами. Именно поэтому вы и читаете данную книгу.
Java 9 объединил множество фрагментов головоломки в один с помощью модулей — системы, вплетенной прямо в ядро платформы, а не в расширение за ее пределами. Система модулей Java не имеет никаких отсылок к прошлому. Она обязана не только сохранить богатство уже существующего кода, чтобы не превращать в хаос действующие экосистемы, но и одновременно предложить нечто для проектов, которым еще только предстоит быть написанными в этом постоянно меняющемся мире.
Природу модулей и зависимостей, детали синтаксиса и компонентизации необходимо понять на механическом уровне. С точки зрения проектирования стоит знать все за и против работы с модулями. Как и в случае с любой другой концепцией, модульность — это не волшебный соус, который можно просто добавить в разработку; она требует осторожности, навыков и внимания. Вам нужно знать ответы на вопросы о том, что произойдет с уже существующим кодом в модульном мире; о том, как это повлияет на развертывание и разработку; и даже о том, о чем вы еще не задумывались.
Вопросов очень много. Вот почему вы сейчас читаете данную книгу.
Николай способен ответить на эти и другие вопросы. Он начал отслеживать модули с того момента, как они появились на горизонте. Он нырнул прямо в глубину и преодолел мелководье JSR и реализаций. Он углубился в такие детали, куда вам, может быть, и не придется заглянуть. Его тщательность и внимание к мелочам позволят вам овладеть всей полнотой знаний — от теории к практике, от уровня новичка до гуру.
Это книга-дар. Открывайте, читайте, наслаждайтесь.
Кевлин Хенни (Kevlin Henney), Curbralan
Я познакомился с системой модулей ранним утром в апреле 2015 года. Перед работой, проверяя входящие сообщения в OpenJFX, я наткнулся на сообщение от пользователя JavaFX, который был обеспокоен тем, что приватные API стали недоступными «из-за ограничений модульности». Я помню, как подумал: «Не может быть. Java ни за что не пошел бы на такое несовместимое изменение». Списав это на недоразумение, я отправился на работу.
Затем, после обеда, мы немного поспорили с коллегой. Ничего особенного, но из-за появившегося раздражения я решил вернуться домой пораньше и насладиться солнечным весенним днем. Выйдя на балкон со стаканом прохладного пива, я решил почитать что-нибудь. Но что именно? Из любопытства я начал просматривать ответы на сообщение, которое я пролистал сегодня утром, — и это меня очень увлекло!
Всю следующую неделю я буквально пожирал любой обрывок информации, какой только смог найти, о Project Jigsaw — базе, на основе которой развивалась система модулей. Оказалось, что опасения пользователя JavaFX вовсе не были беспочвенными.
Сначала я сосредоточился только на том, что могло выйти из строя в Java 9. Намечающаяся выгода пока еще казалась несколько туманной. К счастью, в то время я работал над крупным Java-проектом и постепенно начал понимать, как можно использовать систему модулей для улучшения и сопровождения его общей структуры. Все больше и больше частей складывалось в единую картину, и спустя пару недель я загорелся идеей ввести модули в экосистему — даже если бы это что-нибудь нарушило.
Переход от опасений по поводу совместимости к пониманию ценности, которую модули могут внести в проект, — самый распространенный вариант. Но это еще не все! Помимо беспокойства об уже существующих кодовых базах, вы можете захотеть ввести систему модулей в только-только стартовавший проект либо заинтересоваться изучением растущего влияния модулей на экосистему. Куда бы вы ни направились, данная книга станет вашим путеводителем.
Если вам интересно, куда приведет это путешествие, то вспомните Java 8. Введенные в нем лямбда-выражения стали чем-то гораздо более важным, чем просто новая особенность языка, — они глубоко повлияли на сообщество и экосистему. Они познакомили миллионы Java-разработчиков с основами функционального программирования и стали началом путешествия, которое открыло нам глаза на новые концепции и сделало нас более сильными программистами. Они также повлияли на создание новых библиотек и даже кое-чему научили уже существующие фреймворки.
Имейте это в виду, размышляя о системе модулей. Это больше, чем просто термин, — он приглашает вас в путешествие, в котором вы узнаете о модульности во всех ее проявлениях, научитесь правильно проектировать и сопровождать большие программные проекты, а также более активно использовать модульность в библиотеках, фреймворках и инструментах.
Прежде всего я хочу поблагодарить Марину Майклс (Marina Michaels), моего главного редактора в Manning. Без ее доброты, настойчивости, профессионализма и чувства юмора эта книга не вышла бы никогда. Марина постоянно направляла меня на моем писательском пути. Что еще более важно, она снова и снова помогала мне сконцентрироваться и написать пару новых глав.
По этой же причине я хочу отказаться от благодарностей в адрес создателей Civilization и Stellaris, «Во все тяжкие» и «Экспансии», авторов многих выдающихся научно-фантастических книг, прочитанных мной за последние годы, а также любого человека на американском ночном телевидении: было бы здорово наслаждаться плодами вашего труда, но мне пришлось потратить большую часть этого времени на то, чтобы трудиться самому.
Хотелось бы выделить еще трех человек из Manning: Джеанну Боярски (Jeanne Boyarsky), которая наладила со мной важную техническую обратную связь и занималась данной книгой вплоть до самого ее выпуска, когда я уже не мог этого делать; Вишеслава Радовича (Viseslav Radovic), проявившего бесконечное терпение при создании иллюстраций и подгонке их под мой вкус; а также Жан-Франсуа Морина (Jean-François Morin), который безустанно просматривал книгу и примеры кода в поисках ошибок. Вот еще несколько человек, вовлеченных в превращение почти миллиона писем, разбросанного по куче Asciidoc-файлов, в настоящую книгу: Шерил Вейсман (Cheryl Weisman), Рэйчел Герберт (Rachael Herbert), Дэвид Новак (David Novak), Тиффани Тейлор (Tiffany Taylor), Мелоди Долаб (Melody Dolab), Happenstance Type-O-Rama, Александар Драгосавлевич (Aleksandar Dragosavljevic'), Мэри Пирджис (Mary Piergies) и Мария Тюдор (Marija Tudor). Наконец, я хочу поблагодарить всех рецензентов, которые нашли время, чтобы прочитать мою книгу и оставить к ней комментарии: Анто Аравинта (Anto Aravinth), Бориса Василе (Boris Vasile), Кристиана Кройцер-Бека (Christian Kreutzer-Beck), Конора Редмонда (Conor Redmond), Гаурава Тули (Gaurav Tuli), Джанкарло Массари (Giancarlo Massari), Гвидо Пио Мариотти (Guido Pio Mariotti), Ивана Милосавлевича (Ivan Milosavljevic'), Джеймса Райта (James Wright), Джереми Брайана (Jeremy Bryan), Кэтлин Эстрада (Kathleen Estrada), Марию Джемини (Maria Gemini), Марка Дечампса (Mark Dechamps), Миккеля Арентофта (Mikkel Arentoft), Рамбабу Поса (Rambabu Posa), Себастьяна Чеха (Sebastian Czech), Шобху Айера (Shobha Iyer), Стива Доусонн-Андоха (Steve Dawsonn-Andoh), Татьяну Фесенко (Tatiana Fesenko), Томаша Борека (Tomasz Borek) и Тони Свитса (Tony Sweets). Спасибо всем вам!
Кроме того, я благодарен всем людям из сообщества и Oracle, которые приняли участие в разработке системы модулей. Не только за то, что без их усердного труда просто не было бы ничего, о чем стоило бы писать, но и за качественную документацию и интересные разговоры. Я хочу отдельно упомянуть Марка Рейнхолда (Mark Reinhold), Алекса Бакли (Alex Buckley) и Алана Бейтмана (Alan Bateman) за распространение наших совместных усилий, а также за ответы на многочисленные вопросы, которыми я засыпал почтовый ящик Project Jigsaw. Спасибо вам!
Среди тех, кто, возможно, не ждет благодарности, я хотел бы поблагодарить Роберта Крюгера (Robert Kru..ger), который, сам того не ведая, вызвал во мне интерес к системе модулей тем самым роковым письмом от 8 апреля 2015 года; Кристиана Глёклера (Christian Glo..kler) за спор, впервые приведший меня к Project Jigsaw в тот день; и Бориса Терзиса (Boris Terzic) за то, что всегда вдохновлял меня на следующий шаг (и за разрешение поиграться с каждой новой версией Java на работе). Затем — всех тех прекрасных людей, которые обратились ко мне за последний год и оставили отзывы обо всем, что я уже написал, — ваш всепоглощающий позитив подарил мне кучу энергии. Спасибо вам!
Всем моим друзьям: спасибо, что были рядом, несмотря на то что у меня было так мало времени, и спасибо за вдохновение и поддержку на протяжении всего пути. Моей семье: я очень старался, чтобы работа над книгой не отнимала то драгоценное время, которое мы проводим вместе, — спасибо, что прощали меня, когда мне это не удавалось. Без вашего бесконечного терпения, любви и поддержки всего этого не было бы. Я бы не стал тем, кто я есть, без вас. Я люблю вас.
Java 9 ввел систему модулей для платформы Java в язык и экосистему, сделав примитивы модульности легкодоступными всем Java-разработчикам. Для многих, включая меня, такая концепция нова, поэтому в данной книге обучение начинается с нуля. Мы пройдем путь от самых основ к постижению расширенных функций языка. Более того, книга поможет обновить ваши существующие проекты до Java 9+, постепенно наращивая их модульность.
Обратите внимание: мы не собираемся лишь изучать модульность как таковую. Это сложная тема, на которую написаны отдельные книги (к примеру, Java Application Architecture Кирка Кнорншильда (Kirk Knoernschild, Prentice Hall, 2012). Однако в процессе введения модульности в действие вы просто не сможете избежать изучения причин, по которым это вообще стоит делать.
Система модулей — интересный зверь. Насколько просты ее основные принципы и концепции — настолько же сложно ее влияние на экосистему. Она не удивит сию же секунду, подобно лямбда-выражениям, но ее воздействие на экосистему невозможно недооценить. Однако сейчас все это вряд ли имеет значение. В настоящее время модульная система — такая же часть Java, как компилятор, модификатор privateи оператор if, и каждый разработчик обязан знать и понимать ее так же, как все перечисленное.
К счастью, начинать всегда просто. В основу системы модулей заложено всего несколько простых концепций, которые поймет любой разработчик, хоть немного знающий Java. В целом вы поймете эту систему, если уже знаете принципы работы модификаторов доступа, имеете представление, как использовать javac, jar и java, и знаете, что JVM загружает классы из JAR-файлов.
Если все описанное — о вас и вы любите новые испытания, то я приглашаю прочитать эту книгу. Возможно, вы не сразу расставите все точки над i, но по крайней мере, приобретете четкое понимание системы модулей и многих других фактов, которые сложатся в ваше собственное видение экосистемы Java.
С другой стороны, чтобы расставить все точки над i, нужно иметь многолетний опыт участия в разработке Java-проектов. Если коротко, то чем обширнее эти проекты и чем больше вы вовлечены в развитие их архитектуры, выбор верных зависимостей и борьбу с ошибками, тем больше вы оцените вклад системы модулей в них. И тогда же проще будет определить влияние модульности на ваш проект и экосистему в целом.
Данная книга разбита на несколько уровней. Прежде всего, она разделена на главы (и три большие части), но вам не обязательно изучать их от начала до конца. У меня есть пара предложений, что читать и в каком порядке.
Книга состоит из 15 глав, разделенных на три части.
Часть I, «Привет, модули», показывает недостатки Java, для устранения которых и была придумана система модулей, а также объясняет ее базовые механизмы вкупе с принципами создания, построения и запуска модульных приложений.
• Глава 1, «Первый элемент головоломки», рассказывает о недостатках поддержки Javа модульности на уровне JAR, их негативных последствиях и о том, как система модулей намерена устранить эти недостатки.
• Глава 2, «Структура модульного приложения», показывает, как спроектировать и запустить модульное приложение, и приводит примеры программы, которая будет использоваться на протяжении всей книги. Данная глава обрисует цельную картину, но не будет вдаваться в детали — за нее это сделают остальные главы.
• Глава 3, «Определение модулей и их свойств», описывает модули и основные блоки, из которых и строятся модули, а также то, как система модулей обрабатывает их для достижения приоритетной цели — построения надежного и удобного в сопровождении проекта.
• Глава 4, «Построение модулей от исходного кода до JAR», показывает, как скомпилировать и собрать модульный проект с помощью команд javac и jar.
• Глава 5, «Запуск и отладка модульных приложений», исследует множество новых опций команды java. Запустить модульное приложение достаточно легко, поэтому большая часть данной главы посвящена инструментам, которые понадобятся вам для поиска и устранения неполадок.
Часть II, «Адаптация под реальные проекты», отклоняется от использования полностью модульных приложений и предназначена для перевода существующих проектов на Java 9+ и их постепенной модуляризации.
• Глава 6, «Проблемы совместимости при переходе на Java 9 и выше», исследует наиболее распространенные препятствия, с которыми вы можете столкнуться при переводе действующей кодовой базы на Java 9 (и это еще до создания модулей).
• Глава 7, «Повторяющиеся проблемы при переходе на Java 9 и выше», обсуждает еще два препятствия, вынесенные отдельно, — они не ограничены миграцией, и потому вы можете столкнуться с ними даже после того, как обновите и модуляризуете проект.
• Глава 8, «Постепенная модуляризация существующих проектов», показывает, как взять большой проект, запущенный на Java 9, и перевести его в модули. Хорошая новость — вам не придется делать это за раз.
• Глава 9, «Стратегии миграции и модуляризации», основывается на предыдущих главах и рассматривает стратегии, которые помогут вам перенести и модуляризовать существующую кодовую базу.
Часть III, «Расширенные функции системы модулей», демонстрирует возможности, основанные на базисе, полученном в части I.
• Глава 10, «Использование сервисов для разделения модулей», показывает, как система модулей поддерживает разделение потребителей и разработчиков API.
• Глава 11, «Уточнение API и зависимостей», расширяет основные механизмы зависимости и доступности, рассмотренные в главе 3, предоставляя гибкость, необходимую для разработки под хаотичные реальные сценарии.
• Глава 12, «Рефлексия в модульном мире», обсуждает, как рефлексия потеряла суперсилу; какими должны быть приложения, библиотеки и фреймворки для того, чтобы она заработала; а также новые эффективные возможности, которые были добавлены в рефлексию.
• Глава 13, «Версии и модули: возможное и невозможное», объясняет причины, по которым система модулей в большинстве своем игнорирует информацию о версиях, минимальную поддержку версионности в модулях, а также то, как сложно (но можно) запустить разные версии одного и того же модуля.
• Глава 14, «Настройка образа среды выполнения с помощью jlink», показывает, как можно извлечь выгоду из модуляризованного JDK, если создать собственный образ среды выполнения, включающий только необходимые модули, а также демонстрирует плюсы модуляризованного приложения, включенного в этот образ, — самостоятельной единицы развертывания.
• Глава 15, «Собираем все вместе», показывает, как выглядит приложение, представленное в главе 2, со всеми наворотами из части III. Кроме того, включает советы по оптимальному использованию системы модулей.
Пусть данная книга станет для вас не просто одноразовым инструментом, который научит пользоваться системой модулей, когда вы прочтете ее от корки до корки. Не то чтобы в этом было нечто плохое, но мне хочется чего-то большего. Я хочу, чтобы она стала вашим путеводителем и вы изучили по ней максимально интересные вам темы. И пусть затем она останется у вас на столе, готовая в любое время служить в качестве справочника.
Я предлагаю вам прочитать книгу от корки до корки, однако вы можете этого не делать. Я уже упомянул, что для каждого механизма и инструмента отведена отдельная глава и все подробности собраны в одном месте.
Чтобы упростить погружение в главу, я часто повторяю и сопоставляю факты, представленные в других частях книги, чтобы вы могли прочитать о них, даже пропустив соответствующий материал. Если я слишком часто повторяюсь или переборщил со ссылками, — надеюсь, вы меня простите.
На случай, если вы не книжный червь, ниже я добавил несколько путей на выбор.
У меня всего два часа — покажите, что у вас есть:
•«Цели системы модулей», раздел 1.5;
• «Структура модульного приложения», глава 2;
• «Определение модулей и их свойств», глава 3;
• «Советы для разработчиков модульных приложений», раздел 15.2.
Хочу, чтобы мое приложение работало на Java 9:
•«Первый элемент головоломки», глава 1;
• «Определение модулей и их свойств», глава 3;
• «Проблемы совместимости при переходе на Java 9 и выше», глава 6;
• «Повторяющиеся проблемы при переходе на Java 9 и выше», глава 7;
• «Безымянный модуль, он же путь к классу», раздел 8.2;
• «Стратегии миграции», раздел 9.1.
Я думаю о запуске нового проекта с модулями:
•«Привет, модули», часть I;
• «Использование сервисов для разделения модулей», глава 10;
• «Уточнение API и зависимостей», глава 11;
• «Собираем все вместе», глава 15.
Как система модулей изменила экосистему Java:
•«Первый элемент головоломки», глава 1;
• «Структура модульного приложения», глава 2;
• «Определение модулей и их свойств», глава 3;
• «Проблемы совместимости при переходе на Java 9 и выше», глава 6;
• «Повторяющиеся проблемы при переходе на Java 9 и выше», глава 7;
• «Расширенные функции системы модулей», часть III (возможно, кроме глав 10 и 11).
Меня пригласили на вечеринку. Хочу узнать пару фишек системы модулей для поддержания разговора:
•«Взгляд на модули с высоты птичьего полета», раздел 1.4;
• «Цели системы модулей», раздел 1.5;
• «Структурирование проекта с помощью каталогов», раздел 4.1;
• «Загрузка ресурсов из модулей», раздел 5.2;
• «Отладка модулей и модульных приложений», раздел 5.3;
• что-нибудь из «Проблем совместимости при переходе на Java 9 и выше», глава 6; и «Повторяющихся проблем при переходе на Java 9 и выше», глава 7, поможет начать разговор;
• «Версии и модули: возможное и невозможное», глава 13;
• «Настройка образа среды выполнения с помощью jlink», глава 14.
Это круто. Хочу знать все!
• Прочитайте все подряд. Можете оставить часть II, «Адаптация под реальные проекты», на потом, если у вас еще нет своего проекта.
Какой бы путь вы ни выбрали, обращайтесь к указателям, в частности, в начале и конце главы, чтобы решить, куда пойти дальше.
Эта книга полна новых терминов, примеров, советов и сведений, которые нужно запомнить. Чтобы упростить для вас поиск необходимого материала, я специально выделил два вида информации.
Определения новой концепции, термина, свойства модуля или параметра командной строки выделены курсивом. Особенно значимые даны в рамке с заголовком. Эти абзацы в книге — самые важные, так что к ним стоит обращаться, чтобы уяснить, как работает тот или иной механизм.
Важная информация
Абзацы, отмеченные таким значком, предоставляют самую актуальную информацию о концепции, обсуждаемой в текущий момент, или указывают на некий неочевидный факт, который стоит запомнить, — имейте это в виду!
Во всей книге используется приложение ServiceMonitor для демонстрации поведения и особенностей системы модулей. Его можно найти по ссылке github.com/CodeFX-org/demo-jpms-monitor.
С небольшими различиями приложение используется почти во всех главах. В Git-репозитории расположено несколько веток, которые четко демонстрируют особенности, описанные в части I (в основном master и некоторые из веток break-…) и части III (те или иные ветки feature-… и break-…).
Часть II, посвященная проблемам с миграцией и модуляризацией, также иногда задействует ServiceMonitor как пример, но для нее нет отдельной ветки. Еще один вариант приложения показывает пару других проблем с миграцией: https://github.com/CodeFX-org/demo-java-9-migration.
Все, что нужно для написания кода по мере чтения книги или экспериментов с примерами, — Java версии 9 или выше (см. следующий подраздел), текстовый редактор и минимальные знания командной строки. Если вы решили использовать среду разработки, то выбирайте те, что поддерживают Java 9 (как минимум IntelliJ IDEA 2017.2, Eclipse Oxygen.1a или NetBeans 9). Еще я рекомендую набирать команды вручную или запускать сценарии .sh- или .bat, однако в ряде случаев вам может пригодиться Maven — для создания проектов необходима как минимум версия 3.5.0.
Больше подробностей по каждому из проектов можно найти в их файле README.
Java EE становится Jakarta EE
Система модулей — часть Java Standard Edition 9 (Java SE 9). Помимо Java SE, есть еще Java EE — Java Enterprise Edition (Java EE); его текущая версия — Java EE 8. Когда-то Java SE и EE управлялись одним и тем же процессом под крылом одного и того же хранителя — сначала Sun, затем Oracle.
Все изменилось в 2017 году. Oracle передала технологии Java EE в Eclipse Foundation, который основал проект Eclipse Enterprise для Java (EE4J) в целях управления ими. Платформа Java EE отныне будет называться Jakarta EE, а ее первый выпуск — Jakarta EE 8.
Периодически в данной книге я буду ссылаться на Java EE и Jakarta EE, особенно в разделе 6.1. Во избежание путаницы в названиях двух проектов и вследствие того, что технология все еще формально называется Java EE (или уже Jakarta EE), я буду использовать аббревиатуру JEE.
На момент написания этой книги версия Java 9 активно использовалась и весь код гарантированно работал в ней — если быть более точным, в версии 9.0.4. Он также был протестирован и обновлен для версий Java 10 и 11. Когда книга отправилась в печать, версия 11 еще была в раннем доступе, и, возможно, сейчас в ней есть пара-тройка изменений, на которые нет ссылок в данной книге.
Java 9 не только представил систему модулей — он также стал начальной точкой шестимесячного цикла выпуска. Уже сейчас вышли Java 10 и 11 и даже Java 12 на подходе (вполне вероятно, когда вы читаете это, 12-ю версию уже выпустили). Означает ли вышесказанное, что книга устарела?
К счастью, не совсем. Кроме пары небольших деталей, Java 10 и 11 ничего не изменили в системе модулей; и, если заглядывать в будущее, существенных изменений пока не запланировано. Поэтому, несмотря на то что здесь упоминается только Java 9, книга подходит и для Java 10 и 11, и, скорее всего, для нескольких будущих выпусков.
Это особенно касается проблем совместимости, описанных в части II. У вас не получится списать их со счетов, перепрыгнув на 8-ю или 10-ю версию. К тому же после освоения Java 9 другие версии покажутся гораздо более простыми, поскольку 10-я и 11-я содержат только небольшие изменения без проблем совместимости.
Книга содержит множество примеров исходного кода как в пронумерованных листингах, так и вперемешку с обычным текстом. В обоих случаях исходный код оформлен моношириннымшрифтом, подобно этому, чтобы не смешивать его с остальным текстом. (Хотя названия модулей выделены курсивом — см. ниже.)
Во многих случаях оригинальный исходный код и выходные данные компилятора или виртуальной машины были переформатированы, чтобы поместиться на странице книги:
• добавлены разрывы строк и переработаны отступы;
• урезаны выходные данные, в частности, убраны названия пакетов;
• сокращены сообщения об ошибках.
В редких случаях даже этого было недостаточно, и потому листинги включают в себя маркеры продолжения строки (). Вдобавок комментарии к исходному коду часто убраны из листинга, если код описан в тексте книги. Многие листинги сопровождаются примечаниями, чтобы подчеркнуть важные концепции.
Начиная с Java 8, для ссылки на метод класса часто используется синтаксис ссылки на метод (например, метод add из класса List обозначается как List::add). В отличие от List.add он не похож на обычный вызов метода (хотя куда делись круглые скобки?) и не рождает вопросов о количестве параметров. Фактически List::add обращается ко всем перегрузкам метода add, а не к одной из них. Я использую этот синтаксис на протяжении всей книги.
Важная информация
Имена модулей и пакетов почти одинаковы по длине, вследствие чего фрагменты кода и диаграммы могут увеличиться. Я отказался от этого, так что все мои собственноручно созданные модули имеют короткие имена. Не поступайте так в реальных проектах! Вместо этого обратитесь к руководству в подразделе 3.1.3 «Декларация модулей: определение свойств модулей».
Поскольку имена пакетов и модулей весьма похожи, я решил выделить имена модулей курсивом, а имена пакетов — моноширинным шрифтом. Это позволяет с легкостью различать их, и я призываю вас делать то же самое, если вы пишете о модулях.
Новые возможности, к примеру флаги командной строки и содержание module-info.java, определяются в общих чертах. Поэтому в книге используются ${заместители}, чтобы выделить специфические значения. Их легко распознать по знаку доллара с последующими фигурными скобками.
Этот синтаксис применяется исключительно в данном контексте, и его сходство с тем, как некоторые операционные системы и языки программирования обращаются к аргументам и переменным, не случайно. Однако он никогда не указывает на какой-либо определенный рабочий механизм, и такие заместители не должны использоваться в операционной системе или JVM сами по себе. Вам придется заполнить их самостоятельно, но обычно где-то рядом в тексте можно найти пояснение, что именно поместить в ${заместитель}.
Пример. Из подраздела 4.5.3: «Когда jarиспользуется для упаковки файлов классов в архив, можно определить основной класс с помощью --main-class${класс}, где ${класс} — полное имя (то есть имя пакета, за которым следуют точка и название класса) класса с методом main».
Не сложно, правда?
Лучший способ понять систему модулей — использовать ее напрямую, вызывая команды javac, java и т.д. и читая сообщения, которые Java выведет в командной строке. В данной книге содержится множество переходов от ввода к выводу, от команд к сообщениям. В фрагментах кода перед командами всегда ставится префикс $, перед сообщениями — >, перед моими комментариями — #.
Пример. Вот команда из подраздела 5.3.2:
$ java
--module-path mods
--validate-modules
# урезанные стандартизированные модули Java
# урезанные нестандартизированные модули JDK
> file:.../monitor.rest.jar monitor.rest
> file:.../monitor.observer.beta.jar monitor.observer.beta
Приобретая «Систему модулей Java», вы одновременно получаете доступ к приватному форуму от издательства Manning Publications, где можете оставить отзыв о книге, задать технический вопрос и получить помощь от автора или других пользователей. Чтобы попасть на форум, перейдите по ссылке livebook.manning.com/#!/book/the-java-module-system/discussion. Чтобы узнать больше о форумах Manning и их правилах, откройте livebook.manning.com/#!/discussion.
Обязательство издательства Manning по отношению к читателям состоит в обеспечении площадки, где между автором и отдельными читателями может состояться содержательный диалог. Это не значит, что участие автора в форуме является обязательным, — его вклад остается добровольным (и неоплачиваемым). Мы советуем вам задавать автору интересные и сложные вопросы, чтобы его интерес не угас!
Ваши замечания, предложения, вопросы отправляйте по адресу [email protected] (издательство «Питер», компьютерная редакция).
Мы будем рады узнать ваше мнение!
На сайте издательства www.piter.com вы найдете подробную информацию о наших книгах.
Николай Парлог — 30-летний парень (как выразился бы рассказчик, прищурясь), который нашел свое призвание в разработке программного обеспечения. Он постоянно читает, думает и пишет об этом, а также создает код не только ради денег, но и в свое удовольствие.
Профессионально программируя на Java с 2011 года, Николай впоследствии стал внештатным разработчиком, консультантом и спонсором нескольких проектов с открытым исходным кодом. Он также занимается блогом и новостными рассылками, выступает, записывает видео и общается в чатах — и все это на тему ПО (не одновременно, но весьма эффективно — с высокой пропускной способностью, хотя и с задержками). Его дом — codefx.org, где можно найти ссылки на все его проекты.
Иллюстрация на обложке книги называется «Житель Флориды» и изображает коренного американца из Флориды. Она взята из коллекции нарядных костюмов разных стран из французской книги Costumes civils actuels de tous les peuples connus Жака Грассе де Сен-Совера (Jacques Grasset de Saint-Sauveur) (1775–1810), опубликованной в 1788 году. Каждая иллюстрация создана и раскрашена вручную. Богатство коллекции Грассе де Сен-Совера напоминает о том, как в культурном плане взаимно далеки были города и поселения мира всего 200 лет назад. Изолированные друг от друга, люди говорили на разных языках и диалектах. На улицах города или в селах — везде легко было определить, основываясь лишь на одежде, откуда человек родом, каковы его профессия и материальное положение.
С тех пор манера одеваться значительно изменилась и разнообразие регионов, столь богатое в прошлом, сгладилось. Сегодня сложно различить даже жителей разных континентов, не говоря уже о городах, регионах или странах. Возможно, мы обменяли культурное разнообразие на жизнь, более богатую в личном плане и, конечно, быстро развивающуюся в технологическом.
Сейчас, когда одну техническую книгу сложно отличить от другой, Manning является образцом оригинальности и изобретательности в компьютерном бизнесе, выпуская книги с обложками, которые представляют богатое разнообразие жизни в регионах, имевшееся более двух столетий назад и возрожденное на картинах Грассе де Сен-Совера.
Java 9 считает модули первоклассной концепцией. Но что означают модули? Какие проблемы они решают и какую выгоду можно из них извлечь? И что же подразумевается под словом «первоклассный»?
Книга, которую вы читаете, отвечает на все эти вопросы и на множество других. Она учит, как объявить, спроектировать и запустить модули, а также тому, как они повлияют на проект и какие плюсы привнесут.
Но не все сразу. Эта часть книги начинается с объяснения, что же такое модульность, для чего она необходима и какие цели преследует система модулей (глава 1). Глава 2 сразу бросает вас в бой и показывает примеры кода, объявляющего, проектирующего и запускающего модули, после чего главы 3–5 объясняют три этих шага более подробно. Глава 3 особенно важна, поскольку в ней описываются базовые концепции и механизмы системы модулей.
Часть II книги обсуждает проблемы, с которыми Java 9 сталкивается в существующих приложениях, а часть III представляет отдельные расширенные функции.
В этой главе
• Как модульность формирует систему.
• Почему Java не может обеспечить модульность.
• Как новая система модулей стремится исправить существующие недостатки.
Всем знакома ситуация, когда развертываемое приложение отказывается работать как положено. На это есть множество причин, но один класс проблем настолько отвратителен, что для него даже придумали слишком милостивое прозвище: JAR-ад. Классические его аспекты — неправильные зависимости: одни могут и вовсе отсутствовать, а другие, словно компенсируя данное обстоятельство, появляются несколько раз, и даже в разных версиях. Это верный способ аварийно завершить работу или, что еще хуже, слегка повредить уже запущенные приложения.
Основная проблема, лежащая в основе JAR-ада, заключается в том, что мы рассматриваем JAR как самостоятельные артефакты с идентифицируемостью и зависимостями, в то время как Java видит в JAR лишь простые файловые контейнеры без каких-либо значимых свойств. Подобная разница и приводит к неприятностям.
Первый пример — недостаток значимой инкапсуляции вокруг JAR: все публичные типы видны без ограничений всему коду в приложении. Это позволяет с легкостью и без умысла пользоваться теми типами из библиотек, которые их разработчики считали только деталями реализации и никогда не отшлифовывали для публичного применения. Скорее всего, отладчики скрыли данные типы в пакетах наподобие internal или impl, но это никак не мешает импортировать их отовсюду.
Затем, когда разработчики изменяют эти внутренние типы, наш код ломается. Или, если у нас достаточно влияния на сообщество данной библиотеки, разработчикам приходится оставлять внутренний код нетронутым, что приводит к отсутствию рефакторинга и эволюции кода. Недостаток инкапсуляции приводит к снижению удобства в сопровождении — как для библиотек, так и для приложений.
Менее актуальным для повседневной разработки, но значительно более плохим для экосистемы в целом является то, что критический для безопасности приложения код становится неуправляемым. В Java Development Kit (JDK) это привело к нескольким уязвимостям, которые поспособствовали задержке выхода Java 8 после того, как Oracle выкупила Sun.
Эти и другие проблемы занимали Java-разработчиков более 20 лет, как и обсуждения возможных решений. Java 9 стал первой версией, предоставившей встроенное в язык решение: систему модулей платформы Java (JPMS), разработанную в 2008 году под руководством Project Jigsaw. Она позволяет программистам создавать модули путем добавления метаданных к JAR-файлам, делая последние чем-то большим, чем простые контейнеры. Начиная с Java 9, компилятор и среда выполнения понимают как идентифицируемость модулей, так и связи между ними и потому могут решить проблемы, связанные с дублирующимися зависимостями и недостатком инкапсуляции.
Но система модулей Java не просто заплатка. Она появилась вместе с множеством расширенных функций, которые можно использовать, чтобы написать прекрасное, удобное в сопровождении ПО. Возможно, самое большое преимущество заключается в том, что и разработчик, и все сообщество теперь сталкиваются лицом к лицу с основной концепцией модульности. Больше осведомленных разработчиков, больше модульных библиотек, улучшенная поддержка инструментов — все это можно ожидать от мира, в котором модульность стала почетным гражданином.
Я подозреваю, что многие разработчики просто пропустят несколько прошлых версий Java при обновлении. К примеру, чаще всего Java 8 обновляют сразу до Java 11. Я буду обращать ваше внимание на различия между Java 9, 10 или 11 там, где они появятся. Большинство материалов книги одинаковы для всех версий Java, начиная с 9-й. В некоторых случаях я буду использовать обозначение Java+ как сокращение для «Java версии 9 или выше».
Эта глава начинается с раздела 1.1, где объясняется, что такое модульность и то, как мы обычно воспринимаем структуру программной системы. Суть вот в чем: на определенном уровне абстракции (JAR) виртуальная машина Java видит сущности не так, как мы (см. раздел 1.2). Она просто удаляет тщательно созданную нами структуру! Подобное несоответствие вызывает настоящие проблемы, которые мы обсудим в разделе 1.3. Система модулей была создана в целях превращения артефактов в модули (см. раздел 1.4) и решения проблем, возникающих из-за несоответствия (см. раздел 1.5).
Как вы представляете себе программное обеспечение? Строки кода? Биты и байты? UML-диаграммы? Файлы POM в Maven?
Я хочу, чтобы вы опирались не на определение, а на ощущение. Остановитесь и подумайте о своем любимом проекте (или хотя бы о том, за который вам заплатили). На что он похож? Как вы его визуализируете?
Я вижу кодовые базы как систему взаимодействующих частей. (Да-да, настолько формально.) Каждая часть имеет три основные составляющие: имя, зависимости от остальных частей и особенности, которые она предоставляет другим частям.
Данное представление справедливо для всех уровней абстракции. На очень низком уровне часть — отдельный метод, где ее имя — это имя метода, ее зависимости — методы, вызываемые им, а ее особенности — это его возвращаемое значение или изменение состояния, на которое он повлиял. На очень высоком уровне часть соотносится с сервисом (кто-то сказал «микро»?) или даже со всем приложением.
Представьте сервис оформления заказов: он, как часть интернет-магазина, позволяет пользователям покупать выбранные товары. Чтобы сделать это, ему нужно вызвать сервисы авторизации и платежных карт. Мы снова получили три свойства: имя, зависимости и особенности. Данную информацию можно с легкостью применить для построения схемы (рис. 1.1).
Рис. 1.1. Если изобразить сервис оформления заказов и его зависимости, то они естественным образом сформируются в виде небольшого графа, показывающего их имена, зависимости и особенности
Мы можем рассматривать части на разных уровнях абстракции. Между такими крайностями, как методы и приложения, существуют еще классы, пакеты и JAR-файлы. У них тоже есть имена, зависимости и особенности.
Что самое интересное — мы можем использовать эти сущности для визуализации и анализа системы. Если представить или даже нарисовать узел для каждой части и затем соединить его с другими в соответствии с зависимостями, то получится граф.
Это происходит настолько естественно, что для примера с интернет-магазином все уже готово — а вы, вероятно, и не заметили. Взгляните на другие способы визуализировать программные системы, как на рис. 1.2, — графы появляются везде.
Рис. 1.2. В разработке ПО графы вездесущи. Они бывают разных видов и форм: к примеру, UML-диаграммы (слева), деревья зависимостей Maven (в центре) и графы связности микросервисов (справа)
Диаграммы классов — это графы. Зависимости в выходных данных инструментов сборки структурированы в виде деревьев (если вы используете Gradle или Maven, то попробуйте ввести gradledependencies или mvndependency:tree соответственно), которые являются отдельным видом графа. Вам встречались запутанные диаграммы микросервисов, где ничего нельзя понять? Это тоже графы.
Графы выглядят по-разному в зависимости от того, говорим ли мы о компиляции или выполнении, рассматриваем ли один уровень абстракции или их набор, оцениваем весь жизненный цикл системы или определенный момент времени, а также в зависимости от других возможных различий. Некоторые отличительные признаки станут важными позже, и в настоящее время нет необходимости рассматривать их подробно. Сейчас же подойдет любой из типов графов — просто представьте наиболее удобный для вас.
Визуализация системы в виде графа — это самый распространенный способ оценить ее архитектуру. Многие из принципов проектирования хорошего программного обеспечения влияют на то, как оно выглядит.
Возьмем для примера принцип разделения ответственности. Следуя ему, мы должны стремиться создать ПО, где каждая часть фокусируется на отдельной задаче (наподобие «войти в систему» или «нарисовать карту»). Часто задачи состоят из небольших задач (например, «загрузить пользователя» и «проверить пароль» для входа в систему), и части, которые этим занимаются, тоже должны быть отделены друг от друга. Подобное положение приводит к появлению графа, где отдельные части образуют небольшие кластеры, реализующие четко определенные задачи.
И наоборот: если плохо разделить ответственность, то граф не будет иметь четкой структуры и станет выглядеть так, словно каждый узел в нем соединяется со всеми остальными. Как вы можете увидеть на рис. 1.3, эти два случая легко распознать.
Рис. 1.3. Архитектуры двух систем изображены в виде графов. Узлами могут быть JAR или классы, а линиями — связи между ними. Но детали не так важны: все что нужно — быстро взглянуть и ответить на вопрос, представлено ли здесь правильное разделение ответственности
Другой пример принципа, влияющего на вид графа, — принцип инверсии зависимостей. Во время выполнения высокоуровневый код всегда обращается к низкоуровневому, но правильно спроектированная система инвертирует данные зависимости во время компиляции: высокоуровневый код зависит от интерфейсов, а низкоуровневый применяет их, таким образом инвертируя эти зависимости вверх к интерфейсам. Взглянув на правильный вариант графа (рис. 1.4), можно легко различить эти инверсии.
Рис. 1.4. Система, в которой высокоуровневый код опирается на низкоуровневый, образует граф (слева), отличный от использующего интерфейсы для инвертирования зависимостей вверх (справа). Инверсия позволяет с легкостью идентифицировать и понять значимые компоненты системы
Цель таких принципов, как разделение ответственности и инверсия зависимостей, — распутать граф. Если проигнорировать их, то система превратится в хаос, где ничто не может быть изменено без потенциального вывода из строя чего-то никак с этим не связанного. Если же следовать им — система станет хорошо организованной.
Принципы хорошего проектирования ПО позволяют создавать простые системы. Несмотря на то, что конечная цель — удобные в сопровождении системы, большинство принципов направлены на работу с отдельными частями. Принципы фокусируются не на всей кодовой базе, а на отдельных элементах, поскольку в итоге их свойства определяют свойства собранной из них системы.
Мы уже рассмотрели то, какие преимущества предоставляют разделение ответственности и инверсия зависимостей: фокус на отдельных задачах и зависимость от интерфейсов, а не от способов реализации. Это и есть самые важные особенности частей системы.
Важная информация
Каждый модуль, который я называл до этого частью, имеет четкие обязанности и соглашения для реализации. Он автономен, непрозрачен для своих клиентов и может быть заменен другим модулем, если тот выполняет те же функции. Он немного зависит от API, но не от реализации.
Системы, построенные из подобных модулей, лучше поддаются изменениям, с учетом реализации зависимостей более гибки при запуске и, возможно, даже во время выполнения. И в этом заслуга модульности — она делает хорошо спроектированные модули гибкими и удобными в сопровождении.
Вы уже заметили, как граф взаимосвязанных частей соединяется с парой хороших свойств, в чем и заключается модульность. Но это только идеи и повод поговорить о разработке ПО. Граф — всего лишь строки кода, которые в случае с Java компилируются в байт-кодовые инструкции и запускаются с помощью виртуальной машины Java (JVM). Было бы замечательно, если бы язык, компилятор и JVM (все это я грубо и некорректно обобщу термином Java) могли видеть сущности так же, как их видим мы.
Чаще всего они это и делают! При проектировании класса или интерфейса имя, которое вы ему присвоите, станет идентификатором и для Java. Определенные вами методы — те, что могут быть вызваны другим кодом, — будут для Java теми же методами, с такими же именами и параметрами. Их зависимости ясно видны как в выражениях импорта, так и в полных именах классов, и компилятор с JVM станут использовать классы с этими именами для выполнения.
В качестве примера рассмотрим интерфейс Future, представляющий собой результат вычисления, которое может быть (или нет) завершено. Функциональность типа неважна, поскольку нас интересуют лишь его зависимости:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException,
ExecutionException,
TimeoutException;
}
Просматривая методы, объявленные в Future, можно легко перечислить зависимости:
•InterruptedException;
• ExecutionException;
• TimeUnit;
• TimeoutException.
Применяя тот же анализ к только что определенным типам, создадим граф зависимостей, как на рис. 1.5. Его точная форма не имеет значения. Гораздо важнее то, что граф, который существует у нас в голове, когда мы говорим о типе, в точности совпадает с графом, явно созданным Java.
Рис. 1.5. Граф зависимостей, с которым работает Java для любого из предложенных типов, совпадает с нашим восприятием зависимостей этого типа. Данный граф показывает зависимости интерфейса Future между пакетами java.util.concurrent и java.lang
Поскольку Java сильно и статически типизирован, вы сразу получите сообщение, если что-то даст сбой. Неправильное имя класса? Одна из зависимостей утеряна? Видимость метода была изменена и теперь другие методы не видят его? Java уведомит обо всем этом компилятор во время сборки и JVM во время выполнения.
Проверки во время компиляции позволяет обойти рефлексия (кратко об этом — в приложении Б). По данной причине она считается грубым и потенциально опасным инструментом, который можно использовать только в исключительных случаях. Пока мы ее проигнорируем, но вернемся к ней в следующих главах.
В качестве примера того, как расходятся наше восприятие зависимостей и восприятие Java, рассмотрим уровень сервиса или приложения. Это выходит за рамки Java: он не может знать об используемом приложении, как не в состоянии и предупредить о том, что не имеет доступа к сервису GitHub или базе данных Oracle (упс!), и также не знает о поломке со стороны клиента, вызванной вашими изменениями в API. Он не может составить карту совместной работы приложений или сервисов. И это нормально, поскольку Java работает на уровне отдельных приложений.
Но один уровень абстракции явно находится в сфере влияния Java, невзирая на крайне слабую поддержку до Java 9 — настолько слабую, что усилия по модуляризации фактически были напрасны, приведя к тому, что называется уничтожением модулей. Этот уровень имеет дело с артефактами, или JAR — на языке Java.
Если приложение на этом уровне модуляризовано, то оно состоит из нескольких JAR, а если нет — зависит от библиотек, которые могут иметь свои собственные зависимости. Отобразив их, вы получите уже знакомый граф, но на этот раз для JAR, а не для классов.
В качестве примера рассмотрим приложение под названием ServiceMonitor. Если не слишком вдаваться в подробности, то оно работает следующим образом: проверяет доступность других сервисов и собирает статистику. Все это записывается в базу данных и может быть получено через REST API.
Авторы приложения создали четыре JAR:
•observer — следит за сервисами и проверяет доступность;
• statistics — составляет статистику на основе доступных данных;
• persistence — читает и записывает статистику в базу данных с hibernate;
• monitor — запускает сбор данных и направляет их через statistics в persistence, реализует REST API со spark.
Каждый JAR имеет собственные зависимости, каждая из которых представлена на рис. 1.6.
Рис. 1.6. Для любого приложения можно нарисовать граф зависимостей между его артефактами. Здесь представлено приложение ServiceMonitor, разбитое на JAR, которые имеют зависимости не только друг от друга, но и от сторонних библиотек
Граф включает все, что было описано ранее: у всех JAR есть имена, зависимости друг от друга и специфические особенности, позволяющие другим JAR использовать публичные методы и классы.
При запуске приложения вы должны обязательно перечислить пути классов и JAR, которые планируете применять:
Важная информация
И вот здесь все идет не так — по крайней мере, до Java 9. JVM запускается, ничего не зная о ваших классах. Каждый раз, когда она встречает обращение к неизвестному классу, которое начинается с главного класса, указанного в командной строке, она просматривает все JAR в пути класса в поисках класса с подходящим именем. Если таковой найден, то JVM запускает его со всеми остальными классами и завершает работу. Как видите, в JVM нет концепции среды выполнения, соответствующей JAR-файлам.
Без представления в среде выполнения JAR теряют свою идентифицируемость. Хоть у них и есть собственные имена, для JVM это не имеет значения. Разве не было бы хорошо, если бы сообщения об ошибках могли указывать на JAR, в котором возникла проблема, или если бы JVM могла назвать отсутствующую зависимость?
К слову, о зависимостях — они тоже становятся невидимыми. Работая на уровне классов, JVM не имеет представления о зависимостях между JAR-файлами. Игнорирование артефактов, содержащих классы, делает невозможным инкапсуляцию этих артефактов. И действительно, каждый публичный класс виден всем остальным классам.
Имена, явные зависимости, четко объявленные API — ни компилятор, ни JVM не интересуются тем, что мы так ценим в модулях. Это уничтожает модульную структуру и превращает аккуратно спроектированный граф в комок грязи, как показано на рис. 1.7. И такое не проходит бесследно.
Рис. 1.7. Ни Java-компилятор, ни его виртуальная машина не имеют представления об артефактах и зависимостях между ними. Несмотря на то что JAR относятся к простым контейнерам, вне классов они загружаются в отдельное пространство имен. В итоге классы превращаются в первичный бульон, где каждый публичный тип доступен любому другому
Как вы могли заметить, в Java до версии 9 отсутствовали концепции для правильной поддержки модульности между артефактами. И хотя это вызывало проблемы, их, очевидно, можно было решить (иначе мы бы не использовали Java). Но, когда наконец они проявлялись, обычно в более крупных приложениях, с ними практически нельзя было справиться.
Как я уже упоминал в начале главы, сложности, с максимальной вероятностью влияющие на процесс разработки, объединены под общим названием «JAR-ад»; но есть и другие. Проблемы сопровождения и безопасности, в большей степени актуальные для разработчиков JDK и библиотек, тоже имеют последствия.
Я уверен, что вы также встречались с этими трудностями, и на протяжении следующей главы мы подробно рассмотрим их одну за другой. Не переживайте, если они вам не знакомы, — вам просто повезло с ними не столкнуться. Если же вам знакомы JAR-ад и связанные с ним проблемы, то можете сразу перейти к разделу 1.4, который представит систему модулей.
Если вы разочарованы этим, по видимости, бесконечным потоком неполадок, то расслабьтесь — за ним последует возмездие: раздел 1.5 расскажет, как модули помогут справиться с большинством подобных проблем.
Было ли у вас приложение, которое «падало» с ошибкой NoClassDefFoundError? Это происходило потому, что JVM не могла найти класс, ссылка на который есть в выполняемом коде. Найти зависимый код легко (достаточно просмотреть трассировку стека, чтобы выявить его), и идентификация потерянной зависимости тоже не требует много работы (ее выдает отсутствующее имя класса). Но понять, почему зависимости нет, бывает очень тяжело. Однако, учитывая граф зависимости артефактов, возникает вопрос — почему мы обнаруживаем, что чего-то не хватает, только во время выполнения?
Важная информация
Причина проста: JAR не может выразить, на какой другой JAR-файл он ссылается так, чтобы это было понятно виртуальной машине. Для идентификации и применения зависимостей требуется внешняя сущность.
До того как инструменты сборки научились распознавать и извлекать зависимости, мы и пользовались этими внешними сущностями. Приходилось сканировать документацию на предмет зависимостей, искать необходимые проекты, загружать JAR-файлы и добавлять их в проект. Дополнительные зависимости, когда одному JAR может потребоваться другой JAR для каких-то специфических возможностей, значительно усложняли задачу.
Чтобы приложение работало, ему может понадобиться сразу несколько библиотек. Но каждая из них может потребовать еще несколько и т.д. По мере усугубления проблемы невыраженных зависимостей проект становится все более громоздким и подверженным ошибкам.
Важная информация
Инструменты сборки, такие как Maven и Gradle, во многом решили эту проблему. Они преуспели в превращении неявных зависимостей в явные так, чтобы можно было выследить каждый нужный JAR вдоль множества ребер переходного дерева зависимостей. Тем не менее понимание JVM концепции зависимостей между артефактами повысило бы надежность и переносимость.
Иногда случается так, что разные JAR-файлы в пути класса имеют одинаковые полные имена. Это происходит по целому ряду причин:
• разные версии одной и той же библиотеки;
• JAR может содержать собственные зависимости — это называется «толстый JAR» или «uber-JAR», — однако некоторые из них собраны как отдельные JAR-файлы, поскольку от них зависят другие артефакты;
• библиотека может быть переименована или разделена, и некоторые ее типы непроизвольно будут добавлены в один и тот же путь класса.
Определение: затенение
Поскольку класс будет загружен из первого JAR-файла в пути класса, который его содержит, все другие классы с такими же именами станут недоступными — это значит, что они затенены.
Если варианты классов различаются семантически, то это приведет к чему-либо от «слишком-неявного-чтобы-быть-замеченным» поведения до ошибки, вызывающей хаос и разрушения. Хуже того, форма, в которой проявляется проблема, может оказаться недетерминированной. Все зависит от порядка, в котором были найдены JAR-файлы: он может быть разным в зависимости от используемой среды: к примеру, от применяемой вами среды разработки (IntelliJ, Eclipse или NetBeans) до конечной машины, где код будет непосредственно запускаться.
Возьмем, например, широко используемую компанией Google библиотеку Guava, которая содержит служебный класс com.google.common.collect.Iterators. При переходе от 19-й версии Guava к 20-й метод emptyIterator() был удален. Как показывает рис. 1.8, если обе версии будут находиться в одном и том же пути класса и если версия 20 будет применена первой, то есть весь код, зависящий от Iterators, станет задействовать новую версию, тогда вызов Iterators::emptyIterator из 19-й версии окажется невозможным. Метод становится невидимым, даже несмотря на то, что класс, содержащий его, находится в том же пути класса.
Затенение чаще всего происходит случайно. Но это же поведение может быть использовано нарочно, для перезаписи отдельных классов из сторонних библиотек собственными реализациями с одновременным исправлением библиотеки. Хотя инструменты сборки могут снизить вероятность случайного затенения, обычно им не под силу предотвратить его.
Рис. 1.8. Бывает так, что путь класса содержит разные версии одной и той же библиотеки (сверху) или две библиотеки имеют набор общих типов (снизу). В обоих случаях некоторые типы представлены больше одного раза. Во время сканирования пути класса загрузится только первый вариант (затеняя остальные), поэтому лишь порядок сканирования JAR-файлов определяет, какой код будет запущен
Конфликты версий — проклятие любого большого программного проекта. Когда количество зависимостей обозначается больше чем одной цифрой, вероятность возникновения конфликтов стремится к 1 с ужасающей скоростью.
Определение: конфликт версий
Конфликт версий возникает, когда две необходимые библиотеки зависят от разных, несовместимых версий сторонней библиотеки.
Если обе версии находятся в одном и том же пути класса, то их поведение становится непредсказуемым. Вследствие затенения классы, которые находятся в обеих версиях, будут загружены лишь из одной из них. Хуже того, даже если класс существует только в одной из версий — он загрузится в любом случае. Вызов кода в библиотеке может обнаружить смесь двух версий.
С другой стороны, если одна из версий пропущена, то программа может работать некорректно, поскольку обе версии обязательны и несовместимы и не могут заменить друг друга (рис. 1.9). Аналогично случаям с потерянными зависимостями ошибка будет определена как «неожиданное поведение», или NoClassDefFoundError.
Рис. 1.9. Переходные зависимости конфликтующих версий иногда невозможно разрешить — одна из зависимостей должна быть устранена. Здесь старая версия RichFaces зависит не от той версии Guava, которая используется в приложении. К несчастью, из Guava 16 был удален API, на который ссылался RichFaces
Продолжая рассматривать пример с Guava из раздела о затенении, представьте, что какой-нибудь код ссылается на com.google.common.io.InputSupplier, класс, который представлен в 19-й версии, но исключен из 20-й. JVM первым делом просканирует Guava 20 и, не найдя класс, загрузит его из Guava 19. Внезапно смесь из двух версий Guava продолжит работу! И в заключение представьте, что InputSupplier вызовет Iterators::emptyIterator. Как думаете, насколько весело будет отлаживать все это?
Важная информация
Не существует технического решения этой проблемы, при котором не нужно было бы использовать имеющиеся системы модулей или управлять загрузчиками классов вручную. Инструменты сборки в основном могут определить эти сценарии. Они могут показать предупреждение либо сами разрешат проблему, например, используя последнюю версию.
Наше исследование механизма загрузки класса, начавшееся в разделе 1.2, еще не завершено. Описанное поведение справедливо для приложений, использующих один и тот же загрузчик класса. Но разработчики могут свободно добавлять новые загрузчики, делегируя решение проблем, которые мы обсуждали ранее, от одного загрузчика к другому.
Подобное поведение типично для таких контейнеров, как системы компонентов и веб-серверы. В идеале явное его использование должно быть скрыто от разработчиков приложения, но, как известно, ни одна абстракция не умеет хранить секреты. И при некоторых обстоятельствах разработчики могут явно добавить загрузчик класса для реализации определенных свойств: например, чтобы позволить пользователям расширять приложение путем добавления новых классов или чтобы применять конфликтующие версии для одной и той же зависимости.
Независимо от того, как именно появятся множественные загрузчики класса, вам потребуется изучить эту тему как можно тщательнее. Их использование быстро может привести к сложным механизмам делегирования, из-за чего код может повести себя непредсказуемо.
Модификаторы доступа Java прекрасно подходят для реализации инкапсуляции в классах внутри одного и того же пакета. Но за пределами пакета существует только один вид доступа для типов: public.
Как вы уже заметили, загрузчик класса превращает все загружаемые классы в один большой комок грязи2 — так, что все публичные классы доступны для всех остальных. Из-за такой слабой инкапсуляции становится невозможным создать функциональность, которая будет видна внутри JAR, но не снаружи.
Все описанное очень затрудняет модуляризацию системы. Если некая функциональность необходима разным частям модуля (например, библиотеке или подпроекту вашей системы), но не должна быть видна извне, единственный способ достичь этого — собрать все в один пакет и пользоваться видимостью внутри него. В результате такого упреждающего повиновения вы сами уничтожаете структуру кода вместо того, чтобы предоставить эту возможность виртуальной машине. Даже в том случае, когда видимость внутри пакета решает проблему, все еще существует рефлексия, позволяющая получить доступ к коду извне.
Слабая инкапсуляция позволяет клиентам артефакта проникать внутрь него (рис. 1.10). Это может произойти случайно, если IDE предлагает импортировать классы, помеченные в документации как внутренние. Чаще всего подобное делается намеренно, чтобы обойти проблемы, для которых не находится другого решения (что иногда верно, а иногда — нет). Однако такой выбор дорого обходится!
Рис. 1.10. Сопровождающие Eclipse JGit не предусматривали публичное использование org.eclipse.jgit.internal. К несчастью, поскольку Java не имеет понятия о содержимом JAR, сопровождающие никак не могут предотвратить компиляцию любого com.company.Type. Даже если видимость настроена в пределах пакета, это все еще можно обойти с помощью рефлексии
Теперь клиентский код связан с деталями реализации артефакта. Это делает обновления рискованными для клиента и, если сопровождающие решили составить соглашение для данной связности, мешает изменить внутреннюю реализацию. Это может привести к замедлению или даже остановке эволюции артефакта.
Это может показаться исключительным случаем, но это не так. Пресловутый пример — sun.misc.Unsafe, внутренний класс JDK, который позволяет делать сумасшедшие (по стандартам Java) вещи, такие как прямое выделение и освобождение памяти. Многие особо важные библиотеки Java, например Netty, PowerMock, Neo4J, Apache Hadoop и Hazelcast, используют его. И поскольку многие приложения опираются на эти библиотеки, они также зависят и от их содержимого. Таким образом, Unsafe становится значительной частью инфраструктуры, хотя он не был ни предназначен, ни спроектирован для этого.
Еще один пример — JUnit 4. Во многие инструменты, особенно в IDE, встроены разные удобные возможности, чтобы упростить разработчикам тестирование. Но, поскольку API JUnit 4 недостаточно крут, чтобы все их реализовать, ему приходится вторгаться во внутренний код этих инструментов. Такая связка существенно замедляет разработку JUnit 4 и в конце концов становится причиной перехода на JUnit 5.
Моментальное последствие слабой инкапсуляции в пределах пакета проявляется в том, что функционал, отвечающий за безопасность, распределяется на весь код внутри среды разработки. Это значит, что зараженный код может проникнуть в критически важный функционал, и единственный способ противостоять этому — вручную проверить безопасность на критическом пути выполнения.
Со времен Java 1.1 это достигалось с помощью вызова SecurityManager::checkPackageAccess, который проверял, имеется ли у вызывающего кода доступ к вызываемому пакету — в каждом пути кода, отвечающего за безопасность. Более того, его нужно было вызывать в любом таком пути. Забыть о подобном вызове означало сделать код уязвимым для атак, от которых Java страдал в прошлом, особенно во время перехода от Java 7 к 8.
Конечно, можно поспорить с тем, что необходимо дважды, трижды или четырежды проверять код, отвечающий за безопасность. Однако не стоит забывать о человеческом факторе: ручные проверки безопасности на границах модуля представляют более высокий риск, чем хорошо автоматизированный вариант.
Вы когда-нибудь задумывались, почему многим Java-приложениям, особенно веб-серверам, которые используют мощные фреймворки наподобие Spring, требуется так много времени для загрузки?
Определение: ленивая загрузка
Как вы уже заметили, JVM будет лениво загружать классы по мере их надобности. Чаще всего многие классы доступны сразу после запуска (а не через какой-то промежуток времени). И Java требуется немало времени, прежде чем все они будут загружены.
Одна из причин — загрузчик класса не может заранее знать, из какого JAR-файла будет загружен класс, поэтому линейно проходит по всем JAR в пути класса. Аналогично для идентификации всех вхождений конкретной аннотации требуется проверка всех классов в пути класса.
На самом деле эта проблема не является следствием подхода «большой комок грязи» в JVM, но если мы ее не объявим, то избавиться от нее не получится.
Определение: негибкая среда выполнения
До Java 8 не было никакой возможности установить только выбранные элементы среды выполнения. Все установщики Java включали в себя, например, XML, SQL и Swing, которые во многих случаях являются необязательными.
Хотя это не так существенно для устройств средних размеров (настольные ПК и ноутбуки), очевидно, что это важно для небольших устройств, таких как роутеры, приставки и всевозможные другие приборы, в которых используется Java. Учитывая нынешнюю тенденцию к контейнеризации, это актуально и для серверов, уменьшение площади которых прямо пропорционально влияет на количество расходов на их содержание. Java 8 представила компактные профили, определяющие три подраздела Java SE. Они смягчают проблему, однако не решают ее полностью. Компактные профили фиксированы и потому не могут охватить все текущие и будущие нужды отдельных частей сред выполнения.
Мы только что обсудили несколько проблем. Какое их решение предлагает система модулей платформы Java? Основная идея до невозможности проста!
Важная информация
Модули — это строительные блоки системы модулей платформы Java (неожиданно, правда?). Как и JAR-файлы, они являются контейнерами для типов и ресурсов, но, в отличие от JAR, имеют дополнительные характеристики. Вот самые основные:
• имя, желательно уникальное в глобальном смысле;
• декларация зависимостей от других модулей;
• явно определенный API, который состоит из экспортированных пакетов.
Существуют различные типы модулей. Они будут рассортированы по категориям в подразделе 3.1.4, но есть смысл взглянуть на них уже сейчас. Во время работы над Project Jigsaw OpenJDK была разделена на более чем 100 модулей, также называемых платформенными. Около 30 из них имеют имена, начинающиеся с java.*; это стандартизированные модули, они должны быть включены в каждую JVM (на рис. 1.11 показаны некоторые из них).