Перейти к основному содержимому

Введение в InstantSource в Java 17

· 6 мин. чтения

1. Обзор

В этом руководстве мы углубимся в интерфейс InstantSource , представленный в Java 17, который обеспечивает подключаемое представление текущего момента и избегает ссылок на часовые пояса.

2. Интерфейс InstantSource

Первая цель этого интерфейса, как мы видим в исходном предложении и связанной с ним задаче , — создать абстракцию часового пояса, предоставленную java.time.Clock . Это также упрощает создание заглушек во время тестирования частей кода, которые извлекают мгновенные сообщения.

Он был добавлен в Java 17 , чтобы обеспечить безопасный способ доступа к текущему моменту, как мы можем видеть в следующем примере:

class AQuickTest {
InstantSource source;
...
Instant getInstant() {
return source.instant();
}
}

И тогда мы можем просто получить момент:

var quickTest = new AQuickTest(InstantSource.system());
quickTest.getInstant();

Его реализации создают объекты, которые можно использовать где угодно для извлечения моментов, и он обеспечивает эффективный способ создания реализаций-заглушек для целей тестирования.

Давайте подробнее рассмотрим преимущества использования этого интерфейса.

3. Проблемы и решения

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

3.1. Проблема тестирования

Тестирование кода, связанного с получением Instant , обычно является кошмаром, особенно когда способ получения этого Instant основан на текущих решениях для данных, таких как LocalDateTime.now().

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

Давайте рассмотрим следующий код в качестве примера решения этой проблемы.

Класс InstantExample использует InstantWrapper (или обходной путь) для восстановления мгновенного:

class InstantExample {
InstantWrapper instantWrapper;
Instant getCurrentInstantFromInstantWrapper() {
return instantWrapper.instant();
}
}

А сам наш класс обходного пути InstantWrapper выглядит так:

class InstantWrapper {
Clock clock;
InstantWrapper() {
this.clock = Clock.systemDefaultZone();
}
InstantWrapper(ZonedDateTime zonedDateTime) {
this.clock = Clock.fixed(zonedDateTime.toInstant(), zonedDateTime.getZone());
}
Instant instant() {
return clock.instant();
}
}

Затем мы можем использовать его, чтобы предоставить фиксированный момент времени для тестирования:

// given
LocalDateTime now = LocalDateTime.now();
InstantExample tested = new InstantExample(InstantWrapper.of(now), null);
Instant currentInstant = now.toInstant(ZoneOffset.UTC);
// when
Instant returnedInstant = tested.getCurrentInstantFromWrapper();
// then
assertEquals(currentInstant, returnedInstant);

3.2. Решение проблемы тестирования

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

class InstantExample {
InstantSource instantSource;
Instant getCurrentInstantFromInstantSource() {
return instantSource.instant();
}
}

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

// given
LocalDateTime now = LocalDateTime.now();
InstantSource instantSource = InstantSource.fixed(now.toInstant(ZoneOffset.UTC));
InstantExample tested = new InstantExample(null, instantSource);
Instant currentInstant = instantSource.instant();
// when
Instant returnedInstant = tested.getCurrentInstantFromInstantSource();
// then
assertEquals(currentInstant, returnedInstant);

3.3. Проблема с часовым поясом

Когда нам требуется Instant , у нас есть много разных мест, откуда его можно получить , например Instant.now() , Clock.systemDefaultZone().instant() или даже LocalDateTime.now.toInstant(zoneOffset) . Проблема в том, что в зависимости от выбранного нами варианта могут возникать проблемы с часовым поясом .

Например, давайте посмотрим, что происходит, когда мы запрашиваем мгновение у класса Clock :

Clock.systemDefaultZone().instant();

Этот код даст следующий результат:

2022-01-05T06:47:15.001890204Z

Спросим тот же миг, но из другого источника:

LocalDateTime.now().toInstant(ZoneOffset.UTC);

Это дает следующий результат:

2022-01-05T07:47:15.001890204Z

Мы должны были получить то же самое мгновение, но на самом деле между ними 60-минутная разница.

Хуже всего то, что два или более разработчиков могут работать над одним и тем же кодом, используя эти два мгновенных источника в разных частях кода. Если это так, то у нас проблемы.

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

3.4. Решение проблемы с часовым поясом

InstantSource абстрагирует нас от выбора источника моментов . Этот выбор уже сделан за нас. Возможно, другой программист настроил пользовательскую реализацию для всей системы или мы используем ту, что предоставлена Java 17, как мы увидим в следующем разделе.

Как показывает InstantExample , у нас есть подключенный InstantSource , и нам больше ничего знать не нужно. Мы можем удалить наш обходной путь InstantWrapper и вместо этого просто использовать подключенный InstantSource .

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

4. Фабричные методы

Для создания объекта InstantSource можно использовать следующие фабричные методы:

  • system() – общесистемная реализация по умолчанию
  • tick(InstantSource, Duration) – возвращает InstantSource , усеченный до ближайшего представления заданной длительности.
  • fixed(Instant) — возвращает InstantSource , который всегда создает один и тот же Instant
  • offset(InstantSource, Duration) – возвращает InstantSource , который предоставляет Instant с заданным смещением

Давайте посмотрим на некоторые основные способы использования этих методов.

4.1. система()

Текущей реализацией по умолчанию в Java 17 является класс Clock.SystemInstantSource .

Instant i = InstantSource.system().instant();

4.2. поставить галочку()

На основе предыдущего примера:

Instant i = InstantSource.system().instant();
System.out.println(i);

После запуска этого кода мы получим следующий вывод:

2022-01-05T07:44:44.861040341Z

Но, если мы применим тик продолжительностью 2 часа:

Instant i = InstantSource.tick(InstantSource.system(), Duration.ofHours(2)).instant();

Тогда мы получим результат ниже:

2022-01-05T06:00:00Z

4.3. исправлено()

Этот метод удобен, когда нам нужно создать заглушку InstantSource для целей тестирования:

LocalDateTime fixed = LocalDateTime.of(2022, 1, 1, 0, 0);
Instant i = InstantSource.fixed(fixed.toInstant(ZoneOffset.UTC)).instant();
System.out.println(i);

Приведенное выше всегда возвращает одно и то же мгновение:

2022-01-01T00:00:00Z

4.4. компенсировать()

Основываясь на предыдущем примере, мы применим смещение к фиксированному InstantSource , чтобы увидеть, что он возвращает:

LocalDateTime fixed = LocalDateTime.of(2022, 1, 1, 0, 0);
InstantSource fixedSource = InstantSource.fixed(fixed.toInstant(ZoneOffset.UTC));
Instant i = InstantSource.offset(fixedSource, Duration.ofDays(5)).instant();
System.out.println(i);

После выполнения этого кода мы получим следующий вывод:

2022-01-06T00:00:00Z

5. Методы экземпляра

Методы, доступные для взаимодействия с экземпляром InstantSource : ``

  • Instant () — возвращает текущий Instant , заданный InstantSource
  • millis() – возвращает миллисекундное представление текущего Instant , предоставленное InstantSource.
  • withZone(ZoneId) – получает ZoneId и возвращает часы на основе данного InstantSource с указанным ZoneId

5.1. мгновенный()

Самое основное использование этого метода:

Instant i = InstantSource.system().instant();
System.out.println(i);

Запуск этого кода покажет нам следующий вывод:

2022-01-05T08:29:17.641839778Z

5.2. миллис()

Чтобы получить эпоху из InstantSource :

long m = InstantSource.system().millis();
System.out.println(m);

И, запустив его, мы получим следующее:

1641371476655

5.3. с зоной()

Давайте получим экземпляр Clock для определенного ZoneId :

Clock c = InstantSource.system().withZone(ZoneId.of("-4"));
System.out.println(c);

Это просто напечатает следующее:

SystemClock[-04:00]

6. Заключение

В этой статье мы рассмотрели интерфейс InstantSource , перечислив существенные проблемы, для решения которых он был создан, и показав реальные примеры того, как мы можем использовать его преимущества в нашей повседневной работе.

Как обычно, код доступен на GitHub .