What this post covers
This is the nineteenth post in the "Digging into Spring's Source" series. Part 17 looked at who opens and closes a transaction (PlatformTransactionManager, the @Transactional proxy, and TransactionSynchronizationManager, which ties resources to the thread), and Part 18 showed JdbcTemplate finding that thread-bound connection through DataSourceUtils. This time we see two things built on top of that frame: JPA (an ORM such as Hibernate), and reactive data access (R2DBC), where there is no thread at all. With this post, Phase 4 (transactions and data access) of the map in Part 1 is complete.
There are four questions to answer.
- How does JPA's
EntityManagerFactorybecome a bean, and why is it safe for many threads to share the oneEntityManageryou get from@PersistenceContext? - How does
JpaTransactionManagerfill in the frame from Part 17, and how doesJdbcTemplateend up using the same connection underneath it? - Adding
@Repositoryturns JPA exceptions into Spring'sDataAccessException— who does that, and where? - In a reactive world without threads, where does "the current transaction" live?
R2dbcTransactionManagerand the ReactorContext
| Item | Details |
|---|---|
| Source | Spring Framework 7.1.0-SNAPSHOT |
| Commit | f447f3c310 (2026-10-01, main branch) |
| Modules read most | spring-orm, spring-r2dbc, spring-tx (transaction.reactive, dao.support, dao.annotation) |
| Classes read most | LocalContainerEntityManagerFactoryBean, SharedEntityManagerCreator, JpaTransactionManager, PersistenceExceptionTranslationInterceptor, R2dbcTransactionManager, TransactionContextManager |
Every line number and every detail in this post refers to that commit. In other versions the line numbers will differ and some details may too. Part 1 explains how to get the source at the same commit.
My study notes go much deeper. This post keeps only the main thread that someone seeing Spring for the first time can follow. What was left out is named at the end, under "If you want to go deeper".
Words to know first
JPA (Jakarta Persistence API) is the standard interface for mapping Java objects to database tables (ORM, object-relational mapping). It is only an interface, so an implementation has to do the real work; the examples in this post use Hibernate. JPA has two central objects. An EntityManagerFactory is the heavyweight factory you create once, and an EntityManager is the lightweight working handle it produces, with methods such as persist (save) and find (load). JPA's rule is that an EntityManager is not thread-safe.
A persistence unit is a bundle of settings: "for this database, use these entity classes". Traditionally it lives in a META-INF/persistence.xml file, but Spring can do without the file if you just name the packages.
A FactoryBean is a bean that goes into the container but exposes another object it builds as the actual bean — a small factory registered as a bean. Part 3, section 6 covered it. The EntityManagerFactory in this post becomes a bean that way.
Transaction managers and thread-bound resources. Two things from Part 17 to keep in mind. PlatformTransactionManager is the interface that opens (getTransaction) and closes (commit/rollback) a transaction, and its skeleton implementation AbstractPlatformTransactionManager decides whether to open a new one and leaves how to abstract methods such as doBegin (lines 524–532, line 1159). The resources of an open transaction, such as its connection, are kept by TransactionSynchronizationManager in a per-thread store (ThreadLocal) as "key → resource" pairs (lines 75–78). The key is usually the resource factory, such as a DataSource.
Reactive streams and Reactor. Instead of returning a result right away, a method returns "a pipe that values will flow through later" (Mono for 0–1 values, Flux for many), and the real work happens only when somebody subscribes. One piece of work may hop across several threads, which makes ThreadLocal useless. Reactor instead has a small map called the Context that travels with each subscription. R2DBC is JDBC's reactive counterpart: you still get a Connection from a ConnectionFactory, but every method returns a Mono or a Flux.
1. How EntityManagerFactory becomes a bean
A JPA configuration usually looks like this. The essentials from JpaConfig in the example folder ex/u17/:
// An example written for this post (not Spring's code)
@Bean
LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
emf.setDataSource(dataSource);
emf.setPackagesToScan("ex.u17"); // scan a package for @Entity instead of persistence.xml
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
adapter.setGenerateDdl(true); // hibernate.hbm2ddl.auto=update → creates the tables
adapter.setShowSql(false); // turn off SQL logging
emf.setJpaVendorAdapter(adapter);
return emf;
}
@Bean
PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
The @Bean method returns a LocalContainerEntityManagerFactoryBean, yet the second method is injected with an EntityManagerFactory. How do two different types connect? Because the first class is a FactoryBean.
public abstract class AbstractEntityManagerFactoryBean implements
SmartFactoryBean<EntityManagerFactory>, BeanClassLoaderAware, BeanFactoryAware,
BeanNameAware, InitializingBean, SmartInitializingSingleton, DisposableBean,
EntityManagerFactoryInfo, PersistenceExceptionTranslator, Serializable {
This one declaration holds everything in this section and in section 4. Being a SmartFactoryBean<EntityManagerFactory>, the bean that goes into the container is not this object but the EntityManagerFactory returned by getObject() (lines 655–657). Being an InitializingBean, afterPropertiesSet() is called once the properties are set, and that is where the factory is actually built. PersistenceExceptionTranslator is used for exception translation in section 4.
How the factory gets built. Blue boxes are Spring's procedure, grey is our configuration and what Hibernate produced, green is the bean that enters the container.
The persistence unit — why persistence.xml is optional
LocalContainerEntityManagerFactoryBean holds a DefaultPersistenceUnitManager inside (line 102), and setPackagesToScan simply passes through to it (lines 223–225). afterPropertiesSet() first prepares that manager and then determines the persistence unit info (lines 384–391). This is where the manager reads it.
private List<SpringPersistenceUnitInfo> readPersistenceUnitInfos() {
List<SpringPersistenceUnitInfo> infos = new ArrayList<>(1);
String defaultName = this.defaultPersistenceUnitName;
boolean buildDefaultUnit = (this.persistenceConfiguration != null || this.managedTypes != null ||
this.packagesToScan != null || this.mappingResources != null);
boolean foundDefaultUnit = false;
PersistenceUnitReader reader = new PersistenceUnitReader(this.resourcePatternResolver, this.dataSourceLookup);
SpringPersistenceUnitInfo[] readInfos = reader.readPersistenceUnitInfos(this.persistenceXmlLocations);
// ...
if (buildDefaultUnit) {
// ...
infos.add(buildDefaultPersistenceUnitInfo());
// ...
}
return infos;
}
A persistence.xml is read if it exists at the default location (classpath*:META-INF/persistence.xml, line 91), and it is fine if it does not. When packagesToScan is set, buildDefaultUnit is true and Spring builds the default unit itself. Inside that method a PersistenceManagedTypesScanner walks the packages and collects the @Entity classes (lines 557–598, the scanner at lines 572–574). The JPA standard demands an XML file; Spring does the file's job in code instead.
The vendor adapter — where the Hibernate settings come together
AbstractEntityManagerFactoryBean.afterPropertiesSet() takes three things from the JpaVendorAdapter, if there is one.
@Override
public void afterPropertiesSet() throws PersistenceException {
JpaVendorAdapter jpaVendorAdapter = getJpaVendorAdapter();
if (jpaVendorAdapter != null) {
if (this.persistenceProvider == null) {
this.persistenceProvider = jpaVendorAdapter.getPersistenceProvider();
}
PersistenceUnitInfo pui = getPersistenceUnitInfo();
Map<String, ?> vendorPropertyMap = (pui != null ? jpaVendorAdapter.getJpaPropertyMap(pui) :
jpaVendorAdapter.getJpaPropertyMap());
// ...
if (this.jpaDialect == null) {
this.jpaDialect = jpaVendorAdapter.getJpaDialect();
}
}
// ...
this.nativeEntityManagerFactory = buildNativeEntityManagerFactory();
// ...
// Wrap the EntityManagerFactory in a factory implementing all its interfaces.
// This allows interception of createEntityManager methods to return an
// application-managed EntityManager proxy that automatically joins
// existing transactions.
this.entityManagerFactory = createEntityManagerFactoryProxy(this.nativeEntityManagerFactory);
PersistenceProvider— the entry point of the JPA implementation.HibernateJpaVendorAdapterreturns Hibernate's provider, lightly wrapped for Spring (line 88, lines 144–146).- A property map —
setGenerateDdl(true)is translated tohibernate.hbm2ddl.auto=update,setShowSqltohibernate.show_sql(lines 164–188). This is why you do not have to memorise Hibernate's property names. - A
JpaDialect— the object that handles vendor-specific work the JPA standard does not cover: beginning a transaction, getting hold of the JDBC connection, translating exceptions (JpaDialectline 51). For Hibernate it isHibernateJpaDialect(line 78, line 264). It comes back in sections 3 and 4.
Then buildNativeEntityManagerFactory() calls the abstract createNativeEntityManagerFactory() (lines 437–440, line 566), and the implementation in LocalContainerEntityManagerFactoryBean hands the persistence unit info and the property map to the provider and gets the real factory back (lines 423–447, the call at lines 442–443). This is where Hibernate reads the entity classes and creates the tables.
The bean is a proxy
The last line matters. The factory that came back is not exposed as it is; createEntityManagerFactoryProxy() wraps it once more.
protected EntityManagerFactory createEntityManagerFactoryProxy(@Nullable EntityManagerFactory emf) {
Set<Class<?>> ifcs = new LinkedHashSet<>();
Class<?> entityManagerFactoryInterface = this.entityManagerFactoryInterface;
if (entityManagerFactoryInterface != null) {
ifcs.add(entityManagerFactoryInterface);
}
// ...
ifcs.add(EntityManagerFactoryInfo.class);
try {
return (EntityManagerFactory) Proxy.newProxyInstance(this.beanClassLoader,
ClassUtils.toClassArray(ifcs), new ManagedEntityManagerFactoryInvocationHandler(this));
}
This is the JDK dynamic proxy from Part 10. One extra interface goes into the list, EntityManagerFactoryInfo, and through it other Spring code (the transaction manager, the shared proxy) can ask "which DataSource does this factory use?", "what is the JpaDialect?", "what is the native factory?" (EntityManagerFactoryInfo line 75, line 100, line 112). As the source comment above says, it also lets Spring intercept createEntityManager calls.
Easy to get wrong
- What
getBean(EntityManagerFactory.class)gives you is Spring's proxy, not Hibernate's factory. The example output in section 2 printsemf is JDK proxy : true. If you need Hibernate's own API, unwrap it withEntityManagerFactoryInfo.getNativeEntityManagerFactory()or JPA'sunwrap(lines 769–778). - To get the
FactoryBeanitself, ask for&entityManagerFactory. The&rule from Part 3, section 6. Section 4 shows that thisFactoryBeanobject also acts as an exception translator.
2. Why the @PersistenceContext EntityManager is thread-safe
By JPA's rules an EntityManager is not thread-safe. Yet in Spring you put one EntityManager into a field of a singleton bean and let every request thread use it. The answer is that what goes into the field is not a real EntityManager but a shared proxy.
Who injects it — PersistenceAnnotationBeanPostProcessor
@PersistenceContext is handled by one of the BeanPostProcessors we met in Part 6.
public class PersistenceAnnotationBeanPostProcessor implements InstantiationAwareBeanPostProcessor,
DestructionAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor, BeanRegistrationAotProcessor,
PriorityOrdered, BeanFactoryAware, Serializable {
You do not register it yourself. AnnotationConfigUtils.registerAnnotationConfigProcessors, seen in Part 8, registers this post-processor too whenever JPA is on the classpath (that is, when jakarta.persistence.EntityManagerFactory can be loaded) (lines 122–124, lines 177–189). It works just like the AutowiredAnnotationBeanPostProcessor of Part 5: postProcessProperties finds the list of injection points and tells it to inject (lines 399–409), and the place where one injection point decides "what to inject" is getResourceToInject (lines 758–771). For @PersistenceUnit it injects the factory; for @PersistenceContext an EntityManager, and the default path for the latter is this.
private EntityManager resolveEntityManager(@Nullable String requestingBeanName) {
// Obtain EntityManager reference from JNDI?
EntityManager em = getPersistenceContext(this.unitName, false);
if (em == null) {
// ...
emf = findEntityManagerFactory(this.unitName, requestingBeanName);
// ...
// Inject a shared transactional EntityManager proxy.
if (emf instanceof EntityManagerFactoryInfo emfInfo && emfInfo.getEntityManagerInterface() != null) {
// Create EntityManager based on the info's vendor-specific type
// (which might be more specific than the field's type).
em = SharedEntityManagerCreator.createSharedEntityManager(
emf, this.properties, this.synchronizedWithTransaction);
}
// ...
}
return em;
}
The comment gives the answer: "Inject a shared transactional EntityManager proxy." The one building it is SharedEntityManagerCreator, and it is a JDK proxy again (lines 165–178). One more interface, EntityManagerProxy, is appended to the list (line 174); its getTargetEntityManager() lets you pull out "the real object behind this proxy right now" (EntityManagerProxy line 44). The example uses it.
On every call the proxy looks up "the EntityManager of the current transaction"
Every method call on the proxy comes in through one invoke. After a few specially handled methods, this is the main part.
@Override
public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args) throws Throwable {
// Invocation on EntityManager interface coming in...
switch (method.getName()) {
// ...
case "close" -> {
// Handle close method: suppress, not valid.
return null;
}
case "getTransaction" -> {
throw new IllegalStateException(
"Not allowed to create transaction on shared EntityManager - " +
"use Spring transactions or EJB CMT instead");
}
}
// Determine current EntityManager: either the transactional one
// managed by the factory or a temporary one for the given invocation.
EntityManager target = EntityManagerFactoryUtils.doGetTransactionalEntityManager(
this.targetFactory, this.properties, this.synchronizedWithTransaction);
// ...
if (transactionRequiringMethods.contains(method.getName())) {
// We need a transactional target now, according to the JPA spec.
// Otherwise, the operation would get accepted but remain unflushed...
if (target == null || (!TransactionSynchronizationManager.isActualTransactionActive() &&
!target.getTransaction().isActive())) {
throw new TransactionRequiredException("No EntityManager with actual transaction available " +
"for current thread - cannot reliably process '" + method.getName() + "' call");
}
}
// Regular EntityManager operations.
boolean newTarget = false;
if (target == null) {
// ...
target = EntityManagerFactoryUtils.createEntityManager(this.targetFactory, this.properties);
newTarget = true;
}
// ...
return invokeMethod(method, args, target, newTarget);
}
spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java, lines 316–414
What the proxy does on each call. The yellow box is where it looks into the thread store from Part 17.
Reading from the top:
close()is ignored andgetTransaction()is refused. If one user closed a shared object or opened a transaction on it directly, every other user would break. Transactions are Spring's job.doGetTransactionalEntityManagerlooks for "theEntityManagerbound to the current thread's transaction". It callsTransactionSynchronizationManager.getResource(emf)— the thread store from Part 17, keyed by the factory (EntityManagerFactoryUtilsline 207). If anEntityManagerHolderis bound, theEntityManagerinside it is returned (line 231); if transaction synchronization is not even active, it returnsnull(lines 253–256).persist,merge,remove,flushand friends throwTransactionRequiredExceptionwhen there is no transaction. The list is at lines 76–82. The comment gives a good reason: accepting the call would leave the change silently unflushed.- Any other method (
find, for example) gets a freshEntityManagermade for this one call when there is no transaction, closed when the call is over (thefinallyininvokeMethod, lines 287–289).
That is why it is thread-safe. The proxy itself holds no state; the real EntityManager exists per thread and per transaction, and the proxy just finds it on every call. It is the same shape as JdbcTemplate in Part 18 finding the thread-bound connection through DataSourceUtils.getConnection. The one that binds the EntityManagerHolder is the JpaTransactionManager of section 3.
Example: outside and inside a transaction
This is ex/u17/SharedEntityManagerExample.java. The example was run with the released Spring Framework 7.0.9 and Hibernate 7.1.36 to confirm the output below (the 7.1.0-SNAPSHOT this series reads was not built and run directly).
// An example written for this post (not Spring's code)
@Component
static class MemberDao {
@PersistenceContext
EntityManager em; // injected by PersistenceAnnotationBeanPostProcessor
@Autowired
EntityManager autowiredEm; // since 7.0: the FactoryBean hands it out via SmartFactoryBean.getObject(Class)
}
// ... in main: outside a transaction
System.out.println("em is JDK proxy : " + Proxy.isProxyClass(dao.em.getClass()));
// ...
try {
((EntityManagerProxy) dao.em).getTargetEntityManager();
}
catch (IllegalStateException ex) {
System.out.println("getTargetEntityManager : " + ex.getMessage());
}
try {
dao.em.persist(new Member("kim"));
}
catch (TransactionRequiredException ex) {
System.out.println("persist : " + ex.getClass().getSimpleName() + " - " + ex.getMessage());
}
// reading works — an EntityManager is created for this one call and closed again
System.out.println("find(1L) : " + dao.em.find(Member.class, 1L));
// ... inside a transaction
TransactionTemplate tx = new TransactionTemplate(ctx.getBean(PlatformTransactionManager.class));
tx.executeWithoutResult(status -> {
// ...
EntityManager t1 = ((EntityManagerProxy) dao.em).getTargetEntityManager();
EntityManager t2 = ((EntityManagerProxy) dao.autowiredEm).getTargetEntityManager();
System.out.println("same target for both fields? " + (t1 == t2));
dao.em.persist(new Member("kim"));
// ...
});
em is JDK proxy : true
em.toString() : Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@185339ed]
em is EntityManagerProxy : true
@Autowired em : Shared EntityManager proxy for target factory [org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean@185339ed]
emf is JDK proxy : true
--- outside transaction
EntityManagerHolder bound to thread? false
getTargetEntityManager : No transactional EntityManager available
persist : TransactionRequiredException - No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call
find(1L) : null
--- inside transaction
EntityManagerHolder bound to thread? true
same target for both fields? true
persist : ok
after commit, find(1L) : Member[1, kim]
Outside a transaction the store is empty (false), asking for the real object fails, persist is refused, but find works. Inside a transaction the store holds an EntityManagerHolder, and the proxies in both fields point at the same real object.
Easy to get wrong: @PersistenceContext versus @Autowired EntityManager
In the output above, @Autowired EntityManager received the same shared proxy. The reason is that the FactoryBean from section 1 implements SmartFactoryBean's getObject(Class), and when asked for the EntityManager type it hands out the shared proxy it built for itself (lines 667–676; the proxy is built at line 425). The container calls that method from FactoryBeanRegistrySupport (lines 202–203). The Javadoc of SmartFactoryBean.getObject(Class) says @since 7.0 (line 63). What @Autowired EntityManager does in earlier versions is not something this post checked in the source.
The two annotations are still not identical. @PersistenceContext has unitName (to choose between several factories), type = EXTENDED (an EntityManager that outlives a transaction) and properties, and getResourceToInject looks at them. EXTENDED takes a different path (ExtendedEntityManagerCreator, lines 810–822) that this post does not cover.
3. JpaTransactionManager — filling in the frame from Part 17
JpaTransactionManager extends AbstractPlatformTransactionManager from Part 17 and fills in doGetTransaction, doBegin, doCommit and doRollback (lines 117–118). The parent still decides, from the propagation setting, whether to open a new transaction or join one, so the only thing left to read is "what does opening mean for JPA?".
doGetTransaction() looks into the thread store with two keys — the factory (EntityManagerFactory) for an EntityManagerHolder, and the DataSource for a ConnectionHolder (lines 357–378). You do not have to configure the DataSource; afterPropertiesSet() asks the EntityManagerFactoryInfo from section 1 for it (lines 334–346). Why two keys is answered in doBegin.
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
JpaTransactionObject txObject = (JpaTransactionObject) transaction;
if (txObject.hasConnectionHolder() && !txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
throw new IllegalTransactionStateException(
"Pre-bound JDBC Connection found! JpaTransactionManager does not support " +
"running within DataSourceTransactionManager if told to manage the DataSource itself. " +
"It is recommended to use a single JpaTransactionManager for all transactions " +
"on a single DataSource, no matter whether JPA or JDBC access.");
}
try {
if (!txObject.hasEntityManagerHolder() ||
txObject.getEntityManagerHolder().isSynchronizedWithTransaction()) {
EntityManager newEm = createEntityManagerForTransaction();
// ...
txObject.setEntityManagerHolder(new EntityManagerHolder(newEm), true);
}
EntityManager em = txObject.getEntityManagerHolder().getEntityManager();
// Delegate to JpaDialect for actual transaction begin.
int timeoutToUse = determineTimeout(definition);
Object transactionData = getJpaDialect().beginTransaction(em,
new JpaTransactionDefinition(definition, timeoutToUse, txObject.isNewEntityManagerHolder()));
// ...
// Register the JPA EntityManager's JDBC Connection for the DataSource, if set.
if (getDataSource() != null) {
ConnectionHandle conHandle = getJpaDialect().getJdbcConnection(em, definition.isReadOnly());
if (conHandle != null) {
ConnectionHolder conHolder = new ConnectionHolder(conHandle);
// ...
TransactionSynchronizationManager.bindResource(getDataSource(), conHolder);
txObject.setConnectionHolder(conHolder);
}
// ...
}
// Bind the entity manager holder to the thread.
if (txObject.isNewEntityManagerHolder()) {
TransactionSynchronizationManager.bindResource(
obtainEntityManagerFactory(), txObject.getEntityManagerHolder());
}
txObject.getEntityManagerHolder().setSynchronizedWithTransaction(true);
}
// ...
}
spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java, lines 385–459
One transaction binds two things to the thread. The two yellow boxes are the entries in the store; the two grey boxes below are the parties that look them up, each with its own key.
- A fresh
EntityManageris opened for this transaction (line 400, lines 470–484). This is the real object the shared proxy of section 2 will find. JpaDialect.beginTransactionstarts the JPA transaction (line 411). The Hibernate dialect applies the isolation level or read-only flag to the connection if one was requested, then makes the standard callentityManager.getTransaction().begin()(HibernateJpaDialectline 147).- The JDBC connection Hibernate is holding is taken out and bound under the
DataSourcekey (lines 423–433). This is the reason for the second key.HibernateJpaDialect.getJdbcConnectionunwraps the session and builds a handle that returns the physical connection inside it.
private static class HibernateConnectionHandle implements ConnectionHandle {
private final SessionImplementor session;
public HibernateConnectionHandle(SessionImplementor session) {
this.session = session;
}
@Override
public Connection getConnection() {
return this.session.getJdbcCoordinator().getLogicalConnection().getPhysicalConnection();
}
}
spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java, lines 263–275
- The
EntityManagerHolderis bound under the factory key (lines 444–446).
Now when Part 18's JdbcTemplate calls DataSourceUtils.getConnection(dataSource) on the same thread, it looks in the store under the DataSource key (DataSourceUtils lines 103–106) and receives the very connection Hibernate is using. That is why JPA and SQL can be mixed in one transaction and still commit or roll back together. The class Javadoc makes the same point (lines 80–85).
doCommit calls EntityTransaction.commit() and translates any exception through the JpaDialect before rethrowing (lines 544–567). doRollback calls rollback() (lines 570–596), and afterwards doCleanupAfterCompletion unbinds both keys and closes the EntityManager (lines 609–646).
Example: does JdbcTemplate use the same connection?
The core of ex/u17/JpaJdbcSameConnectionExample.java.
// An example written for this post (not Spring's code)
tx.executeWithoutResult(status -> {
// ...
System.out.println("EntityManagerHolder bound? " + TransactionSynchronizationManager.hasResource(emf));
System.out.println("ConnectionHolder bound? " + TransactionSynchronizationManager.hasResource(ds));
Connection fromJdbc = DataSourceUtils.getConnection(ds); // the path JdbcTemplate takes
Connection fromJpa = em.unwrap(Session.class).doReturningWork(con -> con); // the Hibernate session's connection
System.out.println("same JDBC Connection? " + (fromJdbc == fromJpa));
em.persist(new Member("via-jpa"));
em.flush(); // send the INSERT now
jdbc.update("insert into member(id, name) values (?, ?)", 100, "via-jdbc");
System.out.println("rows JdbcTemplate sees : "
+ jdbc.queryForObject("select count(*) from member", Integer.class));
status.setRollbackOnly();
});
transactionManager : JpaTransactionManager
dataSource auto-detected from EMF? true
--- outside transaction: ConnectionHolder bound? false
--- inside transaction
EntityManagerHolder bound? true
ConnectionHolder bound? true
same JDBC Connection? true
rows JdbcTemplate sees : 2
--- after rollback
rows : 0
ConnectionHolder bound? false
Both keys are bound, the Connection obtained by the two routes is the same object, and the row inserted through JPA and the row inserted through SQL disappear together in a single rollback.
Easy to get wrong
- Do not put
JpaTransactionManagerandDataSourceTransactionManageron the sameDataSource. The check at the top ofdoBeginblocks that case, and the exception message states the recommendation outright: use a singleJpaTransactionManagerfor all transactions on oneDataSource, whether the access is JPA or JDBC (lines 389–394). As we just saw,JpaTransactionManagerbinds the JDBC key as well, so a second manager is not needed. - Nested transactions (savepoints) are off by default. The class Javadoc explains that a savepoint applies only to the JDBC connection, not to the JPA
EntityManagerand its cache (lines 94–101). - Whether the JDBC connection can be taken out depends on the dialect. The default
DefaultJpaDialect.getJdbcConnectionreturnsnull(lines 146–149), in which casedoBeginonly leaves a debug message saying the JPA transaction is not exposed as a JDBC one (lines 435–439). One more reason to set a vendor adapter.
4. Exception translation — what does @Repository do?
Part 18 showed JdbcTemplate turning SQLException into Spring's DataAccessException hierarchy. JPA code does not go through JdbcTemplate, so it needs a different mechanism, and that is @Repository. The annotation itself is just @Component with an extra meaning (Repository lines 57–61); the real work happens once you register a post-processor called PersistenceExceptionTranslationPostProcessor as a bean.
The path an exception takes out of a @Repository bean's method. The red box is the case where it is passed through untranslated.
- The post-processor wraps the bean in a proxy. It is not the auto-proxy creator of Part 11 but a smaller family (
AbstractAdvisingBeanPostProcessor) that carries exactly one advisor. In the after-initialisation hook it builds a proxy withProxyFactorywhenever the advisor applies to the class (lines 88–133; the check isAopUtils.canApply, line 170). The advisor is created insetBeanFactory(PersistenceExceptionTranslationPostProcessorlines 82–90). - Advisor = pointcut + interceptor. The pointcut is an
AnnotationMatchingPointcutasking "does the class carry@Repository?", and the advice isPersistenceExceptionTranslationInterceptor(PersistenceExceptionTranslationAdvisorlines 68–73). The annotation type can be changed, so a non-Spring annotation can get the same effect (lines 76–79). - The interceptor catches the exception and translates it.
@Override
public @Nullable Object invoke(MethodInvocation mi) throws Throwable {
try {
return mi.proceed();
}
catch (RuntimeException ex) {
// Let it throw raw if the type of the exception is on the throws clause of the method.
if (!this.alwaysTranslate && ReflectionUtils.declaresException(mi.getMethod(), ex.getClass())) {
throw ex;
}
else {
PersistenceExceptionTranslator translator = this.persistenceExceptionTranslator;
if (translator == null) {
// ...
translator = detectPersistenceExceptionTranslators(this.beanFactory);
// ...
this.persistenceExceptionTranslator = translator;
}
throw DataAccessUtils.translateIfNecessary(ex, translator);
}
}
}
Unless you specify one, the translator is every PersistenceExceptionTranslator bean in the container, chained into one (lines 169–173). Look back at the declaration in section 1: AbstractEntityManagerFactoryBean implements exactly that interface (line 101). So configuring JPA gave you a translator for free, and that translator delegates to the JpaDialect (lines 579–582). The interface has a single method: return a DataAccessException if you can translate, null if you do not recognise the exception (PersistenceExceptionTranslator lines 35–56). On null, the original exception goes out as it is (DataAccessUtils lines 342–348).
- The Hibernate dialect translates in two steps. A
HibernateExceptiongoes through Hibernate's own table (constraint violation →DataIntegrityViolationException, and so on,HibernateExceptionTranslatorlines 167–169); anything else goes to the standard JPA table (lines 103–112). This is the JPA table.
public static @Nullable DataAccessException convertJpaAccessExceptionIfPossible(RuntimeException ex) {
// ...
if (ex instanceof EntityNotFoundException entityNotFoundException) {
return new JpaObjectRetrievalFailureException(entityNotFoundException);
}
if (ex instanceof NoResultException) {
return new EmptyResultDataAccessException(ex.getMessage(), 1, ex);
}
if (ex instanceof NonUniqueResultException) {
return new IncorrectResultSizeDataAccessException(ex.getMessage(), 1, ex);
}
// ...
if (ex instanceof TransactionRequiredException) {
return new InvalidDataAccessApiUsageException(ex.getMessage(), ex);
}
// If we have another kind of PersistenceException, throw it.
if (ex instanceof PersistenceException) {
return new JpaSystemException(ex);
}
// If we get here, we have an exception that resulted from user code,
// rather than the persistence provider, so we return null to indicate
// that translation should not occur.
return null;
}
spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java, lines 477–525
Example: the same code, different annotation
ex/u17/RepositoryExceptionTranslationExample.java registers two classes with identical methods, one with @Repository and one with @Component.
// An example written for this post (not Spring's code)
@Repository
static class MemberRepository {
@PersistenceContext
EntityManager em;
Member findByName(String name) {
return em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getSingleResult(); // jakarta.persistence.NoResultException if there is no row
}
void save(Member member) {
em.persist(member);
}
}
@Repository bean class : RepositoryExceptionTranslationExample$MemberRepository$$SpringCGLIB$$0
@Component bean class : PlainDao
--- getSingleResult() with no row
@Component : jakarta.persistence.NoResultException
@Repository : org.springframework.dao.EmptyResultDataAccessException (cause: NoResultException)
--- persist() outside a transaction
@Component : jakarta.persistence.TransactionRequiredException
@Repository : org.springframework.dao.InvalidDataAccessApiUsageException (cause: TransactionRequiredException)
--- duplicate name, failure surfaces at commit
[main] WARN org.hibernate.orm.jdbc.error - HHH000247: ErrorCode: 23505, SQLState: 23505
[main] WARN org.hibernate.orm.jdbc.error - Unique index or primary key violation: "PUBLIC.UK9ESVGIKRMTI1V7CI6V453IMDC_INDEX_8 ON PUBLIC.MEMBER(NAME NULLS FIRST) VALUES ( /* 1 */ 'dup' )"; SQL statement:
insert into member (name,id) values (?,?) [23505-232]
@Repository : org.springframework.dao.DataIntegrityViolationException (cause: ConstraintViolationException)
Only the @Repository bean's class name ends in $$SpringCGLIB$$, marking it as a proxy, and the same calls produce Spring exceptions instead of JPA ones. The two WARN lines in the middle are Hibernate's logging.
Easy to get wrong
- The last case (the duplicate name) was not translated by the interceptor.
persistsucceeds and theINSERTonly goes out at commit, so the exception is thrown inJpaTransactionManager.doCommit, afterrepo.save()has already returned. That method translates with the sameJpaDialect(lines 554–560), so the result is the sameDataIntegrityViolationException, but this one would have been translated even without@Repository. - An exception listed in the method's
throwsclause is not translated (lines 139–141). Declaring it is taken as a deliberate decision to expose that exception. @Repositoryon its own, without the post-processor, does nothing. The annotation is only a marker; the post-processor is what builds the proxy. The last@Beanin the example configuration is that post-processor.
5. Reactive transactions — a world without threads
From here on we are in spring-r2dbc and the transaction.reactive package of spring-tx. Everything up to Part 17 leaned on "the current thread": the transaction manager binds resources to the thread, and JdbcTemplate and the shared proxy look them up there. Reactive code breaks that assumption. One piece of work moves across several threads, and one thread serves many pieces of work in turn. Whatever you put into a ThreadLocal may be gone, or belong to someone else, by the next step.
The interfaces come in pairs
| Blocking (Part 17) | Reactive (this post) |
|---|---|
PlatformTransactionManager (line 47) |
ReactiveTransactionManager (line 36) — both extend the marker TransactionManager (line 28) |
getTransaction → TransactionStatus |
getReactiveTransaction → Mono<ReactiveTransaction> (line 63) |
AbstractPlatformTransactionManager |
AbstractReactiveTransactionManager (lines 84–85) — same propagation rules; doBegin and friends return Mono<Void> (line 817) |
DataSourceTransactionManager |
R2dbcTransactionManager (line 84) |
TransactionTemplate |
TransactionalOperator (line 53) |
transaction.support.TransactionSynchronizationManager |
transaction.reactive.TransactionSynchronizationManager |
The last row is the heart of this section. There are two classes with the same name.
public abstract class TransactionSynchronizationManager {
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
public class TransactionSynchronizationManager {
private final TransactionContext transactionContext;
// ...
public static Mono<TransactionSynchronizationManager> forCurrentTransaction() {
return TransactionContextManager.currentContext().map(TransactionSynchronizationManager::new);
}
// ...
public @Nullable Object getResource(Object key) {
Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
return doGetResource(actualKey);
}
/**
* Actually check the value of the resource that is bound for the given key.
*/
private @Nullable Object doGetResource(Object actualKey) {
return this.transactionContext.getResources().get(actualKey);
}
The blocking one is all static methods over a ThreadLocal. The reactive one is an instance, a thin shell around a single TransactionContext object, and the resource map lives inside that object (TransactionContext lines 37–41). So where does the TransactionContext live?
Riding in the Reactor Context — TransactionContextManager
public static Mono<TransactionContext> currentContext() {
return Mono.deferContextual(ctx -> {
if (ctx.hasKey(TransactionContext.class)) {
return Mono.just(ctx.get(TransactionContext.class));
}
if (ctx.hasKey(TransactionContextHolder.class)) {
TransactionContextHolder holder = ctx.get(TransactionContextHolder.class);
if (holder.hasContext()) {
return Mono.just(holder.currentContext());
}
}
return Mono.error(new NoTransactionInContextException());
});
}
Mono.deferContextual is the Reactor operator for "decide at subscription time, looking at that subscription's Context". The TransactionContext sits in the Reactor Context under the key TransactionContext.class. It is attached to the subscription, not to a thread, so it follows the work even when the work changes threads. If it is missing, a lightweight variant of NoTransactionException is emitted (lines 117–121).
The ones that put it there are getOrCreateContext() and getOrCreateContextHolder() (lines 85–92, lines 103–109). The Javadoc explains why there is a separate holder: creating and releasing transactions proceeds top-down, while Reactor's Context is an immutable map propagated bottom-up (from the subscriber), so a mutable holder (with a stack inside, TransactionContextHolder lines 32–34) is placed in the Context and changed in place (lines 96–101).
The place that actually attaches both is the implementation of TransactionalOperator.
@Override
public <T> Flux<T> execute(TransactionCallback<T> action) throws TransactionException {
return TransactionContextManager.currentContext().flatMapMany(context ->
Flux.usingWhen(
this.transactionManager.getReactiveTransaction(this.transactionDefinition),
action::doInTransaction,
this.transactionManager::commit,
this::rollbackOnException,
this.transactionManager::rollback)
.onErrorMap(this::unwrapIfResourceCleanupFailure))
.contextWrite(TransactionContextManager.getOrCreateContext())
.contextWrite(TransactionContextManager.getOrCreateContextHolder());
}
Where "the current transaction" lives in a reactive transaction. The yellow box is the TransactionContext inside the Reactor Context, and everything meets there. The red box is what nobody looks at.
Flux.usingWhen(resource, use, on success, on error, on cancel) is Reactor's "open a resource, use it, close it" operator. What TransactionTemplate.execute did with try/catch in Part 17 (open → run → commit, or roll back on an exception) is done by this one operator. And at the end of the chain sit the two contextWrite lines. In Reactor, contextWrite has to be placed downstream (towards the subscriber) for the operators upstream to see it, which is why it comes last. The transaction manager's getReactiveTransaction starts by pulling that Context out with forCurrentTransaction() (AbstractReactiveTransactionManager lines 116–120).
How @Transactional takes the reactive route
The TransactionInterceptor of Part 17 is unchanged. Its parent, TransactionAspectSupport.invokeWithinTransaction, has one branch near the top.
final TransactionManager tm = determineTransactionManager(txAttr, targetClass);
if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager rtm) {
// ...
ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
// ...
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(reactiveType);
if (adapter == null) {
throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type [" +
method.getReturnType() + "] with specified transaction manager: " + tm);
}
return new ReactiveTransactionSupport(adapter);
});
return txSupport.invokeWithinTransaction(method, targetClass, invocation, txAttr, rtm);
}
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
If the chosen transaction manager is a ReactiveTransactionManager (and Reactive Streams is on the classpath, line 109, lines 192–197), the ReactiveAdapterRegistry is asked whether the method's return type is a reactive type such as Mono or Flux, and if not, an exception is thrown. If it is, the call is handed to the inner class ReactiveTransactionSupport (line 908), whose shape is the same as TransactionalOperatorImpl above: Mono.usingWhen to open, run and commit or roll back, with the two contextWrite lines at the end (lines 921–941).
R2dbcTransactionManager.doBegin
The reactive manager's doBegin receives the TransactionSynchronizationManager as a parameter (lines 183–184). The blocking one could call static methods from anywhere; here "the one for this subscription" has to be passed in. The body opens a connection (connectionFactory.create(), line 197), calls con.beginTransaction(...) (line 242), and on success binds a ConnectionHolder under the ConnectionFactory key.
// Bind the connection holder to the thread.
if (txObject.isNewConnectionHolder()) {
synchronizationManager.bindResource(obtainConnectionFactory(), txObject.getConnectionHolder());
}
The comment says "to the thread", but the entry actually goes into the map inside the Context. The line has the same shape as its counterpart in Part 18's DataSourceTransactionManager, and the comment appears to have come along with it. doCommit and doRollback return a Mono and convert R2DBC exceptions to DataAccessException (lines 297–317).
Easy to get wrong: why ThreadLocal must not be used in reactive code
As we have seen, the reactive transaction manager never touches the ThreadLocal TransactionSynchronizationManager. So inside a reactive transaction, the blocking side's isActualTransactionActive() returns false (the example output below shows it). For the same reason, calling JdbcTemplate or the shared EntityManager proxy from inside a reactive chain makes them see an empty ThreadLocal and behave as if there were no transaction. The two worlds' mechanisms do not mix.
6. DatabaseClient and ConnectionFactoryUtils — the counterpart of DataSourceUtils
Finally, the "template" on the R2DBC side. DatabaseClient is a fluent API closer to JdbcClient than to JdbcTemplate, created with DatabaseClient.create(connectionFactory) (line 63, lines 109–111). The implementation, DefaultDatabaseClient, obtains its connection in a single line (lines 163–165), and that line is ConnectionFactoryUtils.getConnection, the counterpart of Part 18's DataSourceUtils.getConnection.
public static Mono<Connection> doGetConnection(ConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null");
return TransactionSynchronizationManager.forCurrentTransaction().flatMap(synchronizationManager -> {
ConnectionHolder conHolder = (ConnectionHolder) synchronizationManager.getResource(connectionFactory);
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
conHolder.requested();
if (!conHolder.hasConnection()) {
return fetchConnection(connectionFactory).doOnNext(conHolder::setConnection);
}
return Mono.just(conHolder.getConnection());
}
// Else we either got no holder or an empty thread-bound holder here.
Mono<Connection> con = fetchConnection(connectionFactory);
// ...
return con;
}).onErrorResume(NoTransactionException.class, ex -> Mono.from(connectionFactory.create()));
}
Put it next to Part 18's DataSourceUtils.doGetConnection (lines 103–106) and the structure is the same: look in the store under the ConnectionFactory key, return the holder's connection if there is one, otherwise open a new one. Three things differ. The store is taken from the Context with forCurrentTransaction(); the result is a Mono<Connection>; and the last line — when there is no transaction Context at all (NoTransactionException), simply open a new connection. That line is why DatabaseClient can be used outside a transaction. Releasing is symmetric: doReleaseConnection leaves a bound connection open and closes any other (lines 187–196). DefaultDatabaseClient adds Mono.usingWhen and a connection proxy that suppresses close() on top (lines 116–135, lines 191–195).
| Role | JDBC (Part 18) | R2DBC |
|---|---|---|
| Resource factory (the store's key) | DataSource |
ConnectionFactory |
| Store entry | jdbc.datasource.ConnectionHolder (line 43) |
r2dbc.connection.ConnectionHolder (line 42) |
| Connection lookup | DataSourceUtils.getConnection |
ConnectionFactoryUtils.getConnection |
| The store | ThreadLocal |
TransactionContext inside the Reactor Context |
| Template | JdbcTemplate, JdbcClient |
DatabaseClient |
The same shape as DataSourceUtils.getConnection in Part 18, except the storage lives in the Reactor Context instead of a ThreadLocal. "Same connection → true" is the real output of step 3 in the example below.
Example: rollback and the Context
This is ex/u17/R2dbcTransactionExample.java. It connects to an in-memory H2 through the r2dbc-h2 driver.
// An example written for this post (not Spring's code)
R2dbcTransactionManager tm = new R2dbcTransactionManager(cf);
TransactionalOperator txOp = TransactionalOperator.create(tm);
// a piece of work whose second INSERT fails on the primary key
Mono<Void> work = client.sql("insert into member values (1, 'kim')").then()
.then(client.sql("insert into member values (1, 'dup')").then());
// ... the three calls below are shown without their try/catch and printing
txOp.transactional(work).block(); // 1. inside a transaction → the first INSERT is rolled back too
work.block(); // 2. without a transaction → the first INSERT stays
Mono<String> probe = Mono.deferContextual(ctx ->
Mono.just("Reactor Context has TransactionContext? " + ctx.hasKey(TransactionContext.class)))
.doOnNext(System.out::println)
.then(TransactionSynchronizationManager.forCurrentTransaction() // the transaction.reactive one
.map(sm -> "reactive TSM: actualTransactionActive=" + sm.isActualTransactionActive()
+ ", ConnectionHolder bound=" + sm.hasResource(cf)))
.doOnNext(System.out::println)
.then(Mono.fromCallable(() -> "ThreadLocal TSM (transaction.support): actualTransactionActive="
+ org.springframework.transaction.support.TransactionSynchronizationManager.isActualTransactionActive()
+ " on thread " + Thread.currentThread().getName()))
.doOnNext(System.out::println)
.then(Mono.zip(ConnectionFactoryUtils.getConnection(cf), ConnectionFactoryUtils.getConnection(cf))
.map(t -> "same Connection twice inside the transaction? " + (t.getT1() == t.getT2())))
.doOnNext(System.out::println);
txOp.transactional(probe).block(); // 3. where does the transaction live?
--- 1. inside a transaction: the first insert is rolled back too
[main] WARN io.r2dbc.h2.H2Connection - Option{name='readOnly', sensitive=false} + isn't supported in H2 at the transaction level. You must set it on conenction URL. See http://www.h2database.com/html/features.html#read_only
error : DuplicateKeyException
rows : 0
--- 2. without a transaction: the first insert stays
error : DuplicateKeyException
rows : 1
--- 3. where does the transaction live?
[main] WARN io.r2dbc.h2.H2Connection - Option{name='readOnly', sensitive=false} + isn't supported in H2 at the transaction level. You must set it on conenction URL. See http://www.h2database.com/html/features.html#read_only
Reactor Context has TransactionContext? true
reactive TSM: actualTransactionActive=true, ConnectionHolder bound=true
ThreadLocal TSM (transaction.support): actualTransactionActive=false on thread main
same Connection twice inside the transaction? true
--- 4. outside: no TransactionContext in the Reactor Context
NoTransactionInContextException : No transaction in context
The two WARN lines come from the H2 driver and have nothing to do with this post. Comparing 1 and 2, only the work wrapped in TransactionalOperator had its first INSERT undone. Part 3 is the point of this section: although everything ran on the same main thread, the Reactor Context holds a transaction while the ThreadLocal holds none, and calling ConnectionFactoryUtils.getConnection twice yields the same connection. As in part 4, calling currentContext() outside a transaction gives "No transaction in context".
If you want to go deeper
Only the names and locations of what was left out. The EXTENDED persistence context that outlives a transaction, and the ExtendedEntityManagerCreator that builds it (PersistenceAnnotationBeanPostProcessor lines 810–822). The bootstrapExecutor that builds the factory on a background thread (lines 411–417). LocalSessionFactoryBean and HibernateTransactionManager in the orm.jpa.hibernate package, for using Hibernate's own API without JPA. The SharedEntityAgentInvocationHandler, which exposes a JPA 4.0 EntityAgent as a shared proxy (its Javadoc says @since 7.0.4; around SharedEntityManagerCreator line 425). On the reactive side, the branch that treats Kotlin coroutines (suspend functions) as Mono/Flux (TransactionAspectSupport lines 342–349), R2DBC savepoints (R2dbcTransactionManager lines 188–191), NamedParameterExpander (the r2dbc.core package) and BindMarkersFactory (the r2dbc.core.binding package), which turn DatabaseClient's named parameters (:name) into the driver's ? or $1, and TransactionalEventPublisher for reactive transactional events.
Recap
| Concept | In one line |
|---|---|
LocalContainerEntityManagerFactoryBean |
A FactoryBean. In afterPropertiesSet it settles the persistence unit, builds the factory through the provider, wraps it in a proxy and exposes that |
packagesToScan |
Without persistence.xml, DefaultPersistenceUnitManager builds a default unit and scans for @Entity |
JpaVendorAdapter |
Confines vendor differences to three things: provider, properties, JpaDialect |
The EntityManagerFactory bean |
A JDK proxy. Through EntityManagerFactoryInfo it reveals the DataSource, the dialect and the native factory |
@PersistenceContext |
PersistenceAnnotationBeanPostProcessor injects a shared proxy. Since 7.0, @Autowired EntityManager gets the same proxy |
The shared EntityManager proxy |
On every call, looks in the thread store for the current transaction's EntityManager. Without one, writes throw and reads use a temporary object |
JpaTransactionManager.doBegin |
Opens an EntityManager, begins the JPA transaction, then binds two holders — under the factory key and under the DataSource key |
JPA and JdbcTemplate |
JdbcTemplate receives Hibernate's connection through the DataSource key, so commit and rollback happen together in one transaction |
@Repository |
Only a marker. PersistenceExceptionTranslationPostProcessor adds the proxy and the interceptor translates through the PersistenceExceptionTranslator beans |
ReactiveTransactionManager |
The counterpart of PlatformTransactionManager. Same propagation rules, results as Mono |
transaction.reactive.TransactionSynchronizationManager |
A different class with the same name. Looks at the TransactionContext in the Reactor Context instead of a ThreadLocal |
TransactionalOperator / reactive @Transactional |
Open, run and close with usingWhen, and load the TransactionContext with contextWrite at the end |
ConnectionFactoryUtils.getConnection |
The counterpart of DataSourceUtils.getConnection. Returns the connection bound in the Context, or opens a new one when there is no transaction |
What to look at next
With this post, Phase 4 of the map in Part 1 (transactions and data access, U15–U17) is complete. The transaction abstraction of Part 17, JdbcTemplate in Part 18, and the JPA and R2DBC integration here all had the same shape: bind a resource in a store under a key, and let the users find it with that key. The only difference was whether the store is a thread or a Context.
Next comes Phase 5, the web. Before following a single web request all the way to its response, we look at the floor of the spring-web module: HttpMessageConverter, which turns a request body into an object, WebApplicationContext, which starts the Spring container inside a servlet container, and the filters that step in before and after a request.
- Next: Digging into Spring's Source 20: The spring-web Foundation — HttpMessageConverter, WebApplicationContext, Filters
- Previous: Part 18: JdbcTemplate — the Template-Callback Pattern and Exception Translation, Part 17: @Transactional — Who Opens and Who Closes a Transaction
- Posts this one builds on: Part 3: The BeanFactory Hierarchy and BeanDefinition (
FactoryBean), Part 6: Lifecycle Callbacks and PostProcessors, Part 10: How AOP Proxies Are Made - Part 1: How I Read It with AI, and the Map