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="cordform-common_main" 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_test" target="1.8" />
<module name="demobench_main" target="1.8" />
@ -67,8 +70,12 @@
<module name="node_test" target="1.8" />
<module name="notary-demo_main" 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_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_main" target="1.8" />
<module name="rpc_smokeTest" target="1.8" />
@ -109,4 +116,4 @@
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_STRING" value="-parameters" />
</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.cert
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.services.NotaryChangeHandler
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.utilities.*
import net.corda.node.utilities.AddOrRemove.ADD
import net.corda.nodeapi.internal.serialization.DefaultWhitelist
import org.apache.activemq.artemis.utils.ReusableLatch
import org.slf4j.Logger
import rx.Observable
@ -148,20 +149,20 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
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 {
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" }
CordappLoader.createDevMode(System.getProperty("net.corda.node.cordapp.scan.package"))
CordappLoader.createDevMode(scanPackage)
} else {
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. */
@Volatile
var started = false
@ -213,8 +214,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
installCordaServices()
registerCordappFlows()
_services.rpcFlows += cordappLoader.findRPCFlows()
registerCustomSchemas(cordappLoader.findCustomSchemas())
_services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows }
registerCustomSchemas(cordappLoader.cordapps.flatMap { it.customSchemas }.toSet())
runOnStop += network::stop
}
@ -232,7 +233,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
private class ServiceInstantiationException(cause: Throwable?) : Exception(cause)
private fun installCordaServices() {
cordappLoader.findServices(info).forEach {
cordappLoader.cordapps.flatMap { it.filterEnabledServices(info) }.map {
try {
installCordaService(it)
} catch (e: NoSuchMethodException) {
@ -274,7 +275,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
}
private fun registerCordappFlows() {
cordappLoader.findInitiatedFlows()
cordappLoader.cordapps.flatMap { it.initiatedFlows }
.forEach {
try {
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.scanner.ScanResult
import net.corda.core.contracts.Contract
import net.corda.core.flows.ContractUpgradeFlow
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.StartableByRPC
import net.corda.core.internal.*
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.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 net.corda.node.internal.classloading.requireAnnotation
import java.lang.reflect.Modifier
import java.net.JarURLConnection
import java.net.URI
import java.net.URL
import java.net.URLClassLoader
import java.nio.file.Path
import java.nio.file.Paths
import java.util.*
import java.util.stream.Collectors
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>) {
val appClassLoader: ClassLoader = javaClass.classLoader
val scanResult = scanCordapps()
class CordappLoader private constructor(private val cordappJarPaths: List<URL>) {
val cordapps: List<Cordapp> by lazy { loadCordapps() }
@VisibleForTesting
internal val appClassLoader: ClassLoader = javaClass.classLoader
companion object {
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.
*/
fun createDefault(baseDir: Path): CordappLoader {
val pluginsDir = baseDir / "plugins"
return CordappLoader(if (!pluginsDir.exists()) emptyList<Path>() else pluginsDir.list {
it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.collect(Collectors.toList())
return CordappLoader(if (!pluginsDir.exists()) emptyList<URL>() else pluginsDir.list {
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.
*/
fun createDevMode(scanPackage: String): CordappLoader {
val resource = scanPackage.replace('.', '/')
val paths = javaClass.classLoader.getResources(resource)
val paths = this::class.java.classLoader.getResources(resource)
.asSequence()
.map {
val uri = if (it.protocol == "jar") {
@ -62,43 +72,43 @@ class CordappLoader private constructor (val cordappClassPath: List<Path>) {
} else {
URI(it.toExternalForm().removeSuffix(resource))
}
Paths.get(uri)
uri.toURL()
}
.toList()
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>> {
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
}
private fun loadCordapps(): List<Cordapp> {
return cordappJarPaths.map {
val scanResult = scanCordapp(it)
Cordapp(findContractClassNames(scanResult),
findInitiatedFlows(scanResult),
findRPCFlows(scanResult),
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<*>>> {
return scanResult?.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class)
private fun findServices(scanResult: ScanResult): List<Class<out SerializeAsToken>> {
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
?.groupBy { it.requireAnnotation<InitiatedBy>().value.java }
?.map { (initiatingFlow, initiatedFlows) ->
.groupBy { it.requireAnnotation<InitiatedBy>().value.java }
.map { (initiatingFlow, initiatedFlows) ->
val sorted = initiatedFlows.sortedWith(FlowTypeHierarchyComparator(initiatingFlow))
if (sorted.size > 1) {
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}.")
}
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 {
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)
return found + coreFlows
}
fun findCustomSchemas(): Set<MappedSchema> {
return scanResult?.getClassesWithSuperclass(MappedSchema::class)?.toSet() ?: emptySet()
private fun findContractClassNames(scanResult: ScanResult): List<String> {
return scanResult.getNamesOfClassesImplementing(Contract::class.java)
}
private fun scanCordapps(): ScanResult? {
logger.info("Scanning CorDapps in $cordappClassPath")
return if (cordappClassPath.isNotEmpty())
FastClasspathScanner().addClassLoader(appClassLoader).overrideClasspath(cordappClassPath).scan()
else
null
private fun findPlugins(cordappJarPath: URL): List<CordaPluginRegistry> {
return ServiceLoader.load(CordaPluginRegistry::class.java, URLClassLoader(arrayOf(cordappJarPath), null)).toList()
}
private fun findCustomSchemas(scanResult: ScanResult): Set<MappedSchema> {
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<*>>> {
override fun compare(o1: Class<out FlowLogic<*>>, o2: Class<out FlowLogic<*>>): Int {
return if (o1 == o2) {
0
} else if (o1.isAssignableFrom(o2)) {
1
} else if (o2.isAssignableFrom(o1)) {
-1
} else {
throw IllegalArgumentException("${initiatingFlow.name} has been specified as the initiating flow by " +
return when {
o1 == o2 -> 0
o1.isAssignableFrom(o2) -> 1
o2.isAssignableFrom(o1) -> -1
else -> throw IllegalArgumentException("${initiatingFlow.name} has been specified as the initiating flow by " +
"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>? {
return try {
appClassLoader.loadClass(className) as Class<T>
appClassLoader.loadClass(className).asSubclass(type.java)
} catch (e: ClassCastException) {
logger.warn("As $className must be a sub-type of ${type.java.name}")
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())
}
}