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

Наверное, ни для кого не секрет (речь, конечно, про тех, кто кодирует), что на работу программиста большое влияние оказывает набор инструментов, помогающих (или же, мешающих 🙃) быстро писать хороший код. Ну ладно, слово "быстро" можно опустить, как, собственно, и определение "хороший". После этих модификаций останется утверждение, на мой взгляд, неоспоримое: используемые инструменты оказывают существенное влияние на работу программиста.

Ясно, что писать код можно и в notepad-е, вопрос в том, как это будет получаться. Как мне кажется, однако, различные навороченные IDE, облегчая, в принципе, решение рутинных каждодневных задач, тем не менее, в определенной степени, расхолаживают своих пользователей, то есть, нас, программистов. Не хочу быть понятым неправильно, фраза про "расхолаживание" не означает, что я ратую за отказ от хороших сред программирования. Я лишь за то, чтобы предоставляемая автоматизация не заменяла собой знания, использование которых призвана облегчить.

Если, вдруг, поддаться лени и просто пользоваться плодами автоматизации, без глубокого понимая того, а что, собственно, происходит "под капотом", то можно довольно чувствительно "встрять" при переходе на другие средства и инструменты. Они, наверняка, позволят делать все то же самое, что и старые, а, может, и больше (не зря же вы меняете средство производства), но вот с некоторыми особенностями. А дьявол, как известно, и кроется в деталях. Если же иметь представление о предмете, вокруг которого и строятся все эти помощники, то переход между ними будет происходить чуть менее болезненно. Да и вообще, знаний лишних не бывает.

Но что-то я опять ударился в философию. Вернусь, пожалуй, к теме. Класс java.util.ResourceBundle используют, обычно, для хранения текстовых и других ресурсов, которые зависят от используемого языкового окружения. Я, в свою очередь, люблю пользоваться этим классом для хранения текстовых сообщений об ошибках. Во-первых, это позволяет иметь упорядоченное хранилище для подобных строк - все всегда под рукой, не надо искать разбросанные по исходникам строковые константы. Во-вторых, если, вдруг, взбрело в голову предоставить перевод на какой-нибудь язык, все делается очень и очень просто. Вроде бы, где тут может быть подвох? Как обычно, везде.

Начинается вся эта история с того, что, несмотря на использовании в тексте программы именно класса ResourceBundle, на самом деле, создаются и задействуются экземпляры двух его наследников: PropertiesResourceBundle и ListResourceBundle. И это ничуть не удивительно: ResourceBundle - абстрактный класс, а экземпляр такого класса создать, как известно, нельзя, зато можно замечательно использовать такой класс для доступа к методам объектов неабстрактных наследников.

Простейший код по получению нужного объекта выглядит, примерно, следующим образом:

...
import java.util.ResourceBundle;
...
ResourceBundle bndl;
...
bndl = ResourceBundle.getBundle("fully.qualified.bundle.name");
...

То есть, объект (экземпляр либо PropertiesResourceBundle либо ListResourceBundle) создается при помощи вызова getBundle. В приведенном примере использована самая простая форма этого метода, у которого есть множество инкарнаций, но это сейчас к делу не относится. Более интересен вопрос, объект какого класса будет создан: PropertiesResourceBundle или ListResourceBundle.

Имеется два основных режима работы с потомками ResourceBundle: один - создать наследника от ListResourceBundle и заполнить его нужными данными, второй - создать специальный текстовый файл с расширением properties, заполнив его строками вида:

key=value
key1=value1
...

То есть, если отбросить пафос, надо создать самый обычный текстовый файл, такой же, как и файл, используемый для сохранения экземпляров класса Properties Java.

В зависимости от того, какой режим работы вы используете в каждом конкретном случае, значение параметра fully.qualified.bundle.name интерпретируется по разному. В случае, если вы пошли по пути создания наследника от ListResourceBundle, то параметр fully.qualified.bundle.name должен содержать полное квалифицированное имя вашего класса, например, com.mycompany.myproject.MyResourceBundle, где имя вашего класса - MyResourceBundle, а размещен он в пакете com.mycompany.myproject. Если же выбрали путь использования файла со строками key=value, то параметр fully.qualified.bundle.name должен содержать полное квалифицированное имя вашего файла, например, com.mycompany.myproject.MyResourceFile, где имя вашего файла - MyResourceFile.properties, а размещен он в пакете com.mycompany.myproject.

Сам же метод getBundle поступает очень просто: он ищет класс-наследник ListResourceBundle с именем, указанным в параметре, и, если находит - использует его. Если же не находит, то пытается найти файл с именем, указанным в параметре (добавив расширение properties), и, если находит, создает экземпляр класса PropertiesResourceBundle, инициализируя его содержимым файла. Ну а если и файла не находит, тогда выстреливается исключение MissingResourceException.

Надо сказать, что алгоритм поиска класса или файла по имени, который я привел, очень сильно упрощен. Но мне не хочется сейчас останавливаться на правилах, которые применяются во время поиска класса или файла в зависимости от языкового окружения (или явно указанного языкового параметра Locale), отмечу лишь, что добавление расширения к имени файла - лишь часть этих правил. На самом деле, к имени файла или класса можно приписывать дополнительные части, определяющие, для какого языка и для какой страны в нем содержится информация. То есть, можно, например, создать пару файлов: com.mycompany.myproject.MyResourceFile_ru для сообщений на русском языке, и com.mycompany.myproject.MyResourceFile_en - для сообщений на английском. Метод getBundle из примера выше, проанализирует текущие языковые настройки и использует нужный файл. Но это - лишь элементарный пример, все может быть значительно круче.

Из-за того, что значение параметра метода getBundle лишь частично совпадает с реальным именем класса или файла, этот параметр называют базовым именем. При указании полного квалифицированного имени класса или файла в этом параметре, в качестве разделителя, обычно, используется точка (.), но, для обратной совместимости, разрешается использовать и символ / при указании имен файлов (то есть, когда будет использоваться PropertiesResourceBundle).

Предположим, с базовым именем, указываемом в методе getBundle, все более или менее ясно. Теперь вопрос: а где должен быть размещен соответствующий класс или файл? С классом-наследником ListResourceBundle все довольно просто - так как это обычный java файл, он должен быть расположен в каталоге с исходными кодами других классов, входящих в состав того же пакета. А вот с файлом в формате Java Properties есть, что называется, нюансы.

Тут я сделаю небольшое отступление. Совсем небольшое. На тему используемой интегрированной среды разработки. Когда я начинал использовать Java, пришлось попробовать несколько IDE. В 2005 или 2006 году это был JBuilder X (почему-то, дальше не пошло, может, потому, что это был уже не Borland?), потом JDeveloper (потому, что в самом начале это был JBuilder), затем, когда все стали делать свои IDE на базе Eclispe, который вообще никак не хотел со мной находить общий язык, я стал счастливым пользователем NetBeans. Сейчас, когда NetBeans мучительно переживает очередную смену владельца, я, находясь, в полной растерянности, бросаюсь из крайности (Eclipse Che) в крайность (IntelliJ IDEA), продолжая пока использовать старенькую, но проверенную временем версию 8.2 того же NetBeans.

Так вот, в то время, когда только-только состоялось мое знакомство с классом ResourceBundle, я был уже пользователем NetBeans. И главными проблемами для меня было разобраться, как включить properties файл в нужный jar, и как создать и проинициализировать этим файлом экземпляр PropertiesResourceBundle. Скажу честно, последний мне нравился значительно больше, чем ListResourceBundle, просто потому, что мне казалось (и кажется), что проще написать несколько строк в текстовом файле, нежели писать Java код, призванный решить теже задачи. Хотя, может я и не прав.

Надо сказать, что эта проблема (включение файла в результирующий jar) довольно долго оставалась для меня неразрешимой. Чтобы я не делал, вызов getBundle "выстреливал" MissingResourceException. Тому было много причин. Всех перепитий я не помню, но точно могу сказать, что документацию надо читать тщательней. Причем, как к классу, так и к IDE. Каких только ошибок по именованию файла и его расположению я не далал! В конце концов, я все-таки прочитал, все, что должен был, назвал файл, как надо, и поместил его в нужное место.

Следует отметить, при этом, что иногда изменение версии NetBeans приводило к тому, что, на какое-то время, все переставало работать - например, менялись какие-то правила для включения или исключения файлов или каталогов в результирующий jar файл. Но я довольно быстро разбирался с возникшим, скажем так, недопониманием, и все вновь налаживалось. Потом наступил довольно долгий период стабильности. В это время я использовал следующие простые правила:

  • называл (речь только про имя) файл, пользуясь рекомендациями из документации к классу ResourceBundle
  • расширение у файла - только properties
  • размещал я этот файл либо в каталоге с исходными текстами (в том, в котором уже лежали какие-нибудь файлы с расширением java), либо создавал в структуре каталогов исходных кодов отдельный подкаталог для таких файлов
  • использовал полное квалифицированное имя для файла, так, как будто это был класс.

Все время, пока я использовал NetBeans, я создавал обычные его проекты - то есть те, которые использовали для сборки, да и для других задач, ant (с небольшими добавками от самого NetBeans). А тут, в связи с некоторой неопределенностью моих взаимоотношений с NetBeans, я решил попробовать проекты, опирающиеся на maven. И, конечно же, сразу начались разные интересные заморочки, одна из которых оказалась связанной с файлами properties, используемых для инициализации PropertiesResourceBundle. В чем же она заключалась?

Я сделал все, как обычно, разместив properties файл в каталоге с остальными исходным текстами. Для инициализации объекта, как всегда, использовал getBundle с указанием полного квалифицированного имени файла. Но, во время тестового запуска, в логи упало сообщение о том, что нужный файл с сообщениями не найден. Я прошелся по коду загрузки properties файла отладчиком, но лишь убедился в том, что нужный файл не находится. Тогда я просто посмотрел на содержимое jar файла, получившегося в результате сборки проекта, и понял, что нашел причину - нужный файл с сообщениями отсутствовал.

Так стало понятно, что maven (возможно, используемый в паре с NetBeans-ом) не ожидает увидеть файлы properties там, где их ожидал увидеть ant (в той же самой связке). Но что же тогда надо сделать мне, чтобы maven перестал капризничать? В принципе, при помощи Google, я довольно быстро нашел ответ. Для начала, мне попалась парочка предложений использовать специальные плагины maven-а для работы с ресурсными файлами, но потом я нашел вопрос на StackOverflow такого же "страдальца", как я, с той лишь разницей, что он использовал Eclipse, а не NetBeans. Ответ на этот вопрос еще раз доказал две истины: первая заключается в том, что простое решение есть практически всегда, в нашем случае, не надо никаких плагинов, maven прекрасно работает с ресурсными файлами, если они размещены в правильном каталоге - src/main/resources, а не src/main/java; ну а вторая - документацию по используемым инструментам надо читать.

Так как вторую истину я вновь проигнорировал (даже в применении к ответу на найденный вопрос), то и результата удалось добиться не сразу. Сначала я решил, что я - самый умный и решил схитрить. В результате, оказалось, что я - просто самый ленивый. Мне не хотелось воссоздавать в каталоге src/main/resources те же структуры подкаталогов, что и в src/main/java (хотя в ответе на вопрос было прямое указание на это), поэтому ресурсные файлы я перенес с переименованием. Пример моей дурости: файл messages.properties лежал в подкаталоге com/mydomain/myproduct/mymodule и я решил, что если, при переносе, назову его com.mydomain.myproduct.mymodule.messages.properties, то волшебным образом все заработает. Не заработало. Поэтому пришлось в каталоге src/main/resources дублировать структуру подкаталогов, что, конечно же, совершенно не сложно. После того, как я сделал все, как надо, файлы с ресурсными строками вновь стали помещаться в нужные jar файлы и работоспособность ПО была восстановлена.

Какова же мораль? Как я и писал в самом начале, если вы понимаете суть процесса, знаете, какой именно, после всех ваших действий, должен быть получен результат, то смена одного инструмента на другой не должна представлять серьезных проблем. В моем случае, ant понимал нахождение вспомогательных (не файлов с исходным кодом java) в одних каталогах, а maven требовал помещение этих файлов в другие файловые структуры. Но зная, что вспомогательные файлы, в конце концов, должны оказаться в собранных jar-ах, да и понимая, что maven и с файлами, содержащими исходный код, работает несколько иначе, я довольно быстро нашел и саму проблему и ее решение (да, с помощью Google, но ведь известно, что в правильно поставленном вопросе уже содержится 80% ответа). Так что, знание - сила.

Вот, пожалуй, и все, что я хотел сегодня рассказать. На самом деле, у ResourceBundle есть много всяких интересных особенностей, некоторыми из которых я пользуюсь. Возможно, я расскажу и о них как-нибудь, а пока - совет: читайте документацию.