skip to content

What visibility and modifier constraints must a method satisfy for proxy-based Spring annotations to advise it, and why?

level: middleimportance: should knowfreq 58%

answer

  1. public only, no private/static/final
  2. JDK proxy = interfaces; CGLIB = subclass
  3. CGLIB can't override final / subclass final class
  4. Kotlin final-by-default -> kotlin-spring opens beans
  5. AspectJ weaving lifts all restrictions

basics

~20 s

Proxy-based advice only applies to public methods (and by default the target must be a Spring bean). With CGLIB proxies the class and method can't be final, because CGLIB subclasses the target to override methods. Private, static, and final methods aren't advised.

solid answer

~40 s

Spring uses two proxy strategies. A JDK dynamic proxy implements the bean's interfaces, so only interface (public) methods are advisable. A CGLIB proxy subclasses the target class and overrides methods to insert advice, so it cannot override final classes, final methods, private methods, or static methods. In both cases, @Transactional/@Cacheable/@Async are ignored on private, final, and static methods — and by default Spring only looks at public methods for @Transactional (protected/package-private are silently skipped in proxy mode). The rationale: advice is inserted by wrapping or overriding, which the JVM's access and final rules prohibit for those members. If you need to advise non-public methods, switch to AspectJ weaving, which rewrites bytecode directly and has no such restriction.

code

java · 15 lines
java
@Service
public class ReportService {

    @Cacheable("reports")           // OK: public, non-final -> advised
    public Report load(long id) { ... }

    @Cacheable("reports")           // IGNORED: private method cannot be proxied
    private Report loadInternal(long id) { ... }

    @Transactional                  // IGNORED in proxy mode: not public
    void save(Report r) { ... }

    @Async
    public final void notify(Report r) { ... } // IGNORED with CGLIB: final can't be overridden
}

go deeper

for a junior

Should at least know annotations belong on public methods.

for a middle

Should explain JDK vs CGLIB and why private/final/static aren't advised.

for a senior

Should tie the Kotlin final-by-default issue to kotlin-spring and know the silent-ignore behavior.

for a principal

Should reason about proxy strategy selection (proxyTargetClass, interface design) and when AspectJ weaving is justified.

## Two proxy strategies Spring creates AOP proxies in one of two ways: - **JDK dynamic proxy** — used when the bean implements at least one interface (and Spring is not forced to class-proxy). The proxy is a *new class implementing the same interfaces*. Only methods declared on those interfaces (always public) can be intercepted. - **CGLIB proxy** — used when there is no interface, or when `proxyTargetClass = true`. CGLIB generates a **runtime subclass of your class** and overrides each method to call the advice chain, then `super`. Modern Spring Boot defaults to CGLIB (`proxyTargetClass=true`) for most auto-configuration. ## The constraints and why they exist | Modifier | Advised? | Why | |---|---|---| | **public** | yes | Overridable / on the interface | | **protected / package-private** | proxy: no (skipped for @Transactional); | Spring's proxy-mode metadata scan only considers public methods; CGLIB *could* override protected but Spring's interceptors are wired for public entry points | | **private** | no | Cannot be overridden by a subclass (CGLIB) and not on any interface (JDK) | | **static** | no | Belongs to the class, not the instance; not virtual, cannot be overridden | | **final method** | no (CGLIB), silently | A final method cannot be overridden, so CGLIB cannot insert advice | | **final class** | proxy fails / falls back | CGLIB cannot subclass a final class | For `@Transactional` specifically, the reference docs state that in **proxy mode only public methods** annotated are advised; annotations on protected, private, or package-visible methods are **silently ignored** (no error). ## Symptoms - A `@Cacheable` on a `private` helper never caches. - A Kotlin `@Service` where methods are `final` by default (Kotlin classes/methods are final unless `open`) — this is why the **kotlin-spring** compiler plugin auto-opens `@Component`-annotated classes; without it, CGLIB proxying fails or warns. - A `final class` marked `@Transactional` throws at startup (`Cannot subclass final class`) unless it implements an interface (then JDK proxy is used). ## The escape hatch: AspectJ weaving `mode = AdviceMode.ASPECTJ` (e.g. `@EnableTransactionManagement(mode = ASPECTJ)`) weaves advice into the class bytecode via a compile-time or load-time weaver. Because it edits the method body itself rather than subclassing, it can advise **private, protected, and final** methods, and it also solves self-invocation. Cost: you must configure the AspectJ weaver (spring-instrument agent for LTW). ## Practical guidance - Keep annotated methods **public** and **non-final**. - In Kotlin, rely on the `kotlin-spring` plugin (auto-applied by Spring Boot's Kotlin support) so beans are `open`. - Prefer interface-based design if you want JDK proxies; use `proxyTargetClass=true`/CGLIB when you call concrete-class methods not on the interface.

  • Why do Kotlin Spring beans commonly need the kotlin-spring compiler plugin?
    Kotlin classes and methods are final by default. CGLIB proxying (the Spring Boot default) needs to subclass the bean and override methods, which is impossible on final types. The kotlin-spring (all-open) plugin automatically makes @Component/@Service/@Transactional classes and their methods open so CGLIB can proxy them.
  • How does AspectJ weaving remove these visibility restrictions?
    AspectJ weaves the advice directly into the method bytecode (compile-time or load-time weaving) instead of subclassing or implementing an interface. Since it modifies the method body itself, it can advise private, protected, and final methods and also intercepts self-invocation.

saying these in an interview costs you the question

  • Claiming private methods can be advised by the default proxy
  • Saying final has no effect on proxying
  • Thinking Spring throws a clear error for every non-public annotated method (it silently ignores most)

context