Наверное всем иногда приходилось сталкиваться с проблемой перевода времени на локальном компьютере для тестирования программ. Например, вам нужно проверить как сервер обработает платежи через неделю от сегодняшней даты.
Самый простой способ сделать это — подвинуть системное время. Но у него есть несколько недостатков. Некоторые программы, например, Skype, начинают глючить, сохранять сообщения далеко в будущее или в прошлое. Так же системными политиками может быть задано синхронизировать время с корпоративным сервером каждые 5 минут.
Промучавшись с этими проблемами какое-то время я решил, что пора что-то придумать и, после пары часов ожесточённого гуглинга, написал небольшой java-agent, который изменяет время только для нужной JVM и не трогает системную дату машины. Нужная дата берётся из файла, у которого каждый раз проверяется его последняя дата модификации. Возможно это не самый лучший и быстрый способ, но взяв исходники вы можете поправить как вам будет удобнее и, например, добавить сдвиг не только даты, но и времени. Так же есть версия, которая умеет двигать дату программно.
Принцип работы агента очень прост, при помощи Instrumentation он заменяет вызовы к System.currentTimeMillis на мою реализацию MySystem.currentTimeMillis, которая возвращает необходимую дату. Для работы с классами используется библиотека javassist.
Теперь немного подробнее как это всё устроено.
Главный класс java-агента — MainClass, при старте,JVM выполнит его основной метод premain:
public class MainClass {
private static Instrumentation instrumentation; // Сервис, который позволит нам заменить вызовы System.currentTimeMillis на наши
private static ClassTransformer transformer; // Наша реализация ClassFileTransformer
public static File FILE = null; // Файл, из которого берётся нужная нам дата
public static void premain(String args, Instrumentation inst) throws Exception {
System.out.println("dateshift agent starting");
if (args != null && args.length() > 0) { // Если агенту переданы параметр, то он берётся как имя файла для даты
String path = args;
System.out.println("Using dateshift.txt path from args: '" + path + "'");
FILE = new File(path);
} else { // Если параметров нет, то по-умолчанию берётся файл dateshift.txt, который должен быть расположен в каталоге bin tomcat-a
FILE = new File(new File(System.getenv("CATALINA_HOME"), "bin"), "dateshift.txt");
}
System.out.println("Path for dateshift.txt: '" + FILE.getAbsolutePath() + "'");
instrumentation = inst; // Используем сервис, переданный нам JVM
transformer = new ClassTransformer();
instrumentation.addTransformer(transformer, true); // Указываем системе, что она может использовать наш ClassTransformer для изменения классов
Class[] classes = inst.getAllLoadedClasses(); // Получаем список уже загруженных классов, которые могут быть изменены. Классы, которые ещё не загружены, будут изменены при загрузке
ArrayList<Class> classList = new ArrayList<Class>();
for (int i = 0; i < classes.length; i++) {
if (inst.isModifiableClass(classes[i])) { // Если класс можно изменить, добавляем его в список
classList.add(classes[i]);
}
}
// Reload classes, if possible.
Class[] workaround = new Class[classList.size()];
try {
inst.retransformClasses(classList.toArray(workaround)); // Запускаем процесс трансформации
} catch (UnmodifiableClassException e) {
System.err.println("MainClass was unable to retransform early loaded classes: " + e);
}
}
}
Теперь рассмотрим как устроен класс ClassTransformer. Он использует javassist, чтобы заменить все вызовы System.currentTimeMillis на вызовы MySystem.currentTimeMillis. Устроен он достаточно просто:
public class ClassTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer)
throws IllegalClassFormatException {
if(className.startsWith("ru/javaorca/")) return null; // Пропускаем классы агента
try {
ClassPool pool = ClassPool.getDefault();
CtClass s1 = pool.get("java.lang.System");
CtMethod m11 = s1.getDeclaredMethod("currentTimeMillis"); // Находим метод, который нам нужно заменить
CtClass s2 = pool.get("ru.javaorca.MySystem");
CtMethod m21 = s2.getDeclaredMethod("currentTimeMillis"); // Находим метод, на который мы будем заменять
CodeConverter cc = new CodeConverter();
cc.redirectMethodCall(m11, m21); // Указываем что на что нам нужно заменить
CtClass cl = pool.makeClass(new ByteArrayInputStream(classfileBuffer), false); // Загружаем класс, переданный для трансформации
if(cl.isFrozen()) return null;
CtConstructor[] constructors = cl.getConstructors(); // Находим все конструкторы класса
for(CtConstructor constructor : constructors) {
constructor.instrument(cc); // Заменяем вызовы
}
CtMethod[] methods = cl.getDeclaredMethods(); // Находим все методы класса
for(CtMethod method : methods) {
method.instrument(cc); // Заменяем вызовы
}
classfileBuffer = cl.toBytecode();
} catch (Exception ex) {
System.out.println("Exception: " + ex);
ex.printStackTrace();
}
return classfileBuffer; // Возвращаем изменённый класс
}
}
Класс MySystem, который мы будем использовать для замены системного, очень маленький:
public class MySystem {
public static long currentTimeMillis() {
long res = System.currentTimeMillis(); // Получаем настоящее системное время
long res1 = DateShift.getTime(res); // Высчитываем необходимый сдвиг времени
return res1; // Возвращаем новое время
}
}
Остался последний класс DateShift, который загружает время из файла и рассчитывает необходимый относительный сдвиг времени для системной даты.
public class DateShift {
private static volatile long lastModified = 0; // Дата последней модификации файла с датой
private static volatile long timeShift = 0; // Относительный сдвиг времени в миллисекундах
private static final long timeFilter = 86400000L; // 1000*60*60*24, фильтр для отсекания времени из системной даты
public static long getTime(long currentTime) { // Метод, преобразующий системную дату
long res = currentTime;
if ((lastModified > 0 && !MainClass.FILE.exists()) || lastModified < MainClass.FILE.lastModified()) { // Если файл изменился, то загружаем новую дату из него
System.out.println("File modification detected");
synchronized (MainClass.FILE) {
if (MainClass.FILE.exists()) {
lastModified = MainClass.FILE.lastModified();
long newTime = readDateFromFile(); // Загружаем дату из файла
if (newTime > 0) {
timeShift = newTime - ((res / timeFilter) * timeFilter); // Отрезаем от даты время и рассчитываем относительный сдвиг времени
}
} else {
lastModified = 0; // Если файла нет, то убираем сдвиг времени
timeShift = 0;
}
}
}
if (timeShift != 0) {
res += timeShift; // Сдвигаем время
}
return res;
}
private static long readDateFromFile() { // Метод, загружающий дату из файла
System.out.println("Reading data from file '" + MainClass.FILE.getAbsolutePath() + "'");
long res = 0;
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(MainClass.FILE));
String line = br.readLine(); // Читаем первую строчку в файле
if (line != null && !line.trim().isEmpty()) {
SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy", Locale.ROOT); // Формат даты жёстко задан dd.MM.yyyy
try {
Date date = DATE_FORMAT.parse(line);
System.out.println("Loaded date from file: " + date);
Calendar c = Calendar.getInstance();
c.setTime(date);
long offset = c.get(Calendar.ZONE_OFFSET) + c.get(Calendar.DST_OFFSET); // Получаем сдвиг времени для нашей временной зоны. Это необходимо затем, чтобы время нашей JVM не сдвинулось относительно системного
System.out.println("Offset: " + offset);
res = c.getTime().getTime();
res += offset;
} catch (ParseException e) {
System.out.println("ParseException: " + e);
e.printStackTrace(System.out);
}
} else {
System.out.println("File is empty");
}
} catch (IOException e) {
System.out.println("IOException: " + e);
e.printStackTrace(System.out);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
System.out.println("IOException: " + e);
e.printStackTrace(System.out);
}
}
}
return res;
}
}
Сборка агента производится через Maven, который создаёт jar-файл прямо со всеми зависимостями. Я не буду его подробно расписывать, посмотреть его можно в исходниках на bitbucket.
Вот и всё. Как видите ничего сложного в этом нет. Агент получился довольно просто и может быть легко доработан под ваши нужды.
Замеченный недостаток — если двинуть время назад относительно текущей даты, то приложения могут немного глючить. Например, веб-приложения могут отображаться или работать неправильно. Но они точно так же глючат, если сдвинуть системную дату прямо в системе.
Исходники доступны по адресу https://bitbucket.org/javaorca/dateshift/src
Автор: javaorca