Содержание

пятница, 1 апреля 2011 г.

Как работать с датами в приложение распределенном в нескольких временных зонах

Идея простая, чтобы не запутаться, надо все даты хранить и передавать в UTC. Казалось бы, что тут сложного, если есть параметр JVM "-Duser.timezone=GMT". На самом деле это не всегда выход, так как бывают еще клиентские приложения, где важно иметь локальную временную зону клиента, чтобы не заморачиваться с отображением времени. Скорее всего вы еще общаетесь с базой данных, где установить UTC нет возможности. Либо у вас уже много есть завязок на локальную временную зону и переписывать весь код (а в сложной системе, возможно, вы еще и не найдете сразу весь этот код) очень накладно. В данном топике я расскажу как я справлялся со всем этим в одном из проектов, вернее даже не в одном.

Чтобы идти дальше, давайте разберемся как хранится дата и время в java и базе данных Oracle.

java.util.Date

Данный клас абсолютно не хранит никакой информации о временной зоне. Данные в нем представлены в виде количества миллисекунд с начала UNIX эпохи (January 1, 1970, 00:00:00 GMT). Кстати, GMT и UTC, если вы еще не запомнили, это одно и то же. Таким образом передача данного объекта между распределенными компонентами вполне удобна и безопасна, с точки зрения опасности потерять временную зону. Т.е. если вы хотите получить текущее время и передать куда-то, то смело вызывайте new Date(), передавайте объект куда угодно и не партесь по поводу временной зоны. Но помните, что метод toString() вернет вам представление даты со временем в локальной временной зоне. Поэтому для отображения или конструирования этого объекта используйте java.text.DateFormat (или скорее SimpleDateFormat), с выставленной временной зоной. Подробнее об этом немного ниже.

java.util.Calendar.getInstance()

Данный объект помимо миллисекунд от UNIX эпохи хранит еще временную зону. По умолчанию - локальную. Для получения объекта определенной временной зоны используйте Calendar.getInstance(TimeZone.getTimeZone("UTC")). Но так как миллисекунды опять же хранятся в виде абсолютного значения от UNIX эпохи, то они как бы существуют независимо от временной зоны. Например, Calendar.getInstance(TimeZone.getTimeZone("MSD")).getTime() и Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTime() вернут одинаковые объеты java.util.Date.

java.sql.Timestamp

Как и java.util.Date он хранит количество миллисекунд начиная с UNIX эпохи. Кстати, будьте осторожны с java.sql.Date, потому что этот класс хранит округленное количество миллисекунд до целого количества дней в локальной временной зоне. Так что, если вы зачитаете из ResultSet этот тип данных, то окажетесь с датой без времени.

Сохранение времени в базу данных Oracle

В оракл Timestamp хранится в виде набора чисел: год, месяц, день, час, минута, секунда, миллисекунд. Таким образом thin jdbc oracle driver при передачи и чтения даты нужно конвертировать миллисекунды в числовое представление, что он делает используя по умолчанию локальную временную зону. Чтобы попросить его сохранять все в UTC необходимо так же передавать Calendar.getInstance(TimeZone.getTimeZone("UTC")). Если же вы просто просматриваете данные в БД с помощью slqplus или какого-то визуального инструмента, то тут можно не парится - он всегда будет показывать то, что действительно лежит в базе данных, так как конвертировать внутренне представление в визуальное не нужно, ведь мы тоже читаем дату как год, месяц и т.д. Если вы используете хибернейт, то этот трюк уже реализован, надо только в мапинге указать поле специального кастомного типа.

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

Если в своем приложении вы используете таблицы аудитов, в которых хранятся все предыдущие данные, то будьте внимательны с их реализацией. Обычно это делается, с помощью триггеров. Кроме предыдущих данных обычно еще хранят время изменения. Данное время легко можно сгенерить на лету при помощи встроенной процедуры оракла:
Но она вернет вам локальное текущее время. Если же вы хотите сохранить время в UTС, то нужно использовать

Логирование.

Если ваше приложение деплоится в различных временных зонах, то удобно писать логи в UTC, так как не надо будет мучится с переводом времени. Для этого в log4j можно использовать следующий паттерн.
Правда в этом случае вам еще понадобиться дополнительный jar файл apache-log4j-extras.jar. К сожалению, стандартный org.apache.log4j.DailyRollingFileAppender appender будет продолжать роллить файлы все еще основываясь на локальной временной зоне. Хотя его конечно можно самому допилить напильником.

Форматирование

Для форматирования дат в java можно использовать стандартный java.text.DateFormat (java.text.SimpleDateFormat). Для того, чтобы все было как и везде в вашем приложение в UTC необходимо выставить временную зону
Но, будьте внимательны, SimpleDateFormat не threadsafe, поэтому исползовать статическую переменную без синхронизации нельзя. Как более эффективно форматировать дату в многопоточном приложении очень хоршо описано тут.