Diagnosing MissingMethodException Caused by @CompileStatic in Groovy
MissingMethodException after adding @CompileStatic? Static compilation bypasses Groovy's dynamic dispatch. Diagnose methodMissing, propertyMissing, and GDK call failures and fix them selectively.
30 Apr 2026, 04:40 UTC

The recognizable condition
Your code worked fine as plain Groovy, but after adding @CompileStatic to a class (or after someone enabled static compilation for performance), the application starts throwing groovy.lang.MissingMethodException or PropertyNotFoundException at runtime — sometimes at startup, sometimes only when a particular code path executes. The confusing part: the method clearly \"exists\" because it is provided dynamically through methodMissing, propertyMissing, an ExpandoMetaClass tweak, or a Groovy GDK extension.
The takeaway is simple: @CompileStatic compiles Groovy to bytecode with Java-style static dispatch. Every method call must resolve at compile time, so the runtime metaprogramming machinery that makes dynamic dispatch work is bypassed. Anything that depends on it will fail. Your job is to find the dynamic call sites and either make them static-safe or move them out of statically compiled code.
Cause and diagnostic table
| Symptom | Likely cause | Where to look |
|---|---|---|
| MissingMethodException for a method never declared in source | methodMissing or GroovyObject.invokeMethod override is ignored under static compilation | Search the class for methodMissing, invokeMethod |
| PropertyNotFoundException on a \"virtual\" property | propertyMissing / getProperty override bypassed | Search for propertyMissing, getProperty(String) |
MissingMethodException on eachWithIndex, findAll, collect | Receiver is statically typed as Object; GDK extension methods cannot be resolved | Check declared types of variables and method parameters |
| Method exists in tests but fails in production | ExpandoMetaClass mutation applied at runtime is invisible to statically compiled call sites | Search for metaClass assignments, often in bootstrap or test setup code |
Ambiguous method errors after adding @Delegate | Known interaction between @CompileStatic and @Delegate in Groovy 2.5.x | Check Groovy version; consider upgrade to 3.x or explicit delegation |
Ordered checks
- Confirm the annotation. Look for
@CompileStaticon the failing class, its superclass, or a package-levelpackage-info.groovy. Also check the build file for a globalconfigScriptthat applies static compilation by default — some Gradle and Maven setups do this quietly. - Reproduce with a minimal call. In a Groovy console or unit test (run on the same classpath and Groovy version as the app), call the failing method on an instance of the class. If it fails only when the caller is also statically compiled, the problem is at the call site, not the definition.
- Search for dynamic dispatch hooks:
methodMissing,propertyMissing,invokeMethod,getProperty,respondsTo, and anymetaClassmutation. Each one is a candidate failure point. - Check receiver types on GDK calls. A line like
def items = row.values; items.eachWithIndex { ... }fails under static compilation ifvaluesis typed asObject. The GDK method exists, but the compiler cannot see it. - Run the type checker explicitly. From the command line, with the Groovy distribution's bin directory on your PATH and your project classpath assembled (no special permissions needed):
Static compilation errors point directly at members that cannot be resolved. Replace the classpath placeholder with your real dependency list. The risk is low — this only compiles to a target directory — but run it in a scratch output folder so you do not mix artifacts with your build output.groovyc --compile-static -cp build/classes:lib/* src/main/groovy/com/example/FailingClass.groovy
Fixes tied to findings
Dynamic behavior is essential: drop @CompileStatic selectively
If the class genuinely needs methodMissing (for example, a DSL builder), remove @CompileStatic from that class only, or switch to @TypeChecked, which checks types at compile time but keeps dynamic runtime dispatch. You can also keep the annotation on the class and exempt one method:
import groovy.transform.CompileStatic\nimport groovy.transform.TypeCheckingMode\n\n@CompileStatic\nclass ReportBuilder {\n @CompileStatic(TypeCheckingMode.SKIP)\n def methodMissing(String name, args) {\n // dynamic DSL handling stays dynamic\n }\n}This is the lowest-risk fix when the dynamic behavior is intentional API surface.
GDK calls on Object-typed receivers: give the compiler a type
Declare or cast the receiver to a concrete type so the extension method resolves:
List<String> names = row.values as List<String>\nnames.eachWithIndex { String n, int i -> println \"$i: $n\" }Prefer fixing the declared type upstream (method return types, generics) over scattering casts; casts hide future type errors.
methodMissing used as a shortcut: replace with explicit methods or interfaces
If methodMissing was only forwarding a fixed set of calls, generate real methods or implement an interface. You keep static compilation and its performance benefit, and callers get compile-time checking.
ExpandoMetaClass mutations: move them out of statically compiled call paths
Runtime metaclass changes are invisible to statically compiled callers. Either make the mutation unnecessary (add the method to the class properly) or ensure all callers of the mutated method are dynamically compiled.
Closure-related failures after upgrading to Groovy 3.x
Groovy 3 changed how closures behave under @CompileStatic; code relying on implicit this or delegate resolution that compiled under 2.x may now fail. Add explicit parameter types to the closure and qualify delegate calls explicitly.
Escalation criteria
Escalate beyond a local fix when: removing @CompileStatic causes a measurable performance regression on a hot path (verify with your existing benchmarks, not assumptions); the dynamic behavior is spread across many classes (for example, a framework-wide DSL); or the failing call is inside a third-party library you cannot modify. In those cases, isolate the dynamic code into a separate module compiled without @CompileStatic and call it from statically compiled code through a plain interface, or rewrite the hotspot in Java. If neither is acceptable, consult the Groovy static compilation documentation for your exact version before committing to an architecture change.
Verifying the fix
Re-run the failing test or request path and confirm the exception is gone. Then run the full suite: switching between @CompileStatic and @TypeChecked can change overload resolution, so adjacent tests may shift. If performance motivated the annotation in the first place, re-run your benchmark and compare against the number you recorded before the change. Finally, re-run groovyc --compile-static on the touched classes to confirm no unresolved members remain. Do not treat \"it compiles\" as sufficient — the original bug compiled too.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.