# HG changeset patch # User henryjen # Date 1381336900 25200 # Node ID fbf53402134ddf240462b0c4b5c6f225e994e854 # Parent 86a86a94b367b40bf053f63f8b814e78d2da633a 8023524: Mechanism to dump generated lambda classes / log lambda code generation Reviewed-by: plevart, mchung, forax, jjb Contributed-by: brian.goetz@oracle.com, henry.jen@oracle.com diff -r 86a86a94b367 -r fbf53402134d jdk/src/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java --- a/jdk/src/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java Thu Sep 26 15:19:27 2013 -0700 +++ b/jdk/src/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java Wed Oct 09 09:41:40 2013 -0700 @@ -27,12 +27,15 @@ import jdk.internal.org.objectweb.asm.*; import sun.misc.Unsafe; +import sun.security.action.GetPropertyAction; +import java.io.FilePermission; import java.lang.reflect.Constructor; import java.security.AccessController; import java.security.PrivilegedAction; import java.security.ProtectionDomain; import java.util.concurrent.atomic.AtomicInteger; +import java.util.PropertyPermission; import static jdk.internal.org.objectweb.asm.Opcodes.*; @@ -66,12 +69,23 @@ // Used to ensure that each spun class name is unique private static final AtomicInteger counter = new AtomicInteger(0); + // For dumping generated classes to disk, for debugging purposes + private static final ProxyClassesDumper dumper; + + static { + final String key = "jdk.internal.lambda.dumpProxyClasses"; + String path = AccessController.doPrivileged( + new GetPropertyAction(key), null, + new PropertyPermission(key , "read")); + dumper = (null == path) ? null : ProxyClassesDumper.getInstance(path); + } + // See context values in AbstractValidatingLambdaMetafactory private final String implMethodClassName; // Name of type containing implementation "CC" private final String implMethodName; // Name of implementation method "impl" private final String implMethodDesc; // Type descriptor for implementation methods "(I)Ljava/lang/String;" - private final Type[] implMethodArgumentTypes; // ASM types for implementaion method parameters - private final Type implMethodReturnType; // ASM type for implementaion method return type "Ljava/lang/String;" + private final Type[] implMethodArgumentTypes; // ASM types for implementation method parameters + private final Type implMethodReturnType; // ASM type for implementation method return type "Ljava/lang/String;" private final MethodType constructorType; // Generated class constructor type "(CC)void" private final String constructorDesc; // Type descriptor for constructor "(LCC;)V" private final ClassWriter cw; // ASM class writer @@ -259,29 +273,31 @@ final byte[] classBytes = cw.toByteArray(); - /*** Uncomment to dump the generated file - System.out.printf("Loaded: %s (%d bytes) %n", lambdaClassName, - classBytes.length); - try (FileOutputStream fos = new FileOutputStream(lambdaClassName - .replace('/', '.') + ".class")) { - fos.write(classBytes); - } catch (IOException ex) { - PlatformLogger.getLogger(InnerClassLambdaMetafactory.class - .getName()).severe(ex.getMessage(), ex); - } - ***/ + // If requested, dump out to a file for debugging purposes + if (dumper != null) { + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Void run() { + dumper.dumpClass(lambdaClassName, classBytes); + return null; + } + }, null, + new FilePermission("<>", "read, write"), + // createDirectories may need it + new PropertyPermission("user.dir", "read")); + } ClassLoader loader = targetClass.getClassLoader(); ProtectionDomain pd = (loader == null) - ? null - : AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public ProtectionDomain run() { - return targetClass.getProtectionDomain(); - } - } - ); + ? null + : AccessController.doPrivileged( + new PrivilegedAction() { + @Override + public ProtectionDomain run() { + return targetClass.getProtectionDomain(); + } + } + ); return UNSAFE.defineClass(lambdaClassName, classBytes, 0, classBytes.length, diff -r 86a86a94b367 -r fbf53402134d jdk/src/share/classes/java/lang/invoke/ProxyClassesDumper.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jdk/src/share/classes/java/lang/invoke/ProxyClassesDumper.java Wed Oct 09 09:41:40 2013 -0700 @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package java.lang.invoke; + +import sun.util.logging.PlatformLogger; + +import java.io.FilePermission; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Helper class used by InnerClassLambdaMetafactory to log generated classes + * + * @implNote + *

Because this class is called by LambdaMetafactory, make use + * of lambda lead to recursive calls cause stack overflow. + */ +final class ProxyClassesDumper { + private static final char[] HEX = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + private static final char[] BAD_CHARS = { + '\\', ':', '*', '?', '"', '<', '>', '|' + }; + private static final String[] REPLACEMENT = { + "%5C", "%3A", "%2A", "%3F", "%22", "%3C", "%3E", "%7C" + }; + + private final Path dumpDir; + + public static ProxyClassesDumper getInstance(String path) { + if (null == path) { + return null; + } + try { + path = path.trim(); + final Path dir = Paths.get(path.length() == 0 ? "." : path); + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Void run() { + validateDumpDir(dir); + return null; + } + }, null, new FilePermission("<>", "read, write")); + return new ProxyClassesDumper(dir); + } catch (InvalidPathException ex) { + PlatformLogger.getLogger(ProxyClassesDumper.class.getName()) + .warning("Path " + path + " is not valid - dumping disabled", ex); + } catch (IllegalArgumentException iae) { + PlatformLogger.getLogger(ProxyClassesDumper.class.getName()) + .warning(iae.getMessage() + " - dumping disabled"); + } + return null; + } + + private ProxyClassesDumper(Path path) { + dumpDir = Objects.requireNonNull(path); + } + + private static void validateDumpDir(Path path) { + if (!Files.exists(path)) { + throw new IllegalArgumentException("Directory " + path + " does not exist"); + } else if (!Files.isDirectory(path)) { + throw new IllegalArgumentException("Path " + path + " is not a directory"); + } else if (!Files.isWritable(path)) { + throw new IllegalArgumentException("Directory " + path + " is not writable"); + } + } + + public static String encodeForFilename(String className) { + final int len = className.length(); + StringBuilder sb = new StringBuilder(len); + + for (int i = 0; i < len; i++) { + char c = className.charAt(i); + // control characters + if (c <= 31) { + sb.append('%'); + sb.append(HEX[c >> 4 & 0x0F]); + sb.append(HEX[c & 0x0F]); + } else { + int j = 0; + for (; j < BAD_CHARS.length; j++) { + if (c == BAD_CHARS[j]) { + sb.append(REPLACEMENT[j]); + break; + } + } + if (j >= BAD_CHARS.length) { + sb.append(c); + } + } + } + + return sb.toString(); + } + + public void dumpClass(String className, final byte[] classBytes) { + Path file; + try { + file = dumpDir.resolve(encodeForFilename(className) + ".class"); + } catch (InvalidPathException ex) { + PlatformLogger.getLogger(ProxyClassesDumper.class.getName()) + .warning("Invalid path for class " + className); + return; + } + + try { + Path dir = file.getParent(); + Files.createDirectories(dir); + Files.write(file, classBytes); + } catch (Exception ignore) { + PlatformLogger.getLogger(ProxyClassesDumper.class.getName()) + .warning("Exception writing to path at " + file.toString()); + // simply don't care if this operation failed + } + } +} diff -r 86a86a94b367 -r fbf53402134d jdk/test/java/lang/invoke/lambda/LogGeneratedClassesTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jdk/test/java/lang/invoke/lambda/LogGeneratedClassesTest.java Wed Oct 09 09:41:40 2013 -0700 @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8023524 + * @summary tests logging generated classes for lambda + * @library /java/nio/file + * @run testng LogGeneratedClassesTest + */ +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFileAttributeView; +import java.util.stream.Stream; + +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import org.testng.SkipException; + +import static java.nio.file.attribute.PosixFilePermissions.*; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +public class LogGeneratedClassesTest extends LUtils { + String longFQCN; + + @BeforeClass + public void setup() throws IOException { + final List scratch = new ArrayList<>(); + scratch.clear(); + scratch.add("package com.example;"); + scratch.add("public class TestLambda {"); + scratch.add(" interface I {"); + scratch.add(" int foo();"); + scratch.add(" }"); + scratch.add(" public static void main(String[] args) {"); + scratch.add(" I lam = () -> 10;"); + scratch.add(" Runnable r = () -> {"); + scratch.add(" System.out.println(\"Runnable\");"); + scratch.add(" };"); + scratch.add(" r.run();"); + scratch.add(" System.out.println(\"Finish\");"); + scratch.add(" }"); + scratch.add("}"); + + File test = new File("TestLambda.java"); + createFile(test, scratch); + compile("-d", ".", test.getName()); + + scratch.remove(0); + scratch.remove(0); + scratch.add(0, "public class LongPackageName {"); + StringBuilder sb = new StringBuilder("com.example."); + // longer than 255 which exceed max length of most filesystems + for (int i = 0; i < 30; i++) { + sb.append("nonsense."); + } + sb.append("enough"); + longFQCN = sb.toString() + ".LongPackageName"; + sb.append(";"); + sb.insert(0, "package "); + scratch.add(0, sb.toString()); + test = new File("LongPackageName.java"); + createFile(test, scratch); + compile("-d", ".", test.getName()); + + // create target + Files.createDirectory(Paths.get("dump")); + Files.createDirectories(Paths.get("dumpLong/com/example/nonsense")); + Files.createFile(Paths.get("dumpLong/com/example/nonsense/nonsense")); + Files.createFile(Paths.get("file")); + } + + @AfterClass + public void cleanup() throws IOException { + Files.delete(Paths.get("TestLambda.java")); + Files.delete(Paths.get("LongPackageName.java")); + Files.delete(Paths.get("file")); + TestUtil.removeAll(Paths.get("com")); + TestUtil.removeAll(Paths.get("dump")); + TestUtil.removeAll(Paths.get("dumpLong")); + } + + @Test + public void testNotLogging() { + TestResult tr = doExec(JAVA_CMD.getAbsolutePath(), + "-cp", ".", + "-Djava.security.manager", + "com.example.TestLambda"); + tr.assertZero("Should still return 0"); + } + + @Test + public void testLogging() throws IOException { + assertTrue(Files.exists(Paths.get("dump"))); + TestResult tr = doExec(JAVA_CMD.getAbsolutePath(), + "-cp", ".", + "-Djdk.internal.lambda.dumpProxyClasses=dump", + "-Djava.security.manager", + "com.example.TestLambda"); + // dump/com/example + 2 class files + assertEquals(Files.walk(Paths.get("dump")).count(), 5, "Two lambda captured"); + tr.assertZero("Should still return 0"); + } + + @Test + public void testDumpDirNotExist() throws IOException { + assertFalse(Files.exists(Paths.get("notExist"))); + TestResult tr = doExec(JAVA_CMD.getAbsolutePath(), + "-cp", ".", + "-Djdk.internal.lambda.dumpProxyClasses=notExist", + "-Djava.security.manager", + "com.example.TestLambda"); + assertEquals(tr.testOutput.stream() + .filter(s -> s.startsWith("WARNING")) + .peek(s -> assertTrue(s.contains("does not exist"))) + .count(), + 1, "only show error once"); + tr.assertZero("Should still return 0"); + } + + @Test + public void testDumpDirIsFile() throws IOException { + assertTrue(Files.isRegularFile(Paths.get("file"))); + TestResult tr = doExec(JAVA_CMD.getAbsolutePath(), + "-cp", ".", + "-Djdk.internal.lambda.dumpProxyClasses=file", + "-Djava.security.manager", + "com.example.TestLambda"); + assertEquals(tr.testOutput.stream() + .filter(s -> s.startsWith("WARNING")) + .peek(s -> assertTrue(s.contains("not a directory"))) + .count(), + 1, "only show error once"); + tr.assertZero("Should still return 0"); + } + + @Test + public void testDumpDirNotWritable() throws IOException { + if (! Files.getFileStore(Paths.get(".")) + .supportsFileAttributeView(PosixFileAttributeView.class)) { + // No easy way to setup readonly directory + throw new SkipException("Posix not supported"); + } + + Files.createDirectory(Paths.get("readOnly"), + asFileAttribute(fromString("r-xr-xr-x"))); + + TestResult tr = doExec(JAVA_CMD.getAbsolutePath(), + "-cp", ".", + "-Djdk.internal.lambda.dumpProxyClasses=readOnly", + "-Djava.security.manager", + "com.example.TestLambda"); + assertEquals(tr.testOutput.stream() + .filter(s -> s.startsWith("WARNING")) + .peek(s -> assertTrue(s.contains("not writable"))) + .count(), + 1, "only show error once"); + tr.assertZero("Should still return 0"); + + TestUtil.removeAll(Paths.get("readOnly")); + } + + @Test + public void testLoggingException() throws IOException { + assertTrue(Files.exists(Paths.get("dumpLong"))); + TestResult tr = doExec(JAVA_CMD.getAbsolutePath(), + "-cp", ".", + "-Djdk.internal.lambda.dumpProxyClasses=dumpLong", + "-Djava.security.manager", + longFQCN); + assertEquals(tr.testOutput.stream() + .filter(s -> s.startsWith("WARNING: Exception")) + .count(), + 2, "show error each capture"); + // dumpLong/com/example/nosense/nosense + assertEquals(Files.walk(Paths.get("dumpLong")).count(), 5, "Two lambda captured failed to log"); + tr.assertZero("Should still return 0"); + } +}