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.
This commit is contained in:
Andy Clement
2015-05-05 17:50:57 -07:00
parent d313c506ca
commit 56d0c00822
32 changed files with 503 additions and 126 deletions

View File

@@ -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));

View File

@@ -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!)
*/

View File

@@ -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;
* <li>Creates catchers for inherited methods. Catchers are simply passed through unless a new version of the class
* provides an implementation
* </ul>
*
*
* @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, "<init>", "()V");
mv.visitMethodInsn(INVOKESPECIAL, tStaticStateManager, "<init>", "()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", "<init>", "()V");
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/AbstractMethodError", "<init>", "()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, "<init>", "()V");
mv.visitMethodInsn(INVOKESPECIAL, tStaticStateManager, "<init>", "()V", false);
mv.visitFieldInsn(PUTSTATIC, slashedname, fStaticFieldsName, lStaticStateManager);
mv.visitLabel(l1);
}

View File

@@ -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 <T> 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() {
}

View File

@@ -29,7 +29,7 @@ import org.springsource.loaded.TypeRegistry;
/**
* Class pre-processor.
*
*
* @author Andy Clement
* @since 0.5.0
*/

View File

@@ -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<Class<?>> 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);
// }
// }

View File

@@ -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<File, ReloadableType> correspondingReloadableTypes = new HashMap<File, ReloadableType>();
Map<File, Set<JarEntry>> watchedJarContents = new HashMap<File, Set<JarEntry>>();
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<JarEntry> 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<JarEntry> jarEntries = watchedJarContents.get(file);
if (jarEntries == null) {
jarEntries = new HashSet<JarEntry>();
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);
}
}
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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<String> cs, String element) {
for (String s : cs) {
if (s.equals(element)) {
return;
}
}
fail("Did not find '" + element + "' in collection: " + cs);
}
@Ignore

View File

@@ -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);
}
}

View File

@@ -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!!");
}

View File

@@ -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?

1
testdata/jars/README vendored Normal file
View File

@@ -0,0 +1 @@
For the watching/updating from jars feature, these are the jar testdata

6
testdata/jars/one/Bar.java vendored Normal file
View File

@@ -0,0 +1,6 @@
package test;
public class Bar {
public static void run() {
new Wibble().foo();
}
}

BIN
testdata/jars/one/Foo.class vendored Normal file

Binary file not shown.

8
testdata/jars/one/Foo.java vendored Normal file
View File

@@ -0,0 +1,8 @@
public class Foo {
public static void run() {
m();
}
public static void m() {
System.out.println("m() running");
}
}

6
testdata/jars/one/Wibble.java vendored Normal file
View File

@@ -0,0 +1,6 @@
package test;
public class Wibble {
public void foo() {
System.out.println("Wibble.foo() running, version 1");
}
}

BIN
testdata/jars/one/bar.jar vendored Normal file

Binary file not shown.

4
testdata/jars/one/build.sh vendored Executable file
View File

@@ -0,0 +1,4 @@
javac -d . *.java
jar -cvMf foo.jar Foo.class
jar -cvMf bar.jar test/*.class

BIN
testdata/jars/one/foo.jar vendored Normal file

Binary file not shown.

BIN
testdata/jars/one/test/Bar.class vendored Normal file

Binary file not shown.

BIN
testdata/jars/one/test/Wibble.class vendored Normal file

Binary file not shown.

6
testdata/jars/two/Bar.java vendored Normal file
View File

@@ -0,0 +1,6 @@
package test;
public class Bar {
public static void run() {
new Wibble().foo();
}
}

BIN
testdata/jars/two/Foo.class vendored Normal file

Binary file not shown.

8
testdata/jars/two/Foo.java vendored Normal file
View File

@@ -0,0 +1,8 @@
public class Foo {
public static void run() {
n();
}
public static void n() {
System.out.println("n() running");
}
}

6
testdata/jars/two/Wibble.java vendored Normal file
View File

@@ -0,0 +1,6 @@
package test;
public class Wibble {
public void foo() {
System.out.println("Wibble.foo() running, version 2");
}
}

BIN
testdata/jars/two/bar.jar vendored Normal file

Binary file not shown.

4
testdata/jars/two/build.sh vendored Executable file
View File

@@ -0,0 +1,4 @@
javac -d . *.java
jar -cvMf foo.jar Foo.class
jar -cvMf bar.jar test/*.class

BIN
testdata/jars/two/foo.jar vendored Normal file

Binary file not shown.

BIN
testdata/jars/two/test/Bar.class vendored Normal file

Binary file not shown.

BIN
testdata/jars/two/test/Wibble.class vendored Normal file

Binary file not shown.