# HG changeset patch # User henryjen # Date 1380059812 25200 # Node ID 0602269be85c4bf0cef371e1550792f9ec95e9a9 # Parent 094657697e0a9941dad493adc07805f5a8b5436b 8023524: Mechanism to dump generated lambda classes / log lambda code generation Reviewed-by: Contributed-by: brian.goetz@oracle.com, henry.jen@oracle.com diff --git a/src/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java b/src/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java --- a/src/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java +++ b/src/share/classes/java/lang/invoke/InnerClassLambdaMetafactory.java @@ -27,11 +27,16 @@ import jdk.internal.org.objectweb.asm.*; import sun.misc.Unsafe; +import sun.util.logging.PlatformLogger; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; import java.lang.reflect.Constructor; import java.security.AccessController; import java.security.PrivilegedAction; import java.security.ProtectionDomain; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import static jdk.internal.org.objectweb.asm.Opcodes.*; @@ -51,6 +56,17 @@ private static final String NAME_CTOR = ""; //Serialization support + 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 static final String NAME_SERIALIZED_LAMBDA = "java/lang/invoke/SerializedLambda"; private static final String DESCR_METHOD_WRITE_REPLACE = "()Ljava/lang/Object;"; private static final String NAME_METHOD_WRITE_REPLACE = "writeReplace"; @@ -66,6 +82,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 String DUMP_PATH_PROPERTY = "jdk.internal.lambda.dumpProxyClasses"; + private static final String dumpDir; + private static AtomicBoolean invalidDir = new AtomicBoolean(false); + static { + dumpDir = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public String run() { + String path = System.getProperty(DUMP_PATH_PROPERTY); + if (path != null) { + validateDumpDir(path); + } + return 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" @@ -198,6 +231,56 @@ } } + private static File validateDumpDir(String path) { + File dirPath = new File(path); + String errMsg = null; + if (!dirPath.exists()) { + errMsg = "Directory at jdk.internal.lambda.dumpProxyClasses does not exist"; + } else if (!dirPath.isDirectory()) { + errMsg = "Path at jdk.internal.lambda.dumpProxyClasses is not a directory"; + } else if (!dirPath.canWrite()) { + errMsg = "Directory at jdk.internal.lambda.dumpProxyClasses is not writable"; + } else { + // validate dump directory, ready to go + return dirPath; + } + + // show error message about invalid directory once + if (! invalidDir.getAndSet(true)) { + PlatformLogger.getLogger(InnerClassLambdaMetafactory.class.getName()) + .warning(errMsg); + } + return null; + } + + private 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(); + } + /** * Generate a class file which implements the functional * interface, define and return the class. @@ -259,29 +342,37 @@ 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 (dumpDir != null) { + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Void run() { + File dirPath = validateDumpDir(dumpDir); + if (dirPath != null) { + File out = new File(dirPath, encodeForFilename(lambdaClassName) + ".class"); + try (FileOutputStream fos = new FileOutputStream(out)) { + fos.write(classBytes); + } catch (IOException ex) { + PlatformLogger.getLogger(InnerClassLambdaMetafactory.class.getName()) + .warning("Exception writing to path at jdk.internal.lambda.dumpProxyClasses"); + } + } + return null; + } + }); + } 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 --git a/test/java/lang/invoke/lambda/LogGeneratedClassesTest.java b/test/java/lang/invoke/lambda/LogGeneratedClassesTest.java new file mode 100644 --- /dev/null +++ b/test/java/lang/invoke/lambda/LogGeneratedClassesTest.java @@ -0,0 +1,190 @@ +/* + * 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.Path; +import java.nio.file.Paths; +import java.nio.file.LinkOption; +import java.util.stream.Stream; + +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +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.createDirectory(Paths.get("dumpLong")); + Files.createFile(Paths.get("file")); + Files.createDirectory(Paths.get("readOnly"), + asFileAttribute(fromString("r-xr-xr-x"))); + } + + @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")); + TestUtil.removeAll(Paths.get("readOnly")); + } + + @Test + public void testNotLogging() { + TestResult tr = doExec(JAVA_CMD.getAbsolutePath(), + "-cp", ".", + "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", + "com.example.TestLambda"); + assertEquals(Files.list(Paths.get("dump")).count(), 2, "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", + "com.example.TestLambda"); + assertEquals(tr.testOutput.stream() + .filter(s -> s.startsWith("WARNING")) + .peek(s -> assertTrue(s.endsWith("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", + "com.example.TestLambda"); + assertEquals(tr.testOutput.stream() + .filter(s -> s.startsWith("WARNING")) + .peek(s -> assertTrue(s.endsWith("not a directory"))) + .count(), + 1, "only show error once"); + tr.assertZero("Should still return 0"); + } + + @Test + public void testDumpDirNotWritable() throws IOException { + assertTrue(Files.isRegularFile(Paths.get("file"))); + TestResult tr = doExec(JAVA_CMD.getAbsolutePath(), + "-cp", ".", + "-Djdk.internal.lambda.dumpProxyClasses=readOnly", + "com.example.TestLambda"); + assertEquals(tr.testOutput.stream() + .filter(s -> s.startsWith("WARNING")) + .peek(s -> assertTrue(s.endsWith("not writable"))) + .count(), + 1, "only show error once"); + tr.assertZero("Should still return 0"); + } + + @Test + public void testLoggingException() throws IOException { + assertTrue(Files.exists(Paths.get("dumpLong"))); + TestResult tr = doExec(JAVA_CMD.getAbsolutePath(), + "-cp", ".", + "-Djdk.internal.lambda.dumpProxyClasses=dumpLong", + longFQCN); + assertEquals(tr.testOutput.stream() + .filter(s -> s.startsWith("WARNING: Exception")) + .count(), + 2, "show error each capture"); + assertEquals(Files.list(Paths.get("dumpLong")).count(), 0, "Two lambda captured failed to log"); + tr.assertZero("Should still return 0"); + } +}