GraalVM Native Image: Как включить полные данные локали для форматирования даты/времени?
Я столкнулся с проблемой в GraalVM native-image, где локализованное форматирование даты/времени работает некорректно для определенных локалей. Конкретно, при форматировании дат на арабском языке вывод возвращается к английским сокращениям вместо отображения полного арабского текста.
Описание проблемы
При запуске того же Java-кода в режиме JVM я получаю ожидаемый вывод:
arabicGregorianDate = السبت 18\10\2025 م
englishGregorianDate = Saturday 18/10/2025 AD
Однако при компиляции в GraalVM native-image и запуске вывод получается таким:
arabicGregorianDate = Sat 18\10\2025 CE
englishGregorianDate = Saturday 18/10/2025 AD
Обратите внимание, что:
- Арабское слово для субботы (السبت) заменяется на “Sat”
- Арабский маркер эры (م) заменяется на “CE”
Пример кода
final var ARABIC_GREGORIAN_DATE_FORMATTER = DateTimeFormatter.ofPattern("EEEE dd\\MM\\yyyy G").localizedBy(Locale.forLanguageTag("ar"));
final var ENGLISH_GREGORIAN_DATE_FORMATTER = DateTimeFormatter.ofPattern("EEEE dd/MM/yyyy G").localizedBy(Locale.ENGLISH);
var now = Instant.now();
String arabicGregorianDate = ARABIC_GREGORIAN_DATE_FORMATTER.format(now.atZone(ZoneId.systemDefault()));
String englishGregorianDate = ENGLISH_GREGORIAN_DATE_FORMATTER.format(now.atZone(ZoneId.systemDefault()));
Log.info("arabicGregorianDate = " + arabicGregorianDate);
Log.info("englishGregorianDate = " + englishGregorianDate);
Текущая конфигурация
Я уже использую следующую конфигурацию:
-H:IncludeLocales=ar,en
Я также пробовал использовать агент трассировки GraalVM, но это не решило проблему.
Вопрос
Как настроить GraalVM native-image на включение необходимых данных локали (возможно, данных CLDR), чтобы форматирование даты на арабском языке работало корректно?
Пример воспроизведения доступен по адресу: https://github.com/Eng-Fouad/graal-native-date-locale
GraalVM Native Image имеет определенные ограничения, связанные с включением данных локали, что приводит к тому, что форматирование дат на арабском языке возвращается к английскому. Для решения этой проблемы необходимо явно включить требуемые ресурсы локали с помощью параметров конфигурации GraalVM Native Image и убедиться, что в ваше приложение упакованы правильные данные CLDR.
Содержание
- Понимание ограничений локали в GraalVM
- Параметры конфигурации для данных локали
- Пошаговые решения
- Альтернативные подходы
- Лучшие практики и соображения
- Отладка и проверка
Понимание ограничений локали в GraalVM
GraalVM Native Image работает иначе, чем традиционное выполнение JVM, в отношении обработки ресурсов. По умолчанию он включает только минимальный набор данных локали, необходимый для работы приложения, что часто приводит к неполной поддержке локали.
Согласно GitHub issue о поддержке локали в Graal, начиная с GraalVM 20.2.0, нативные исполняемые файлы содержат только одну локаль, которая была возвращена Locale.getDefault() в сборочной JVM. Это ограничение влияет на форматирование даты/времени, форматирование чисел и другие операции, чувствительные к локали.
Суть проблемы заключается в том, как GraalVM обрабатывает ресурсы локали:
- Native Image выполняет статический анализ во время сборки
- Он включает только ресурсы, которые статически достижимы
- Данные локали считаются ресурсом, который должен быть явно включен
- Данные Common Locale Data Repository (CLDR) не включаются автоматически
Ключевое понимание: Данные CLDR, поддерживаемые Консорциумом Unicode, обеспечивают более высокое качество данных локали, чем устаревшие данные в JDK 8, как упоминается в JEP 252: Use CLDR Locale Data by Default.
Параметры конфигурации для данных локали
GraalVM предоставляет несколько параметров конфигурации для включения данных локали в нативные образы:
1. Включение конкретных локалей
Параметр -H:IncludeLocales позволяет указать, какие локали включить. Однако, как вы обнаружили, этого может быть недостаточно для форматирования даты:
-H:IncludeLocales=ar,en
Формат тегов локали следует стандарту IETF BCP 47. Для арабского языка можно использовать:
ar- общий арабскийar-SA- арабский для Саудовской Аравииar-EG- арабский для Египта
2. Включение всех локалей
Для всесторонней поддержки локали можно использовать:
-H:+IncludeAllLocales
Как указано в документации GraalVM по ресурсам, этот параметр включает все локали, но значительно увеличивает размер результирующего исполняемого файла.
3. Конфигурация ресурсов
Также можно настроить ресурсы через файл конфигурации JSON:
{
"resources": {
"includes": [
"META-INF/**"
],
"bundles": [
"java.text.resources.*"
]
}
}
Пошаговые решения
Решение 1: Расширенная конфигурация локали
Попробуйте более комплексную спецификацию локали:
-H:IncludeLocales=ar,en,ar-SA,ar-EG
Это гарантирует, что различные варианты арабской локали будут включены.
Решение 2: Правильное использование трассирующего агента
Трассирующий агент нужно запускать с фактическим кодом приложения, который использует форматирование даты локали. Вот правильный подход:
- Запустите приложение с трассирующим агентом:
java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image \
-jar your-application.jar
-
Убедитесь, что вы фактически вызываете форматирование даты на арабском языке во время этого запуска
-
Пересоберите нативный образ с сгенерированной конфигурацией:
native-image --config-dir=src/main/resources/META-INF/native-image -H:IncludeLocales=ar,en \
-jar your-application.jar
Решение 3: Конфигурация набора ресурсов
Создайте файл конфигурации ресурсов (src/main/resources/META-INF/native-image/resource-config.json):
{
"resources": {
"includes": [
"**/*.json",
"**/*.properties"
],
"bundles": [
"java.text.resources.DateFormatData",
"java.text.resources.DateFormatData_en",
"java.text.resources.DateFormatData_ar"
]
}
}
Затем соберите с помощью:
native-image -H:ConfigurationFileDirectories=src/main/resources/META-INF/native-image \
-H:IncludeLocales=ar,en -jar your-application.jar
Решение 4: Использование конфигурации времени сборки
Для приложений Quarkus можно использовать подход @AutomaticFeature, упомянутый в обсуждении на Stack Overflow:
@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void configureNativeImage(NativeImageBuildItem nativeImageBuildItem) {
nativeImageBuildItem.getBuildArgs()
.add("--enable-url-protocols=http,https")
.add("-H:IncludeLocales=ar,en");
}
Альтернативные подходы
1. Реализация пользовательских данных локали
Если встроенные данные локали недостаточны, вы можете реализовать пользовательское решение:
public class ArabicDateFormatter {
private static final Map<String, String> ARABIC_DAY_NAMES = Map.of(
"Saturday", "السبت",
"Sunday", "الأحد",
// ... другие дни
);
private static final Map<String, String> ARABIC_ERA_NAMES = Map.of(
"AD", "م",
"CE", "م"
);
public static String formatArabicDate(Instant instant) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEEE dd\\MM\\yyyy G")
.withLocale(Locale.ENGLISH);
String formatted = formatter.format(instant.atZone(ZoneId.systemDefault()));
// Замена английских имен на арабские
for (Map.Entry<String, String> entry : ARABIC_DAY_NAMES.entrySet()) {
formatted = formatted.replace(entry.getKey(), entry.getValue());
}
for (Map.Entry<String, String> entry : ARABIC_ERA_NAMES.entrySet()) {
formatted = formatted.replace(entry.getKey(), entry.getValue());
}
return formatted;
}
}
2. Внешние ресурсы локали
Упакуйте требуемые ресурсы локали как внешние файлы и загружайте их во время выполнения:
// Загрузка ресурсов локали из внешнего файла
private static void loadLocaleResources() {
try (InputStream is = ArabicLocaleSupport.class.getResourceAsStream("/locales/arabic-date-format.properties")) {
if (is != null) {
// Загрузка и настройка данных локали
}
} catch (IOException e) {
throw new RuntimeException("Не удалось загрузить ресурсы арабской локали", e);
}
}
Лучшие практики и соображения
1. Стратегия тестирования локали
Всегда тестируйте ваше приложение с целевыми локалями перед развертыванием:
public class LocaleTest {
@Test
void testArabicDateFormatting() {
Locale arabicLocale = Locale.forLanguageTag("ar-SA");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEEE dd\\MM\\yyyy G")
.localizedBy(arabicLocale);
String result = formatter.format(Instant.now().atZone(ZoneId.systemDefault()));
assertTrue(result.contains("السبت"), "Должен содержать арабскую субботу");
assertTrue(result.contains("م"), "Должен содержать маркер эры на арабском");
}
}
2. Оптимизация конфигурации сборки
Сбалансируйте между поддержкой локали и размером исполняемого файла:
# Для производственных сборок с конкретными локалями
native-image -H:IncludeLocales=ar,en,ar-SA,ar-EG,fr,de \
--no-fallback -jar your-application.jar
# Для разработки со всеми локалями
native-image -H:+IncludeAllLocales \
-jar your-application-dev.jar
3. Обнаружение локали во время выполнения
Реализуйте обнаружение локали во время выполнения как резервный вариант:
public static DateTimeFormatter createLocalizedFormatter(String languageTag) {
try {
Locale locale = Locale.forLanguageTag(languageTag);
return DateTimeFormatter.ofPattern("EEEE dd\\MM\\yyyy G").localizedBy(locale);
} catch (Exception e) {
// Откат к английскому
return DateTimeFormatter.ofPattern("EEEE dd\\MM\\yyyy G").localizedBy(Locale.ENGLISH);
}
}
Отладка и проверка
1. Проверка включения локали
Проверьте, какие локали фактически включены в вашем нативном образе:
import java.text.spi.DateFormatProvider;
import java.util.Locale;
public class LocaleDebugger {
public static void debugLocales() {
Locale[] availableLocales = Locale.getAvailableLocales();
System.out.println("Доступные локали: " + availableLocales.length);
for (Locale locale : availableLocales) {
if (locale.getLanguage().equals("ar")) {
System.out.println("Найдена арабская локаль: " + locale);
}
}
// Тестирование форматирования даты
testDateFormatting(Locale.forLanguageTag("ar"));
}
private static void testDateFormatting(Locale locale) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("EEEE dd\\MM\\yyyy G")
.localizedBy(locale);
System.out.println("Отформатированная дата в " + locale + ": " +
formatter.format(Instant.now().atZone(ZoneId.systemDefault())));
}
}
2. Анализ нативного образа
Используйте инструменты GraalVM для анализа вашего нативного образа:
# Анализ ресурсов нативного образа
native-image -H:+PrintAnalysisResources -jar your-application.jar
# Проверка отсутствующих ресурсов
native-image -H:+ReportExceptionStackTraces -jar your-application.jar
3. Воспроизведение проблемы
На основе вашего репозитория GitHub, вот минимальная настройка для воспроизведения:
public class ArabicDateReproducer {
public static void main(String[] args) {
final var ARABIC_GREGORIAN_DATE_FORMATTER = DateTimeFormatter.ofPattern("EEEE dd\\MM\\yyyy G")
.localizedBy(Locale.forLanguageTag("ar"));
final var ENGLISH_GREGORIAN_DATE_FORMATTER = DateTimeFormatter.ofPattern("EEEE dd/MM/yyyy G")
.localizedBy(Locale.ENGLISH);
var now = Instant.now();
String arabicGregorianDate = ARABIC_GREGORIAN_DATE_FORMATTER.format(now.atZone(ZoneId.systemDefault()));
String englishGregorianDate = ENGLISH_GREGORIAN_DATE_FORMATTER.format(now.atZone(ZoneId.systemDefault()));
System.out.println("arabicGregorianDate = " + arabicGregorianDate);
System.out.println("englishGregorianDate = " + englishGregorianDate);
}
}
Соберите и протестируйте с помощью:
# Режим JVM (должен работать правильно)
java ArabicDateReproducer.java
# Нативный режим (может быть проблематичным)
native-image ArabicDateReproducer
./ArabicDateReproducer
Заключение
-
Основная проблема: GraalVM Native Image не автоматически включает полные данные локали, что приводит к тому, что форматирование дат на арабском языке возвращается к английским сокращениям.
-
Основные решения: Используйте
-H:IncludeLocales=ar,enс полными тегами локали или-H:+IncludeAllLocalesдля полной поддержки локали. -
Расширенная конфигурация: Реализуйте файлы конфигурации ресурсов и правильное использование трассирующего агента для обеспечения включения всех требуемых ресурсов локали.
-
Стратегия тестирования: Всегда тестируйте с целевыми локалями перед развертыванием и реализуйте механизмы резервного варианта для крайних случаев.
-
Компромисс производительности: Включение всех локалей увеличивает размер исполняемого файла, поэтому балансируйте между функциональностью и использованием ресурсов в зависимости от требований вашего приложения.
Для наиболее надежного решения объедините несколько подходов: укажите точные требования к локали, используйте трассирующий агент для перехвата отсутствующих ресурсов и реализуйте пользовательские механизмы резервного варианта для производственных сред.
Источники
- Документация GraalVM Native Image Resources
- GitHub Issue: Support for non-default locale
- JEP 252: Use CLDR Locale Data by Default
- Stack Overflow: Quarkus GraalVM native image DateTimeFormatter and Localization
- Stack Overflow: GraalVM native-image Java i18n Locale problem
- GraalVM Native Image Build Configuration