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

  1. Wrap the value in RuntimeValue<T> in the recorder method signature so only construction is recorded, not method proxying
  2. Remove final from the offending public method if you own the class
  3. Use an interface type or a non-final superclass as the recorder return type
  4. 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

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


AI-assisted analysis of quarkusio/quarkus@e1c734241f (2026-09-05). Data as JSON: /api/errors/6900b3055836b129. Report an issue: GitHub.