diff --git a/springloaded/src/main/java/org/springsource/loaded/ReloadableType.java b/springloaded/src/main/java/org/springsource/loaded/ReloadableType.java index c3deced..7fc1690 100644 --- a/springloaded/src/main/java/org/springsource/loaded/ReloadableType.java +++ b/springloaded/src/main/java/org/springsource/loaded/ReloadableType.java @@ -44,15 +44,15 @@ import org.springsource.loaded.ri.JavaMethodCache; /** * Represents a type that has been processed such that it can be reloaded at runtime. - * + * * @author Andy Clement * @since 0.5.0 */ public class ReloadableType { // TODO when a field is shadowed or renamed and the old one never accessed again, it may be holding onto something and prevent it from GC. - // Thinking about a solution that involves a tag in the FieldAccessor object so that we can - // check whether a 'repair' is needed on a field accessor (because the type has been reloaded and + // Thinking about a solution that involves a tag in the FieldAccessor object so that we can + // check whether a 'repair' is needed on a field accessor (because the type has been reloaded and // the map in the accessor hasnt been repaired yet) private static Logger log = Logger.getLogger(ReloadableType.class.getName()); @@ -140,13 +140,14 @@ public class ReloadableType { return clazz; } + @Override public String toString() { return dottedtypename; } /** * Construct a new ReloadableType with the specified name and the specified initial bytecode. - * + * * @param dottedtypename the dotted name * @param initialBytes the bytecode for the initial version * @param id for this reloadable type, allocated by the registry @@ -190,7 +191,7 @@ public class ReloadableType { /** * Gets the 'orignal' method corresponding to given name and method descriptor. This only considers methods that * exist in the first (non-reloaded) version of the type. - * + * * @param name method name * @param descriptor method descriptor (e.g (Ljava/lang/String;)I) * @return the MethodMember or an exception if not found @@ -221,7 +222,7 @@ public class ReloadableType { * This method will attempt to apply any pre-existing transforms to the provided bytecode, if it is thought to be * necessary. Currently 'necessary' is determined by finding ourselves running under tcServer and Spring Insight * being turned on. - * + * * @param bytes the new bytes to be possibly transformed. * @return either the original bytes or a transformed set of bytes */ @@ -300,7 +301,7 @@ public class ReloadableType { /** * Load a new version of this type, using the specified suffix to tag the newly generated artifact class names. - * + * * @param versionsuffix the String suffix to append to classnames being created for the reloaded class * @param newbytedata the class bytes for the new version of this class * @return true if the reload succeeded @@ -328,16 +329,17 @@ public class ReloadableType { // Not allowed to change the type reload = false; - s = new StringBuilder("Spring Loaded: Cannot reload new version of ").append(this.dottedtypename).append( - "\n"); + s = new StringBuilder("Spring Loaded: Cannot reload new version of ").append( + this.dottedtypename).append( + "\n"); if (td.hasTypeAccessChanged()) { - s.append(" Reason: Type modifiers changed\n"); + s.append(" Reason: Type modifiers changed from=0x" + Integer.toHexString(td.oAccess) + " to=0x" + + Integer.toHexString(td.nAccess) + "\n"); cantReload = true; } if (td.hasTypeSupertypeChanged()) { s.append(" Reason: Supertype changed from ").append(td.oSuperName).append(" to ").append( - td.nSuperName) - .append("\n"); + td.nSuperName).append("\n"); cantReload = true; } if (td.hasTypeInterfacesChanged()) { @@ -370,8 +372,8 @@ public class ReloadableType { } } if (!justGroovyObjectMoved) { - s.append(" Reason: Interfaces changed from ").append(td.oInterfaces).append(" to ") - .append(td.nInterfaces).append("\n"); + s.append(" Reason: Interfaces changed from ").append(td.oInterfaces).append(" to ").append( + td.nInterfaces).append("\n"); cantReload = true; } } @@ -535,17 +537,19 @@ public class ReloadableType { /** * Go through proxies we know about in this registry and see if any of them are for the type we have just reloaded. * If they are, regenerate them and reload them. - * + * * @param versionsuffix the suffix to use when reloading the proxies (it matches what is being used to reload the * type) */ private void reloadProxiesIfNecessary(String versionsuffix) { ReloadableType proxy = typeRegistry.cglibProxies.get(this.slashedtypename); if (proxy != null) { - Object[] strategyAndGeneratorPair = CglibPluginCapturing.clazzToGeneratorStrategyAndClassGeneratorMap.get(getClazz()); + Object[] strategyAndGeneratorPair = CglibPluginCapturing.clazzToGeneratorStrategyAndClassGeneratorMap.get( + getClazz()); if (strategyAndGeneratorPair == null) { if (log.isLoggable(Level.SEVERE)) { - log.severe("Unable to find regeneration methods for cglib proxies - proxies will be out of date for this type"); + log.severe( + "Unable to find regeneration methods for cglib proxies - proxies will be out of date for this type"); } return; } @@ -572,9 +576,10 @@ public class ReloadableType { proxy = typeRegistry.cglibProxiesFastClass.get(this.slashedtypename); if (proxy != null) { - Object[] strategyAndFCGeneratorPair = CglibPluginCapturing.clazzToGeneratorStrategyAndFastClassGeneratorMap - .get(getClazz()); - strategyAndFCGeneratorPair = CglibPluginCapturing.clazzToGeneratorStrategyAndFastClassGeneratorMap.get(getClazz()); + Object[] strategyAndFCGeneratorPair = CglibPluginCapturing.clazzToGeneratorStrategyAndFastClassGeneratorMap.get( + getClazz()); + strategyAndFCGeneratorPair = CglibPluginCapturing.clazzToGeneratorStrategyAndFastClassGeneratorMap.get( + getClazz()); // System.out.println("need to reload fastclass " + proxy + " os=" + os); if (strategyAndFCGeneratorPair != null) { Object a = strategyAndFCGeneratorPair[0]; @@ -617,8 +622,8 @@ public class ReloadableType { } } catch (Throwable t) { - new RuntimeException("Unexpected problem trying to reload proxy for interface " + this.dottedtypename, t) - .printStackTrace(); + new RuntimeException("Unexpected problem trying to reload proxy for interface " + this.dottedtypename, + t).printStackTrace(); } } @@ -744,7 +749,7 @@ public class ReloadableType { Class class_ClassInfo = typeRegistry.getClass_ClassInfo(); Field field_globalClassSet = class_ClassInfo.getDeclaredField("globalClassSet"); field_globalClassSet.setAccessible(true); - Object/*ClassInfoSet*/instance_classInfoSet = field_globalClassSet.get(null); + Object/*ClassInfoSet*/ instance_classInfoSet = field_globalClassSet.get(null); Method method_ClassInfoSetRemove = instance_classInfoSet.getClass().getMethod("remove", Object.class); Object retval = method_ClassInfoSetRemove.invoke(instance_classInfoSet, this.clazz); @@ -853,7 +858,7 @@ public class ReloadableType { /** * Gets the method corresponding to given name and descriptor, taking into consideration changes that have happened * by reloading. - * + * * @param name the member name * @param descriptor the member descriptor (e.g. (Ljava/lang/String;)I) * @return the MethodMember for that name and descriptor. Null if not found on a live version, or an exception if @@ -870,7 +875,7 @@ public class ReloadableType { /** * Gets the method corresponding to given name and descriptor, from the original type descriptor. - * + * * @param nameAndDescriptor the method name and descriptor (e.g. foo(Ljava/lang/String;)I) * @return the MethodMember for the name and descriptor if it exists, otherwise null */ @@ -973,7 +978,7 @@ public class ReloadableType { RewriteClassAdaptor rca = (RewriteClassAdaptor) cv; if (rca.isEnum && rca.fieldcount > GlobalConfiguration.enumLimit) { // that is too many fields, marking this as not reloadable - // TODO ... + // TODO ... } TypeRewriter.RewriteClassAdaptor a = (TypeRewriter.RewriteClassAdaptor) rca.getClassVisitor(); return ((ClassWriter) a.getClassVisitor()).toByteArray(); @@ -1028,7 +1033,7 @@ public class ReloadableType { /** * Check if the specified method is different to the original form from the type as loaded. - * + * * @param methodId the ID of the method currently executing * @return 0 if the method cannot have changed. 1 if the method has changed. 2 if the method has been deleted in a * new version. @@ -1116,7 +1121,7 @@ public class ReloadableType { /** * Intended to handle dynamic dispatch. This will determine the right type to handle the specified method and return * a dispatcher that can handle it. - * + * * @param instance the target instance for the invocation * @param nameAndDescriptor an encoded method name and descriptor, e.g. foo(Ljava/langString;)V * @return a dispatcher that can handle the method indicated @@ -1255,7 +1260,7 @@ public class ReloadableType { * the non-reloadable types. This method also avoids interfaces because it is looking for instance fields. This is * slightly naughty but if we assume the code we are reloading is valid code, it should never be referring to * interface fields. - * + * * @param name the name of the field to locate * @return the FieldMember or null if the field is not found */ @@ -1283,7 +1288,7 @@ public class ReloadableType { * Search for a static field from this type upwards, as far as the topmost reloadable types. This is searching for a * field, it is not checking the result. It is up to the caller to check they have not ended up with an instance * field and throw the appropriate exception. - * + * * @param name the name of the field to look for * @return a FieldMember for the named field or null if not found */ @@ -1320,7 +1325,7 @@ public class ReloadableType { /** * Attempt to set the value of a field on an instance to the specified value. - * + * * @param instance the object upon which to set the field (maybe null for static fields) * @param fieldname the name of the field * @param isStatic whether the field is static @@ -1349,7 +1354,8 @@ public class ReloadableType { } } - private Set> liveInstances = Collections.synchronizedSet(new HashSet>()); + private Set> liveInstances = Collections.synchronizedSet( + new HashSet>()); private ReferenceQueue liveInstancesRQ = new ReferenceQueue(); @@ -1361,7 +1367,7 @@ public class ReloadableType { /** * Attempt to set the value of a field on an instance to the specified value. Simply locate the field, which returns * an object capable of reading/writing it, then use that to retrieve the value. - * + * * @param instance the object upon which to set the field (maybe null for static fields) * @param fieldname the name of the field * @param isStatic whether the field is static or not @@ -1607,7 +1613,7 @@ public class ReloadableType { * Return the ReloadableType representing the superclass of this type. If the supertype is not reloadable, this * method will return null. The ReloadableType that is returned may not be within the same type registry, if the * supertype was loaded by a different classloader. - * + * * @return the ReloadableType for the supertype or null if it is not reloadable */ public ReloadableType getSuperRtype() { diff --git a/springloaded/src/main/java/org/springsource/loaded/support/Java8.java b/springloaded/src/main/java/org/springsource/loaded/support/Java8.java index 0f4d652..b127d09 100644 --- a/springloaded/src/main/java/org/springsource/loaded/support/Java8.java +++ b/springloaded/src/main/java/org/springsource/loaded/support/Java8.java @@ -26,11 +26,14 @@ import java.lang.reflect.Method; import org.objectweb.asm.Handle; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; +import org.springsource.loaded.CurrentLiveVersion; +import org.springsource.loaded.MethodMember; import org.springsource.loaded.ReloadableType; +import org.springsource.loaded.TypeRegistry; /** * This class encapsulates dependencies on Java 8 APIs (e.g. LambdaMetafactory). - * + * * @author Andy Clement * @since 1.2 */ @@ -38,30 +41,30 @@ public class Java8 { /** * Notes: - * + * * Useful to have an example of how this code behaves. Here is a bit of code: - * + * * class basic.LambdaA { interface Foo { int m(); } static int run() { Foo f = null; f = () -> 77; return f.m(); } } - * + * * Here is a bootstrap method entry in the constant pool: - * + * * 0: #31 invokestatic java/lang/invoke/LambdaMetafactory.metafactory: * (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/ * lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType; * Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite; Method arguments: #32 * ()I #33 invokestatic basic/LambdaA.lambda$run$0:()I #32 ()I - * + * * At the invokedynamic site: bsmId = 0 nameAndDescriptor = m()Lbasic/LambdaA$Foo; - * + * * When invoking the metafactory bootstrap method the first two parameters are stacked by the VM automatically, * namely the MethodHandles$Lookup instance (caller) and the first String (invokedName). What the VM actually sees * is this: - * + * * metaFactory parameters: 0:MethodHandles$Lookup caller = basic.LambdaA 1:String invokedName = "m" 2:MethodType * invokedType = "()Foo" 3:MethodType samMethodType = "()int" 4:MethodHandle implMethod = (actually a * DirectMethodHandle where memberName is "basic.LambdaA.lambda$run$0()int/invokeStatic") 5:MethodType * instantiatedMethodType = "()int" - * + * * With all that information then the calls in this case are relatively straightforward: CallSite callsite = * LambdaMetafactory.metafactory(caller, invokedName, invokedType, samMethodType, implMethod, * instantiatedMethodType); callsite.dynamicInvoker().invokeWithArguments((Object[])null); @@ -70,7 +73,7 @@ public class Java8 { /** * Programmatic emulation of INVOKEDYNAMIC so initialize the callsite via use of the bootstrap method then invoke * the result. - * + * * @param executorClass the executor that will contain the lambda function, null if not yet reloaded * @param handle bootstrap method handle * @param bsmArgs bootstrap method arguments @@ -104,9 +107,9 @@ public class Java8 { MethodType invokedType = MethodType.fromMethodDescriptorString( indyNameAndDescriptor.substring(descriptorStart), callerLoader); - // Use bsmArgs to build the parameters + // Use bsmArgs to build the parameters MethodType samMethodType = MethodType.fromMethodDescriptorString( - (String) (((Type) bsmArgs[0]).getDescriptor()), callerLoader); + (((Type) bsmArgs[0]).getDescriptor()), callerLoader); Handle bsmArgsHandle = (Handle) bsmArgs[1]; String owner = bsmArgsHandle.getOwner(); @@ -121,7 +124,7 @@ public class Java8 { implMethod = caller.findStatic(caller.lookupClass(), name, implMethodType); break; case Opcodes.H_INVOKESPECIAL: - // If there is an executor, the lambda function is actually modified from 'private instance' to 'public static' so adjust lookup. The method + // If there is an executor, the lambda function is actually modified from 'private instance' to 'public static' so adjust lookup. The method // will be static with a new leading parameter. if (executorClass == null) { // TODO is final parameter here correct? @@ -133,15 +136,56 @@ public class Java8 { } break; case Opcodes.H_INVOKEVIRTUAL: - // If there is an executor, the lambda function is actually modified from 'private instance' to 'public static' so adjust lookup. The method - // will be static with a new leading parameter. - if (executorClass == null) { - // TODO when can this scenario occur? Aren't we only here if reloading has happened? - implMethod = caller.findVirtual(caller.lookupClass(), name, implMethodType); + // There is a possibility to 'shortcut' here. Basically we are trying to resolve a callsite reference + // to the method that satisfies it. The easiest option is to just find the method on the originally + // loaded version of the target class and return that. A more optimal shortcut could return the + // method on the executor class if the target has been reloaded (effectively bypassing the method + // on the originally loaded version since we know that it will be acting as a pass through). But this + // opens up a can of worms related to visibility. The executor is loaded into the child classloader, + // and if the caller has not been reloaded it will not be able to 'see' the executor (since it is in + // a child classloader). So, basically keep this dumb (but reliable) for now. + + TypeRegistry typeRegistry = rtype.getTypeRegistry(); + ReloadableType ownerRType = typeRegistry.getReloadableType(owner); + if (null == ownerRType || !ownerRType.hasBeenReloaded()) { + // target containing the reference/lambdaMethod has not been reloaded, no need to get over + // complicated. + Class ownerClazz = ownerRType.getClazz(); + implMethod = caller.findVirtual(ownerClazz, name, implMethodType); } else { - implMethod = caller.findStatic(caller.lookupClass(), name, MethodType.fromMethodDescriptorString( - "(L" + owner + ";" + descriptor.substring(1), callerLoader)); + MethodMember targetReferenceMethodMember = ownerRType.getCurrentMethod(name, descriptor); + String targetReferenceDescriptor = targetReferenceMethodMember.getDescriptor(); + MethodType targetReferenceMethodType = MethodType.fromMethodDescriptorString( + targetReferenceDescriptor, callerLoader); + Class targetReferenceClass = ownerRType.getClazz(); + MethodMember currentMethod = ownerRType.getCurrentMethod(name, descriptor); + + if (currentMethod.original == null) { + // null means this method did not exist on the original version of the target. + // Assert that the caller must have been reloaded, otherwise how would it + // have a reference to something that did not exist on the first version of the type. In that + // case we know we can return the method on the executor class because both the reloaded + // caller and reloaded target are in the same child classloader (no visibility problem). + if (!rtype.hasBeenReloaded()) { + throw new IllegalStateException( + "Assertion violated: When a method added on reload is being referenced" + + "in target type '" + ownerRType.getName() + + "', expected the caller to also have been reloaded: '" + + rtype.getName() + "'"); + } + CurrentLiveVersion ownerLiveVersion = ownerRType.getLiveVersion(); + Class ownerExecutorClass = ownerLiveVersion.getExecutorClass(); + Method executorMethod = ownerLiveVersion.getExecutorMethod(currentMethod); + String methodDescriptor = Type.getType(executorMethod).getDescriptor(); + MethodType type = MethodType.fromMethodDescriptorString(methodDescriptor, callerLoader); + implMethod = caller.findStatic(ownerExecutorClass, name, type); + } + else { + // This finds the reference method on the originally loaded class. It will pass through + // to the actual code on the reloaded version. + implMethod = caller.findVirtual(targetReferenceClass, name, targetReferenceMethodType); + } } break; case Opcodes.H_INVOKEINTERFACE: @@ -150,7 +194,7 @@ public class Java8 { // TODO Should there not be a more direct way to this than classloading? // TODO What about when this is a method added to the interface on a reload? It won't really exist, should we point // to the executor? or something else? (maybe just directly the real method that will satisfy the interface - if it can be worked out) - Class interfaceClass = callerLoader.loadClass(interfaceOwner.replace('/', '.')); // interface type, eg StreamB$Foo + Class interfaceClass = callerLoader.loadClass(interfaceOwner.replace('/', '.')); // interface type, eg StreamB$Foo implMethod = caller.findVirtual(interfaceClass, name, implMethodType); break; default: @@ -158,16 +202,17 @@ public class Java8 { } MethodType instantiatedMethodType = MethodType.fromMethodDescriptorString( - (String) (((Type) bsmArgs[2]).getDescriptor()), callerLoader); + (((Type) bsmArgs[2]).getDescriptor()), callerLoader); return LambdaMetafactory.metafactory(caller, invokedName, invokedType, samMethodType, implMethod, instantiatedMethodType); + } /** * The metafactory we are enhancing is responsible for generating the anonymous classes that will call the lambda * methods in our type - * + * * @param bytes the class bytes for the InnerClassLambdaMetaFactory that is going to be modified * @return the class bytes for the modified InnerClassLambdaMetaFactory */ diff --git a/springloaded/src/test/java/org/springsource/loaded/test/Java8Tests.java b/springloaded/src/test/java/org/springsource/loaded/test/Java8Tests.java index 8874fc1..d6815d0 100644 --- a/springloaded/src/test/java/org/springsource/loaded/test/Java8Tests.java +++ b/springloaded/src/test/java/org/springsource/loaded/test/Java8Tests.java @@ -27,7 +27,7 @@ import org.springsource.loaded.test.infra.Result; /** * Test reloading of Java 8. - * + * * @author Andy Clement * @since 1.2 */ @@ -294,6 +294,116 @@ public class Java8Tests extends SpringLoadedTests { assertEquals("fooab", r.returnValue); } + // https://github.com/spring-projects/spring-loaded/issues/87 + @Test + public void lambdaMethodReference() throws Exception { + String t = "basic.LambdaM"; + TypeRegistry typeRegistry = getTypeRegistry("basic..*"); + + byte[] sc = loadBytesForClass(t); + ReloadableType rtype = typeRegistry.addType(t, sc); + + Class simpleClass = rtype.getClazz(); + Result r = runUnguarded(simpleClass, "run"); + + r = runUnguarded(simpleClass, "run"); + assertEquals("{5=test3}", r.returnValue); + + rtype.loadNewVersion("2", retrieveRename(t, t + "2"));//, t + "2$Foo:" + t + "$Foo")); + + r = runUnguarded(simpleClass, "run"); + assertEquals("{10=test3}", r.returnValue); + + } + + // https://github.com/spring-projects/spring-loaded/issues/87 + // This variant reloads both pieces + @Test + public void lambdaMethodReferenceInAnotherClass() throws Exception { + TypeRegistry typeRegistry = getTypeRegistry("basic..*"); + + byte[] sc = loadBytesForClass("basic.LambdaN"); + ReloadableType rtype = typeRegistry.addType("basic.LambdaN", sc); + byte[] helperBytes = loadBytesForClass("basic.HelperN"); + ReloadableType htype = typeRegistry.addType("basic.HelperN", helperBytes); + + Class simpleClass = rtype.getClazz(); + Result r = runUnguarded(simpleClass, "run"); + + r = runUnguarded(simpleClass, "run"); + assertEquals("{15=test3}", r.returnValue); + + rtype.loadNewVersion(sc); + htype.loadNewVersion(helperBytes); + + r = runUnguarded(simpleClass, "run"); + assertEquals("{15=test3}", r.returnValue); + } + + // This variant reloads only the caller + @Test + public void lambdaMethodReferenceInAnotherClass2() throws Exception { + TypeRegistry typeRegistry = getTypeRegistry("basic..*"); + + byte[] sc = loadBytesForClass("basic.LambdaN"); + ReloadableType rtype = typeRegistry.addType("basic.LambdaN", sc); + byte[] helperBytes = loadBytesForClass("basic.HelperN"); + typeRegistry.addType("basic.HelperN", helperBytes); + + Class simpleClass = rtype.getClazz(); + Result r = runUnguarded(simpleClass, "run"); + + r = runUnguarded(simpleClass, "run"); + assertEquals("{15=test3}", r.returnValue); + + rtype.loadNewVersion(sc); + // htype.loadNewVersion(helperBytes); // don't reload the helper + + r = runUnguarded(simpleClass, "run"); + assertEquals("{15=test3}", r.returnValue); + } + + // This variant reloads only the helper (target) + @Test + public void lambdaMethodReferenceInAnotherClass3() throws Exception { + TypeRegistry typeRegistry = getTypeRegistry("basic..*"); + + byte[] sc = loadBytesForClass("basic.LambdaN"); + ReloadableType rtype = typeRegistry.addType("basic.LambdaN", sc); + byte[] helperBytes = loadBytesForClass("basic.HelperN"); + ReloadableType htype = typeRegistry.addType("basic.HelperN", helperBytes); + + Class simpleClass = rtype.getClazz(); + Result r = runUnguarded(simpleClass, "run"); + + r = runUnguarded(simpleClass, "run"); + assertEquals("{15=test3}", r.returnValue); + + // rtype.loadNewVersion(sc); + htype.loadNewVersion(helperBytes); + + // try { + r = runUnguarded(simpleClass, "run"); + assertEquals("{15=test3}", r.returnValue); + // fail("did not expect that to work"); + // } + // catch (Exception e) { + // e.printStackTrace(); + // // Caused by: java.lang.NoClassDefFoundError: basic/HelperN$$E2 + // // at basic.LambdaN$$Lambda$8/2085857771.apply(Unknown Source) + // // at java.util.stream.Collectors.lambda$toMap$172(Collectors.java:1320) + // // at java.util.stream.Collectors$$Lambda$5/1521118594.accept(Unknown Source) + // // That happens because LambdaN, which has not been reloaded, is loaded by classloader X, the computed + // // method to satisfy the lambda is in the executor for the helper, which is in a child classloader - that + // // is not visible from the one that loaded LambdaN. + // // However, part of the resolution process in the Java8 handling forces LambdaN to reload, so next + // // time we go in, the class can be seen because LambdaN$$E2 is in the same classloader. That is + // // why when we repeat what we just did, it'll work + // } + // r = runUnguarded(simpleClass, "run"); + // assertEquals("{15=test3}", r.returnValue); + } + @Test public void streamWithLambda() throws Exception { String t = "basic.StreamA"; 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 010abe7..eff1c5f 100644 --- a/springloaded/src/test/java/org/springsource/loaded/test/ReloadingJVM.java +++ b/springloaded/src/test/java/org/springsource/loaded/test/ReloadingJVM.java @@ -113,7 +113,8 @@ public class ReloadingJVM { "/bin/java -noverify -javaagent:" + agentJarLocation + " -cp " + javaclasspath + " " + AGENT_OPTION_STRING + " " + OPTS + " " - + ReloadingJVMCommandProcess.class.getName(), new String[] { OPTS }); + + ReloadingJVMCommandProcess.class.getName(), + new String[] { OPTS }); writer = new DataOutputStream(process.getOutputStream()); reader = new DataInputStream(process.getInputStream()); readerErrors = new DataInputStream(process.getErrorStream()); @@ -172,9 +173,9 @@ public class ReloadingJVM { @Override public String toString() { - StringBuilder s = new StringBuilder("==STDOUT==\n").append(stdout).append("\n").append("==STDERR==\n").append( - stderr) - .append("\n==========\n"); + StringBuilder s = new StringBuilder("==STDOUT==\n").append(stdout).append("\n").append( + "==STDERR==\n").append( + stderr).append("\n==========\n"); return s.toString(); } } @@ -261,22 +262,27 @@ public class ReloadingJVM { return sendAndReceive("run " + classname); } - public void copyToTestdataDirectory(String classname) { - if (DEBUG_CLIENT_SIDE) { - System.out.println("(client) copying class to test data directory: " + classname); + public void copyToTestdataDirectory(String... classnames) { + for (String classname : classnames) { + if (DEBUG_CLIENT_SIDE) { + System.out.println("(client) copying class to test data directory: " + classname); + } + String classfile = classname.replaceAll("\\.", File.separator) + ".class"; + File f = new File("../testdata/bin", classfile); + if (!f.exists()) { + f = new File("../testdata-groovy/bin", classfile); + } + if (!f.exists()) { + f = new File("../testdata-java8/bin", classfile); + } + byte[] data = Utils.load(f); + // Ensure directories exist + int dotPos = classname.lastIndexOf("."); + if (dotPos != -1) { + new File(testdataDirectory, classname.substring(0, dotPos).replaceAll("\\.", File.separator)).mkdirs(); + } + Utils.write(new File(testdataDirectory, classfile), data); } - String classfile = classname.replaceAll("\\.", File.separator) + ".class"; - File f = new File("../testdata/bin", classfile); - if (!f.exists()) { - f = new File("../testdata-groovy/bin", classfile); - } - byte[] data = Utils.load(f); - // Ensure directories exist - int dotPos = classname.lastIndexOf("."); - if (dotPos != -1) { - new File(testdataDirectory, classname.substring(0, dotPos).replaceAll("\\.", File.separator)).mkdirs(); - } - Utils.write(new File(testdataDirectory, classfile), data); } /** diff --git a/springloaded/src/test/java/org/springsource/loaded/test/SpringLoadedTests.java b/springloaded/src/test/java/org/springsource/loaded/test/SpringLoadedTests.java index 450fed3..fcceb75 100644 --- a/springloaded/src/test/java/org/springsource/loaded/test/SpringLoadedTests.java +++ b/springloaded/src/test/java/org/springsource/loaded/test/SpringLoadedTests.java @@ -76,7 +76,7 @@ import org.springsource.loaded.test.infra.TestClassLoader; /** * Abstract root test class containing helper functions. - * + * * @author Andy Clement * @since 1.0 */ @@ -100,6 +100,9 @@ public abstract class SpringLoadedTests implements Constants { // TODO [java8] replace this with project dependency when Java8 is out protected String Java8CodeJar = findJar("../testdata-java8/build/libs", "testdata-java8"); + protected String TestDataJava8Path = TestUtils.getPathToClasses("../testdata-java8/"); + + protected String GroovyrtJar = "../testdata-groovy/groovy-all-1.8.6.jar"; protected Result result; @@ -111,7 +114,8 @@ public abstract class SpringLoadedTests implements Constants { public void setup() throws Exception { SpringLoadedPreProcessor.disabled = true; NameRegistry.reset(); - binLoader = new TestClassLoader(toURLs(TestDataPath, TestDataAspectJPath, AspectjrtJar, CodeJar, Java8CodeJar), + binLoader = new TestClassLoader( + toURLs(TestDataPath, TestDataAspectJPath, AspectjrtJar, CodeJar, Java8CodeJar, TestDataJava8Path), this.getClass().getClassLoader()); } @@ -127,7 +131,7 @@ public abstract class SpringLoadedTests implements Constants { /** * Convert an array of string paths to an array of URLs - * + * * @param paths the string paths * @return the converted URLs */ @@ -472,7 +476,7 @@ public abstract class SpringLoadedTests implements Constants { /** * retargets are "from.this.thing:to.this.thing" - * + * * @param newName * @param name * @param retargets of the form "this.from:this.to" @@ -672,8 +676,8 @@ public abstract class SpringLoadedTests implements Constants { // if (attrs = !null) { // sb.append("attrs(").append(toStringAttributes(attrs)).append(") "); // } - sb.append("0x").append(Integer.toHexString(fieldNode.access)).append("(") - .append(ClassPrinter.toAccessForMember(fieldNode.access)).append(") "); + sb.append("0x").append(Integer.toHexString(fieldNode.access)).append("(").append( + ClassPrinter.toAccessForMember(fieldNode.access)).append(") "); sb.append(fieldNode.name).append(" "); sb.append(fieldNode.desc).append(" "); if (fieldNode.signature != null) { @@ -710,7 +714,7 @@ public abstract class SpringLoadedTests implements Constants { /** * From asm: - * + * * The name value pairs of this annotation. Each name value pair is stored as two consecutive elements in the list. * The name is a {@link String}, and the value may be a {@link Byte}, {@link Boolean}, {@link Character}, * {@link Short}, {@link Integer}, {@link Long}, {@link Float}, {@link Double}, {@link String} or @@ -999,7 +1003,7 @@ public abstract class SpringLoadedTests implements Constants { /** * Create a type registry, configure it with the specified reloadable type/packages and return it. - * + * * @return new TypeRegistry */ protected TypeRegistry getTypeRegistry(String includePatterns) { 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 41f7c34..2a5f6ae 100644 --- a/springloaded/src/test/java/org/springsource/loaded/test/SpringLoadedTestsInSeparateJVM.java +++ b/springloaded/src/test/java/org/springsource/loaded/test/SpringLoadedTestsInSeparateJVM.java @@ -76,6 +76,110 @@ public class SpringLoadedTestsInSeparateJVM extends SpringLoadedTests { assertStdout("Hello World!\n", output); } + @Test + public void testReferenceInstanceMethodOfObject() throws Exception { + JVMOutput jo = null; + + jvm.copyToTestdataDirectory("basic.LambdaL"); + jvm.copyToTestdataDirectory("basic.LambdaL$Foo"); + + // jvm.newInstance("l", "basic.LambdaL", true); + + jo = jvm.run("basic.LambdaL"); + + // Total output: + // original static initializer + // original instance + // in first foo + // fooa + assertStdoutContains("in first foo", jo); + assertStdoutContains("fooa", jo); + + jvm.updateClass("basic.LambdaL", loadBytesForClass("basic.LambdaL")); + pause(2); + + // Run the same thing as before: + jo = jvm.run("basic.LambdaL"); + assertStdoutContains("in first foo", jo); + assertStdoutContains("fooa", jo); + + // New version: Foo interface has one method argument + jvm.updateClass("basic.LambdaL$Foo", + retrieveRename("basic.LambdaL$Foo", "basic.LambdaL2$Foo")); + waitForReloadToOccur(); + + jvm.updateClass("basic.LambdaL", + retrieveRename("basic.LambdaL", "basic.LambdaL2", "basic.LambdaL2$Foo:basic.LambdaL$Foo")); + waitForReloadToOccur(); + + // Run the new version + jo = jvm.run("basic.LambdaL"); + assertStdoutContains("in second foo", jo); + assertStdoutContains("fooab", jo); + } + + @Test + public void testStaticMethodReference() throws Exception { + JVMOutput jo = null; + + jvm.copyToTestdataDirectory("basic.StaticMethodReference", "basic.StaticMethodReference$Foo", + "basic.StaticMethodReference$Bar"); + jo = jvm.run("basic.StaticMethodReference"); + assertStdoutContains("in 1st static Method", jo); + assertStdoutContains("staticsa", jo); + + // Reload itself + jvm.updateClass("basic.StaticMethodReference", loadBytesForClass("basic.StaticMethodReference")); + waitForReloadToOccur(); + + jo = jvm.run("basic.StaticMethodReference"); + assertStdoutContains("in 1st static Method", jo); + assertStdoutContains("staticsa", jo); + + jvm.updateClass("basic.StaticMethodReference$Foo", + retrieveRename("basic.StaticMethodReference$Foo", "basic.StaticMethodReference2$Foo")); + jvm.updateClass("basic.StaticMethodReference$Bar", + retrieveRename("basic.StaticMethodReference$Bar", "basic.StaticMethodReference2$Bar")); + jvm.updateClass("basic.StaticMethodReference", + retrieveRename("basic.StaticMethodReference", "basic.StaticMethodReference2", + "basic.StaticMethodReference2$Foo:basic.StaticMethodReference$Foo", + "basic.StaticMethodReference2$Bar:basic.StaticMethodReference$Bar")); + waitForReloadToOccur(); + + jo = jvm.run("basic.StaticMethodReference"); + assertStdoutContains("in 2nd static Method", jo); + assertStdoutContains("staticsasb", jo); + + // // New version: Foo interface has one method argument + // jvm.updateClass("basic.LambdaL$Foo", + // retrieveRename("basic.LambdaL$Foo", "basic.LambdaL2$Foo")); + // waitForReloadToOccur(); + // + // jvm.updateClass("basic.LambdaL", + // retrieveRename("basic.LambdaL", "basic.LambdaL2", "basic.LambdaL2$Foo:basic.LambdaL$Foo")); + // waitForReloadToOccur(); + // + // // Run the new version + // jo = jvm.run("basic.LambdaL"); + // assertStdoutContains("in second foo", jo); + // assertStdoutContains("fooab", jo); + // compile("/original/", "/original/basic/StaticMethodReference.java.file"); + //+ JVMOutput output = jvm.sendAndReceive("run basic.StaticMethodReference"); + //+ + //+ assertStdoutContains("in 1st static Method", output); + //+ assertStdoutContains("staticsa", output); + //+ + //+ compile("/modified/", "/modified/basic/StaticMethodReference.java.file"); + //+ jvm.reload("basic.StaticMethodReference"); + //+ waitForReloadToOccur(); + //+ + //+ output = jvm.sendAndReceive("run basic.StaticMethodReference"); + //+ assertStdoutContains("in 2nd static Method", output); + //+ assertStdoutContains("staticsasb", output); + //+ } + // + } + @Test public void serialization() throws Exception { jvm.copyToTestdataDirectory("remote.Serialize"); @@ -146,7 +250,8 @@ public class SpringLoadedTestsInSeparateJVM extends SpringLoadedTests { @Test public void testCreatingAndInvokingMethodsOnInstance() throws Exception { - assertStderrContains("creating new instance 'a' of type 'jvmtwo.Runner'", jvm.newInstance("a", "jvmtwo.Runner")); + assertStderrContains("creating new instance 'a' of type 'jvmtwo.Runner'", + jvm.newInstance("a", "jvmtwo.Runner")); assertStdout("jvmtwo.Runner.run1() running", jvm.call("a", "run1")); } diff --git a/testdata-java8/src/main/java/basic/LambdaL.java b/testdata-java8/src/main/java/basic/LambdaL.java new file mode 100644 index 0000000..6f80d6d --- /dev/null +++ b/testdata-java8/src/main/java/basic/LambdaL.java @@ -0,0 +1,34 @@ +package basic; + +public class LambdaL { + static { + System.out.println("original static initializer"); + } + + public LambdaL() { + System.out.println("original instance"); + } + + public interface Foo { String m(String s); } + + public String getFoo(String s) { + System.out.println("in first foo"); + return "foo"+s; + } + + public static void main(String[] args) { + run(); + } + + public static String run() { + String res= new LambdaL().run2(); + System.out.println(res); + return res; + } + + public String run2() { + Foo f = this::getFoo; + return f.m("a"); + } + +} diff --git a/testdata-java8/src/main/java/basic/LambdaL2.java b/testdata-java8/src/main/java/basic/LambdaL2.java new file mode 100644 index 0000000..c4a3cf7 --- /dev/null +++ b/testdata-java8/src/main/java/basic/LambdaL2.java @@ -0,0 +1,27 @@ +package basic; + +public class LambdaL2 { + + public interface Foo { String m(String s, String t); } + + public String getFoo(String s, String t) { + System.out.println("in second foo"); + return "foo"+s+t; + } + + public static void main(String[] args) { + run(); + } + + public static String run() { + String res= new LambdaL2().run2(); + System.out.println(res); + return res; + } + + public String run2() { + Foo f = this::getFoo; + return f.m("a","b"); + } + +} \ No newline at end of file diff --git a/testdata-java8/src/main/java/basic/LambdaM.java b/testdata-java8/src/main/java/basic/LambdaM.java new file mode 100644 index 0000000..08008c5 --- /dev/null +++ b/testdata-java8/src/main/java/basic/LambdaM.java @@ -0,0 +1,32 @@ +package basic; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class LambdaM { + +// public interface Foo { String m(String s); } + +// public String getFoo(String s) { +// return "foo"+s; +// } + + public static void main(String[] args) { + run(); + } + + public static String run() { + return new LambdaM().run2(); + } + + public String run2() { + List list = new ArrayList<>(); + list.add("test3"); + Map m = list.stream().collect(Collectors.toMap(String::length, Function.identity())); + return m.toString(); + } + +} diff --git a/testdata-java8/src/main/java/basic/LambdaM2.java b/testdata-java8/src/main/java/basic/LambdaM2.java new file mode 100644 index 0000000..1f30089 --- /dev/null +++ b/testdata-java8/src/main/java/basic/LambdaM2.java @@ -0,0 +1,30 @@ +package basic; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class LambdaM2 { + + public static void main(String[] args) { + run(); + } + + public static String run() { + return new LambdaM2().run2(); + } + + public String run2() { + List list = new ArrayList<>(); + list.add("test3"); + Map m = list.stream().collect(Collectors.toMap(this::foo, Function.identity())); + return m.toString(); + } + + public int foo(String input) { + return input.length()*2; + } + +} diff --git a/testdata-java8/src/main/java/basic/LambdaN.java b/testdata-java8/src/main/java/basic/LambdaN.java new file mode 100644 index 0000000..6fe7d10 --- /dev/null +++ b/testdata-java8/src/main/java/basic/LambdaN.java @@ -0,0 +1,35 @@ +package basic; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class LambdaN { + + public static void main(String[] args) { + run(); + } + + public static String run() { + return new LambdaN().run2(); + } + + public String run2() { + List list = new ArrayList<>(); + HelperN h = new HelperN(); + list.add("test3"); + Map m = list.stream().collect(Collectors.toMap(h::foo, Function.identity())); + return m.toString(); + } + + +} + +class HelperN { + public int foo(String input) { + return input.length()*3; + } + +} \ No newline at end of file diff --git a/testdata-java8/src/main/java/basic/StaticMethodReference.java b/testdata-java8/src/main/java/basic/StaticMethodReference.java new file mode 100644 index 0000000..ed3b061 --- /dev/null +++ b/testdata-java8/src/main/java/basic/StaticMethodReference.java @@ -0,0 +1,39 @@ +package basic; + +public class StaticMethodReference { + + public static String staticMethod(String s1) { + System.out.println("in 1st static Method"); + return "static" + s1; + } + + public interface Bar { String sm(String s);} + + public interface Foo { String m(String s); } + + public String getFoo(String s) { + return "foo"+s; + } + + public static void main(String[] args) { + run(); + } + + public static String run() { + StaticMethodReference l = new StaticMethodReference(); + String r = l.run3(); + System.out.println(r); + return l.run2(); + } + + public String run2() { + Foo f = this::getFoo; + return f.m("a"); + } + + public String run3() { + Bar b = StaticMethodReference::staticMethod; + return b.sm("sa"); + } + +} \ No newline at end of file diff --git a/testdata-java8/src/main/java/basic/StaticMethodReference2.java b/testdata-java8/src/main/java/basic/StaticMethodReference2.java new file mode 100644 index 0000000..e75696b --- /dev/null +++ b/testdata-java8/src/main/java/basic/StaticMethodReference2.java @@ -0,0 +1,39 @@ +package basic; + +public class StaticMethodReference2 { + + public static String staticMethod(String s1, String s2) { + System.out.println("in 2nd static Method"); + return "static" + s1 + s2; + } + + public interface Bar { String sm(String s, String s1);} + + public interface Foo { String m(String s); } + + public String getFoo(String s) { + return "foo"+s; + } + + public static void main(String[] args) { + run(); + } + + public static String run() { + StaticMethodReference2 l = new StaticMethodReference2(); + String r = l.run3(); + System.out.println(r); + return l.run2(); + } + + public String run2() { + Foo f = this::getFoo; + return f.m("a"); + } + + public String run3() { + Bar b = StaticMethodReference2::staticMethod; + return b.sm("sa", "sb"); + } + +} \ No newline at end of file