Add Cordapp class to define CorDapps inside Corda and encapsulate Cordapp loading (#1494)

Introduced a Cordapp class which contains all relevant information about a Cordapp. The Cordapp loader now produces Cordapps instead of lists of classes and services without any relation to the original Cordapp. 

Added new static constructor to CordappLoader to be able to take arbitrary paths to load cordapps from in dev mode. 

Moved the CordappLoader into the cordapp package.
This commit is contained in:
Clinton 2017-09-13 14:07:50 +01:00 committed by GitHub
parent 5504493c8d
commit 1ef4cec0cd
7 changed files with 202 additions and 110 deletions

9
.idea/compiler.xml generated
View File

@ -19,6 +19,9 @@
<module name="corda-webserver_test" target="1.8" /> <module name="corda-webserver_test" target="1.8" />
<module name="cordform-common_main" target="1.8" /> <module name="cordform-common_main" target="1.8" />
<module name="cordform-common_test" target="1.8" /> <module name="cordform-common_test" target="1.8" />
<module name="cordformation_main" target="1.8" />
<module name="cordformation_runnodes" target="1.8" />
<module name="cordformation_test" target="1.8" />
<module name="core_main" target="1.8" /> <module name="core_main" target="1.8" />
<module name="core_test" target="1.8" /> <module name="core_test" target="1.8" />
<module name="demobench_main" target="1.8" /> <module name="demobench_main" target="1.8" />
@ -67,8 +70,12 @@
<module name="node_test" target="1.8" /> <module name="node_test" target="1.8" />
<module name="notary-demo_main" target="1.8" /> <module name="notary-demo_main" target="1.8" />
<module name="notary-demo_test" target="1.8" /> <module name="notary-demo_test" target="1.8" />
<module name="publish-utils_main" target="1.8" />
<module name="publish-utils_test" target="1.8" />
<module name="quasar-hook_main" target="1.8" /> <module name="quasar-hook_main" target="1.8" />
<module name="quasar-hook_test" target="1.8" /> <module name="quasar-hook_test" target="1.8" />
<module name="quasar-utils_main" target="1.8" />
<module name="quasar-utils_test" target="1.8" />
<module name="rpc_integrationTest" target="1.8" /> <module name="rpc_integrationTest" target="1.8" />
<module name="rpc_main" target="1.8" /> <module name="rpc_main" target="1.8" />
<module name="rpc_smokeTest" target="1.8" /> <module name="rpc_smokeTest" target="1.8" />
@ -109,4 +116,4 @@
<component name="JavacSettings"> <component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_STRING" value="-parameters" /> <option name="ADDITIONAL_OPTIONS_STRING" value="-parameters" />
</component> </component>
</project> </project>

View File

@ -31,7 +31,7 @@ import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.NetworkHostAndPort import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.cert import net.corda.core.utilities.cert
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.node.internal.classloading.CordappLoader import net.corda.node.internal.cordapp.CordappLoader
import net.corda.node.internal.classloading.requireAnnotation import net.corda.node.internal.classloading.requireAnnotation
import net.corda.node.services.NotaryChangeHandler import net.corda.node.services.NotaryChangeHandler
import net.corda.node.services.NotifyTransactionHandler import net.corda.node.services.NotifyTransactionHandler
@ -68,6 +68,7 @@ import net.corda.node.services.vault.NodeVaultService
import net.corda.node.services.vault.VaultSoftLockManager import net.corda.node.services.vault.VaultSoftLockManager
import net.corda.node.utilities.* import net.corda.node.utilities.*
import net.corda.node.utilities.AddOrRemove.ADD import net.corda.node.utilities.AddOrRemove.ADD
import net.corda.nodeapi.internal.serialization.DefaultWhitelist
import org.apache.activemq.artemis.utils.ReusableLatch import org.apache.activemq.artemis.utils.ReusableLatch
import org.slf4j.Logger import org.slf4j.Logger
import rx.Observable import rx.Observable
@ -148,20 +149,20 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
CordaX500Name.build(cert.subject).copy(commonName = null) CordaX500Name.build(cert.subject).copy(commonName = null)
} }
/** Fetch CordaPluginRegistry classes registered in META-INF/services/net.corda.core.node.CordaPluginRegistry files that exist in the classpath */
open val pluginRegistries: List<CordaPluginRegistry> by lazy {
ServiceLoader.load(CordaPluginRegistry::class.java).toList()
}
val cordappLoader: CordappLoader by lazy { val cordappLoader: CordappLoader by lazy {
if (System.getProperty("net.corda.node.cordapp.scan.package") != null) { val scanPackage = System.getProperty("net.corda.node.cordapp.scan.package")
if (scanPackage != null) {
check(configuration.devMode) { "Package scanning can only occur in dev mode" } check(configuration.devMode) { "Package scanning can only occur in dev mode" }
CordappLoader.createDevMode(System.getProperty("net.corda.node.cordapp.scan.package")) CordappLoader.createDevMode(scanPackage)
} else { } else {
CordappLoader.createDefault(configuration.baseDirectory) CordappLoader.createDefault(configuration.baseDirectory)
} }
} }
open val pluginRegistries: List<CordaPluginRegistry> by lazy {
cordappLoader.cordapps.flatMap { it.plugins } + DefaultWhitelist()
}
/** Set to true once [start] has been successfully called. */ /** Set to true once [start] has been successfully called. */
@Volatile @Volatile
var started = false var started = false
@ -213,8 +214,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
installCordaServices() installCordaServices()
registerCordappFlows() registerCordappFlows()
_services.rpcFlows += cordappLoader.findRPCFlows() _services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows }
registerCustomSchemas(cordappLoader.findCustomSchemas()) registerCustomSchemas(cordappLoader.cordapps.flatMap { it.customSchemas }.toSet())
runOnStop += network::stop runOnStop += network::stop
} }
@ -232,7 +233,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
private class ServiceInstantiationException(cause: Throwable?) : Exception(cause) private class ServiceInstantiationException(cause: Throwable?) : Exception(cause)
private fun installCordaServices() { private fun installCordaServices() {
cordappLoader.findServices(info).forEach { cordappLoader.cordapps.flatMap { it.filterEnabledServices(info) }.map {
try { try {
installCordaService(it) installCordaService(it)
} catch (e: NoSuchMethodException) { } catch (e: NoSuchMethodException) {
@ -274,7 +275,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
} }
private fun registerCordappFlows() { private fun registerCordappFlows() {
cordappLoader.findInitiatedFlows() cordappLoader.cordapps.flatMap { it.initiatedFlows }
.forEach { .forEach {
try { try {
registerInitiatedFlowInternal(it, track = false) registerInitiatedFlowInternal(it, track = false)

View File

@ -0,0 +1,58 @@
package net.corda.node.internal.cordapp
import net.corda.core.flows.FlowLogic
import net.corda.core.node.CordaPluginRegistry
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.ServiceType
import net.corda.core.schemas.MappedSchema
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor
import java.net.URL
/**
* Defines a CorDapp
*
* @property contractClassNames List of contracts
* @property initiatedFlows List of initiatable flow classes
* @property rpcFlows List of RPC initiable flows classes
* @property servies List of RPC services
* @property plugins List of Corda plugin registries
* @property jarPath The path to the JAR for this CorDapp
*/
data class Cordapp(
val contractClassNames: List<String>,
val initiatedFlows: List<Class<out FlowLogic<*>>>,
val rpcFlows: List<Class<out FlowLogic<*>>>,
val services: List<Class<out SerializeAsToken>>,
val plugins: List<CordaPluginRegistry>,
val customSchemas: Set<MappedSchema>,
val jarPath: URL) {
companion object {
private val logger = loggerFor<Cordapp>()
}
fun filterEnabledServices(info: NodeInfo): List<Class<out SerializeAsToken>> {
return services.filter {
val serviceType = getServiceType(it)
if (serviceType != null && info.serviceIdentities(serviceType).isEmpty()) {
logger.debug {
"Ignoring ${it.name} as a Corda service since $serviceType is not one of our " +
"advertised services"
}
false
} else {
true
}
}
}
private fun getServiceType(clazz: Class<*>): ServiceType? {
return try {
clazz.getField("type").get(null) as ServiceType
} catch (e: NoSuchFieldException) {
logger.warn("${clazz.name} does not have a type field, optimistically proceeding with install.")
null
}
}
}

View File

@ -1,60 +1,70 @@
package net.corda.node.internal.classloading package net.corda.node.internal.cordapp
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult
import net.corda.core.contracts.Contract
import net.corda.core.flows.ContractUpgradeFlow import net.corda.core.flows.ContractUpgradeFlow
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.StartableByRPC import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.CordaPluginRegistry
import net.corda.core.node.services.CordaService import net.corda.core.node.services.CordaService
import net.corda.core.node.services.ServiceType import net.corda.core.node.services.ServiceType
import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.MappedSchema
import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SerializeAsToken
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.node.internal.classloading.requireAnnotation
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
import java.net.JarURLConnection import java.net.JarURLConnection
import java.net.URI import java.net.URI
import java.net.URL
import java.net.URLClassLoader
import java.nio.file.Path import java.nio.file.Path
import java.nio.file.Paths import java.nio.file.Paths
import java.util.* import java.util.*
import java.util.stream.Collectors import java.util.stream.Collectors
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.streams.toList
/** /**
* Handles CorDapp loading and classpath scanning * Handles CorDapp loading and classpath scanning of CorDapp JARs
*
* @property cordappJarPaths The classpath of cordapp JARs
*/ */
class CordappLoader private constructor (val cordappClassPath: List<Path>) { class CordappLoader private constructor(private val cordappJarPaths: List<URL>) {
val appClassLoader: ClassLoader = javaClass.classLoader val cordapps: List<Cordapp> by lazy { loadCordapps() }
val scanResult = scanCordapps()
@VisibleForTesting
internal val appClassLoader: ClassLoader = javaClass.classLoader
companion object { companion object {
private val logger = loggerFor<CordappLoader>() private val logger = loggerFor<CordappLoader>()
/** /**
* Creates the default CordappLoader intended to be used in non-dev or non-test environments. * Creates a default CordappLoader intended to be used in non-dev or non-test environments.
* *
* @param basedir The directory that this node is running in. Will use this to resolve the plugins directory * @param baseDir The directory that this node is running in. Will use this to resolve the plugins directory
* for classpath scanning. * for classpath scanning.
*/ */
fun createDefault(baseDir: Path): CordappLoader { fun createDefault(baseDir: Path): CordappLoader {
val pluginsDir = baseDir / "plugins" val pluginsDir = baseDir / "plugins"
return CordappLoader(if (!pluginsDir.exists()) emptyList<Path>() else pluginsDir.list { return CordappLoader(if (!pluginsDir.exists()) emptyList<URL>() else pluginsDir.list {
it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.collect(Collectors.toList()) it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.map { it.toUri().toURL() }.toList()
}) })
} }
/** /**
* Creates the dev mode CordappLoader intended to only be used in dev or test environments. * Creates a dev mode CordappLoader intended to only be used in test environments.
* *
* @param scanPackage Resolves the JARs that contain scanPackage and use them as the source for * @param scanPackage Resolves the JARs that contain scanPackage and use them as the source for
* the classpath scanning. * the classpath scanning.
*/ */
fun createDevMode(scanPackage: String): CordappLoader { fun createDevMode(scanPackage: String): CordappLoader {
val resource = scanPackage.replace('.', '/') val resource = scanPackage.replace('.', '/')
val paths = javaClass.classLoader.getResources(resource) val paths = this::class.java.classLoader.getResources(resource)
.asSequence() .asSequence()
.map { .map {
val uri = if (it.protocol == "jar") { val uri = if (it.protocol == "jar") {
@ -62,43 +72,43 @@ class CordappLoader private constructor (val cordappClassPath: List<Path>) {
} else { } else {
URI(it.toExternalForm().removeSuffix(resource)) URI(it.toExternalForm().removeSuffix(resource))
} }
Paths.get(uri) uri.toURL()
} }
.toList() .toList()
return CordappLoader(paths) return CordappLoader(paths)
} }
/**
* Creates a dev mode CordappLoader intended only to be used in test environments
*
* @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection
*/
@VisibleForTesting
internal fun createDevMode(scanJars: List<URL>) = CordappLoader(scanJars)
} }
fun findServices(info: NodeInfo): List<Class<out SerializeAsToken>> { private fun loadCordapps(): List<Cordapp> {
fun getServiceType(clazz: Class<*>): ServiceType? { return cordappJarPaths.map {
return try { val scanResult = scanCordapp(it)
clazz.getField("type").get(null) as ServiceType Cordapp(findContractClassNames(scanResult),
} catch (e: NoSuchFieldException) { findInitiatedFlows(scanResult),
logger.warn("${clazz.name} does not have a type field, optimistically proceeding with install.") findRPCFlows(scanResult),
null findServices(scanResult),
} findPlugins(it),
findCustomSchemas(scanResult),
it)
} }
return scanResult?.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class)
?.filter {
val serviceType = getServiceType(it)
if (serviceType != null && info.serviceIdentities(serviceType).isEmpty()) {
logger.debug {
"Ignoring ${it.name} as a Corda service since $serviceType is not one of our " +
"advertised services"
}
false
} else {
true
}
} ?: emptyList<Class<SerializeAsToken>>()
} }
fun findInitiatedFlows(): List<Class<out FlowLogic<*>>> { private fun findServices(scanResult: ScanResult): List<Class<out SerializeAsToken>> {
return scanResult?.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class) return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class)
}
private fun findInitiatedFlows(scanResult: ScanResult): List<Class<out FlowLogic<*>>> {
return scanResult.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class)
// First group by the initiating flow class in case there are multiple mappings // First group by the initiating flow class in case there are multiple mappings
?.groupBy { it.requireAnnotation<InitiatedBy>().value.java } .groupBy { it.requireAnnotation<InitiatedBy>().value.java }
?.map { (initiatingFlow, initiatedFlows) -> .map { (initiatingFlow, initiatedFlows) ->
val sorted = initiatedFlows.sortedWith(FlowTypeHierarchyComparator(initiatingFlow)) val sorted = initiatedFlows.sortedWith(FlowTypeHierarchyComparator(initiatingFlow))
if (sorted.size > 1) { if (sorted.size > 1) {
logger.warn("${initiatingFlow.name} has been specified as the inititating flow by multiple flows " + logger.warn("${initiatingFlow.name} has been specified as the inititating flow by multiple flows " +
@ -106,41 +116,43 @@ class CordappLoader private constructor (val cordappClassPath: List<Path>) {
"specific sub-type for registration: ${sorted[0].name}.") "specific sub-type for registration: ${sorted[0].name}.")
} }
sorted[0] sorted[0]
} ?: emptyList<Class<out FlowLogic<*>>>() }
} }
fun findRPCFlows(): List<Class<out FlowLogic<*>>> { private fun findRPCFlows(scanResult: ScanResult): List<Class<out FlowLogic<*>>> {
fun Class<out FlowLogic<*>>.isUserInvokable(): Boolean { fun Class<out FlowLogic<*>>.isUserInvokable(): Boolean {
return Modifier.isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || Modifier.isStatic(modifiers)) return Modifier.isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || Modifier.isStatic(modifiers))
} }
val found = scanResult?.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class)?.filter { it.isUserInvokable() } ?: emptyList<Class<out FlowLogic<*>>>() val found = scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() }
val coreFlows = listOf(ContractUpgradeFlow.Initiator::class.java) val coreFlows = listOf(ContractUpgradeFlow.Initiator::class.java)
return found + coreFlows return found + coreFlows
} }
fun findCustomSchemas(): Set<MappedSchema> { private fun findContractClassNames(scanResult: ScanResult): List<String> {
return scanResult?.getClassesWithSuperclass(MappedSchema::class)?.toSet() ?: emptySet() return scanResult.getNamesOfClassesImplementing(Contract::class.java)
} }
private fun scanCordapps(): ScanResult? { private fun findPlugins(cordappJarPath: URL): List<CordaPluginRegistry> {
logger.info("Scanning CorDapps in $cordappClassPath") return ServiceLoader.load(CordaPluginRegistry::class.java, URLClassLoader(arrayOf(cordappJarPath), null)).toList()
return if (cordappClassPath.isNotEmpty()) }
FastClasspathScanner().addClassLoader(appClassLoader).overrideClasspath(cordappClassPath).scan()
else private fun findCustomSchemas(scanResult: ScanResult): Set<MappedSchema> {
null return scanResult.getClassesWithSuperclass(MappedSchema::class).toSet()
}
private fun scanCordapp(cordappJarPath: URL): ScanResult {
logger.info("Scanning CorDapp in $cordappJarPath")
return FastClasspathScanner().addClassLoader(appClassLoader).overrideClasspath(cordappJarPath).scan()
} }
private class FlowTypeHierarchyComparator(val initiatingFlow: Class<out FlowLogic<*>>) : Comparator<Class<out FlowLogic<*>>> { private class FlowTypeHierarchyComparator(val initiatingFlow: Class<out FlowLogic<*>>) : Comparator<Class<out FlowLogic<*>>> {
override fun compare(o1: Class<out FlowLogic<*>>, o2: Class<out FlowLogic<*>>): Int { override fun compare(o1: Class<out FlowLogic<*>>, o2: Class<out FlowLogic<*>>): Int {
return if (o1 == o2) { return when {
0 o1 == o2 -> 0
} else if (o1.isAssignableFrom(o2)) { o1.isAssignableFrom(o2) -> 1
1 o2.isAssignableFrom(o1) -> -1
} else if (o2.isAssignableFrom(o1)) { else -> throw IllegalArgumentException("${initiatingFlow.name} has been specified as the initiating flow by " +
-1
} else {
throw IllegalArgumentException("${initiatingFlow.name} has been specified as the initiating flow by " +
"both ${o1.name} and ${o2.name}") "both ${o1.name} and ${o2.name}")
} }
} }
@ -148,7 +160,7 @@ class CordappLoader private constructor (val cordappClassPath: List<Path>) {
private fun <T : Any> loadClass(className: String, type: KClass<T>): Class<out T>? { private fun <T : Any> loadClass(className: String, type: KClass<T>): Class<out T>? {
return try { return try {
appClassLoader.loadClass(className) as Class<T> appClassLoader.loadClass(className).asSubclass(type.java)
} catch (e: ClassCastException) { } catch (e: ClassCastException) {
logger.warn("As $className must be a sub-type of ${type.java.name}") logger.warn("As $className must be a sub-type of ${type.java.name}")
null null

View File

@ -1,38 +0,0 @@
package net.corda.node.classloading
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.node.internal.classloading.CordappLoader
import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import java.net.URLClassLoader
import java.nio.file.Path
import java.nio.file.Paths
class DummyFlow : FlowLogic<Unit>() {
override fun call() { }
}
@InitiatedBy(DummyFlow::class)
class LoaderTestFlow : FlowLogic<Unit>() {
override fun call() { }
}
class CordappLoaderTest {
@Test
fun `test that classes that aren't in cordapps aren't loaded`() {
// Basedir will not be a corda node directory so the dummy flow shouldn't be recognised as a part of a cordapp
val loader = CordappLoader.createDefault(Paths.get("."))
Assert.assertNull(loader.findInitiatedFlows().find { it == LoaderTestFlow::class })
}
@Test
fun `test that classes that are in a cordapp are loaded`() {
val loader = CordappLoader.createDevMode("net.corda.node.classloading")
val initiatedFlows = loader.findInitiatedFlows()
val expectedClass = loader.appClassLoader.loadClass("net.corda.node.classloading.LoaderTestFlow")
Assert.assertNotNull(initiatedFlows.find { it == expectedClass })
}
}

View File

@ -0,0 +1,52 @@
package net.corda.node.cordapp
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.node.internal.cordapp.Cordapp
import net.corda.node.internal.cordapp.CordappLoader
import org.junit.Assert
import org.junit.Test
import java.nio.file.Paths
import org.assertj.core.api.Assertions.assertThat
class DummyFlow : FlowLogic<Unit>() {
override fun call() { }
}
@InitiatedBy(DummyFlow::class)
class LoaderTestFlow : FlowLogic<Unit>() {
override fun call() { }
}
class CordappLoaderTest {
@Test
fun `test that classes that aren't in cordapps aren't loaded`() {
// Basedir will not be a corda node directory so the dummy flow shouldn't be recognised as a part of a cordapp
val loader = CordappLoader.createDefault(Paths.get("."))
assertThat(loader.cordapps).isEmpty()
}
@Test
fun `test that classes that are in a cordapp are loaded`() {
val loader = CordappLoader.createDevMode("net.corda.node.cordapp")
val initiatedFlows = loader.cordapps.first().initiatedFlows
val expectedClass = loader.appClassLoader.loadClass("net.corda.node.cordapp.LoaderTestFlow").asSubclass(FlowLogic::class.java)
assertThat(initiatedFlows).contains(expectedClass)
}
@Test
fun `isolated JAR contains a CorDapp with a contract`() {
val isolatedJAR = CordappLoaderTest::class.java.getResource("isolated.jar")!!
val loader = CordappLoader.createDevMode(listOf(isolatedJAR))
val expectedCordapp = Cordapp(
listOf("net.corda.finance.contracts.isolated.AnotherDummyContract"),
emptyList(),
listOf(loader.appClassLoader.loadClass("net.corda.core.flows.ContractUpgradeFlow\$Initiator").asSubclass(FlowLogic::class.java)),
emptyList(),
emptyList(),
emptySet(),
isolatedJAR)
val expected = arrayOf(expectedCordapp)
Assert.assertArrayEquals(expected, loader.cordapps.toTypedArray())
}
}