What is a RepositoryProxyPostProcessor, when in the lifecycle is it invoked, and give real examples of what it contributes.
answer
- postProcess(ProxyFactory, RepositoryInformation)
- Runs at proxy creation, once per repository
- Adds AOP advice before getProxy()
- Transaction interception + exception translation
- CrudMethodMetadata (lock/entity-graph)
basics
~20 sIt is a callback that lets Spring Data add AOP advice to a repository proxy before the proxy is created. During getRepository(), each registered post-processor's postProcess(ProxyFactory, RepositoryInformation) runs. Examples: adding transaction interception and JPA exception translation.
solid answer
~40 s`RepositoryProxyPostProcessor` is a single-method SPI: `postProcess(ProxyFactory factory, RepositoryInformation information)`. Post-processors are registered on the `RepositoryFactorySupport` (via `addRepositoryProxyPostProcessor`), and inside `getRepository(...)` — after the `ProxyFactory` is set up with the target and interfaces but *before* `getProxy()` — the factory invokes every post-processor in order, giving each a chance to add `Advisor`/`Advice` to the proxy or inspect repository metadata. This is the extension point through which cross-cutting concerns are layered onto repositories: `TransactionalRepositoryProxyPostProcessor` (adds transaction interception so CRUD methods run in transactions), `PersistenceExceptionTranslationRepositoryProxyPostProcessor` (translates JPA/persistence exceptions to Spring's DataAccessException hierarchy), `CrudMethodMetadataPostProcessor` (exposes per-call CRUD method metadata like LockModeType), and `ExposeInvocationInterceptor` support. It runs once per repository at proxy-creation time, not per method call.
code
java · 26 lines// A custom post-processor that times every repository method call by
// weaving an interceptor into each repository proxy at creation time.
public class TimingRepositoryProxyPostProcessor implements RepositoryProxyPostProcessor {
@Override
public void postProcess(ProxyFactory factory, RepositoryInformation information) {
// Runs ONCE per repository, before factory.getProxy().
// 'information' tells us the interface/domain type if we want to filter.
factory.addAdvice((MethodInterceptor) invocation -> {
long start = System.nanoTime();
try {
return invocation.proceed();
} finally {
long micros = (System.nanoTime() - start) / 1_000;
// record metric: information.getRepositoryInterface() + method + micros
}
});
}
}
// Register it on the factory (e.g. via a RepositoryFactoryCustomizer bean):
@Bean
RepositoryFactoryCustomizer timingCustomizer() {
return factory -> factory.addRepositoryProxyPostProcessor(
new TimingRepositoryProxyPostProcessor());
}go deeper
Usually unaware of this SPI.
May know 'something adds transactions' but not name the mechanism.
Names the SPI, its creation-time lifecycle, and concrete built-ins (transaction, exception translation).
Reasons about advisor ordering, custom weaving for cross-cutting concerns, and interaction with the transaction interceptor.
**The interface.** `RepositoryProxyPostProcessor` (package `org.springframework.data.repository.core.support`) is a functional SPI: ```java void postProcess(ProxyFactory factory, RepositoryInformation repositoryInformation); ``` - `ProxyFactory` — the Spring AOP builder for *this* repository's proxy; you can `addAdvice(...)`/`addAdvisor(...)` to layer in interceptors. - `RepositoryInformation` — metadata about the repository being built: the interface type, domain type, id type, which methods are base vs. query vs. custom. Lets a post-processor decide *whether* and *how* to add advice. **Lifecycle — exactly when it runs.** Post-processors are collected on the `RepositoryFactorySupport`. The factory itself calls `addRepositoryProxyPostProcessor(...)` for its built-ins, and `RepositoryFactoryBeanSupport` adds framework ones (e.g. the exception-translation processor) before the factory builds anything. Then, inside `RepositoryFactorySupport.getRepository(...)`: 1. metadata/`RepositoryInformation` computed, 2. target (base impl + fragments) created, 3. a `ProxyFactory` configured with the target and interfaces, 4. **each registered `RepositoryProxyPostProcessor.postProcess(factory, information)` invoked in registration order,** 5. the core `QueryExecutorMethodInterceptor` added, 6. `factory.getProxy()` produces the JDK proxy. So it is a **creation-time, once-per-repository** hook — *not* invoked on every method call (the advice it *adds* is what runs per call). **Real examples (what actually gets contributed).** - **`TransactionalRepositoryProxyPostProcessor`** (spring-data-commons, used by JPA): adds a transaction interceptor / `TransactionalProxy` marker so base CRUD methods (`save`, `delete`, …) execute within a transaction using the configured `PlatformTransactionManager`. This is *why* `save()` is transactional even though you never annotated it, and why explicit method-level `@Transactional` on repository interfaces is honored. - **`PersistenceExceptionTranslationRepositoryProxyPostProcessor`**: adds a `PersistenceExceptionTranslationInterceptor` so store-specific exceptions (JPA `PersistenceException`, `ConstraintViolationException`, etc.) are translated into Spring's consistent `DataAccessException` hierarchy. - **`CrudMethodMetadataPostProcessor`** (JPA): captures per-invocation CRUD method metadata (e.g. `@Lock` lock mode, `@QueryHints`, `@EntityGraph` on CRUD methods) and exposes it to `SimpleJpaRepository` via a thread-bound proxy. - **`ExposeInvocationInterceptor.INSTANCE`** support: makes the current `MethodInvocation` available (needed by some advisors). **Terms.** - *SPI (Service Provider Interface)*: an interface the framework calls into so behavior can be plugged in. - *Advisor/Advice*: Spring AOP interceptors added to the proxy. - *DataAccessException*: Spring's technology-neutral, unchecked data-access exception hierarchy. **Gotchas / when-to-use.** - You rarely implement this yourself; it's mostly framework-internal. But it *is* the documented extension point if you need to weave custom advice into *every* repository proxy (e.g. metrics/tracing around all repository calls) — you could register a custom `RepositoryProxyPostProcessor` via a `RepositoryFactoryCustomizer` / by extending the factory. - Because it runs at *creation* time, it cannot see call-time arguments; it only shapes the advisor chain. Decisions that depend on the actual method go into the `Advice` it installs, keyed off `RepositoryInformation`. - Ordering matters: advisors added earlier sit further from the target. Adding a post-processor in the wrong order (e.g. relative to the transaction advice) can change whether your interceptor runs inside or outside the transaction. - It is invoked once per repository interface, so N repositories ⇒ N invocations, each with its own `ProxyFactory`.
- Is a RepositoryProxyPostProcessor called on every repository method invocation?No. It runs once per repository at proxy-creation time to shape the advisor chain. The advice it installs (e.g. transaction or timing interceptor) is what executes per method call.
- Which post-processor makes repository CRUD methods transactional without any @Transactional you wrote?TransactionalRepositoryProxyPostProcessor, which adds a transaction interceptor so base methods like save/delete run within a transaction managed by the PlatformTransactionManager.
- How would you add tracing/metrics to every repository call cleanly?Register a custom RepositoryProxyPostProcessor (e.g. via RepositoryFactoryCustomizer) that adds a MethodInterceptor to each ProxyFactory, being mindful of ordering relative to the transaction advice.
saying these in an interview costs you the question
- Saying it runs on every method call
- Thinking it can see method arguments (it only sees creation-time metadata)
- Confusing it with Spring's BeanPostProcessor (different SPI, different lifecycle)