📜 ⬆️ ⬇️

Moving time on the fly for JVM

Probably everyone sometimes had to deal with the problem of transferring time on a local computer for testing programs. For example, you need to check how the server processes payments in a week from today's date.

The easiest way to do this is to move the system time. But he has several flaws. Some programs, such as Skype, begin to fail, save messages far into the future or into the past. Also, system policies can be set to synchronize time with the corporate server every 5 minutes.

After having had to deal with these problems for some time, I decided that it was time to come up with something and, after a couple of hours of fierce googling, I wrote a small java-agent that changes the time only for the desired JVM and does not touch the system date of the machine. The desired date is taken from the file, which every time its last modification date is checked. Perhaps this is not the best and fastest way, but by taking the source you can fix it as you prefer, for example, adding a shift not only dates but also time. There is also a version that can move the date programmatically.

The principle of the agent is very simple, using Instrumentation, it replaces calls to System.currentTimeMillis with my implementation of MySystem.currentTimeMillis, which returns the required date. To work with classes, the javassist library is used.
')
Now a little more about how it all works.

The main class of the java-agent is MainClass, on startup, the JVM will execute its main method 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); } } } 


Now let's look at how the ClassTransformer class works. It uses javassist to replace all System.currentTimeMillis calls with MySystem.currentTimeMillis calls. It is arranged quite simply:
 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; //    } } 


The MySystem class, which we will use to replace the system one, is very small:
 public class MySystem { public static long currentTimeMillis() { long res = System.currentTimeMillis(); //     long res1 = DateShift.getTime(res); //     return res1; //    } } 


The last DateShift class remains, which loads the time from the file and calculates the required relative time shift for the system date.
 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; } } 


The agent is built via Maven, which creates a jar file directly with all dependencies. I will not paint it in detail, you can see it in the source code for bitbucket.

To connect the agent to your program, you need to add an additional parameter for jvm:
 -javaagent:D:\development\srv\dateshift-1.4-jar-with-dependencies.jar=D:\development\srv\dateshift.txt 


That's all. As you can see there is nothing difficult in this. The agent is pretty simple and can be easily customized for your needs.

The noticed disadvantage is that if you move the time back relative to the current date, then applications can be a bit buggy. For example, web applications may not display or work correctly. But they are just as buggy if you move the system date right in the system.

Sources are available at https://bitbucket.org/javaorca/dateshift/src

Source: https://habr.com/ru/post/170401/


All Articles