From 56d0c00822f37b804ac8441973ccc7d97d564d48 Mon Sep 17 00:00:00 2001 From: Andy Clement Date: Tue, 5 May 2015 17:50:57 -0700 Subject: [PATCH] Rudimentary jar watching New options watchJars takes a colon separated list of jars (just the jar names, not paths). For example: -Dspringloaded=watchJars=foo.jar:bar.jar Committed some very basic tests that are passing, they are in the SpringLoadedTestsInSeparateJVM test class, see the tests with 'jar' in the name. --- .../loaded/GlobalConfiguration.java | 15 ++- .../org/springsource/loaded/TypeRegistry.java | 119 +++++++++++------- .../org/springsource/loaded/TypeRewriter.java | 17 ++- .../java/org/springsource/loaded/Utils.java | 101 ++++++++------- .../agent/ClassPreProcessorAgentAdapter.java | 2 +- .../springsource/loaded/agent/JVMPlugin.java | 10 +- .../agent/ReloadableFileChangeListener.java | 85 ++++++++++++- .../agent/SpringLoadedPreProcessor.java | 16 ++- .../ri/test/FieldGetAnnotationTest.java | 7 +- .../loaded/test/FileSystemWatcherTests.java | 37 +++++- .../loaded/test/ReloadingJVM.java | 89 ++++++++++++- .../test/ReloadingJVMCommandProcess.java | 37 +++++- .../test/SpringLoadedTestsInSeparateJVM.java | 45 ++++++- testdata/jars/README | 1 + testdata/jars/one/Bar.java | 6 + testdata/jars/one/Foo.class | Bin 0 -> 445 bytes testdata/jars/one/Foo.java | 8 ++ testdata/jars/one/Wibble.java | 6 + testdata/jars/one/bar.jar | Bin 0 -> 788 bytes testdata/jars/one/build.sh | 4 + testdata/jars/one/foo.jar | Bin 0 -> 440 bytes testdata/jars/one/test/Bar.class | Bin 0 -> 282 bytes testdata/jars/one/test/Wibble.class | Bin 0 -> 416 bytes testdata/jars/two/Bar.java | 6 + testdata/jars/two/Foo.class | Bin 0 -> 445 bytes testdata/jars/two/Foo.java | 8 ++ testdata/jars/two/Wibble.java | 6 + testdata/jars/two/bar.jar | Bin 0 -> 788 bytes testdata/jars/two/build.sh | 4 + testdata/jars/two/foo.jar | Bin 0 -> 440 bytes testdata/jars/two/test/Bar.class | Bin 0 -> 282 bytes testdata/jars/two/test/Wibble.class | Bin 0 -> 416 bytes 32 files changed, 503 insertions(+), 126 deletions(-) create mode 100644 testdata/jars/README create mode 100644 testdata/jars/one/Bar.java create mode 100644 testdata/jars/one/Foo.class create mode 100644 testdata/jars/one/Foo.java create mode 100644 testdata/jars/one/Wibble.java create mode 100644 testdata/jars/one/bar.jar create mode 100755 testdata/jars/one/build.sh create mode 100644 testdata/jars/one/foo.jar create mode 100644 testdata/jars/one/test/Bar.class create mode 100644 testdata/jars/one/test/Wibble.class create mode 100644 testdata/jars/two/Bar.java create mode 100644 testdata/jars/two/Foo.class create mode 100644 testdata/jars/two/Foo.java create mode 100644 testdata/jars/two/Wibble.java create mode 100644 testdata/jars/two/bar.jar create mode 100755 testdata/jars/two/build.sh create mode 100644 testdata/jars/two/foo.jar create mode 100644 testdata/jars/two/test/Bar.class create mode 100644 testdata/jars/two/test/Wibble.class diff --git a/springloaded/src/main/java/org/springsource/loaded/GlobalConfiguration.java b/springloaded/src/main/java/org/springsource/loaded/GlobalConfiguration.java index c38d910..f7ae75e 100644 --- a/springloaded/src/main/java/org/springsource/loaded/GlobalConfiguration.java +++ b/springloaded/src/main/java/org/springsource/loaded/GlobalConfiguration.java @@ -30,7 +30,7 @@ import org.springsource.loaded.agent.SpringPlugin; * Encapsulates configurable elements - these are set (to values other than the defaults) in TypeRegistry when the * system property springloaded configuration is processed. Some of the options are only used by testcases to make the * testcases easier to write and more straightforward. - * + * * @author Andy Clement * @since 0.5.0 */ @@ -104,6 +104,13 @@ public class GlobalConfiguration { public final static boolean logNonInterceptedReflectiveCalls = false; + /** + * Holds a list of fully qualified paths to jars that should be 'watched' for changes. Types + * within these jars will be made reloadable. Set via option 'watchJars' which + * takes a colon separated list of jars. + */ + public static String[] jarsToWatch = null; + /** * Global control for checking assertions */ @@ -255,6 +262,12 @@ public class GlobalConfiguration { log.info("configuration: dumpFolder = " + dumpFolder); } } + else if (key.equals("watchJars")) { + if (isRuntimeLogging && log.isLoggable(Level.INFO)) { + log.info("Watching jars: " + kv.substring(equals + 1)); + } + jarsToWatch = kv.substring(equals + 1).split(":"); + } else if (key.equals("maxClassDefinitions")) { try { maxClassDefinitions = Integer.parseInt(kv.substring(equals + 1)); diff --git a/springloaded/src/main/java/org/springsource/loaded/TypeRegistry.java b/springloaded/src/main/java/org/springsource/loaded/TypeRegistry.java index 391fbeb..7a438ed 100644 --- a/springloaded/src/main/java/org/springsource/loaded/TypeRegistry.java +++ b/springloaded/src/main/java/org/springsource/loaded/TypeRegistry.java @@ -58,7 +58,7 @@ import org.springsource.loaded.support.Java8; * The type registry tracks all reloadable types loaded by a specific class loader. It is configurable via a * springloaded.properties file (which it will discover as resources through the classloader) or directly via a * configure(Properties) method call. - * + * * @author Andy Clement * @since 0.5.0 */ @@ -97,13 +97,13 @@ public class TypeRegistry { "javax.management.remote.rmi.NoCallStackClassLoader", "org.springsource.loaded.ChildClassLoader", // "groovy.lang.GroovyClassLoader$InnerLoader", - // not excluding GCL$InnerLoader because we want the reflection stuff rewritten - think we need to separate out + // not excluding GCL$InnerLoader because we want the reflection stuff rewritten - think we need to separate out // reflection rewriting from the rest of callside rewriting. Although do we still need to rewrite call sites anyway, although the code there may not change (i.e. TypeRewriter not // required), the targets for some calls may come and go (may not have been in the original loaded version) "org.apache.jasper.servlet.JasperLoader", // tc server configuration... - // "org.apache.catalina.loader.StandardClassLoader" + // "org.apache.catalina.loader.StandardClassLoader" }; // @formatter:on @@ -231,7 +231,7 @@ public class TypeRegistry { /** * Check if a type registry exists for a specific type registry ID. Enables parts of the system (for example the * FileSystemWatcher) to check if a type registry is still alive/active. - * + * * @param typeRegistryId the ID of a type registry * @return true if that type registry is still around, false otherwise */ @@ -246,7 +246,7 @@ public class TypeRegistry { /** * Factory access method for obtaining TypeRegistry instances. Returns a TypeRegistry for the specified classloader. - * + * * @param classloader The classloader to create/retrieve the type registry for * @return the TypeRegistry for the classloader */ @@ -308,7 +308,7 @@ public class TypeRegistry { /** * Only checks the reloadable types this registry knows about, it doesn't search beyond that. - * + * * @param slashedClassname the slashed classname (e.g. java/lang/String) * @return the TypeDescriptor or null if that classname is unknown */ @@ -432,7 +432,7 @@ public class TypeRegistry { /** * Configure this TypeRegistry using a specific set of properties - this will override any previous configuration. * It is mainly provided for testing purposes. - * + * * @param properties the properties to use to configure this type registry */ public void configure(Properties properties) { @@ -605,7 +605,7 @@ public class TypeRegistry { * Determine if the named type could be reloadable. This method is invoked if the user has not setup any inclusions. * With no inclusions specified, something is considered reloadable if it is accessible by the classloader for this * registry and is not in a jar - * + * * @param slashedName the typename of interest (e.g. com/foo/Bar) * @return true if the type should be considered reloadable */ @@ -693,6 +693,26 @@ public class TypeRegistry { // new RuntimeException().printStackTrace(); // } reloadable = protocol.equals("file"); + // Check if it is from a jar that is being watched + if (!reloadable && protocol.equals("jar")) { + if (GlobalConfiguration.jarsToWatch != null) { + // 1.3 feature, can watch jars! + String urlstring = url.toString(); + // example path: jar:file:/var/folders/cn/p3n4rh_n6z7gm6zwk53mtfc80000gp/T/_sl308369570226517394/foo.jar!/Foo.class + int bangSlash = urlstring.lastIndexOf("!/"); + if (bangSlash != -1) { + String pathInJar = urlstring.substring(bangSlash + 2); + String remainingPrefix = urlstring.substring(0, bangSlash); + int lastSlash = remainingPrefix.lastIndexOf("/"); // TODO windoze? + String jarname = remainingPrefix.substring(lastSlash + 1); + for (String jarToWatch : GlobalConfiguration.jarsToWatch) { + if (jarname.equals(jarToWatch)) { + reloadable = true; + } + } + } + } + } } if (packageName != null) { if (reloadable) { @@ -719,7 +739,7 @@ public class TypeRegistry { /** * Determine if the type specified is a reloadable type. This method works purely by name, it does not load * anything. - * + * * @param slashedName the type name, eg. a/b/c/D * @param protectionDomain the protection domain this class is being loaded under * @param bytes the class bytes for the class being loaded @@ -851,7 +871,7 @@ public class TypeRegistry { /** * Lookup the type ID for a string. First checks those allocated but not yet registered, then those that are already * registered. If not found then a new one is allocated and recorded. - * + * * @param slashname the slashed type name, eg. a/b/c/D * @param allocateIfNotFound determines whether an id should be allocated for the type if it cannot be found * @return the unique ID number @@ -900,6 +920,13 @@ public class TypeRegistry { rtype.loadNewVersion(versionstamp, newBytes); } + public void loadNewVersion(ReloadableType rtype, long lastModTime, InputStream is) { + String versionstamp = Utils.encode(lastModTime); + // load bytes for new version + byte[] newBytes = Utils.loadFromStream(is); + rtype.loadNewVersion(versionstamp, newBytes); + } + /** * Map from a registry ID number to a registry instance. ID numbers are used in the rewritten code. WeakReferences * so that we aren't preventing collection of TypeRegistry objects when their classloaders are GC'd. @@ -952,7 +979,7 @@ public class TypeRegistry { /** * Add a type to the registry. The name should have already passed the isReloadableTypeName() test. - * + * * @param dottedname type name of the form a.b.c.D * @param initialbytes the first version of the bytes as loaded * @return the ReloadableType or null if it cannot be made reloadable @@ -1053,7 +1080,7 @@ public class TypeRegistry { /** * Sometimes we discover the reloadabletype during program execution, for example A calls B and we haven't yet seen * B. We find B has been loaded by a parent classloader, let's remember B here so we can do fast lookups for it. - * + * * @param typeId the id for the type * @param rtype the ReloadableType to associate with the id */ @@ -1070,7 +1097,7 @@ public class TypeRegistry { /** * Determine the reloadabletype object representation for a specified class. If the caller already knows the ID for * the type, that would be a quicker way to locate the reloadable type object. - * + * * @param slashedClassName the slashed (e.g. java/lang/String) class name * @return the ReloadableType */ @@ -1095,7 +1122,7 @@ public class TypeRegistry { * For a specific classname, this method will search in the current type registry and any parent type registries * (similar to a regular classloader delegation strategy). Returns null if the type is not found. It does not * attempt to load anything in. - * + * * @param classname the type being searched for, e.g. com/foo/Bar * @return the ReloadableType if found, otherwise null */ @@ -1125,7 +1152,7 @@ public class TypeRegistry { * Find the ReloadableType object for a given classname. If the allocateIdIfNotYetLoaded option is set then a new id * will be allocated for this classname if it hasn't previously been seen before. This method does not create new * ReloadableType objects, they are expected to come into existence when defined by the classloader. - * + * * @param slashedClassname the slashed class name (e.g. java/lang/String) * @param allocateIdIfNotYetLoaded if true an id will be allocated because sometime later the type will be loaded * (and made reloadable) @@ -1206,7 +1233,7 @@ public class TypeRegistry { /** * Used to determine if the invokedynamic needs to be intercepted. - * + * * @return null if nothing has been reloaded */ @UsedByGeneratedCode @@ -1223,7 +1250,7 @@ public class TypeRegistry { * Determine if something has changed in a particular type related to a particular descriptor and so the dispatcher * interface should be used. The type registry ID and class ID are merged in the 'ids' parameter. This method is for * INVOKESTATIC rewrites and so performs additional checks because it assumes the target is static. - * + * * @param ids packed representation of the registryId (top 16bits) and typeId (bottom 16bits) * @param nameAndDescriptor the name and descriptor of the method about to be INVOKESTATIC'd * @return null if the original code can run otherwise return the dispatcher to use @@ -1364,7 +1391,7 @@ public class TypeRegistry { /** * See notes.md#001 - * + * * @param instance the object instance on which the INVOKEINTERFACE is being called * @param params the parameters to the INVOKEINTERFACE call * @param instance2 the object instance on which the INVOKEINTERFACE is being called @@ -1470,7 +1497,7 @@ public class TypeRegistry { * changes about the descriptor it is considered an entirely different method (and the old form is a deleted * method). For this reason there is no need to consider 'changed' methods, because the static-ness nor visibility * cannot change. - * + * * @param ids packed representation of the registryId (top 16bits) and typeId (bottom 16bits) * @param nameAndDescriptor the name and descriptor of the method about to be INVOKEINTERFACE'd * @return true if the original method operation must be intercepted @@ -1523,19 +1550,19 @@ public class TypeRegistry { * "can i call what I was going to call, or not?" * The answer to that question primarily depends on whether the method was previously defined in the target hierarchy. If it was then * yes, make the call and let catchers sort it out. If not then we need to jump through firey hoops. - * + * * For example, this code: * public int run1() { XX zz = new ZZ(); return zz.foo(); } - * + * * results in this invokevirtual: - * + * INVOKEVIRTUAL invokevirtual/XX.foo()I - * + * * Notice the static type of the variable is used in the method descriptor for the invoke. - * + * * The rewriter then turns it into this: LDC 65537 LDC foo()I @@ -1554,28 +1581,28 @@ public class TypeRegistry { L5 * * What that says is: call ivicheck for 65537,foo()I (65537 embodies the type registry id and the class ID, XX in our case, as per the descriptor). - * + * * vcheck should return true for methods that do not exist - since we can't run the invokevirtual - * + * * If vcheck returns false, do what you were going to do anyway: * this will actually cause us to jump into a catcher method. * If vcheck returns true, call the __execute() method on the type XX - however, due to virtual dispatch and all the types implementing __execute() we * will end up in the one for the dynamic type (ZZ.__execute()) - * + * * These two paths proceed as follows. - * + * * 1) If we jumped into a catcher method, we actually hit the catcher ZZ.foo() * The catcher works as follows - grab the latest version of this type (if it has been reloaded) and call foo() on the dispatcher, otherwise call super.foo(). * The catcher exists because the type did not originally implement the method. It exists to enable the type to implement the method later. The same sequence * will continue (through catchers) until a type is hit that provides an implementation which did not used to, or an original implementation is hit, or we run out * of options and an NSME is created. The catcher code is below. - * + * * 2) In the ZZ.__execute() method we actually ask the type registry to tell us what to call - we call determineDispatcher which uses the runtime type for the call * and discovers which dispatcher should be used. it is a bit naughty in that if it finds an reloadabletype that is the right answer but that hasn't been reloaded, * it forces a reload of the original code to create a dispatcher instance that can be returned. - * + * * __execute is for methods that were never there at all - * + * METHOD: 0x0001(public) foo()I CODE GETSTATIC invokevirtual/ZZ.r$type Lorg/springsource/loaded/ReloadableType; @@ -1592,14 +1619,14 @@ public class TypeRegistry { ALOAD 0 INVOKESPECIAL invokevirtual/YY.foo()I IRETURN - * - * - * + * + * + * */ /** * Called for a field operation - trying to determine whether a particular field needs special handling. - * + * * @param ids packed representation of the registryId (top 16bits) and typeId (bottom 16bits) * @param name the name of the instance field about to be accessed * @return true if the field operation must be intercepted @@ -1664,7 +1691,7 @@ public class TypeRegistry { /** * Used in code the generated code replaces invokevirtual calls. Determine if the code can run as it was originally * compiled. - * + * * This method will return FALSE if nothing has changed to interfere with the invocation and it should proceed. This * method will return TRUE if something has changed and the caller needs to do something different. * @@ -1758,7 +1785,7 @@ public class TypeRegistry { /** * This method discovers the reloadable type instance for the registry and type id specified. - * + * * @param typeRegistryId the type registry id * @param typeId the type id * @return the ReloadableType (if there is no ReloadableType an exception will be thrown) @@ -1787,6 +1814,7 @@ public class TypeRegistry { return reloadableType; } + @Override public String toString() { StringBuilder s = new StringBuilder(); s.append("TypeRegistry(id="); @@ -1828,7 +1856,8 @@ public class TypeRegistry { log.info("Called to monitor " + rtype.dottedtypename + " from " + externalForm); } - if (!watching.contains(externalForm)) { + boolean watchingContainsIt = watching.contains(externalForm); + if (!watchingContainsIt || externalForm.endsWith(".jar")) { // classFileToType.put(externalForm, rtype.slashedtypename); File f = new File(externalForm); if (fileChangeListener == null) { @@ -1838,8 +1867,10 @@ public class TypeRegistry { fsWatcher = new FileSystemWatcher(fileChangeListener, id, getClassLoaderName()); } fileChangeListener.register(rtype, f); - fsWatcher.register(f); - watching.add(externalForm); + if (!watchingContainsIt) { + fsWatcher.register(f); + watching.add(externalForm); + } } } @@ -1916,7 +1947,7 @@ public class TypeRegistry { /** * Process some type pattern objects from the supplied value. For example the value might be * 'com.foo.Bar,!com.foo.Goo' - * + * * @param value string defining a comma separated list of type patterns * @return list of TypePatterns */ @@ -2010,7 +2041,7 @@ public class TypeRegistry { /** * To avoid leaking permgen we want to periodically discard the child classloader and recreate a new one. We will * need to then redefine types again over time as they are used (the most recent variants of them). - * + * * @param currentlyDefining the reloadable type currently being defined reloaded */ public void checkChildClassLoader(ReloadableType currentlyDefining) { @@ -2101,7 +2132,7 @@ public class TypeRegistry { /** * When an invokedynamic instruction is reached, we allocate an id that recognizes that bsm and the parameters to * that bsm. The index can be used when rewriting that invokedynamic - * + * * @param slashedClassName the slashed class name containing the bootstrap method * @param bsm the bootstrap methods * @param bsmArgs the bootstrap method arguments (asm types) @@ -2148,7 +2179,7 @@ public class TypeRegistry { /** * Called from the static initializer of a reloadabletype, allowing it to connect itself to the parent type, such * that when reloading occurs we can mark all relevant types in the hierarchy as being impacted by the reload. - * + * * @param child the ReloadableType actively being initialized * @param parent the superclass of the reloadable type (may or may not be reloadable!) */ diff --git a/springloaded/src/main/java/org/springsource/loaded/TypeRewriter.java b/springloaded/src/main/java/org/springsource/loaded/TypeRewriter.java index f9c4531..1f03fe2 100644 --- a/springloaded/src/main/java/org/springsource/loaded/TypeRewriter.java +++ b/springloaded/src/main/java/org/springsource/loaded/TypeRewriter.java @@ -27,7 +27,6 @@ import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; import org.springsource.loaded.Utils.ReturnType; @@ -41,7 +40,7 @@ import org.springsource.loaded.Utils.ReturnType; *
  • Creates catchers for inherited methods. Catchers are simply passed through unless a new version of the class * provides an implementation * - * + * * @author Andy Clement * @since 0.5.0 */ @@ -311,7 +310,7 @@ public class TypeRewriter implements Constants { mv.visitJumpInsn(IFNONNULL, l2); mv.visitTypeInsn(NEW, tStaticStateManager); mv.visitInsn(DUP); - mv.visitMethodInsn(INVOKESPECIAL, tStaticStateManager, "", "()V"); + mv.visitMethodInsn(INVOKESPECIAL, tStaticStateManager, "", "()V", false); mv.visitFieldInsn(PUTSTATIC, slashedname, fStaticFieldsName, lStaticStateManager); mv.visitLabel(l2); mv.visitFieldInsn(GETSTATIC, slashedname, fStaticFieldsName, lStaticStateManager); @@ -319,7 +318,7 @@ public class TypeRewriter implements Constants { mv.visitVarInsn(ALOAD, 0); mv.visitVarInsn(ALOAD, 1); mv.visitMethodInsn(INVOKEVIRTUAL, tStaticStateManager, "setValue", "(" + lReloadableType - + "Ljava/lang/Object;Ljava/lang/String;)V"); + + "Ljava/lang/Object;Ljava/lang/String;)V", false); mv.visitInsn(RETURN); mv.visitMaxs(4, 2); mv.visitEnd(); @@ -406,7 +405,7 @@ public class TypeRewriter implements Constants { mv.visitVarInsn(ALOAD, lvInstance); mv.visitVarInsn(ALOAD, lvNewValue); mv.visitVarInsn(ALOAD, lvName); - // setValue(ReloadableType rtype, Object instance, Object value, String name) + // setValue(ReloadableType rtype, Object instance, Object value, String name) mv.visitMethodInsn(INVOKEVIRTUAL, tInstanceStateManager, "setValue", "(" + lReloadableType + "Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/String;)V"); mv.visitInsn(RETURN); @@ -714,13 +713,13 @@ public class TypeRewriter implements Constants { if (MethodMember.isCatcherForInterfaceMethod(method)) { mv.visitTypeInsn(NEW, "java/lang/IllegalStateException"); mv.visitInsn(DUP); - mv.visitMethodInsn(INVOKESPECIAL, "java/lang/AbstractMethodError", "", "()V"); + mv.visitMethodInsn(INVOKESPECIAL, "java/lang/AbstractMethodError", "", "()V", false); mv.visitInsn(ATHROW); } else { mv.visitVarInsn(ALOAD, 0); // load this Utils.createLoadsBasedOnDescriptor(mv, method.getDescriptor(), 1); - mv.visitMethodInsn(INVOKESPECIAL, supertypeName, method.getName(), method.getDescriptor()); + mv.visitMethodInsn(INVOKESPECIAL, supertypeName, method.getName(), method.getDescriptor(), false); Utils.addCorrectReturnInstruction(mv, methodReturnType, false); } @@ -765,7 +764,7 @@ public class TypeRewriter implements Constants { /** * For the fields that need it (protected fields from a non-reloadable supertype), create the getters and * setters so that the executor can read/write them. - * + * */ private void createProtectedFieldGetterSetter(FieldMember field) { String descriptor = field.descriptor; @@ -847,7 +846,7 @@ public class TypeRewriter implements Constants { mv.visitJumpInsn(IFNONNULL, l1); mv.visitTypeInsn(NEW, tStaticStateManager); mv.visitInsn(DUP); - mv.visitMethodInsn(INVOKESPECIAL, tStaticStateManager, "", "()V"); + mv.visitMethodInsn(INVOKESPECIAL, tStaticStateManager, "", "()V", false); mv.visitFieldInsn(PUTSTATIC, slashedname, fStaticFieldsName, lStaticStateManager); mv.visitLabel(l1); } diff --git a/springloaded/src/main/java/org/springsource/loaded/Utils.java b/springloaded/src/main/java/org/springsource/loaded/Utils.java index ad35a55..e2e113c 100644 --- a/springloaded/src/main/java/org/springsource/loaded/Utils.java +++ b/springloaded/src/main/java/org/springsource/loaded/Utils.java @@ -45,12 +45,12 @@ import org.objectweb.asm.tree.FieldNode; import org.springsource.loaded.Utils.ReturnType.Kind; -// TODO debugging tests - how is the experience? rewriting of field accesses will really +// TODO debugging tests - how is the experience? rewriting of field accesses will really // affect field navigation in the debugger /** * Utility functions for use throughout SpringLoaded - * + * * @author Andy Clement * @since 0.5.0 */ @@ -72,7 +72,7 @@ public class Utils implements Opcodes, Constants { /** * Convert a number (base10) to base62 encoded string - * + * * @param number the number to convert * @return the base 62 encoded string */ @@ -90,7 +90,7 @@ public class Utils implements Opcodes, Constants { /** * Decode a base62 encoded string into a number (base10). (More expensive than encoding) - * + * * @param s the string to decode * @return the number */ @@ -104,7 +104,7 @@ public class Utils implements Opcodes, Constants { /** * Depending on the signature of the return type, add the appropriate instructions to the method visitor. - * + * * @param mv where to visit to append the instructions * @param returnType return type descriptor * @param createCast whether to include CHECKCAST instructions for return type values @@ -153,7 +153,7 @@ public class Utils implements Opcodes, Constants { /** * Return the number of parameters in the descriptor. Copes with primitives and arrays and reference types. - * + * * @param methodDescriptor a method descriptor of the form (Ljava/lang/String;I[[Z)I * @return number of parameters in the descriptor */ @@ -183,7 +183,7 @@ public class Utils implements Opcodes, Constants { /** * Create the set of LOAD instructions to load the method parameters. Take into account the size and type. - * + * * @param mv the method visitor to recieve the load instructions * @param descriptor the complete method descriptor (eg. "(ILjava/lang/String;)V") - params and return type are * skipped @@ -310,7 +310,7 @@ public class Utils implements Opcodes, Constants { /** * Return a simple sequence for the descriptor where type references are collapsed to 'O', so * (IILjava/lang/String;Z) will return IIOZ. - * + * * @param descriptor method descriptor, for example (IILjava/lang/String;Z)V * @return sequence where all parameters are represented by a single character - or null if no parameters */ @@ -595,7 +595,7 @@ public class Utils implements Opcodes, Constants { /** * Create a descriptor for some set of parameter types. The descriptor will be of the form "([Ljava/lang/String;)" - * + * * @param params the (possibly null) list of parameters for which to create the descriptor * @return a descriptor or "()" for no parameters */ @@ -614,7 +614,7 @@ public class Utils implements Opcodes, Constants { /** * Given a method descriptor, extract the parameter descriptor and convert into corresponding Class objects. This * requires a reference to a class loader to convert type names into Class objects. - * + * * @param methodDescriptor a method descriptor (e.g (Ljava/lang/String;)I) * @param classLoader a class loader that can be used to lookup types * @return an array for classes representing the types in the method descriptor @@ -633,7 +633,7 @@ public class Utils implements Opcodes, Constants { /** * Convert an asm Type into a corresponding Class object, requires a reference to a ClassLoader to be able to * convert classnames to class objects. - * + * * @param type the asm Type * @param classLoader a class loader that can be used to find types * @return the JVM Class for the type @@ -672,7 +672,7 @@ public class Utils implements Opcodes, Constants { * Construct the method descriptor for a method. For example 'String bar(int)' would return '(I)Ljava/lang/String;'. * If the first parameter is skipped, the leading '(' is also skipped (the caller is expect to build the right * prefix). - * + * * @param method method for which the descriptor should be created * @param ignoreFirstParameter whether to include the first parameter in the output descriptor * @return a method descriptor @@ -744,7 +744,7 @@ public class Utils implements Opcodes, Constants { /** * Create the string representation of an integer and pad it to a particular width using leading zeroes. - * + * * @param value the value to convert to a string * @param width the width (in chars) that the resultant string should be * @return the padded string @@ -761,7 +761,7 @@ public class Utils implements Opcodes, Constants { /** * Access the specified class as a resource accessible through the specified loader and return the bytes. The * classname should be 'dot' separated (eg. com.foo.Bar) and not suffixed .class - * + * * @param loader the classloader against which getResourceAsStream() will be invoked * @param dottedclassname the dot separated classname without .class suffix * @return the byte data defining that class @@ -785,7 +785,7 @@ public class Utils implements Opcodes, Constants { /** * Access the specified class as a resource accessible through the specified loader and return the bytes. The * classname should be 'dot' separated (eg. com.foo.Bar) and not suffixed .class - * + * * @param loader the classloader against which getResourceAsStream() will be invoked * @param slashedclassname the dot separated classname without .class suffix * @return the byte data defining that class @@ -833,8 +833,8 @@ public class Utils implements Opcodes, Constants { /** * Load all the byte data from an input stream. - * - * @param stream thr input stream from which to read + * + * @param stream the input stream from which to read * @return a byte array containing all the data from the stream */ public static byte[] loadBytesFromStream(InputStream stream) { @@ -876,7 +876,7 @@ public class Utils implements Opcodes, Constants { * Generate the name for the executor class. Must use '$' so that it is considered by some code (eclipse debugger * for example) to be an inner type of the original class (thus able to consider itself as being from the same * source file). - * + * * @param name the name prefix for the executor class * @param versionstamp the suffix string for the executor class name * @return an executor class name @@ -892,7 +892,7 @@ public class Utils implements Opcodes, Constants { * Strip the first parameter out of a method descriptor and return the shortened method descriptor. Since primitive * types cannot be reloadable, there is no need to handle that case - it should always be true that the first * parameter will exist and will end with a semi-colon. For example: (Ljava/lang/String;II)V becomes (IIV) - * + * * @param descriptor method descriptor to be shortened * @return new version of input descriptor with first parameter taken out */ @@ -911,7 +911,7 @@ public class Utils implements Opcodes, Constants { /** * Discover the descriptor for the return type. It may be a primitive (so one char) or a reference type (so a/b/c, * with no 'L' or ';') or it may be an array descriptor ([Ljava/lang/String;). - * + * * @param methodDescriptor method descriptor * @return return type descriptor (with any 'L' or ';' trimmed off) */ @@ -952,7 +952,7 @@ public class Utils implements Opcodes, Constants { /** * Descriptor for a reference type has already been stripped of L and ; - * + * * @param descriptor descriptor, either one char for a primitive or slashed name for a reference type or * [La/b/c; for array type * @param kind one of primitive, array or reference @@ -1052,7 +1052,7 @@ public class Utils implements Opcodes, Constants { * Generate the instructions in the specified method visitor to unpack an assumed array (on top of the stack) * according to the specified descriptor. For example, if the descriptor is (I)V then the array contains a single * Integer that must be unloaded from the array then unboxed to an int. - * + * * @param mv the method visitor to receive the unpack instructions * @param toCallDescriptor the descriptor for the method whose parameters describe the array contents * @param arrayVariableIndex index of the array variable @@ -1127,7 +1127,7 @@ public class Utils implements Opcodes, Constants { /** * Dump the specified bytes under the specified name in the filesystem. If the location hasn't been configured then * File.createTempFile() is used to determine where the file will be put. - * + * * @param slashname the slashed class name (e.g. java/lang/String) * @param bytes the bytes to dump * @return the path to the file @@ -1172,7 +1172,7 @@ public class Utils implements Opcodes, Constants { /** * Return the size of a type. The size is usually 1 except for double and long which are of size 2. The descriptor * passed in is the full descriptor, including any leading 'L' and trailing ';'. - * + * * @param typeDescriptor the descriptor for a single type, may be primitive. For example: I, J, Z, * Ljava/lang/String; * @return the size of the descriptor (number of slots it will consume), either 1 or 2 @@ -1192,7 +1192,7 @@ public class Utils implements Opcodes, Constants { /** * Dump some bytes into the specified file. - * + * * @param file full filename for where to dump the stuff (e.g. c:/temp/Foo.class) * @param bytes the bytes to write to the file */ @@ -1226,7 +1226,7 @@ public class Utils implements Opcodes, Constants { /** * Compute the size required for a specific method descriptor. - * + * * @param descriptor a method descriptor, for example (Ljava/lang/String;ZZ)V * @return number of stack/var entries necessary for that descriptor */ @@ -1391,7 +1391,7 @@ public class Utils implements Opcodes, Constants { /** * Load the contents of an input stream. - * + * * @param stream input stream that contains the bytes to load * @return the byte array loaded from the input stream */ @@ -1427,10 +1427,10 @@ public class Utils implements Opcodes, Constants { /** * If the flags indicate it is not public, private or protected, then it is default and make it public. - * + * * Default visibility needs promoting because package visibility is determined by classloader+package, not just * package. - * + * * @param access incoming access modifiers * @return adjusted modifiers */ @@ -1439,9 +1439,9 @@ public class Utils implements Opcodes, Constants { // is default return (access | Modifier.PUBLIC); } - if ((access & Constants.ACC_PROTECTED) != 0) { + if ((access & Opcodes.ACC_PROTECTED) != 0) { // was protected, need to 'publicize' it - return access - Constants.ACC_PROTECTED + Constants.ACC_PUBLIC; + return access - Opcodes.ACC_PROTECTED + Opcodes.ACC_PUBLIC; } // if ((access & Constants.ACC_PRIVATE) != 0) { // // was private, need to 'publicize' it @@ -1455,18 +1455,18 @@ public class Utils implements Opcodes, Constants { // is default return (access | Modifier.PUBLIC); } - if ((access & Constants.ACC_PROTECTED) != 0) { + if ((access & Opcodes.ACC_PROTECTED) != 0) { // was protected, need to 'publicize' it - return access - Constants.ACC_PROTECTED + Constants.ACC_PUBLIC; + return access - Opcodes.ACC_PROTECTED + Opcodes.ACC_PUBLIC; } - if (isEnum && (access & Constants.ACC_PRIVATE) != 0) { + if (isEnum && (access & Opcodes.ACC_PRIVATE) != 0) { // was private, need to 'publicize' it - return access - Constants.ACC_PRIVATE + Constants.ACC_PUBLIC; + return access - Opcodes.ACC_PRIVATE + Opcodes.ACC_PUBLIC; } if ((access & Constants.ACC_PRIVATE_STATIC_SYNTHETIC) == ACC_PRIVATE_STATIC_SYNTHETIC && name.startsWith("lambda")) { // Special case for lambda, may need to generalize for general invokedynamic support - return access - Constants.ACC_PRIVATE + Constants.ACC_PUBLIC; + return access - Opcodes.ACC_PRIVATE + Opcodes.ACC_PUBLIC; } return access; } @@ -1476,9 +1476,9 @@ public class Utils implements Opcodes, Constants { // is default return (access | Modifier.PUBLIC); } - if ((access & Constants.ACC_PROTECTED) != 0) { + if ((access & Opcodes.ACC_PROTECTED) != 0) { // was protected, need to 'publicize' it - return access - Constants.ACC_PROTECTED + Constants.ACC_PUBLIC; + return access - Opcodes.ACC_PROTECTED + Opcodes.ACC_PUBLIC; } // if ((access & Constants.ACC_PRIVATE) != 0) { // // was private, need to 'publicize' it @@ -1489,7 +1489,7 @@ public class Utils implements Opcodes, Constants { /** * Utility method similar to Java 1.6 Arrays.copyOf, used instead of that method to stick to Java 1.5 only API. - * + * * @param the type of the array entries * @param array the array to copy * @param newSize the size of the new array @@ -1504,7 +1504,7 @@ public class Utils implements Opcodes, Constants { /** * Modify visibility to be public. - * + * * @param access existing access * @return modified access, adjusted to public non-final */ @@ -1556,7 +1556,7 @@ public class Utils implements Opcodes, Constants { * Convert a value to the requested descriptor. For null values where the caller needs a primitive, this returns the * appropriate (boxed) default. This method will not attempt conversion, it is basically checking what to do if the * result is null - and ensuring the caller gets back what they expect (the appropriate primitive default). - * + * * @param value the value * @param desc the type the caller would like it to be * @return the converted value or possibly a default value for the type if the incoming value is null @@ -1598,7 +1598,7 @@ public class Utils implements Opcodes, Constants { * Check that the value we have discovered is of the right type. It may not be if the field has changed type during * a reload. When this happens we will default the value for the new field and forget the one we were holding onto. * note: array forms are not compatible (e.g. int[] and Integer[]) - * + * * @param registry the type registry that can be quizzed for type information * @param result the result we have discovered and are about to return - this is never null * @param expectedTypeDescriptor the type we are looking for (will be primitive or Ljava/lang/String style) @@ -1654,7 +1654,7 @@ public class Utils implements Opcodes, Constants { /* * Determine if the type specified in lookingFor is a supertype (class/interface) of the specified typedescriptor, i.e. can an * object of type 'candidate' be assigned to a variable of typ 'lookingFor'. - * + * * @return true if it is a supertype */ public static boolean isAssignableFrom(String lookingFor, TypeDescriptor candidate) { @@ -1680,7 +1680,7 @@ public class Utils implements Opcodes, Constants { /* * Produce the bytecode that will collapse the stack entries into an array - the descriptor describes what is being packed. - * + * * @param mv the method visitor to receive the instructions to package the data * @param desc the descriptor for the method that shows (through its parameters) the contents of the stack */ @@ -1750,7 +1750,7 @@ public class Utils implements Opcodes, Constants { /** * Looks at the supplied descriptor and inserts enough pops to remove all parameters. Should be used when about to * avoid a method call. - * + * * @param mv the method visitor to append instructions to * @param desc the method descriptor for the parameter sequence (e.g. (Ljava/lang/String;IZZ)V) * @return number of parameters popped @@ -1834,7 +1834,7 @@ public class Utils implements Opcodes, Constants { /** * Determine the interfaces implemented by a given class (supplied as bytes) - * + * * @param classbytes the classfile bytes * @return array of interface names (slashed descriptors) */ @@ -1855,34 +1855,43 @@ public class Utils implements Opcodes, Constants { public String[] interfaces; + @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { this.interfaces = interfaces; } + @Override public void visitSource(String source, String debug) { } + @Override public void visitOuterClass(String owner, String name, String desc) { } + @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { return null; } + @Override public void visitAttribute(Attribute attr) { } + @Override public void visitInnerClass(String name, String outerName, String innerName, int access) { } + @Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { return null; } + @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { return null; } + @Override public void visitEnd() { } diff --git a/springloaded/src/main/java/org/springsource/loaded/agent/ClassPreProcessorAgentAdapter.java b/springloaded/src/main/java/org/springsource/loaded/agent/ClassPreProcessorAgentAdapter.java index 6305d90..eb55544 100644 --- a/springloaded/src/main/java/org/springsource/loaded/agent/ClassPreProcessorAgentAdapter.java +++ b/springloaded/src/main/java/org/springsource/loaded/agent/ClassPreProcessorAgentAdapter.java @@ -29,7 +29,7 @@ import org.springsource.loaded.TypeRegistry; /** * Class pre-processor. - * + * * @author Andy Clement * @since 0.5.0 */ diff --git a/springloaded/src/main/java/org/springsource/loaded/agent/JVMPlugin.java b/springloaded/src/main/java/org/springsource/loaded/agent/JVMPlugin.java index 1965faf..1975da6 100644 --- a/springloaded/src/main/java/org/springsource/loaded/agent/JVMPlugin.java +++ b/springloaded/src/main/java/org/springsource/loaded/agent/JVMPlugin.java @@ -19,13 +19,11 @@ package org.springsource.loaded.agent; import java.beans.BeanInfo; import java.beans.Introspector; import java.lang.ref.Reference; -import java.lang.ref.ReferenceQueue; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.security.ProtectionDomain; import java.util.Collection; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.springsource.loaded.GlobalConfiguration; @@ -37,7 +35,7 @@ import org.springsource.loaded.ReloadEventProcessorPlugin; * Reloading plugin for 'poking' JVM classes that are known to cache reflective state. Some of the behaviour is switched * ON based on which classes are loaded. For example the Introspector clearing logic is only activated if the * Introspector gets loaded. - * + * * @author Andy Clement * @since 0.7.3 */ @@ -109,7 +107,7 @@ public class JVMPlugin implements ReloadEventProcessorPlugin, LoadtimeInstrument // /** queue for WeakReferences to field reflectors keys */ // private static final ReferenceQueue> reflectorsQueue = // new ReferenceQueue<>(); - // } + // } } @@ -129,7 +127,7 @@ public class JVMPlugin implements ReloadEventProcessorPlugin, LoadtimeInstrument beanInfoCacheCleared = clearThreadGroupContext(clazz); } - // GRAILS-9505 - had to introduce the flushFromCaches(). The appcontext we seem to be able to + // GRAILS-9505 - had to introduce the flushFromCaches(). The appcontext we seem to be able to // access from AppContext.getAppContext() isn't the same one the Introspector will be using // so we can fail to clean up the cache. Strangely calling getAppContexts() and clearing them // all (the code commented out below) doesn't fetch all the contexts. I'm sure it is a nuance of @@ -157,7 +155,7 @@ public class JVMPlugin implements ReloadEventProcessorPlugin, LoadtimeInstrument // if (map != null) { // if (GlobalConfiguration.debugplugins) { // System.err.println("JVMPlugin: clearing out BeanInfo for " + clazz.getName()); - // } + // } // map.remove(clazz); // } // } diff --git a/springloaded/src/main/java/org/springsource/loaded/agent/ReloadableFileChangeListener.java b/springloaded/src/main/java/org/springsource/loaded/agent/ReloadableFileChangeListener.java index 169d614..ee3abe8 100644 --- a/springloaded/src/main/java/org/springsource/loaded/agent/ReloadableFileChangeListener.java +++ b/springloaded/src/main/java/org/springsource/loaded/agent/ReloadableFileChangeListener.java @@ -17,10 +17,16 @@ package org.springsource.loaded.agent; import java.io.File; +import java.io.IOException; +import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; import org.springsource.loaded.FileChangeListener; import org.springsource.loaded.GlobalConfiguration; @@ -29,7 +35,7 @@ import org.springsource.loaded.TypeRegistry; /** - * + * * @author Andy Clement * @since 0.5.0 */ @@ -41,6 +47,23 @@ public class ReloadableFileChangeListener implements FileChangeListener { private Map correspondingReloadableTypes = new HashMap(); + Map> watchedJarContents = new HashMap>(); + + static class JarEntry { + + final ReloadableType rtype; + + final String slashname; + + long lmt; + + public JarEntry(ReloadableType rtype, String slashname, long lmt) { + this.rtype = rtype; + this.slashname = slashname; + this.lmt = lmt; + } + } + public ReloadableFileChangeListener(TypeRegistry typeRegistry) { this.typeRegistry = typeRegistry; } @@ -50,11 +73,65 @@ public class ReloadableFileChangeListener implements FileChangeListener { log.info(" processing change for " + file); } ReloadableType rtype = correspondingReloadableTypes.get(file); - typeRegistry.loadNewVersion(rtype, file); + if (file.getName().endsWith(".jar")) { + if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.INFO)) { + log.info(" processing change for JAR " + file); + } + try { + ZipFile zf = new ZipFile(file); + Set entriesBeingWatched = watchedJarContents.get(file); + for (JarEntry entryBeingWatched : entriesBeingWatched) { + ZipEntry ze = zf.getEntry(entryBeingWatched.slashname); + long lmt = ze.getLastModifiedTime().toMillis(); + if (lmt > entryBeingWatched.lmt) { + // entry in jar has been updated + if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.INFO)) { + log.info(" detected update to jar entry. jar=" + file.getName() + " class=" + + entryBeingWatched.slashname + " OLD LMT=" + new Date(entryBeingWatched.lmt) + + " NEW LMT=" + new Date(lmt)); + } + typeRegistry.loadNewVersion(entryBeingWatched.rtype, lmt, zf.getInputStream(ze)); + entryBeingWatched.lmt = lmt; + } + } + zf.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + } + else { + typeRegistry.loadNewVersion(rtype, file); + } } public void register(ReloadableType rtype, File file) { - correspondingReloadableTypes.put(file, rtype); + if (file.getName().endsWith(".jar")) { + // Compute the last mod time of the entry in the jar + try { + ZipFile zf = new ZipFile(file); + String slashname = rtype.getSlashedName() + ".class"; + ZipEntry ze = zf.getEntry(slashname); + long lmt = ze.getLastModifiedTime().toMillis(); + JarEntry je = new JarEntry(rtype, slashname, lmt); + zf.close(); + Set jarEntries = watchedJarContents.get(file); + if (jarEntries == null) { + jarEntries = new HashSet(); + watchedJarContents.put(file, jarEntries); + } + jarEntries.add(je); + if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.INFO)) { + log.info(" watching jar file entry. Jar=" + file + " file=" + rtype.getSlashedName() + " lmt=" + + lmt); + } + } + catch (IOException e) { + e.printStackTrace(); + } + } + else { + correspondingReloadableTypes.put(file, rtype); + } } - } diff --git a/springloaded/src/main/java/org/springsource/loaded/agent/SpringLoadedPreProcessor.java b/springloaded/src/main/java/org/springsource/loaded/agent/SpringLoadedPreProcessor.java index cab6a97..a9f7b6e 100644 --- a/springloaded/src/main/java/org/springsource/loaded/agent/SpringLoadedPreProcessor.java +++ b/springloaded/src/main/java/org/springsource/loaded/agent/SpringLoadedPreProcessor.java @@ -606,11 +606,23 @@ public class SpringLoadedPreProcessor implements Constants { // great! nothing to do } else if (file.getName().endsWith(".jar")) { - if (GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.WARNING)) { + boolean found = false; + if (GlobalConfiguration.jarsToWatch != null) { + // Check if it is one to watch + String candidate = file.getName(); + for (String jarToWatch : GlobalConfiguration.jarsToWatch) { + if (candidate.equals(jarToWatch)) { + found = true; + break; + } + } + } + if (!found && GlobalConfiguration.isRuntimeLogging && log.isLoggable(Level.WARNING)) { log.warning("unable to watch this jar file entry: " + slashedClassName.replace('/', '.') + ". Computed location=" + file.toString()); } - return null; + if (!found) + return null; } else if (file.toString().equals("/groovy/script") || file.toString().equals("\\groovy\\script")) { // nothing to do, compiled/loaded by a GroovyClassLoader$InnerLoader - there is nothing to watch. If the type is to be diff --git a/springloaded/src/test/java/org/springsource/loaded/ri/test/FieldGetAnnotationTest.java b/springloaded/src/test/java/org/springsource/loaded/ri/test/FieldGetAnnotationTest.java index bd1cc04..4f7701a 100644 --- a/springloaded/src/test/java/org/springsource/loaded/ri/test/FieldGetAnnotationTest.java +++ b/springloaded/src/test/java/org/springsource/loaded/ri/test/FieldGetAnnotationTest.java @@ -21,7 +21,6 @@ import static org.springsource.loaded.test.SpringLoadedTests.runOnInstance; import java.lang.annotation.Annotation; import java.lang.reflect.Field; -import java.util.Arrays; import org.junit.runner.RunWith; import org.springsource.loaded.ri.ReflectiveInterceptor; @@ -34,12 +33,12 @@ import org.springsource.loaded.testgen.RejectedChoice; /** * Tests the following methods: - * + * * Field.getAnnotation Field.isAnnotationPresent - * + * * It is convenient to test both of these here, since they have the kinds of argument types, which means generation of * test parameters is the same. - * + * * @author kdvolder */ @RunWith(ExploreAllChoicesRunner.class) diff --git a/springloaded/src/test/java/org/springsource/loaded/test/FileSystemWatcherTests.java b/springloaded/src/test/java/org/springsource/loaded/test/FileSystemWatcherTests.java index 47532e3..60c43a6 100644 --- a/springloaded/src/test/java/org/springsource/loaded/test/FileSystemWatcherTests.java +++ b/springloaded/src/test/java/org/springsource/loaded/test/FileSystemWatcherTests.java @@ -17,11 +17,14 @@ package org.springsource.loaded.test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import org.junit.Assert; @@ -74,8 +77,38 @@ public class FileSystemWatcherTests { pause(3000); watcher.shutdown(); System.out.println(listener.changesDetected); - assertEquals("abc.txt", listener.changesDetected.get(0)); - assertEquals("abcd.txt", listener.changesDetected.get(1)); + assertContains(listener.changesDetected, "abc.txt"); + assertContains(listener.changesDetected, "abcd.txt"); + } + + @Test + public void jars() throws IOException { + TestFileChangeListener listener = new TestFileChangeListener(); + File dir = getTempDir(); + FileSystemWatcher watcher = new FileSystemWatcher(listener, -1, "test"); + pause(1000); + File j1 = create(dir, "foo.jar"); + watcher.register(j1); + pause(1100); + File j2 = create(dir, "bar.jar"); + watcher.register(j2); + pause(1100); + watcher.setPaused(true); + touch(j2); + watcher.setPaused(false); + pause(3000); + watcher.shutdown(); + assertTrue(listener.changesDetected.size() != 0); + assertContains(listener.changesDetected, "bar.jar"); + } + + private void assertContains(Collection cs, String element) { + for (String s : cs) { + if (s.equals(element)) { + return; + } + } + fail("Did not find '" + element + "' in collection: " + cs); } @Ignore diff --git a/springloaded/src/test/java/org/springsource/loaded/test/ReloadingJVM.java b/springloaded/src/test/java/org/springsource/loaded/test/ReloadingJVM.java index a6fdfad..d5bc3bb 100644 --- a/springloaded/src/test/java/org/springsource/loaded/test/ReloadingJVM.java +++ b/springloaded/src/test/java/org/springsource/loaded/test/ReloadingJVM.java @@ -20,7 +20,13 @@ import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; +import java.nio.file.attribute.FileTime; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; import org.springsource.loaded.Utils; @@ -138,7 +144,7 @@ public class ReloadingJVM { return captureOutput(message); } - private final static boolean DEBUG_CLIENT_SIDE = true; + private final static boolean DEBUG_CLIENT_SIDE = false; private JVMOutput sendAndReceive(String message) { try { @@ -165,6 +171,7 @@ public class ReloadingJVM { this.stderr = stderr; } + @Override public String toString() { StringBuilder s = new StringBuilder("==STDOUT==\n").append(stdout).append("\n").append("==STDERR==\n").append( stderr) @@ -242,11 +249,16 @@ public class ReloadingJVM { return sendAndReceive("echo " + string); } + public JVMOutput run(String classname) { + return run(classname, true); + } + /** * Call the static main() method on the specified class. */ - public JVMOutput run(String classname) { - copyToTestdataDirectory(classname); + public JVMOutput run(String classname, boolean copy) { + if (copy) + copyToTestdataDirectory(classname); return sendAndReceive("run " + classname); } @@ -268,6 +280,65 @@ public class ReloadingJVM { Utils.write(new File(testdataDirectory, classfile), data); } + /** + * + * @param fromJarName + * @param toJarName + * @param touchList list of jar entries that should be 'touched' to give them a current last mod time + */ + public String copyJarToTestdataDirectory(String fromJarName, String toJarName, String... touchList) { + if (DEBUG_CLIENT_SIDE) { + System.out.println("(client) copying jar to test data directory: " + fromJarName + " as: " + toJarName); + } + File f = new File("../testdata/jars", fromJarName); + // byte[] data = Utils.load(f); + // Ensure directories exist + int lastSlash = toJarName.lastIndexOf("/"); + if (lastSlash != -1) { + new File(testdataDirectory, toJarName.substring(0, lastSlash)).mkdirs(); + } + byte[] buffer = new byte[2048]; + File target = new File(testdataDirectory, toJarName); + try { + ZipInputStream zis = new ZipInputStream(new FileInputStream(f)); + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(target)); + ZipEntry ze; + long newtime = System.currentTimeMillis(); + while ((ze = zis.getNextEntry()) != null) { + ZipEntry newZipEntry = new ZipEntry(ze.getName()); + boolean found = false; + for (String touchEntry : touchList) { + if (touchEntry.equals(ze.getName())) { + found = true; + break; + } + } + // System.out.println("LMT for " + ze.getName() + " is " + new Date(ze.getLastModifiedTime().toMillis())); + if (!found) { + newZipEntry.setLastModifiedTime(ze.getLastModifiedTime()); + } + else { + newZipEntry.setLastModifiedTime(FileTime.fromMillis(newtime)); + } + // System.out.println("LMT for " + ze.getName() + " is updated to " + // + new Date(newZipEntry.getLastModifiedTime().toMillis())); + zos.putNextEntry(newZipEntry); // problem with the size being set already on ze? + int len = 0; + while ((len = zis.read(buffer)) > 0) { + zos.write(buffer, 0, len); + } + } + zos.close(); + zis.close(); + } + catch (IOException e) { + e.printStackTrace(); + } + // + // Utils.write(target, data); + return target.getAbsolutePath(); + } + public void copyResourceToTestDataDirectory(String resourcename) { if (DEBUG_CLIENT_SIDE) { System.out.println("(client) copying resource to test data directory: " + resourcename); @@ -303,7 +374,13 @@ public class ReloadingJVM { } public JVMOutput newInstance(String instanceName, String classname) { - copyToTestdataDirectory(classname); + return newInstance(instanceName, classname, true); + } + + public JVMOutput newInstance(String instanceName, String classname, boolean copy) { + if (copy) { + copyToTestdataDirectory(classname); + } return sendAndReceive("new " + instanceName + " " + classname); } @@ -334,4 +411,8 @@ public class ReloadingJVM { Utils.write(new File(testdataDirectory, classfile), newdata); } + public JVMOutput extendCp(String path) { + return sendAndReceive("extendcp " + path); + } + } diff --git a/springloaded/src/test/java/org/springsource/loaded/test/ReloadingJVMCommandProcess.java b/springloaded/src/test/java/org/springsource/loaded/test/ReloadingJVMCommandProcess.java index f41a0b7..5b3cc6f 100644 --- a/springloaded/src/test/java/org/springsource/loaded/test/ReloadingJVMCommandProcess.java +++ b/springloaded/src/test/java/org/springsource/loaded/test/ReloadingJVMCommandProcess.java @@ -17,8 +17,12 @@ package org.springsource.loaded.test; import java.io.DataInputStream; +import java.io.File; import java.io.IOException; import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -32,13 +36,33 @@ import org.springsource.loaded.TypeRegistry; /** * When a ReloadingJVM is launched, this is the program it runs. It can be driven by commands and instructed to do * things. - * + * * @author Andy Clement * @since 0.7.3 */ public class ReloadingJVMCommandProcess { + static class ExtensibleClassLoader extends URLClassLoader { + + public ExtensibleClassLoader(ClassLoader parent) { + super(new URL[0], parent); + } + + public void addJar(String jar) { + try { + addURL(new File(jar).toURI().toURL()); + } + catch (MalformedURLException e) { + e.printStackTrace(); + } + } + + } + + static ExtensibleClassLoader cl; + public static void main(String[] argv) throws IOException { + cl = new ExtensibleClassLoader(ReloadingJVMCommandProcess.class.getClassLoader()); System.err.println("ReloadingJVM:started"); System.err.flush(); try { @@ -74,6 +98,9 @@ public class ReloadingJVMCommandProcess { else if (commandName.equals("reload")) { reloadCommand(arguments.get(0), arguments.size() == 1 ? null : arguments.get(1)); } + else if (commandName.equals("extendcp")) { + extendClasspath(arguments.get(0)); + } else { System.out.println("Don't understand command '" + commandName + "' !!"); } @@ -157,6 +184,12 @@ public class ReloadingJVMCommandProcess { } } + private static void extendClasspath(String newClasspathEntry) { + System.err.println("Extending classpath to include: " + newClasspathEntry); + cl.addJar(newClasspathEntry); + System.err.println("!!"); + } + private static void reloadCommand(String classname, String data) { try { Class clazz = Class.forName(classname); @@ -187,7 +220,7 @@ public class ReloadingJVMCommandProcess { private static void newCommand(String instanceName, String classname) { try { System.err.println("(jvm) creating new instance '" + instanceName + "' of type '" + classname + "'"); - Class clazz = Class.forName(classname); + Class clazz = Class.forName(classname, true, cl); instances.put(instanceName, clazz.newInstance()); System.err.println("(jvm) instance successfully created!!"); } diff --git a/springloaded/src/test/java/org/springsource/loaded/test/SpringLoadedTestsInSeparateJVM.java b/springloaded/src/test/java/org/springsource/loaded/test/SpringLoadedTestsInSeparateJVM.java index 821c7c1..41f7c34 100644 --- a/springloaded/src/test/java/org/springsource/loaded/test/SpringLoadedTestsInSeparateJVM.java +++ b/springloaded/src/test/java/org/springsource/loaded/test/SpringLoadedTestsInSeparateJVM.java @@ -16,6 +16,8 @@ package org.springsource.loaded.test; +import static org.junit.Assert.assertEquals; + import org.junit.After; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -26,7 +28,7 @@ import org.springsource.loaded.test.ReloadingJVM.JVMOutput; /** * These tests use a harness that forks a JVM with the agent attached, closely simulating a real environment. The forked * process is running a special class that can be sent commands. - * + * * @author Andy Clement */ public class SpringLoadedTestsInSeparateJVM extends SpringLoadedTests { @@ -44,6 +46,7 @@ public class SpringLoadedTestsInSeparateJVM extends SpringLoadedTests { jvm.shutdown(); } + @Override @After public void teardown() throws Exception { super.teardown(); @@ -186,6 +189,46 @@ public class SpringLoadedTestsInSeparateJVM extends SpringLoadedTests { assertStdoutContains("second", jvm.call("b", "run")); } + @Test + public void testReloadingJarsInOtherVM() throws Exception { + jvm.shutdown(); + jvm = ReloadingJVM.launch("watchJars=foo.jar", false); // ;verbose=true;logging=true + String path = jvm.copyJarToTestdataDirectory("one/foo.jar", "foo.jar"); + JVMOutput output = jvm.extendCp(path); + jvm.newInstance("a", "Foo", false); + JVMOutput jo = jvm.call("a", "run"); + assertEquals("m() running", jo.stdout.trim()); + String s = jvm.copyJarToTestdataDirectory("two/foo.jar", "foo.jar", "Foo.class"); + pause(2); + jo = jvm.call("a", "run"); + String stdout = jo.stdout.trim(); + if (stdout.startsWith("Reloading:")) { + stdout = stdout.substring(stdout.indexOf("\n") + 1); + } + assertEquals("n() running", stdout); + } + + + @Test + public void testReloadingJarsInOtherVM_packages() throws Exception { + jvm.shutdown(); + jvm = ReloadingJVM.launch("watchJars=foo.jar:bar.jar", false);//;verbose=true;logging=true + String path = jvm.copyJarToTestdataDirectory("one/bar.jar", "bar.jar"); + jvm.extendCp(path); + jvm.newInstance("a", "test.Bar", false); + JVMOutput jo = jvm.call("a", "run"); + assertEquals("Wibble.foo() running, version 1", jo.stdout.trim()); + jvm.copyJarToTestdataDirectory("two/bar.jar", "bar.jar", "test/Wibble.class"); + pause(2); + jo = jvm.call("a", "run"); + String stdout = jo.stdout.trim(); + if (stdout.startsWith("Reloading:")) { + System.out.println("retpos = " + stdout.indexOf("\n")); + stdout = stdout.substring(stdout.indexOf("\n") + 1); + } + assertEquals("Wibble.foo() running, version 2", stdout); + } + // TODO tidyup test data area after each test? // TODO flush/replace classloader in forked VM to clear it out after each test? diff --git a/testdata/jars/README b/testdata/jars/README new file mode 100644 index 0000000..1b25bd8 --- /dev/null +++ b/testdata/jars/README @@ -0,0 +1 @@ +For the watching/updating from jars feature, these are the jar testdata diff --git a/testdata/jars/one/Bar.java b/testdata/jars/one/Bar.java new file mode 100644 index 0000000..6cd98d0 --- /dev/null +++ b/testdata/jars/one/Bar.java @@ -0,0 +1,6 @@ +package test; +public class Bar { + public static void run() { + new Wibble().foo(); + } +} diff --git a/testdata/jars/one/Foo.class b/testdata/jars/one/Foo.class new file mode 100644 index 0000000000000000000000000000000000000000..9b272c231a6f862a9431140d3fe9a34acbb0843a GIT binary patch literal 445 zcmZvY%}T>S6ot=i(o7Sht*!O{r!Iunh26NTxDmQ4Dpa~p>rhgfiNr+kv2>y0!Uyo7 z#FGe$;tbrG`R1ItckcVg>l=V$?0HbAd2q1oqK-xdD;}CyRajG47dR)O3DZ-7)$WW0 z_E~(b1=T@l^m!JIb#fJq=Ok8=8G%FsZy0CEMEAoSSNd^$Gz;zm9~HPh7U3#v_}Ijj zU?FOETJ)LFOcl0$>|mEK(ITkjrtUm2)9%H1rYGqyG`zn}HFH^U#*5~l5QlO1G6_vO zOcNbMJwf9iEKoPOXwGVz?Jr}Ig)WxS>x{6^W+*lXu)rU{W%PL3;VN9qhtH4)ZukR6 zr+`A4|1E8i=fi$M=|}0IL}qc?RI(Lx<_{kf9B?0D)$J#g|5_YYp(tLWxP^L8>OF($PV85x;7D~c|vStw6kS>$67|C_t%>%8@<3oV0Z{#jABQRkV`JtM4{_?XnbDw0L5;@;AY#Tavl1-(+i#t@QD_ zxASE2mxRyHOC%Ncc6@DIu2=pjkH2q$t^AMDu;p!R(M_|zwx-7(lsxu|<1@o+xeu?s zyLX>eS#!Ame}FeTM?&)Ll$*dHkYZ$D2*4RIfX0&Vwm zXD{5*nccOa#N$Op?b#6 z%6sK)cPp#wD_IL#blF37u3dCl-KNT(Tau%5`-M?Rf2+u^mW6MADz}|lx$7a%*@YAS z*!U;Ui*+q@T2ixGq*-{Cjr3>Z#DK?9N0!L?EUtWV$nEU0uI|t^hHq*-uRQs=S7&mRri)cFumr!F|S|N-NJx|C3tD>Cg6$qc&oTO0ieMGjoyGA-Tb6Q`YWH z-@v}>f|y1acgC9D{MUJb%wOH7+)1h^ZnBew|*dM@9W#>3-t+Y`c7{Uv{ zq0Go6!hlGC$T0v)fT#d7IR<#6>PI#k6i)~s3A7C()&jg)*+5E|fUq7&*E4~50BKDq Ad;kCd literal 0 HcmV?d00001 diff --git a/testdata/jars/one/build.sh b/testdata/jars/one/build.sh new file mode 100755 index 0000000..6d335f0 --- /dev/null +++ b/testdata/jars/one/build.sh @@ -0,0 +1,4 @@ +javac -d . *.java +jar -cvMf foo.jar Foo.class +jar -cvMf bar.jar test/*.class + diff --git a/testdata/jars/one/foo.jar b/testdata/jars/one/foo.jar new file mode 100644 index 0000000000000000000000000000000000000000..9f315bf288cbd83109589cb468f5aed6fdb886f1 GIT binary patch literal 440 zcmWIWW@Zs#;Nak3DD7VA#()Gk8CV$H^7HkQa}tY-|D9rB$erNpf5<_g?f$~0-r0gI zAEl2rdwxvNzh@BVV_}*Vba?NBHl2mh!O_C?)8wvHd|>~vxaab$2kvtY*Vz0%_vX#M z`aSy?Eso7&Zd}I9t!);Saec$91{NtmZZ=o9QTvy3eN)rS??^wkA%JIlE~030K~2-Bvk`zT(k0)O3!XEb zzHYs;N}=rnvs@?J_kX#vU*h}|mEWz0e&6=_6zewWv!l9(f5@ZwWnSrt(@tr~3EcXh z(6mEym1X|zk4^H*#>Xx@*0&x_z~t`=bck8aCU4L;4B%Qzt|`(_3O& zGxKNDsqL2>m&#-vNJ*_-Xs2PBV1FoMrvHk6xf?6E52nog%q2BHz?+@JJbmLr9YzL* oy+9n`&B!FefCy-0IZ!~O0+e76@MdKLNizbWHIPmR8ppr@0Lc-l_5c6? literal 0 HcmV?d00001 diff --git a/testdata/jars/one/test/Bar.class b/testdata/jars/one/test/Bar.class new file mode 100644 index 0000000000000000000000000000000000000000..92852b194fd9e0c0bfc61080f28f35d1b9d9def7 GIT binary patch literal 282 zcmYL^y-veG5QJyXzt|=}gqDH|NP!zllmetwQBnj2QJ%wrE=p|4*t{1_B7wvM@KA`k z6k&_mz0rJocR#w8Awn!j8}ruWkG3F?Vc@KIZ!st6xn#c9 zJd+8?7|)lcaRSbyqy~EO~puhh+YtNm3~1z?cimu zlsHL65!}Pf>^sjpyUfSu+dF_0?AWMb*+LVoI#z77(cxI-=rYt!rIO~1q3n7ihRV6V z6AaCvRN^vAB9Y#NQA|qtUTX$B&{;Yb7c!R`w=$yM(KMWg4mj!@YYx`2!Law;B9rU& z(o89-CWpPbNM};3-Z96fgDq^+5F=*BU*HU_e3l=FYT{o-Q!zGwq~LL8L_&Yj88y1Y z0+!mpPNgz|Nky0p7&`yR4E!O7Vnsf8VJu!D7yWGDjnGGxmXx0o2uGcwMYmnbjB@qh p73_%$+5*K|A?9e~2i-5Mfc1=0yZnO6FK$x{s1RBui$iq@OJAduRBHeL literal 0 HcmV?d00001 diff --git a/testdata/jars/two/Bar.java b/testdata/jars/two/Bar.java new file mode 100644 index 0000000..6cd98d0 --- /dev/null +++ b/testdata/jars/two/Bar.java @@ -0,0 +1,6 @@ +package test; +public class Bar { + public static void run() { + new Wibble().foo(); + } +} diff --git a/testdata/jars/two/Foo.class b/testdata/jars/two/Foo.class new file mode 100644 index 0000000000000000000000000000000000000000..6e0f9f3f96950f2c926ae95f8028b470ed7c920d GIT binary patch literal 445 zcmZvYPfx-y7{;Hnv9dV@!TBG-g9&(W7jGCZCY*?xV7L!#NC|XiZYF*#J*e^E2k=7~ zpUp(0u}$B$Pk&GQ_I>|&eFJcemIsNN2M4Pz>S$E3<{`kk#D>JCfODdaPEQ4_PIn|= zpT*ZopgPb-oo9=&O0J^uoQjoXMnG_Q!#GPOs;_go(vRb#S#%fqsKE8H3|C^y$2N8Z zmQ1JHrq5_ImDu&s#2#;=MWB|O!g*w-;l+5SCh0F}cz>I!1=%>`K`IPO_yf3%9(PU7f^+%s8RCHp{(#Xb yL_y5|M%&c$X1}2HLwYDtv)FBtECrqU#Rr8BxR0>v_7lo~Ee^AggjWccQ2hk7UrXr# literal 0 HcmV?d00001 diff --git a/testdata/jars/two/Foo.java b/testdata/jars/two/Foo.java new file mode 100644 index 0000000..f4660f7 --- /dev/null +++ b/testdata/jars/two/Foo.java @@ -0,0 +1,8 @@ +public class Foo { + public static void run() { + n(); + } + public static void n() { + System.out.println("n() running"); + } +} diff --git a/testdata/jars/two/Wibble.java b/testdata/jars/two/Wibble.java new file mode 100644 index 0000000..7168407 --- /dev/null +++ b/testdata/jars/two/Wibble.java @@ -0,0 +1,6 @@ +package test; +public class Wibble { + public void foo() { + System.out.println("Wibble.foo() running, version 2"); + } +} diff --git a/testdata/jars/two/bar.jar b/testdata/jars/two/bar.jar new file mode 100644 index 0000000000000000000000000000000000000000..1e0cfe5ed2312c54b055d42280268c7aa567f9d0 GIT binary patch literal 788 zcmWIWW@Zs#;Nak32xwjE#()I)7+4reQj1IUof3=ml5-M^i~pTsU~ui*n|H`Tpe6mU z{B1_2OwAQS8jsq90tGs`R9yu+KIQbi(Al7R{9N_L1{NXy3GGfdiy5QO+^aL+TwZ_u zGV1|>^L8>OF($PV85x;7D~c|vStw6kS>$67|C_t%>%8@<3oV0Z{#jABQRkV`JtM4{_?XnbDw0L5;@;AY#Tavl1-(+i#t@QD_ zxASE2mxRyHOC%Ncc6@DIu2=pjkH2q$t^AMDu;p!R(M_|zwx-7(lsxu|<1@o+xeu?s zyLX>eS#!Ame}FeTM?&)Ll$*dHkYZ$D2*4RIfX0&Vwm zXD{5*nccOa#N$Op?b#6 z%6sK)cPp#wD_IL#blF37u3dCl-KNT(Tau%5`-M?Rf2+u^mW6MADz}|lx$7a%*@YAS z*!U;Ui*+q@T2ixGq*-{Cjr3>Z#DK?9N0!L?EUtWV$nEU0uI|t^hHq*-uRQs=S7&mRri)cFumr!F|S|N-NJx|C3tD>Cg6$qc&oTO0ieMGjoyGA-Tb6Q`YWH z-@v}>f|y1acgC9D{MUJb%wOH7+)1h^ZnBew|*dM@9W#@=|x!YJB7{Uv{ zq0Go6!hlGC$T0v)fT#d7IR<#6>PI#k6i)~s3A7C()&jg)*+5E|fUq7&*E4~502Xv8 Az5oCK literal 0 HcmV?d00001 diff --git a/testdata/jars/two/build.sh b/testdata/jars/two/build.sh new file mode 100755 index 0000000..6d335f0 --- /dev/null +++ b/testdata/jars/two/build.sh @@ -0,0 +1,4 @@ +javac -d . *.java +jar -cvMf foo.jar Foo.class +jar -cvMf bar.jar test/*.class + diff --git a/testdata/jars/two/foo.jar b/testdata/jars/two/foo.jar new file mode 100644 index 0000000000000000000000000000000000000000..5da03fe380d486aef36bfb6e3fda97cb07d39bbe GIT binary patch literal 440 zcmWIWW@Zs#;Nak32xwjE#()Gk8CV$H^7HkQa}tY-|D9rB$erNpf5<_g?f$~0-r0gI zAEl2rd%kwmyJryRVyY9>JLBt<0;Dx`&u{+e zp%(rzD?r~oC{1H4(;K+=prXbq&(fyOZ~002h-r|kd$ literal 0 HcmV?d00001 diff --git a/testdata/jars/two/test/Bar.class b/testdata/jars/two/test/Bar.class new file mode 100644 index 0000000000000000000000000000000000000000..92852b194fd9e0c0bfc61080f28f35d1b9d9def7 GIT binary patch literal 282 zcmYL^y-veG5QJyXzt|=}gqDH|NP!zllmetwQBnj2QJ%wrE=p|4*t{1_B7wvM@KA`k z6k&_mz0rJocR#w8Awn!j8}ruWkG3F?Vc@KIZ!st6xn#c9 zJd+8?7|)lcaRSbyqy~EO~puhh+YtNm3~1z?cimu zlsHL65!}Pf>^sjpyUfSu+dF_0?AWMb*+LVoI#z77(cxI-=rYt!rIO~1q3n7ihRV6V z6AaCvRN^vAB9Y#NQA|qtUTX$B&{;Yb7c!R`w=$yM(KMWg4mj!@YYx`2!Law;B9rU& z(o89-CWpPbNM};3-Z96fgDq^+5F=*BU*HU_e3l=FYT{o-Q!zGwq~LL8L_&Yj88y1Y z0+!mpPNgz|Nky0p7&`yR4E!O7Vnsf8VJu!D7yWGDjnGGxmXx0o2uGcwMYmnbjB@qh p73_%$+5*K|A?9e~2i-5Mfc1=0yZnO6FK$x{s1RBui$iq@OJAg$RBQkM literal 0 HcmV?d00001