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