Switch plugin and annotation processor loading to the own implementation of ServiceLoader

'ServiceLoader' in JDK8 leaks file handles (https://bugs.openjdk.java.net/browse/JDK-8156014).
New implementation uses the ZipFile API, it also doesn't operate on the whole classpath which is not often needed.
This commit is contained in:
Yan Zhulanow
2018-10-29 16:12:15 +09:00
parent 11e23ecc70
commit 0ffa901859
8 changed files with 459 additions and 6 deletions
@@ -0,0 +1,78 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.cli.serviceLoaderLite
import org.jetbrains.kotlin.test.TestCaseWithTmpdir
import java.io.File
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import javax.annotation.processing.Processor
abstract class AbstractServiceLoaderLiteTest : TestCaseWithTmpdir() {
protected fun applyForDirAndJar(name: String, vararg entries: Entry, block: (File) -> Unit) {
val zip = writeJar("$name.jar", *entries)
block(zip)
val dir = writeDir(name, *entries)
block(dir)
}
protected fun writeDir(dirName: String, vararg entries: Entry): File {
val dir = File(tmpdir, dirName)
if (dir.exists()) {
throw IllegalStateException("Directory $dirName already exists")
}
dir.mkdir()
for ((name, content) in entries) {
val file = File(dir, name).also { it.parentFile.mkdirs() }
file.writeBytes(content)
}
return dir
}
protected fun writeJar(fileName: String, vararg entries: Entry): File {
val file = File(tmpdir, fileName)
if (file.exists()) {
throw IllegalStateException("File $fileName already exists")
}
file.outputStream().use { os ->
ZipOutputStream(os).use { zos ->
for ((name, content) in entries) {
zos.putNextEntry(ZipEntry(name))
zos.write(content)
}
}
}
return file
}
protected inline fun <reified E : Throwable> assertThrows(block: () -> Unit) {
try {
block()
} catch (e: Throwable) {
if (e !is E) {
fail(E::class.java.name + " exception expected, got " + e.javaClass.name)
}
return
}
fail(E::class.java.name + " exception expected, got nothing")
}
protected data class Entry(val name: String, val content: ByteArray) {
constructor(name: String, content: String) : this(name, content.toByteArray())
}
protected fun processors(content: String) = Entry(
"META-INF/services/" + Processor::class.java.name,
content
)
}
@@ -0,0 +1,116 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.cli.serviceLoaderLite
import org.jetbrains.kotlin.cli.jvm.plugins.ServiceLoaderLite
import org.jetbrains.kotlin.cli.jvm.plugins.ServiceLoaderLite.ServiceLoadingException
import javax.annotation.processing.Processor
class ServiceLoaderLiteTest : AbstractServiceLoaderLiteTest() {
fun testSimple() = applyForDirAndJar("test", processors("test.Foo")) { file ->
val impls = ServiceLoaderLite.findImplementations<Processor>(listOf(file))
assertEquals("test.Foo", impls.single())
}
fun testEmpty() = applyForDirAndJar("test") { file ->
val impls = ServiceLoaderLite.findImplementations<Processor>(listOf(file))
assertEquals(0, impls.size)
}
fun testEmpty2() = applyForDirAndJar("test", Entry("foo", "bar")) { file ->
val impls = ServiceLoaderLite.findImplementations<Processor>(listOf(file))
assertEquals(0, impls.size)
}
fun testEmpty3() = applyForDirAndJar("test", processors("")) { file ->
val impls = ServiceLoaderLite.findImplementations<Processor>(listOf(file))
assertEquals(0, impls.size)
}
fun testSeveralProcessors() {
val processorsContent = buildString { appendln("test.Foo").appendln("test.Bar") }
applyForDirAndJar("test", processors(processorsContent)) { file ->
val impls = ServiceLoaderLite.findImplementations(Processor::class.java, listOf(file))
assertEquals(2, impls.size)
assertTrue("test.Foo" in impls)
assertTrue("test.Bar" in impls)
}
}
fun testSeveralEntries() = applyForDirAndJar("test", processors("test.Foo"), Entry("foo", "bar")) { file ->
val impls = ServiceLoaderLite.findImplementations<Processor>(listOf(file))
assertEquals("test.Foo", impls.single())
}
fun testSeveralJars() {
val jar1 = writeJar("test.jar", processors("test.Foo"))
val jar2 = writeJar("test2.jar", processors("ap.Bar"))
val impls = ServiceLoaderLite.findImplementations<Processor>(listOf(jar1, jar2))
assertEquals(2, impls.size)
assertTrue("test.Foo" in impls)
assertTrue("ap.Bar" in impls)
}
fun testSeveralDirs() {
val dir1 = writeDir("test", processors("test.Foo"))
val dir2 = writeDir("test2", processors("ap.Bar"))
val impls = ServiceLoaderLite.findImplementations<Processor>(listOf(dir1, dir2))
assertEquals(2, impls.size)
assertTrue("test.Foo" in impls)
assertTrue("ap.Bar" in impls)
}
fun testDirAndJar() {
val jar = writeJar("test", processors("test.Foo"))
val dir = writeDir("test2", processors("ap.Bar"))
val impls = ServiceLoaderLite.findImplementations<Processor>(listOf(jar, dir))
assertEquals(2, impls.size)
assertTrue("test.Foo" in impls)
assertTrue("ap.Bar" in impls)
}
fun testParsingError() {
applyForDirAndJar("test", processors("5")) { file ->
assertThrows<ServiceLoadingException> {
ServiceLoaderLite.findImplementations(Processor::class.java, listOf(file))
}
}
}
fun testParsingError2() {
applyForDirAndJar("test", processors("a b c")) { file ->
assertThrows<ServiceLoadingException> {
ServiceLoaderLite.findImplementations(Processor::class.java, listOf(file))
}
}
}
fun testCommentsAndWhitespaces() {
val processorsContent = buildString {
appendln(" test.Foo #comment")
appendln("#comment2")
appendln().appendln()
appendln("test.Bar #anotherComemnt")
appendln("test.Zoo ")
}
applyForDirAndJar("test", processors(processorsContent)) { file ->
val impls = ServiceLoaderLite.findImplementations(Processor::class.java, listOf(file))
assertEquals(3, impls.size)
assertTrue("test.Foo" in impls)
assertTrue("test.Bar" in impls)
assertTrue("test.Zoo" in impls)
}
}
}
@@ -0,0 +1,128 @@
/*
* Copyright 2010-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.cli.serviceLoaderLite
import org.jetbrains.kotlin.cli.jvm.plugins.ServiceLoaderLite
import java.net.URLClassLoader
import kotlin.reflect.KClass
interface Intf
class Component1 : Intf
class Component2 : Intf
class ComponentWithParameters(val a: String) : Intf
class UnrelatedComponent
enum class EnumComponent : Intf
class ServiceLoaderLiteTestWithClassLoader : AbstractServiceLoaderLiteTest() {
class NestedComponent : Intf
inner class InnerComponent : Intf
fun testClassloader1() {
@Suppress("RemoveExplicitTypeArguments")
val entries = arrayOf(impls<Intf>(Component1::class, Component2::class), clazz<Component1>(), clazz<Component2>())
classLoaderTest("test", *entries) { classLoader ->
val impls = ServiceLoaderLite.loadImplementations<Intf>(classLoader)
assertTrue(impls.any { it is Component1 })
assertTrue(impls.any { it is Component2 })
}
}
fun testNestedComponent() {
classLoaderTest("test", impls<Intf>(NestedComponent::class), clazz<NestedComponent>()) { classLoader ->
val impls = ServiceLoaderLite.loadImplementations<Intf>(classLoader)
assertTrue(impls.single() is NestedComponent)
}
}
fun testInnerComponent() {
classLoaderTest("test", impls<Intf>(InnerComponent::class), clazz<InnerComponent>()) { classLoader ->
assertThrows<InstantiationException> {
ServiceLoaderLite.loadImplementations<Intf>(classLoader)
}
}
}
fun testComponentWithParameters() {
classLoaderTest("test", impls<Intf>(ComponentWithParameters::class), clazz<ComponentWithParameters>()) { classLoader ->
assertThrows<InstantiationException> {
ServiceLoaderLite.loadImplementations<Intf>(classLoader)
}
}
}
fun testInterface() {
@Suppress("RemoveExplicitTypeArguments")
classLoaderTest("test", impls<Intf>(Intf::class), clazz<Intf>()) { classLoader ->
assertThrows<InstantiationException> {
ServiceLoaderLite.loadImplementations<Intf>(classLoader)
}
}
}
fun testEnum() {
classLoaderTest("test", impls<Intf>(EnumComponent::class), clazz<EnumComponent>()) { classLoader ->
assertThrows<InstantiationException> {
ServiceLoaderLite.loadImplementations<Intf>(classLoader)
}
}
}
fun testUnrelatedComponent() {
val implsEntry = Entry("META-INF/services/" + Intf::class.java.name, UnrelatedComponent::class.java.name)
classLoaderTest("test", implsEntry, clazz<UnrelatedComponent>()) { classLoader ->
assertThrows<ClassCastException> {
ServiceLoaderLite.loadImplementations<Intf>(classLoader)
}
}
}
fun testNestedClassLoaders() {
val entries1 = arrayOf(impls<Intf>(Component1::class), clazz<Component1>())
val entries2 = arrayOf(impls<Intf>(Component2::class), clazz<Component2>())
var index = 0
classLoaderTest("test" + index++, *entries1) { classLoader1 ->
val impls1 = ServiceLoaderLite.loadImplementations<Intf>(classLoader1)
assertTrue(impls1.single() is Component1)
classLoaderTest("test2" + index++, *entries2, parent = classLoader1) { classLoader2 ->
val impls2 = ServiceLoaderLite.loadImplementations<Intf>(classLoader2)
assertTrue(impls2.single() is Component2)
}
}
}
fun testEmpty() {
val classLoader = URLClassLoader(emptyArray(), ServiceLoaderLiteTestWithClassLoader::class.java.classLoader)
val impls = ServiceLoaderLite.loadImplementations<Intf>(classLoader)
assertTrue(impls.isEmpty())
}
private fun classLoaderTest(name: String, vararg entries: Entry, parent: ClassLoader? = null, block: (URLClassLoader) -> Unit) {
applyForDirAndJar(name, *entries) { file ->
val parentClassLoader = parent ?: ServiceLoaderLiteTestWithClassLoader::class.java.classLoader
val classLoader = URLClassLoader(arrayOf(file.toURI().toURL()), parentClassLoader)
block(classLoader)
}
}
private inline fun <reified T : Any> clazz() = Entry(T::class.java.name.replace('.', '/'), bytecode(T::class.java))
private fun bytecode(clazz: Class<*>): ByteArray {
val resourcePath = clazz.name.replace('.', '/') + ".class"
return clazz.classLoader.getResource(resourcePath).readBytes()
}
private inline fun <reified Intf : Any> impls(vararg impls: KClass<out Intf>): Entry {
val content = buildString {
for (impl in impls) {
appendln(impl.java.name)
}
}
return Entry("META-INF/services/" + Intf::class.java.name, content)
}
}