Many Spring annotations describe behavior that a proxy applies around a method call. The annotation does not rewrite the method body. A caller holds a proxy, the proxy invokes matching interceptors, and those interceptors decide when or whether to call the target object.
That distinction explains why the same method can behave differently when called through an injected bean and when called directly inside its own class.
The reference determines the call path
Compare two entry points on the same bean. These partial application classes assume Spring's @Service, @Component, @Transactional, and TransactionSynchronizationManager from org.springframework.transaction.support:
@Service
class TransactionProbe {
@Transactional
public boolean insideTransaction() {
return TransactionSynchronizationManager.isActualTransactionActive();
}
public boolean throughThis() {
return this.insideTransaction();
}
}
@Component
class ProbeCaller {
private final TransactionProbe probe;
ProbeCaller(TransactionProbe probe) {
this.probe = probe;
}
public void compare() {
System.out.println(probe.insideTransaction());
System.out.println(probe.throughThis());
}
}
Assume these classes are scanned, proxy-based transaction management is enabled, and a transaction manager such as JdbcTransactionManager is configured with a working datasource. Invoke compare() on a managed ProbeCaller after startup, with no transaction already active. Expected output:
true
false
Both calls use the injected probe reference. Only the first call crosses the proxy at the annotated method. Follow the state changes below:
Injected annotated method
- Caller holds the proxy
ProbeCaller invokes probe.insideTransaction(). No transaction is active before the call.
- The interceptor opens a transaction
The proxy finds transaction metadata on insideTransaction(). The transaction manager begins a transaction before invoking the target.
Fig 1Cross the proxy to enter transaction adviceInspect one part at a timeCaller- Caller
An external caller invokes the proxied operation.
- Proxy
The proxy finds transaction metadata and the transaction manager begins a transaction.
- Target
The target method runs with that transaction active.
The proxy interceptor begins a transaction before invoking the target.
How to read this model
An advice latch before the target execution fixture. The object is a teaching analogy. Inspecting a part highlights its role; it does not execute or simulate the software. Cabinet trays are records, workpieces are data, and latches are checks. Unless an explanation states a quantity, compartments and sample workpieces do not denote data counts. Part dimensions are not a quantitative scale.
- Caller
- The target returns true
The transaction probe observes an active transaction. On normal return, the interceptor commits, releases resources, and returns true to the caller. The caller then prints true.
Self-invoked annotated method
- The unannotated entry point is called
ProbeCaller invokes probe.throughThis(). The reference is still a proxy, but throughThis() has no transaction annotation, so this entry does not open a transaction.
- The target calls itself
Inside throughThis(), this refers to the target. Calling this.insideTransaction() does not return through the proxy, so its transaction interceptor never gets this inner call.
Fig 2A self-call does not cross the proxyInspect one part at a timeInside target- Inside target
throughThis() executes on the target.
- Self-call
this.insideTransaction() calls the target directly without going through the proxy.
- Consequence
This inner call does not begin a transaction through that interceptor. An already-existing outer transaction is a separate question.
Proxy-based transaction advice does not intercept this direct call on this.
How to read this model
An internal return track that stays inside the target fixture. The object is a teaching analogy. Inspecting a part highlights its role; it does not execute or simulate the software. Cabinet trays are records, workpieces are data, and latches are checks. Unless an explanation states a quantity, compartments and sample workpieces do not denote data counts. Part dimensions are not a quantitative scale.
- Inside target
- The target returns false
The probe observes no active transaction. It returns false through throughThis(), and the caller prints false. The annotation is present, but the necessary interception boundary is missing.
Omitting this changes nothing. Self-invocation also bypasses the inner method's cache, async, or method-security interceptor. It does not undo behavior already applied to the outer call.
If a transactional placeOrder method calls an internal method marked REQUIRES_NEW, the outer transaction can still be active. The internal call does not create a separate transaction. The boolean probe would return true in that case, so it cannot prove that REQUIRES_NEW ran. The missing inner boundary is the failure, not necessarily the absence of every transaction.
When two operations need different interception boundaries, put the independently invoked operation on a separate bean and inject it. A programmatic API, such as TransactionTemplate, can also express a local transaction boundary directly. Self-injection and AopContext.currentProxy() hide the design problem behind framework-aware calls.
Objects created with new do not receive proxy behavior merely because their classes contain annotations. Initialization callbacks also run before the final proxy is ready. Do not depend on interception inside @PostConstruct.
JDK and class proxies expose different methods
A JDK dynamic proxy implements interfaces and intercepts calls through those interfaces. It is not an instance of the concrete implementation class. Injecting the interface keeps callers independent of that implementation and avoids concrete-type assumptions.
A class-based proxy is a generated subclass, using Spring's repackaged CGLIB support. It cannot subclass a final class. It cannot advise final or private methods because it cannot override them. An inherited package-private method from another package is also inaccessible for this purpose. Static methods are not instance calls through a proxy.
Spring Framework ordinarily favors interface proxies when suitable interfaces exist. Boot 4.0's AOP auto-configuration defaults to class proxies. spring.aop.proxy-target-class=false selects interface proxying where possible. Other proxy configuration can affect that choice, so an interface's presence alone does not prove that a bean uses a JDK proxy.
Framework 7.0 unifies more proxy processors under global proxy defaults, including async processing. It also supports per-bean proxy selection through @Proxyable. Neither change removes self-invocation or subclassing restrictions.
Visibility depends on the intercepted feature
Do not reduce proxy behavior to either "all methods work" or "only public methods work." The proxy must be able to intercept a method, and the feature must recognize its metadata.
Since Framework 6.0, class-based transaction proxies support protected and package-visible transactional methods by default. Interface-based transactional methods must be public and declared on the proxied interface. This applies to the Framework 7.0 baseline used by Boot 4.0.
Proxy-based caching still expects public annotated methods. Private methods are not a safe place for any proxy-enforced boundary. Public use-case methods on concrete service classes are the least surprising place for annotations shared across these features.
Interceptors need infrastructure and ordering
@Transactional needs transaction-management infrastructure and a transaction manager. @Cacheable needs enabled caching and a cache provider. @Async needs async support and an executor. Method authorization needs @EnableMethodSecurity; the security starter alone does not enable it.
Multiple interceptors can apply to one call. A cache hit can skip the target method, and async advice changes where execution occurs. Verify the intended order when combining caching, transactions, and authorization. A cached result must not bypass an authorization decision that should run for every caller.
AspectJ weaving modifies bytecode rather than relying on these proxy call paths. It can intercept self-invocation, but requires different build or runtime infrastructure. It is not what Boot's ordinary proxy configuration enables.
Continue with transactions, caching, and security.