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

в 17:02, , рубрики: date, java, метки: ,

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

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js