Двигаем время на лету для JVM

от автора

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

Самый простой способ сделать это — подвинуть системное время. Но у него есть несколько недостатков. Некоторые программы, например, 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

ссылка на оригинал статьи http://habrahabr.ru/post/170401/


Комментарии

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

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