Skip to content

Commit 088ecfa

Browse files
authored
Merge pull request #837 from graphql-java-kickstart/bugfix/541
Speed up resolver method lookup for large schemas
2 parents 924ad5a + 037e234 commit 088ecfa

2 files changed

Lines changed: 43 additions & 11 deletions

File tree

‎src/main/kotlin/graphql/kickstart/tools/resolver/FieldResolverScanner.kt‎

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
2929

3030
private val allowedLastArgumentTypes = listOfNotNull(DataFetchingEnvironment::class.java, GraphQLContext::class.java, options.contextClass)
3131

32+
private val methodsByNameCache = mutableMapOf<Pair<Class<out Any>, Boolean>, Map<String, List<Method>>>()
33+
3234
fun findFieldResolver(field: FieldDefinition, resolverInfo: ResolverInfo): FieldResolver {
3335
val searches = resolverInfo.getFieldSearches()
3436

@@ -86,29 +88,34 @@ internal class FieldResolverScanner(val options: SchemaParserOptions) {
8688
}
8789

8890
private fun findResolverMethod(field: FieldDefinition, search: Search): Method? {
89-
val methods = getAllMethods(search)
91+
val methodsByName = getMethodsByName(search)
9092
val argumentCount = field.inputValueDefinitions.size + if (search.requiredFirstParameterType != null) 1 else 0
9193
val name = field.name
94+
val capitalizedName = name.replaceFirstChar(Char::titlecase)
9295

9396
// Check for the following one by one:
9497
// 1. Method with exact field name
9598
// 2. Method that returns a boolean with "is" style getter
9699
// 3. Method with "get" style getter
97100
// 4. Method with "getField" style getter
98101
// 5. Method with "get" style getter with the field name converted from snake_case to camelCased. ex: key_ops -> getKeyOps()
99-
return methods.find {
100-
it.name == name && verifyMethodArguments(it, argumentCount, search)
101-
} ?: methods.find {
102-
(isBoolean(field.type) && it.name == "is${name.replaceFirstChar(Char::titlecase)}") && verifyMethodArguments(it, argumentCount, search)
103-
} ?: methods.find {
104-
it.name == "get${name.replaceFirstChar(Char::titlecase)}" && verifyMethodArguments(it, argumentCount, search)
105-
} ?: methods.find {
106-
it.name == "getField${name.replaceFirstChar(Char::titlecase)}" && verifyMethodArguments(it, argumentCount, search)
107-
} ?: methods.find {
108-
it.name == "get${name.snakeToCamelCase()}" && verifyMethodArguments(it, argumentCount, search)
102+
return listOfNotNull(
103+
name,
104+
if (isBoolean(field.type)) "is$capitalizedName" else null,
105+
"get$capitalizedName",
106+
"getField$capitalizedName",
107+
"get${name.snakeToCamelCase()}"
108+
).firstNotNullOfOrNull { methodName ->
109+
methodsByName[methodName]?.find { verifyMethodArguments(it, argumentCount, search) }
109110
}
110111
}
111112

113+
// root fields are searched on every root resolver, so each class's methods are only indexed once
114+
private fun getMethodsByName(search: Search): Map<String, List<Method>> =
115+
methodsByNameCache.getOrPut(search.type.unwrap() to search.isSubscription) {
116+
getAllMethods(search).groupBy { it.name }
117+
}
118+
112119
private fun getAllMethods(search: Search): List<Method> {
113120
val type = search.type.unwrap()
114121
val declaredMethods = type.declaredNonProxyMethods

‎src/test/kotlin/graphql/kickstart/tools/FieldResolverScannerTest.kt‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,19 @@ class FieldResolverScannerTest {
9898
Locale.setDefault(default)
9999
}
100100

101+
@Test
102+
fun `scanner finds field resolver methods in priority order`() {
103+
val resolverInfo = RootResolverInfo(listOf(PriorityQuery()), options)
104+
105+
fun methodName(name: String, type: String) =
106+
(scanner.findFieldResolver(FieldDefinition(name, TypeName(type)), resolverInfo) as MethodFieldResolver).method.name
107+
108+
assertEquals(methodName("name", "String"), "name")
109+
assertEquals(methodName("active", "Boolean"), "isActive")
110+
assertEquals(methodName("enabled", "String"), "getEnabled")
111+
assertEquals(methodName("count", "Int"), "getFieldCount")
112+
}
113+
101114
class RootQuery1 : GraphQLQueryResolver {
102115
fun field1() {}
103116
}
@@ -118,6 +131,18 @@ class FieldResolverScannerTest {
118131
fun getId(): HullType = HullType()
119132
}
120133

134+
class PriorityQuery : GraphQLQueryResolver {
135+
fun name(): String = "name"
136+
fun getName(): String = "name"
137+
fun getFieldName(): String = "name"
138+
fun active(flag: Boolean): Boolean = flag
139+
fun isActive(): Boolean = true
140+
fun getActive(): Boolean = true
141+
fun isEnabled(): Boolean = true
142+
fun getEnabled(): String = "enabled"
143+
fun getFieldCount(): Int = 1
144+
}
145+
121146
class HullType
122147

123148
open class ParentPropertyQuery {

0 commit comments

Comments
 (0)