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 0000000..9b272c2
Binary files /dev/null and b/testdata/jars/one/Foo.class differ
diff --git a/testdata/jars/one/Foo.java b/testdata/jars/one/Foo.java
new file mode 100644
index 0000000..15b23fb
--- /dev/null
+++ b/testdata/jars/one/Foo.java
@@ -0,0 +1,8 @@
+public class Foo {
+ public static void run() {
+ m();
+ }
+ public static void m() {
+ System.out.println("m() running");
+ }
+}
diff --git a/testdata/jars/one/Wibble.java b/testdata/jars/one/Wibble.java
new file mode 100644
index 0000000..03ff73c
--- /dev/null
+++ b/testdata/jars/one/Wibble.java
@@ -0,0 +1,6 @@
+package test;
+public class Wibble {
+ public void foo() {
+ System.out.println("Wibble.foo() running, version 1");
+ }
+}
diff --git a/testdata/jars/one/bar.jar b/testdata/jars/one/bar.jar
new file mode 100644
index 0000000..a0dd07a
Binary files /dev/null and b/testdata/jars/one/bar.jar differ
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 0000000..9f315bf
Binary files /dev/null and b/testdata/jars/one/foo.jar differ
diff --git a/testdata/jars/one/test/Bar.class b/testdata/jars/one/test/Bar.class
new file mode 100644
index 0000000..92852b1
Binary files /dev/null and b/testdata/jars/one/test/Bar.class differ
diff --git a/testdata/jars/one/test/Wibble.class b/testdata/jars/one/test/Wibble.class
new file mode 100644
index 0000000..f9cfa7e
Binary files /dev/null and b/testdata/jars/one/test/Wibble.class differ
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 0000000..6e0f9f3
Binary files /dev/null and b/testdata/jars/two/Foo.class differ
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 0000000..1e0cfe5
Binary files /dev/null and b/testdata/jars/two/bar.jar differ
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 0000000..5da03fe
Binary files /dev/null and b/testdata/jars/two/foo.jar differ
diff --git a/testdata/jars/two/test/Bar.class b/testdata/jars/two/test/Bar.class
new file mode 100644
index 0000000..92852b1
Binary files /dev/null and b/testdata/jars/two/test/Bar.class differ
diff --git a/testdata/jars/two/test/Wibble.class b/testdata/jars/two/test/Wibble.class
new file mode 100644
index 0000000..9d7e4eb
Binary files /dev/null and b/testdata/jars/two/test/Wibble.class differ