quarkusio/quarkus · error · RuntimeException
Public method ${methodInfo} cannot be proxied as it is final
Error message
Public method ${methodInfo} cannot be proxied as it is final What it means
During bytecode recording of a proxy class, ProxyFactory.addMethodsOfClass refuses to proxy a public non-static final method, because a generated subclass proxy cannot override final methods. The superclass's final method would leak through unproxied, silently breaking recording semantics, so the factory fails fast.
Source
Thrown at core/deployment/src/main/java/io/quarkus/deployment/proxy/ProxyFactory.java:133
private void addMethodsOfClass(Class<?> clazz) {
addMethodsOfClass(clazz, new HashSet<>());
}
private void addMethodsOfClass(Class<?> clazz, Set<MethodKey> seen) {
for (Method methodInfo : clazz.getDeclaredMethods()) {
MethodKey key = new MethodKey(methodInfo.getReturnType(), methodInfo.getName(), methodInfo.getParameterTypes());
if (seen.contains(key)) {
continue;
}
seen.add(key);
if (methodInfo.getName().equals("finalize") && methodInfo.getParameterCount() == 0) {
continue;
}
int modifiers = methodInfo.getModifiers();
if (Modifier.isPublic(modifiers) && Modifier.isFinal(modifiers) && !Modifier.isStatic(modifiers)
&& clazz != Object.class) {
throw new RuntimeException("Public method " + methodInfo + " cannot be proxied as it is final");
}
if (!Modifier.isStatic(modifiers) &&
!Modifier.isFinal(modifiers) &&
!methodInfo.getName().equals("<init>")) {
methods.add(methodInfo);
}
}
if (clazz.getSuperclass() != null) {
addMethodsOfClass(clazz.getSuperclass(), seen);
}
}
public Class<? extends T> defineClass() {
synchronized (lock) {
if (!classDefined) {
doDefineClass();
if (injectConstructor == null) {
try {View on GitHub (pinned to e1c734241f)
Solutions
- Wrap the value in RuntimeValue<T> in the recorder method signature so only construction is recorded, not method proxying
- Remove final from the offending public method if you own the class
- Use an interface type or a non-final superclass as the recorder return type
- Make the method static if it does not need instance state
Example fix
// before
@Recorder
public MyConfig config() { return new MyConfig(); } // MyConfig has public final methods
// after
@Recorder
public RuntimeValue<MyConfig> config() { return new RuntimeValue<>(new MyConfig()); } Defensive patterns
Strategy: type-guard
Validate before calling
static boolean proxySafe(Class<?> c) {
for (Method m : c.getMethods()) {
if (Modifier.isPublic(m.getModifiers()) && Modifier.isFinal(m.getModifiers())
&& !Modifier.isStatic(m.getModifiers()) && m.getDeclaringClass() != Object.class) {
return false;
}
}
return true;
} Type guard
static boolean proxySafe(Class<?> c) {
for (Method m : c.getMethods()) {
if (Modifier.isPublic(m.getModifiers()) && Modifier.isFinal(m.getModifiers())
&& !Modifier.isStatic(m.getModifiers()) && m.getDeclaringClass() != Object.class) {
return false;
}
}
return true;
} Prevention
- Prefer RuntimeValue<T> for recorder return types you do not control
- Avoid final public methods on recorder-exposed classes
- Add a build-time test that instantiates proxies for recorder return types
When it happens
Trigger: A recorder method return type / proxy target class (other than java.lang.Object) declares a public, non-static final method that ProxyFactory tries to add while building the proxy for recorded invocations.
Common situations: Using a return type in a @Recorder method (not wrapped in RuntimeValue) whose class has final public methods (e.g. final methods on config/record-like classes); library classes finalized for security.
Related errors
- Cannot use ${method} as a recorder method as the return type
- Unknown value of annotation member ${name}
- Method ${method} not implemented
- Invalid proxy passed to recorder. Parameter ${i} of type ${m
- Failed to record call to method ${call.method}
AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05).
Data as JSON: /api/errors/6900b3055836b129.
Report an issue: GitHub.