From c92bdde0b3515d16af93a8366f3945a9691feb6d Mon Sep 17 00:00:00 2001 From: lzpeng723 <1500913306@qq.com> Date: Mon, 23 Nov 2020 00:21:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0Java=E6=BA=90=E7=A0=81?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/compiler/JavaClassFileManager.java | 105 +++++++ .../core/compiler/JavaClassFileObject.java | 63 ++++ .../core/compiler/JavaSourceCompiler.java | 292 ++++++++++++++++++ .../core/compiler/JavaSourceFileObject.java | 98 ++++++ .../cn/hutool/core/compiler/package-info.java | 6 + .../core/compiler/JavaSourceCompilerTest.java | 42 +++ .../src/test/resources/test-compile/a/A.java | 24 ++ .../src/test/resources/test-compile/b/B.java | 8 + .../src/test/resources/test-compile/c/C.java | 9 + 9 files changed, 647 insertions(+) create mode 100644 hutool-core/src/main/java/cn/hutool/core/compiler/JavaClassFileManager.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/compiler/JavaClassFileObject.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceCompiler.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceFileObject.java create mode 100644 hutool-core/src/main/java/cn/hutool/core/compiler/package-info.java create mode 100644 hutool-core/src/test/java/cn/hutool/core/compiler/JavaSourceCompilerTest.java create mode 100644 hutool-core/src/test/resources/test-compile/a/A.java create mode 100644 hutool-core/src/test/resources/test-compile/b/B.java create mode 100644 hutool-core/src/test/resources/test-compile/c/C.java diff --git a/hutool-core/src/main/java/cn/hutool/core/compiler/JavaClassFileManager.java b/hutool-core/src/main/java/cn/hutool/core/compiler/JavaClassFileManager.java new file mode 100644 index 000000000..ce2d4af49 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/compiler/JavaClassFileManager.java @@ -0,0 +1,105 @@ +package cn.hutool.core.compiler; + +import cn.hutool.core.io.IoUtil; + +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaFileManager; +import javax.tools.JavaFileObject; +import javax.tools.JavaFileObject.Kind; +import java.io.InputStream; +import java.security.SecureClassLoader; +import java.util.HashMap; +import java.util.Map; + +/** + * Java 字节码文件对象 + * 正常我们使用javac命令编译源码时会将class文件写入到磁盘中,但在运行时动态编译类不适合保存在磁盘中 + * 我们采取此对象来管理运行时动态编译类生成的字节码 + * + * @author lzpeng + * @see JavaSourceCompilerBak#compile() + * @see com.sun.tools.javac.api.ClientCodeWrapper.WrappedJavaFileManager#getJavaFileForOutput(javax.tools.JavaFileManager.Location, java.lang.String, javax.tools.JavaFileObject.Kind, javax.tools.FileObject) + */ +final class JavaClassFileManager extends ForwardingJavaFileManager { + + /** + * 存储java字节码文件对象映射 + */ + private final Map javaFileObjectMap = new HashMap<>(); + + /** + * 加载动态编译生成类的父类加载器 + */ + private final ClassLoader parent; + + /** + * 构造 + * + * @param parent 父类加载器 + * @param fileManager 字节码文件管理器 + * @see JavaSourceCompilerBak#compile() + */ + protected JavaClassFileManager(final ClassLoader parent, final JavaFileManager fileManager) { + super(fileManager); + if (parent == null) { + this.parent = Thread.currentThread().getContextClassLoader(); + } else { + this.parent = parent; + } + } + + /** + * 获得动态编译生成的类的类加载器 + * + * @param location 源码位置 + * @return 动态编译生成的类的类加载器 + * @see JavaSourceCompilerBak#compile() + */ + @Override + public ClassLoader getClassLoader(final Location location) { + return new SecureClassLoader(parent) { + + /** + * 查找类 + * @param name 类名 + * @return 类的class对象 + * @throws ClassNotFoundException 未找到类异常 + */ + @Override + protected Class findClass(final String name) throws ClassNotFoundException { + final JavaFileObject javaFileObject = javaFileObjectMap.get(name); + if (javaFileObject != null) { + try { + final InputStream inputStream = javaFileObject.openInputStream(); + final byte[] bytes = IoUtil.readBytes(inputStream); + final Class c = defineClass(name, bytes, 0, bytes.length); + return c; + } catch (Exception e) { + e.printStackTrace(); + } + } + throw new ClassNotFoundException(name); + } + }; + } + + /** + * 获得Java字节码文件对象 + * 编译器编译源码时会将Java源码对象编译转为Java字节码对象 + * + * @param location 源码位置 + * @param className 类名 + * @param kind 文件类型 + * @param sibling 将Java源码对象 + * @return Java字节码文件对象 + * @see com.sun.tools.javac.api.lientCodeWrapper.WrappedJavaFileManager#getJavaFileForOutput(javax.tools.JavaFileManager.Location, java.lang.String, javax.tools.JavaFileObject.Kind, javax.tools.FileObject) + */ + @Override + public JavaFileObject getJavaFileForOutput(final Location location, final String className, final Kind kind, final FileObject sibling) { + final JavaFileObject javaFileObject = new JavaClassFileObject(className, kind); + javaFileObjectMap.put(className, javaFileObject); + return javaFileObject; + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/compiler/JavaClassFileObject.java b/hutool-core/src/main/java/cn/hutool/core/compiler/JavaClassFileObject.java new file mode 100644 index 000000000..c9f92bc70 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/compiler/JavaClassFileObject.java @@ -0,0 +1,63 @@ +package cn.hutool.core.compiler; + + +import javax.tools.SimpleJavaFileObject; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; + +/** + * Java 字节码文件对象 + * + * @author lzpeng + * @see JavaClassFileManager#getClassLoader(javax.tools.JavaFileManager.Location + * @see JavaClassFileManager#getJavaFileForOutput(javax.tools.JavaFileManager.Location, java.lang.String, javax.tools.JavaFileObject.Kind, javax.tools.FileObject) + * @see com.sun.tools.javac.jvm.ClassWriter.ClassWriter#writeClass(com.sun.tools.javac.code.Symbol.ClassSymbol) + */ +final class JavaClassFileObject extends SimpleJavaFileObject { + + /** + * 字节码输出流 + */ + private final ByteArrayOutputStream byteArrayOutputStream; + + /** + * 构造 + * + * @param className 需要编译的类名 + * @param kind 需要编译的文件类型 + * @see JavaClassFileManager#getJavaFileForOutput(javax.tools.JavaFileManager.Location, java.lang.String, javax.tools.JavaFileObject.Kind, javax.tools.FileObject) + */ + protected JavaClassFileObject(final String className, final Kind kind) { + super(URI.create("string:///" + className.replaceAll("\\.", "/") + kind.extension), kind); + this.byteArrayOutputStream = new ByteArrayOutputStream(); + } + + /** + * 获得字节码输入流 + * 编译器编辑源码后,我们将通过此输出流获得编译后的字节码,以便运行时加载类 + * + * @return 字节码输入流 + * @see JavaClassFileManager#getClassLoader(javax.tools.JavaFileManager.Location) + */ + @Override + public InputStream openInputStream() { + final byte[] bytes = byteArrayOutputStream.toByteArray(); + return new ByteArrayInputStream(bytes); + } + + /** + * 获得字节码输出流 + * 编译器编辑源码时,会将编译结果输出到本输出流中 + * + * @return 字节码输出流 + * @see com.sun.tools.javac.jvm.ClassWriter.ClassWriter#writeClass(com.sun.tools.javac.code.Symbol.ClassSymbol) + */ + @Override + public OutputStream openOutputStream() { + return this.byteArrayOutputStream; + } + +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceCompiler.java b/hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceCompiler.java new file mode 100644 index 000000000..c88e579fa --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceCompiler.java @@ -0,0 +1,292 @@ +package cn.hutool.core.compiler; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.URLUtil; + +import javax.tools.*; +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.JavaFileObject.Kind; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.*; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +/** + * Java 源码编译器 + * + * @author lzpeng + */ +public final class JavaSourceCompiler { + + /** + * java 编译器 + */ + private static final JavaCompiler JAVA_COMPILER = ToolProvider.getSystemJavaCompiler(); + + /** + * 待编译的文件 可以是 .java文件 压缩文件 文件夹 递归搜索文件夹内的zip包和jar包 + */ + private final List sourceFileList = new ArrayList<>(); + + /** + * 编译时需要加入classpath中的文件 可以是 压缩文件 文件夹递归搜索文件夹内的zip包和jar包 + */ + private final List libraryFileList = new ArrayList<>(); + + /** + * 源码映射 key: 类名 value: 类源码 + */ + private final Map sourceCodeMap = new LinkedHashMap<>(); + + /** + * 编译类时使用的父类加载器 + */ + private final ClassLoader parentClassLoader; + + + /** + * 构造 + * + * @param parent 父类加载器 + */ + private JavaSourceCompiler(ClassLoader parent) { + this.parentClassLoader = parent; + } + + + /** + * 创建Java源码编译器 + * + * @param parent 父类加载器 + * @return Java源码编译器 + */ + public static JavaSourceCompiler create(ClassLoader parent) { + return new JavaSourceCompiler(parent); + } + + + /** + * 向编译器中加入待编译的文件 支持 .java, 文件夹, 压缩文件 递归搜索文件夹内的压缩文件和jar包 + * + * @param files 待编译的文件 支持 .java, 文件夹, 压缩文件 递归搜索文件夹内的压缩文件和jar包 + * @return Java源码编译器 + */ + public JavaSourceCompiler addSource(final File... files) { + if (ArrayUtil.isNotEmpty(files)) { + this.sourceFileList.addAll(Arrays.asList(files)); + } + return this; + } + + /** + * 向编译器中加入待编译的源码Map + * + * @param sourceCodeMap 源码Map key: 类名 value 源码 + * @return Java源码编译器 + */ + public JavaSourceCompiler addSource(final Map sourceCodeMap) { + if (MapUtil.isNotEmpty(sourceCodeMap)) { + this.sourceCodeMap.putAll(sourceCodeMap); + } + return this; + } + + /** + * 加入编译Java源码时所需要的jar包 + * + * @param files 编译Java源码时所需要的jar包 + * @return Java源码编译器 + */ + public JavaSourceCompiler addLibrary(final File... files) { + if (ArrayUtil.isNotEmpty(files)) { + this.libraryFileList.addAll(Arrays.asList(files)); + } + return this; + } + + /** + * 向编译器中加入待编译的源码Map + * + * @param className 类名 + * @param sourceCode 源码 + * @return Java文件编译器 + */ + public JavaSourceCompiler addSource(final String className, final String sourceCode) { + if (className != null && sourceCode != null) { + this.sourceCodeMap.put(className, sourceCode); + } + return this; + } + + /** + * 编译所有文件并返回类加载器 + * + * @return 类加载器 + */ + public ClassLoader compile() { + final ClassLoader parent; + if (this.parentClassLoader == null) { + parent = Thread.currentThread().getContextClassLoader(); + } else { + parent = this.parentClassLoader; + } + // 获得classPath + final List classPath = getClassPath(); + final URL[] urLs = URLUtil.getURLs(classPath.toArray(new File[0])); + final URLClassLoader ucl = URLClassLoader.newInstance(urLs, parent); + if (sourceCodeMap.isEmpty() && sourceFileList.isEmpty()) { + // 没有需要编译的源码 + return ucl; + } + // 没有需要编译的源码文件返回加载zip或jar包的类加载器 + final Iterable javaFileObjectList = getJavaFileObject(); + // 创建编译器 + final JavaFileManager standardJavaFileManager = JAVA_COMPILER.getStandardFileManager(null, null, null); + final JavaFileManager javaFileManager = new JavaClassFileManager(ucl, standardJavaFileManager); + final DiagnosticCollector diagnosticCollector = new DiagnosticCollector<>(); + final List options = new ArrayList<>(); + if (!classPath.isEmpty()) { + final List cp = classPath.stream().map(File::getAbsolutePath).collect(Collectors.toList()); + options.add("-cp"); + options.addAll(cp); + } + // 编译文件 + final CompilationTask task = JAVA_COMPILER.getTask(null, javaFileManager, diagnosticCollector, + options, null, javaFileObjectList); + final Boolean result = task.call(); + if (Boolean.TRUE.equals(result)) { + return javaFileManager.getClassLoader(StandardLocation.CLASS_OUTPUT); + } else { + // 编译失败,收集错误信息 + final List diagnostics = diagnosticCollector.getDiagnostics(); + final String errorMsg = diagnostics.stream().map(String::valueOf) + .collect(Collectors.joining(System.lineSeparator())); + // CompileException + throw new RuntimeException(errorMsg); + } + } + + /** + * 获得编译源码时需要的classpath + * + * @return 编译源码时需要的classpath + */ + private List getClassPath() { + List classPathFileList = new ArrayList<>(); + for (File file : libraryFileList) { + List jarOrZipFile = FileUtil.loopFiles(file, this::isJarOrZipFile); + classPathFileList.addAll(jarOrZipFile); + if (file.isDirectory()) { + classPathFileList.add(file); + } + } + return classPathFileList; + } + + /** + * 获得待编译的Java文件对象 + * + * @return 待编译的Java文件对象 + */ + private Iterable getJavaFileObject() { + final Collection collection = new ArrayList<>(); + for (File file : sourceFileList) { + // .java 文件 + final List javaFileList = FileUtil.loopFiles(file, this::isJavaFile); + for (File javaFile : javaFileList) { + collection.add(getJavaFileObjectByJavaFile(javaFile)); + } + // 压缩包 + final List jarOrZipFileList = FileUtil.loopFiles(file, this::isJarOrZipFile); + for (File jarOrZipFile : jarOrZipFileList) { + collection.addAll(getJavaFileObjectByZipOrJarFile(jarOrZipFile)); + } + } + // 源码Map + collection.addAll(getJavaFileObjectByMap(this.sourceCodeMap)); + return collection; + } + + /** + * 通过源码Map获得Java文件对象 + * + * @param sourceCodeMap 源码Map + * @return Java文件对象集合 + */ + private Collection getJavaFileObjectByMap(final Map sourceCodeMap) { + if (MapUtil.isNotEmpty(sourceCodeMap)) { + return sourceCodeMap.entrySet().stream() + .map(entry -> new JavaSourceFileObject(entry.getKey(), entry.getValue(), Kind.SOURCE)) + .collect(Collectors.toList()); + } + return Collections.emptySet(); + } + + /** + * 通过.java文件创建Java文件对象 + * + * @param file .java文件 + * @return Java文件对象 + */ + private JavaFileObject getJavaFileObjectByJavaFile(final File file) { + return new JavaSourceFileObject(file.toURI(), Kind.SOURCE); + } + + /** + * 通过zip包或jar包创建Java文件对象 + * + * @param file 压缩文件 + * @return Java文件对象 + */ + private Collection getJavaFileObjectByZipOrJarFile(final File file) { + final Collection collection = new ArrayList<>(); + try { + final ZipFile zipFile = new ZipFile(file); + final Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + final ZipEntry zipEntry = entries.nextElement(); + final String name = zipEntry.getName(); + if (name.endsWith(".java")) { + final InputStream inputStream = zipFile.getInputStream(zipEntry); + final JavaSourceFileObject fileObject = new JavaSourceFileObject(name, inputStream, Kind.SOURCE); + collection.add(fileObject); + } + } + return collection; + } catch (IOException e) { + e.printStackTrace(); + } + return Collections.emptyList(); + } + + + /** + * 是否是jar 或 zip 文件 + * + * @param file 文件 + * @return 是否是jar 或 zip 文件 + */ + private boolean isJarOrZipFile(final File file) { + final String fileName = file.getName(); + return fileName.endsWith(".jar") || fileName.endsWith(".zip"); + } + + /** + * 是否是.java文件 + * + * @param file 文件 + * @return 是否是.java文件 + */ + private boolean isJavaFile(final File file) { + final String fileName = file.getName(); + return fileName.endsWith(".java"); + } + +} diff --git a/hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceFileObject.java b/hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceFileObject.java new file mode 100644 index 000000000..a13cc4841 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/compiler/JavaSourceFileObject.java @@ -0,0 +1,98 @@ +package cn.hutool.core.compiler; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.charset.Charset; + +import javax.tools.SimpleJavaFileObject; + +import cn.hutool.core.io.IoUtil; + +/** + * Java 源码文件对象 + * + * @author lzpeng + * @see JavaSourceCompilerBak#getJavaFileObjectByJavaFile(java.io.File) + * @see JavaSourceCompilerBak#getJavaFileObjectByZipOrJarFile(java.io.File) + * @see JavaSourceCompilerBak#getJavaFileObject(java.util.Map) + * @see com.sun.tools.javac.api.ClientCodeWrapper.WrappedFileObject#getCharContent(boolean) + */ +final class JavaSourceFileObject extends SimpleJavaFileObject { + + /** + * 输入流 + */ + private InputStream inputStream; + + /** + * 构造 + * + * @param uri 需要编译的文件uri + * @param kind 需要编译的文件类型 + * @see JavaSourceCompilerBak#getJavaFileObjectByJavaFile(java.io.File) + */ + protected JavaSourceFileObject(URI uri, Kind kind) { + super(uri, kind); + } + + /** + * 构造 + * + * @param name 需要编译的文件名 + * @param inputStream 输入流 + * @param kind 需要编译的文件类型 + * @see JavaSourceCompilerBak#getJavaFileObjectByZipOrJarFile(java.io.File) + */ + protected JavaSourceFileObject(final String name, final InputStream inputStream, final Kind kind) { + super(URI.create("string:///" + name), kind); + this.inputStream = inputStream; + } + + /** + * 构造 + * + * @param className 需要编译的类名 + * @param code 需要编译的类源码 + * @param kind 需要编译的文件类型 + * @see JavaSourceCompilerBak#getJavaFileObject(java.util.Map) + */ + protected JavaSourceFileObject(final String className, final String code, final Kind kind) { + super(URI.create("string:///" + className.replaceAll("\\.", "/") + kind.extension), kind); + this.inputStream = new ByteArrayInputStream(code.getBytes()); + } + + /** + * 获得类源码的输入流 + * + * @return 类源码的输入流 + * @throws IOException IO 异常 + */ + @Override + public InputStream openInputStream() throws IOException { + if (inputStream == null) { + inputStream = toUri().toURL().openStream(); + } + return new BufferedInputStream(inputStream); + } + + /** + * 获得类源码 + * 编译器编辑源码前,会通过此方法获取类的源码 + * + * @param ignoreEncodingErrors 是否忽略编码错误 + * @return 需要编译的类的源码 + * @throws IOException IO异常 + * @see com.sun.tools.javac.api.ClientCodeWrapper.WrappedFileObject#getCharContent(boolean) + */ + @Override + public CharSequence getCharContent(final boolean ignoreEncodingErrors) throws IOException { + final InputStream in = openInputStream(); + final String code = IoUtil.read(in, Charset.defaultCharset()); + IoUtil.close(in); + return code; + } + +} \ No newline at end of file diff --git a/hutool-core/src/main/java/cn/hutool/core/compiler/package-info.java b/hutool-core/src/main/java/cn/hutool/core/compiler/package-info.java new file mode 100644 index 000000000..a0f3ef2b4 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/compiler/package-info.java @@ -0,0 +1,6 @@ +/** + * 运行时编译java源码,动态从字符串或外部文件加载类 + * + * @author : Lzpeng + */ +package cn.hutool.core.compiler; \ No newline at end of file diff --git a/hutool-core/src/test/java/cn/hutool/core/compiler/JavaSourceCompilerTest.java b/hutool-core/src/test/java/cn/hutool/core/compiler/JavaSourceCompilerTest.java new file mode 100644 index 000000000..b8ce0a6c7 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/compiler/JavaSourceCompilerTest.java @@ -0,0 +1,42 @@ +package cn.hutool.core.compiler; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.ZipUtil; +import org.junit.Assert; +import org.junit.Test; + +import java.io.File; +import java.io.InputStream; + +/** + * Java源码编译器测试 + * + * @author lzpeng + */ +public class JavaSourceCompilerTest { + + /** + * 测试编译Java源码 + */ + @Test + public void testCompile() throws ClassNotFoundException { + final File libFile = ZipUtil.zip(FileUtil.file("lib.jar"), + new String[]{"a/A.class", "a/A$1.class", "a/A$InnerClass.class"}, + new InputStream[]{ + FileUtil.getInputStream("test-compile/a/A.class"), + FileUtil.getInputStream("test-compile/a/A$1.class"), + FileUtil.getInputStream("test-compile/a/A$InnerClass.class") + }); + final ClassLoader classLoader = JavaSourceCompiler.create(null) + .addSource(FileUtil.file("test-compile/b/B.java")) + .addSource("c.C", FileUtil.readUtf8String("test-compile/c/C.java")) + .addLibrary(libFile) + .compile(); + final Class clazz = classLoader.loadClass("c.C"); + Object obj = ReflectUtil.newInstance(clazz); + Assert.assertTrue(String.valueOf(obj).startsWith("c.C@")); + FileUtil.del(libFile); + } + +} \ No newline at end of file diff --git a/hutool-core/src/test/resources/test-compile/a/A.java b/hutool-core/src/test/resources/test-compile/a/A.java new file mode 100644 index 000000000..8424934d5 --- /dev/null +++ b/hutool-core/src/test/resources/test-compile/a/A.java @@ -0,0 +1,24 @@ +package a; + +import cn.hutool.core.lang.ConsoleTable; +import cn.hutool.core.lang.caller.CallerUtil; + +public class A { + private class InnerClass { + } + + public A() { + new InnerClass() {{ + int i = 0; + Class caller = CallerUtil.getCaller(i); + final ConsoleTable t = new ConsoleTable(); + t.addHeader("类名", "类加载器"); + System.out.println("初始化 " + getClass() + " 的调用链为: "); + while (caller != null) { + t.addBody(caller.toString(), caller.getClassLoader().toString()); + caller = CallerUtil.getCaller(++i); + } + t.print(); + }}; + } +} \ No newline at end of file diff --git a/hutool-core/src/test/resources/test-compile/b/B.java b/hutool-core/src/test/resources/test-compile/b/B.java new file mode 100644 index 000000000..84c294dea --- /dev/null +++ b/hutool-core/src/test/resources/test-compile/b/B.java @@ -0,0 +1,8 @@ +package b; +import a.A; + +public class B { + public B() { + new A(); + } +} \ No newline at end of file diff --git a/hutool-core/src/test/resources/test-compile/c/C.java b/hutool-core/src/test/resources/test-compile/c/C.java new file mode 100644 index 000000000..7053de08d --- /dev/null +++ b/hutool-core/src/test/resources/test-compile/c/C.java @@ -0,0 +1,9 @@ +package c; + +import b.B; + +public class C { + public C() { + new B(); + } +} \ No newline at end of file