Skip to main content
spring internals

Repository Proxy Generation and the FactoryBean Pattern

6 min read Chapter 53 of 78

Repository Proxy Generation and the FactoryBean Pattern

The container holds a bean named orderRepository. You inject it. You call methods on it. You never wrote the class. The proxy was built at startup through a chain of indirection that starts with FactoryBean and ends with a JDK dynamic proxy identical in mechanism to the AOP proxies from CH8-S1.

This section traces that chain, step by step.

FactoryBean: The Bean That Returns a Different Bean

Spring’s FactoryBean<T> interface has one central method:

public interface FactoryBean<T> {
    T getObject() throws Exception;
    Class<?> getObjectType();
    default boolean isSingleton() { return true; }
}

When the container encounters a bean definition whose class implements FactoryBean, it does not put the FactoryBean instance into the context directly. Instead, it calls getObject() and registers the returned object as the bean.

This is the mechanism Spring Data uses. JpaRepositoryFactoryBean implements FactoryBean<Repository<T, ID>>. The container calls getObject(), and the return value is the proxy that implements your repository interface.

If you need the factory bean itself (rare), you prefix the bean name with &:

// Returns the proxy (the repository)
OrderRepository repo = context.getBean(OrderRepository.class);

// Returns the JpaRepositoryFactoryBean itself
JpaRepositoryFactoryBean<?, ?, ?> factory =
    (JpaRepositoryFactoryBean<?, ?, ?>) context.getBean("&orderRepository");

The Registration Chain

The proxy creation starts before any beans are instantiated. It begins during bean definition registration.

Step 1: @EnableJpaRepositories Triggers Scanning

@EnableJpaRepositories imports JpaRepositoriesRegistrar, which extends RepositoryBeanDefinitionRegistrarSupport. In Spring Boot, auto-configuration applies this implicitly through JpaRepositoriesAutoConfiguration.

The registrar implements ImportBeanDefinitionRegistrar, which means it runs during the @Configuration class processing phase. It receives the BeanDefinitionRegistry and can add bean definitions programmatically.

Step 2: Interface Scanning

The registrar scans the configured base packages for interfaces that extend Repository or any of its sub-interfaces (CrudRepository, ListCrudRepository, JpaRepository). The scanning uses Spring’s component scanning infrastructure but filters specifically for interfaces, not classes.

For the SaaS backend:

// All of these are discovered
public interface OrderRepository extends JpaRepository<Order, UUID> { }
public interface TenantRepository extends JpaRepository<Tenant, UUID> { }
public interface CustomerRepository extends JpaRepository<Customer, UUID> { }

Step 3: BeanDefinition Registration

For each discovered interface, the registrar creates a BeanDefinition with:

  • Bean class: JpaRepositoryFactoryBean
  • Constructor argument: the repository interface class (OrderRepository.class)
  • Properties: EntityManager reference, query lookup strategy, etc.

This bean definition is added to the BeanDefinitionRegistry. At this point, no proxy exists yet. The container has only recorded that a bean named orderRepository should be created from JpaRepositoryFactoryBean.

Step 4: FactoryBean Instantiation and Proxy Creation

When the container instantiates beans (during refresh()), it creates the JpaRepositoryFactoryBean. The factory bean’s afterPropertiesSet() method triggers the actual proxy creation:

// Simplified flow inside JpaRepositoryFactoryBean
public void afterPropertiesSet() {
    RepositoryFactorySupport factory = createRepositoryFactory();
    this.repository = factory.getRepository(repositoryInterface);
}

The createRepositoryFactory() call creates a JpaRepositoryFactory. The getRepository() call is where the proxy is built.

ProxyFactory Setup

Inside RepositoryFactorySupport.getRepository(), Spring creates a ProxyFactory (the same class from CH8):

// Simplified from RepositoryFactorySupport.getRepository()
ProxyFactory result = new ProxyFactory();
result.setTarget(targetRepository);          // SimpleJpaRepository instance
result.setInterfaces(repositoryInterface);   // OrderRepository.class

// Add method interceptors
result.addAdvice(new QueryExecutorMethodInterceptor(queries));
result.addAdvice(new ImplementationMethodExecutionInterceptor(fragments));

return (T) result.getProxy(classLoader);

The ProxyFactory produces a JDK dynamic proxy because the target is specified as an interface. This is the same decision logic from CH8-S1: interface present means JDK proxy, no interface means CGLIB.

Repository proxies are always JDK dynamic proxies. You declared an interface. The proxy implements that interface. There is no concrete class to subclass.

The Target: SimpleJpaRepository

The proxy’s target object is an instance of SimpleJpaRepository<Order, UUID>. This class provides implementations for all standard CRUD methods:

// SimpleJpaRepository handles:
repo.save(order);           // -> entityManager.persist() or merge()
repo.findById(id);          // -> entityManager.find()
repo.findAll();             // -> JPQL "SELECT x FROM Order x"
repo.deleteById(id);        // -> entityManager.remove()
repo.count();               // -> JPQL "SELECT COUNT(x) FROM Order x"
repo.existsById(id);        // -> query with COUNT check

Each of these methods is annotated with @Transactional in SimpleJpaRepository. When you call repo.save(order), the proxy first passes through transaction interceptors (CH10), then reaches the SimpleJpaRepository.save() method.

QueryExecutorMethodInterceptor: The Custom Method Router

For methods not defined in JpaRepository (your custom query methods), the proxy uses QueryExecutorMethodInterceptor. This interceptor maintains a map of method to query:

// Conceptual structure
Map<Method, RepositoryQuery> queries = new HashMap<>();
// findByTenantIdAndStatus -> PartTreeJpaQuery
// findOrderSummaries      -> SimpleJpaQuery (from @Query)

When the proxy intercepts a call to findByTenantIdAndStatus, the QueryExecutorMethodInterceptor looks up the corresponding RepositoryQuery and executes it. The query was already parsed and validated at startup (covered in CH18-S2).

Proving It: Inspecting the Proxy

In the SaaS backend, add a startup listener to inspect what Spring Data created:

@Component
public class RepositoryInspector implements ApplicationRunner {

    private final OrderRepository orderRepository;

    public RepositoryInspector(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Override
    public void run(ApplicationArguments args) {
        System.out.println("Type: " + orderRepository.getClass().getName());
        System.out.println("Interfaces: " +
            Arrays.toString(orderRepository.getClass().getInterfaces()));

        if (Proxy.isProxyClass(orderRepository.getClass())) {
            InvocationHandler handler = Proxy.getInvocationHandler(orderRepository);
            System.out.println("Handler: " + handler.getClass().getName());
        }
    }
}

Output:

Type: jdk.proxy3.$Proxy142
Interfaces: [interface com.saas.order.repository.OrderRepository,
             interface org.springframework.data.repository.Repository,
             interface org.springframework.transaction.interceptor.TransactionalProxy,
             interface org.springframework.aop.framework.Advised]
Handler: org.springframework.aop.framework.JdkDynamicAopProxy

The proxy implements OrderRepository, the marker interfaces, and Advised (which lets you introspect the AOP configuration at runtime). The invocation handler is JdkDynamicAopProxy, the same class from CH8-S1.

The Failure Mode: Injecting by Concrete Type

// BROKEN: there is no concrete class implementing OrderRepository
@Service
public class OrderService {
    private final SimpleJpaRepository<Order, UUID> orderRepository;

    public OrderService(SimpleJpaRepository<Order, UUID> orderRepository) {
        this.orderRepository = orderRepository;
    }
}

This fails at startup:

No qualifying bean of type 'SimpleJpaRepository<Order, UUID>' available

SimpleJpaRepository is the target inside the proxy, but it is not registered as a bean. The bean in the context is the proxy, and the proxy’s type is OrderRepository (the interface). You cannot inject by the concrete target type because the container does not know about it directly.

Even if you try to inject JpaRepository<Order, UUID>, it works only if there is exactly one repository for Order. With multiple repositories sharing the same generic types, you get ambiguity errors.

// BROKEN: ambiguous if multiple repos share the same entity type
@Service
public class OrderService {
    private final JpaRepository<Order, UUID> orderRepository;
    // ...
}

The Correct Pattern

// CORRECT: inject by the specific interface type
@Service
public class OrderService {
    private final OrderRepository orderRepository;

    public OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
}

Always inject by the repository interface you declared. The proxy implements that interface. The type match is exact. There is no ambiguity.

If you need access to the EntityManager directly (for criteria queries or custom native SQL), inject EntityManager separately:

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final EntityManager entityManager;

    public OrderService(OrderRepository orderRepository, EntityManager entityManager) {
        this.orderRepository = orderRepository;
        this.entityManager = entityManager;
    }
}

The EntityManager injected here is also a proxy (a SharedEntityManagerCreator proxy), but that is a story for CH19.

The pattern is consistent with CH8: proxies implement interfaces. Inject by interface. The proxy does the rest.