Как мапятся даты до java 8 и после
Перейти к содержимому

Как мапятся даты до java 8 и после

  • автор:

Работа с датой в Spring Boot

В предыдущих статьях мы уже создавали rest-приложение (Spring Boot Restful Service, Работа с БД в Spring Boot на примере postgresql). А теперь давайте рассмотрим, как работать с датой и временем в Spring Boot на уровне rest-запросов и на уровне БД.

Предположим, перед нами стоит задача фиксировать в специальной таблице все действия пользователя (регистрация, вход, выход и т.п.) Таблица для СУБД Postgres в самом простом случае будет выглядеть так:

CREATE TABLE user_action
(
id serial NOT NULL ,
action_date timestamp without time zone NOT NULL ,
user_id integer NOT NULL ,
action_type integer NOT NULL ,
CONSTRAINT user_action_pk PRIMARY KEY (id)
)

Тип serial представляет собой поле, которое автоматически увеличивается на единицу для каждой новой записи, поэтому его удобно использовать в качестве первичного ключа для записи.

Тип timestamp without time zone позволяет хранить метку времени без привязки к часовому поясу.

user_id и action_type представляют собой числовые id пользователя и тип действия соответственно. В реальном приложении каждое из них должно быть внешним ключом на соответствующие таблицы, но в нашем примере для простоты такой привязки нет.

Для типов действий удобно создать enum, чтобы не запоминать конкретные id.

public enum ActionType <

LOGIN ( 1 ),
LOGOUT ( 2 );

ActionType( int id) this .id = id;
>

public int getId() return id;
>

public static ActionType getById( int id) for (ActionType action : values()) if (action.getId() == id) return action;
>
>
throw new RuntimeException( «Unknown action type: » + id);
>
>

В этом enum имеется приватное поле id. Возможные значения (LOGIN, LOGOUT) инициализируются тут же через приватный конструктор. Статический метод getById() позволяет по числовому значению получить одно из значений enum, что очень удобно при чтении записи из БД. Все доступные значения мы получаем через метод values() и последовательно проверяем их. Как только нашли соответствие – сразу же возвращаем его, выходя из метода (а значит, и из цикла). Если же вдруг мы не смогли найти подходящего значения для указанного id – это является ошибкой времени выполнения и мы кидаем исключение после цикла. Однако если вы всегда пишете в БД тип действия при помощи этого enum, то такой ошибки произойти не может.

Добавление записи

Теперь мы готовы реализовать добавление записи о действии пользователя на уровне БД.

@Repository
public class UserActionDao

private static final String SQL_ADD_ACTION =
«insert into user_action (action_date, user_id, action_type) values (:actionDate, :userId, :actionId)» ;

@Autowired
private NamedParameterJdbcTemplate jdbcTemplate;

public void addAction(LocalDateTime actionDate, int userId, ActionType action) MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue( «actionDate» , Timestamp.valueOf(actionDate));
params.addValue( «userId» , userId);
params.addValue( «actionId» , action.getId());
jdbcTemplate.update( SQL_ADD_ACTION , params);
>
>

Здесь в общем-то всё прозрачно. Формируем параметры запроса через класс MapSqlParameterSource, а затем выполняем sql-запрос через метод update() класса NamedParameterJdbcTemplate. В тексте запроса значения параметров начинаются с двоеточия. В параметры дата должна передаваться в виде Timestamp. Но у этого класса есть статический метод valueOf(), который на вход принимает именно наш LocalDateTime.

Класс, в который будет мапиться json-запрос на добавление записи выглядит так:

public class UserAction <

@NotNull
private LocalDateTime actionDate;

@NotNull
@Min ( 1 )
private Integer userId;

@NotNull
private ActionType action;

// далее идут геттеры и сеттеры.
>

Здесь мы используем валидацию параметров. Аннотация @NotNull означает, что параметр обязательный. Чтобы корректно работала эта проверка с числовыми типами, в запросе используем не примитивный int, который по умолчанию примет значение 0, а объектный Integer, который по умолчанию null. Аннотация @Min указывает, какое минимальное значение допустимо для числового поля. В данном примере исходим из того, что все id положительные и начинаются с 1.

Пример json-запроса, который соответствует этому классу:

<
«actionDate» : «2018-05-10T11:22:33» ,
«userId» : 1 ,
«action» : «LOGIN»
>

Обратите внимание, что параметр «action» имеет здесь не числовой тип, а строковый, причём допустимые значения – это имена значений из перечисления ActionType.

Не менее интересен формат даты и времени. Сначала идёт год, затем месяц, затем день, затем литерал «T», затем время. Это стандартный формат даты, но чтобы он корректно работал в Spring Boot, нужно добавить новую dependency в наш pom-файл:


com.fasterxml.jackson.datatype
jackson-datatype-jsr310

Также нужно добавить новый параметр в конфигурационный файл приложения, путь до которого указывается в командной строке через параметр —spring.config.location:

spring.jackson.serialization.write_dates_as_timestamps=false

Теперь мы готовы написать наш rest-контроллер:

@RestController
@RequestMapping (value = «/users» , produces = MediaType. APPLICATION_JSON_VALUE )
public class UserController

@Autowired
private UserActionDao userActionDao;

@PostMapping ( «/actions» )
@ResponseStatus (HttpStatus. CREATED )
public void writeAction( @Valid @RequestBody UserAction request) userActionDao.addAction(request.getActionDate(), request.getUserId(), request.getAction());
>
>

В rest-сервисах добавление новых записей происходит черeз POST-запрос (аннотация @PostMapping). Через аннотацию @ResponseStatus мы указываем, что http-статус ответа будет 201 – «Created». @RequestBody указывает, какой именно параметр метода должен быть связан с телом json-запроса, а @Valid нужна для того, чтобы работала валидация параметров, которую мы рассматривали выше.

Теперь мы можем запустить наше приложение и отправить указанный выше POST-запрос. Не забудьте добавить http-заголовок «Content-Type: application/json». Из командной строки запрос можно выполнить так:

curl -X POST -H ‘Content-Type: application/json’ -i ‘http://127.0.0.1:8080/users/actions’ —data ‘ <
«actionDate» : «2018-05-10T11:22:33» ,
«userId» : 1,
«action» : «LOGIN»
> ‘

Если всё сделано правильно, в нашей таблице появится новая запись.

Просмотр истории записей

А теперь давайте создадим запрос на чтение ранее добавленных записей, причём будем фильтровать по дате добавления, передавая на вход нужный нам диапазон дат. На выходе будем получать список из тех же объектов UserAction, который использовали ранее. Поэтому нам понадобится RowMapper, который будет построчно преобразовывать полученный из БД набор данных в нужные нам объекты:

@Component
public class UserActionMapper implements RowMapper

@Override
public UserAction mapRow(ResultSet rs, int i) throws SQLException UserAction action = new UserAction();
action.setUserId(rs.getInt( «user_id» ));
action.setAction(ActionType.getById(rs.getInt( «action_type» )));
action.setActionDate(rs.getTimestamp( «action_date» ).toLocalDateTime());
return action;
>
>

При чтении данных из БД для даты действия получаем объект Timestamp, который имеет встроенный метод для преобразования в привычный нам LocalDateTime.

Теперь добавим в UserActionDao метод для чтения истории. Также нам потребуется подгрузить наш маппер:

private static final String SQL_GET_HISTORY =
«select * from user_action where action_date >= :dateFrom and action_date < :dateTo" +
» and user_id = :userId order by action_date desc» ;

@Autowired
private UserActionMapper userActionMapper;

public List getUserActionHistory( int userId, LocalDate dateFrom, LocalDate dateTo) MapSqlParameterSource params = new MapSqlParameterSource();
params.addValue( «userId» , userId);
params.addValue( «dateFrom» , Timestamp.valueOf(dateFrom.atTime(LocalTime. MIN )));
params.addValue( «dateTo» , Timestamp.valueOf(dateTo.atTime(LocalTime. MAX )));
return jdbcTemplate.query( SQL_GET_HISTORY , params, userActionMapper);
>

Обратите внимание, что метод получает на вход LocalDate, т.е. дату без времени. Указывая диапазон дат, мы подразумеваем промежуток с первой секунды начальной даты до последней секунды конечной даты. Поэтому преобразуем дату в дату со временем при помощи метода atTime() и констант, доступных в классе LocalTime.

Теперь добавим новый метод в наш контроллер:

@GetMapping ( «//action/history» )
public List getUserActionHistory(
@PathVariable ( «userId» ) int userId,
@RequestParam ( «dateFrom» ) @DateTimeFormat (iso = DateTimeFormat. ISO . DATE ) LocalDate dateFrom,
@RequestParam ( «dateTo» ) @DateTimeFormat (iso = DateTimeFormat. ISO . DATE ) LocalDate dateTo
) return userActionDao.getUserActionHistory(userId, dateFrom, dateTo);
>

Аннотация @GetMapping определяет, какой тип запроса и какой адрес связывается с данным методом, причём здесь мы указываем переменную userId. Её значение подставляем в соответствующий параметр при помощи аннотации @PathVariable. Поскольку у GET-запроса не может быть body, в отличие от POST и PUT запросов, диапазон передаём в виде параметров запроса (аннотация @RequestParam). Формат даты задаём через @DateTimeFormat («ГГГГ-ММ-ДД»). В теле обработчика вызываем наш dao-слой.

Если всё сделано правильно, нам достаточно запустить наше приложение и выполнить следующий GET-запрос:

http://127.0.0.1:8080/users/1/action/history?dateFrom=2018-05-10&dateTo=2018-05-11

В этом примере мы запрашиваем историю действий для пользователя с userId=1 с 10 по 11 мая 2018 года. Формат ответа будет таким:

[ <
«actionDate» : «2018-05-10T11:22:33» ,
«userId» : 1 ,
«action» : «LOGIN»
>, <
«actionDate» : «2018-05-10T11:22:33» ,
«userId» : 1 ,
«action» : «LOGOUT»
>]

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

Дата и время в Java 8. Сравнение даты и времени

Русский Українська

В новом Date Time API также появились удобные методы для сравнения дат и времени: compareTo(), isAfter(), isBefore() и isEqual(). Рассмотрим все эти методы на примерах.

 import java.time.*; public class Main < public static void main(String[] args) < LocalDateTime date = LocalDateTime.of(2002, Month.JANUARY, 10, 22, 56); LocalDateTime sameDate = LocalDateTime.of(2002, Month.JANUARY, 10, 22, 56); LocalDateTime dateMinusOneMinute = sameDate.minusMinutes(1); LocalDateTime datePlusOneMinute = sameDate.plusMinutes(1); ZonedDateTime zonedDate = ZonedDateTime.of(date, ZoneId.of("Brazil/East")); System.out.println("date: " + date); System.out.println("sameDate: " + sameDate); System.out.println("dateMinusOneMinute: " + dateMinusOneMinute); System.out.println("datePlusOneMinute: " + datePlusOneMinute); System.out.println("zonedDate: " + zonedDate); System.out.println(); System.out.println("compareTo #1: " + date.compareTo( sameDate )); System.out.println("compareTo #2: " + date.compareTo( dateMinusOneMinute )); System.out.println("compareTo #3: " + date.compareTo( datePlusOneMinute )); System.out.println(); System.out.println("isAfter #1: " + date.isAfter( sameDate )); System.out.println("isAfter #2: " + date.isAfter( dateMinusOneMinute )); System.out.println("isAfter #3: " + date.isAfter( datePlusOneMinute )); System.out.println(); System.out.println("isBefore #1: " + date.isBefore( sameDate )); System.out.println("isBefore #2: " + date.isBefore( dateMinusOneMinute )); System.out.println("isBefore #3: " + date.isBefore( datePlusOneMinute )); System.out.println(); System.out.println("isEqual #1: " + date.isEqual( sameDate )); System.out.println("isEqual #2: " + date.isEqual( dateMinusOneMinute )); System.out.println("isEqual #3: " + date.isEqual( datePlusOneMinute )); System.out.println(); System.out.println("equals #1: " + date.equals( sameDate )); System.out.println("equals #2: " + date.equals( zonedDate )); >> // output: // date: 2002-01-10T22:56 // sameDate: 2002-01-10T22:56 // dateMinusOneMinute: 2002-01-10T22:55 // datePlusOneMinute: 2002-01-10T22:57 // zonedDate: 2002-01-10T22:56-02:00[Brazil/East] // // compareTo #1: 0 // compareTo #2: 1 // compareTo #3: -1 // // isAfter #1: false // isAfter #2: true // isAfter #3: false // // isBefore #1: false // isBefore #2: false // isBefore #3: true // // isEqual #1: true // isEqual #2: false // isEqual #3: false // // equals #1: true // equals #2: false 
  • 0 — если оба экземпляра равны;
  • 1 — если дата, метод которой вызывается, находится после даты, которая поступает в метод как параметром;
  • -1 — если дата, метод которой вызывается, находится до даты, которая поступает в метод как параметр.

Метод isAfter() возвращает true ТОЛЬКО тогда, когда дата, метод которой вызывается, находится ПОСЛЕ даты, которая поступает в метод как параметром. Т.е., если для этих же объектов выполнить метод compareTo(), то он вернет 1.

Метод isBefore() возвращает true ТОЛЬКО тогда, когда дата, метод которой вызывается, находится ДО даты, которая поступает в метод как параметр. Т.е., если для этих же объектов выполнить метод compareTo(), то он вернет -1.

Метод isEqual() возвращает true если обе даты одинаковы.

Классы LocalDate и LocalTime имеют аналогичные методы для сравнения дат.

Напишите первое сообщение!

Управление временем в Java приложениях

Сегодня я хочу поговорить об управлении временем в Java приложениях: зачем это нужно, и как это можно делать.

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

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

А что там на уровне СУБД?

Сначала давайте посмотрим, как устроена работа с датой и временем на уровне СУБД, например, PostgreSQL. Это пригодится для дальнейшего понимания концепции, которую я продемонстрирую.

В PostgreSQL метку времени можно получить с помощью функции now() — в пределах одной транзакции она всегда возвращает один и тот же результат. Таким образом, если вы добавляете или изменяете несколько записей в одной транзакции, то у всех из них будет одинаковое время создания/модификации. Это удобно и классно опять же до того момента, пока вам не нужно протестировать поведение системы в другой момент времени.

Современная разработка ПО должна быть управляемой и предсказуемой, а это невозможно без автоматизированного тестирования. Именно по этой причине мы вынуждены отказаться от работы с датой временем на уровне СУБД и вынести её на уровень приложения.

Больше никаких вызов now() без параметров

Начиная с 8-й версии Java, нам доступен современный и удобный API для работы со временем. Эта тема неоднократно рассматривалась на Хабре. Подробнее можно почитать тут и тут.

Типовой подход в Java для получения даты или времени заключается в использовании статических методов now() . Я видел такой код сотни раз в разных проектах.

И вот первая рекомендация: откажитесь от использования now() без параметров в вашем коде. Всегда и везде нужно использовать перегруженную версию, принимающую на вход объект Clock :

Clock clock = Clock.systemUTC(); LocalDate date = LocalDate.now(clock); LocalDateTime time = LocalDateTime.now(clock); OffsetDateTime offsetTime = OffsetDateTime.now(clock);

Теперь дата и время зависят от используемых часов: если изменим часы и их поведение, то изменим получение времени внутри всего приложения! Всё гениальное просто, а разработчики JDK о нас уже позаботились.

Часы должны быть одни и только одни

Следующая задача, которую предстоит решить, это получение и использование одного и того же экземпляра часов внутри всего нашего приложения.

На текущий момент в стандартной автоконфигурации Spring Boot’а нет bean’а с часами, и в ближайшее время он точно не появится, поэтому всё приходится делать самостоятельно.

Если вы используете Spring без JPA (или с JPA, но без EntityListeners), то можно использовать следующий вариант:

@Configuration public class ClockConfig < @Bean public Clock clock() < return Clock.systemDefaultZone(); >>

Где-нибудь в сервисе просто инжектим и используем этот bean:

@Service @Transactional(readOnly = true) @RequiredArgsConstructor public class EmployeeService

Если вы активно используете Bean Validation API, то, возможно, вы захотите использовать его интерфейс ClockProvider. Лично я считаю его применение избыточным: использование Clock проще и очевиднее (core team Spring’а считает так же).

Однако, их можно совмещать:

@Bean public ClockProvider clockProvider(@Nonnull final Clock clock) < return () ->clock; >

Ситуация несколько осложняется случае активного использования JPA и EntityListeners ( @PrePersist / @PreUpdate и т.п.), поскольку инжектить бины в entity как-то. не принято. Именно так было на проекте, куда я пришёл несколько месяцев назад. В этом случае мы выбрали использование отдельного класса ClockHolder :

@Slf4j @UtilityClass public final class ClockHolder < private static final AtomicReferenceCLOCK_REFERENCE = new AtomicReference<>(Clock.systemDefaultZone()); @Nonnull public static Clock getClock() < return CLOCK_REFERENCE.get(); >/** * Atomically sets the value to and returns the old value. * * @param newClock the new value * @return the previous value of clock */ @Nonnull public static Clock setClock(@Nonnull final Clock newClock) < Objects.requireNonNull(newClock, "newClock cannot be null"); final Clock oldClock = CLOCK_REFERENCE.getAndSet(newClock); log.info("Set new clock <>. Old clock is <>", newClock, oldClock); return oldClock; > >

И пример его использования:

@Getter @Setter @SuperBuilder @NoArgsConstructor @MappedSuperclass public abstract class BaseEntity < @Id @NotNull @Column(updatable = false, nullable = false) private UUID id; @Column(name = "created_at", updatable = false, nullable = false) private LocalDateTime createdAt; @PrePersist public void beforePersist() < createdAt = LocalDateTime.now(ClockHolder.getClock()); >>

В Spring-конфигурацию bean clock в этом случае лучше не добавлять: везде следует использовать ClockHolder .

Пока готовил статью к публикации, пришёл к другому (более spring-style) варианту через отдельный класс, реализующий обработчики EntityListener’а. Плюс в том, что ClockHolder не нужен, и используется тот же самый bean clock . Если знаете другие варианты для этого случая, напишите в комментариях.

@Component @NoArgsConstructor public class ClockAwareEntityListener < // Couldn't use constructor injection here @Autowired private Clock clock; @PrePersist public void initCreatedAt(@Nonnull final BaseEntity entity) < if (entity.getCreatedAt() == null) < entity.setCreatedAt(LocalDateTime.now(clock)); >> >
@Getter @Setter @SuperBuilder @NoArgsConstructor @MappedSuperclass @EntityListeners(ClockAwareEntityListener.class) public abstract class BaseEntity

Фиксируйте время в тестах

Итак, теперь мы имеем в коде единые часы, от которых зависит получение времени во всём приложении, но в тестах эти часы по-прежнему будут выдавать монотонно возрастающий недетерминированный результат при каждом запуске. Вероятно, это не совсем то, чего бы нам хотелось. К счастью, мы можем «остановить» время в тестах, используя Clock.fixed , например так:

@ActiveProfiles("test") @SpringBootTest(classes = ) class CustomConfigurationExampleTest < private static final LocalDateTime MILLENNIUM = LocalDateTime.of(2000, Month.JANUARY, 1, 0, 0, 0); @Autowired private Clock clock; @Test void clockAlsoShouldBeFixed() < final LocalDateTime realNow = LocalDateTime.now(Clock.systemDefaultZone()); assertThat(LocalDateTime.now(clock)) .isBefore(realNow) .isEqualTo(LocalDateTime.of(2000, Month.JANUARY, 1, 0, 0, 0)); >@TestConfiguration static class CustomClockConfiguration < @Bean @Primary public Clock fixedClock() < return Clock.fixed(MILLENNIUM.toInstant(ZoneOffset.UTC), ZoneOffset.UTC); >> >

В случае использования ClockHolder время можно зафиксировать в базовом классе:

@ActiveProfiles("test") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public abstract class TestBase < protected static final LocalDateTime BEFORE_MILLENNIUM = LocalDateTime.of(1999, Month.DECEMBER, 31, 23, 59, 59); @BeforeAll static void setUpClock() < final Clock fixed = Clock.fixed(BEFORE_MILLENNIUM.toInstant(ZoneOffset.UTC), ZoneOffset.UTC); ClockHolder.setClock(fixed); >>

А в тестах это будет выглядеть следующим образом:

class EmployeeRepositoryTest extends TestBase < @Autowired private EmployeeRepository employeeRepository; @Test void createdAtShouldBeSetAutomaticallyOnSave() < final Employee notSaved = prepareIvanIvanov(); assertThat(notSaved.getCreatedAt()) .isNull(); final Employee saved = employeeRepository.save(notSaved); assertThat(saved) .isNotNull() .satisfies(e ->assertThat(e.getCreatedAt()) .isEqualTo(LocalDateTime.now(ClockHolder.getClock())) .isEqualTo(LocalDateTime.of(1999, Month.DECEMBER, 31, 23, 59, 59)) .isBefore(LocalDateTime.now(Clock.systemDefaultZone()))); > >

В конкретном тесте время можно изменить следующим образом:

@Test void canBeSavedInFuture() < final LocalDateTime distantFuture = LocalDateTime.of(3000, Month.JANUARY, 1, 0, 0, 0); final Clock fixed = Clock.fixed(distantFuture.toInstant(ZoneOffset.UTC), ZoneOffset.UTC); final Clock oldClock = ClockHolder.setClock(fixed); try < final Employee notSaved = prepareIvanIvanov(); assertThat(notSaved.getCreatedAt()) .isNull(); final Employee saved = employeeRepository.save(notSaved); assertThat(saved) .isNotNull() .satisfies(e ->assertThat(e.getCreatedAt()) .isEqualTo(LocalDateTime.now(ClockHolder.getClock())) .isEqualTo(LocalDateTime.of(3000, Month.JANUARY, 1, 0, 0, 0)) .isAfter(LocalDateTime.now(Clock.systemDefaultZone()))); > finally < ClockHolder.setClock(oldClock); >>

Получается многословно, не правда ли?

Используйте в тестах MutableClock

Стандартные часы Clock из JDK являются неизменяемыми (иммутабельными). Это очень классно, но только не в тестах: там было бы удобнее иметь возможность манипулировать временем. К счастью, уже есть ряд готовых решений для этого. Я остановил свой выбор на имплементации MutableClock из ThreeTen-Extra.

В коде это будет выглядеть примерно так: объявляем bean с изменяемым часами, переопределяем через него bean clock и после каждого теста восстанавливаем исходное значение фиксированных часов (чтобы в других тестах не заботиться об этом).

@ActiveProfiles("test") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @ContextConfiguration(classes = TestBase.CustomClockConfiguration.class) public abstract class TestBase < protected static final LocalDateTime BEFORE_MILLENNIUM = LocalDateTime.of(1999, Month.DECEMBER, 31, 23, 59, 59); @Autowired protected MutableClock mutableClock; @Autowired protected Clock clock; @AfterEach void resetClock() < mutableClock.setInstant(getTestInstant()); >static Instant getTestInstant() < return BEFORE_MILLENNIUM.toInstant(ZoneOffset.UTC); >@TestConfiguration static class CustomClockConfiguration < @Bean public MutableClock mutableClock() < return MutableClock.of(getTestInstant(), ZoneOffset.UTC); >@Bean @Primary public Clock fixedClock(@Nonnull final MutableClock mutableClock) < return mutableClock; >> >

Зато в тестах теперь очень легко изменять время как в прошлое, так и в будущее:

@Test void clockCanBeChangedLocally() < mutableClock.add(1_000L, ChronoUnit.YEARS); // Назад в будущее! assertThat(LocalDateTime.now(clock)) .isAfter(LocalDateTime.now(Clock.systemDefaultZone())) .isEqualTo(LocalDateTime.of(2999, Month.DECEMBER, 31, 23, 59, 59)); >

Эмулируйте поведение СУБД, если нужно

Помните, про поведение функции now() в PostgreSQL? Такого же поведения вы можете добиться внутри своих методов/транзакций. Это нужно далеко не всегда, но может быть полезно при выявлении аномалий/разборе инцидентов. Простейший вариант этого добиться — получить текущее время в начале транзакции, запомнить его и затем пробрасывать во все последующие методы как параметр. Если вариант с параметром кажется многословным, то посмотрите в сторону ThreadLocal / MDC .

Не забывайте о различиях в точности

Точность времени зависит от используемой платформы. Я уже упоминал об этом в одной из своих предыдущих статей: на macOS (M1, Monterey), например, секунды измеряются с точностью до 6 знаков после запятой, а на build-агенте под управлением Linux — 9 знаков после запятой. Иногда это мешает в тестах. Решение простое: транкейтить до 6 знаков.

@Nonnull public static LocalDateTime localDateTimeNow() < return LocalDateTime.now(clock()).truncatedTo(ChronoUnit.MICROS); >@Nonnull public static Instant instantNow()

Как приучить разработчиков, правильно работать с датой/временем?

Ключевой момент, который может помешать вам достичь успеха в управлении временем внутри вашего приложения, это использование метода now() без указания часов.

Разумеется, все ваши разработчики в команде должны об этом знать и использовать правильную перегруженную версию. Также вы можете контролировать этот момент на этапе code review, но я предпочитаю другой вариант — запрет на уровне Checkstyle, используя правило IllegalMethodCall :

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

На этом у меня всё. Итоговые примеры кода можно найти на GitHub.

Работа с датами и временем в Java 8: LocalDateTime

В Java 8 был добавлен новый API для работы с датами и временем, известный как java.time (JSR 310). Этот API предоставляет набор классов для представления различных временных концепций, включая даты, времена, даты-время и промежутки времени.

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

Преобразование строки в LocalDateTime

Часто приложениям требуется преобразовать строку в LocalDateTime . Например, приложение может получить дату и время в виде строки (например, «2014-04-08 12:30») и требовать преобразования этой строки в LocalDateTime .

Преобразование строки в LocalDateTime можно выполнить с помощью метода parse() класса LocalDateTime , передав в этот метод строку и объект DateTimeFormatter :

String str = "2014-04-08 12:30"; DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); LocalDateTime dateTime = LocalDateTime.parse(str, formatter);

Преобразование LocalDateTime в строку

После работы с LocalDateTime может потребоваться преобразование его обратно в строку. Для этого можно использовать метод format() класса LocalDateTime , передав в этот метод объект DateTimeFormatter :

LocalDateTime dateTime = LocalDateTime.of(2014, Month.APRIL, 8, 12, 30); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); String str = dateTime.format(formatter);

Таким образом, с помощью класса LocalDateTime и DateTimeFormatter можно легко преобразовывать строки в даты-время и обратно.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *