Skip to content

Spring Boot AOT ORM Integration

The Spring Boot starter registers generated Mapper implementations and binds each Mapper package to one named DataSource. Generated classes remain Spring-neutral and continue to depend only on SqlExecutor.

Registration is explicit even when the application has one DataSource:

lynxus:
enabled: true
mapper-bindings:
- package-name: com.example.user.mapper
data-source: usersDataSource
- package-name: com.example.order.mapper
data-source: ordersDataSource

Each binding owns the generated Mappers in that package and its subpackages: com.example.user.mapper uses usersDataSource, while com.example.order.mapper uses ordersDataSource. Packages must be disjoint; overlapping parent and child rules fail at startup instead of selecting a DataSource implicitly.

The processor writes the compile-time mapper index: one mapper metadata resource per generated Mapper, at META-INF/lynxus/mappers/<generated-implementation-class>.properties. The index is that set of resources, not a single file. At startup the starter loads it from the runtime classpath and:

  1. matches each metadata package to one configured binding;
  2. resolves the named Spring DataSource bean;
  3. creates one Spring-aware SqlExecutor for that DataSource;
  4. registers each implementation under the JavaBeans-decapped interface name.

Generated Mapper implementations remain plain Java classes. The processor does not parse Spring configuration files, generate @Component, or require a Spring-specific compiler option. A class named *MapperImpl without processor metadata is not discovered.

Mapper package bindings must not overlap. Applications with several DataSources use disjoint Mapper packages and a matching transaction manager for each domain.

Package binding selects only the Spring DataSource domain. It does not select a JDBC mapping family. Standard Java/JDBC values are routed by Core’s fixed, database-independent TypeHandlerManager; the Starter does not inspect the database product, register package mappings, or choose handlers per package.

Inside @Transactional, SpringConnectionHandleFactory obtains and releases the thread-bound connection through DataSourceUtils. Spring owns commit and rollback timing; Lynxus does not duplicate the transaction boundary.

The PlatformTransactionManager must manage the same DataSource named by the Mapper package binding. A mismatch fails explicitly.

For read/write separation, bind the Mapper package to one Spring AbstractRoutingDataSource. Spring owns the route context and maps keys to the read and write pools; Lynxus sees only the routing DataSource:

@Bean
DataSource readWriteDataSource(
@Qualifier("writerDataSource") DataSource writer,
@Qualifier("readerDataSource") DataSource reader) {
AbstractRoutingDataSource routing = new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
return ReadWriteContext.current();
}
};
routing.setDefaultTargetDataSource(writer);
routing.setTargetDataSources(Map.of("read", reader, "write", writer));
return routing;
}

Bind the package to readWriteDataSource:

lynxus:
mapper-bindings:
- package-name: com.example.mapper
data-source: readWriteDataSource

Application code sets ReadWriteContext before invoking a Mapper and clears it in a finally block. The route is selected when Spring obtains the connection; do not change the key during one transaction. Configure a transaction manager for the same routing DataSource so Spring and Lynxus share one transaction domain. This policy is exercised by the Starter’s routing integration tests.

Java 21 is the current baseline. The repository build uses Spring Boot 3.1.5; the external consumer and runtime Starter suite are verified against Boot 3.1.5, 3.5.16, and 4.1.1. These anchors do not automatically cover every minor or patch release; an unlisted line remains unverified until it passes both gates. The independent consumer fixture is documented in external-spring-boot-consumer. For a complete two-package setup, see the runnable multi-datasource-spring-boot example.

Spring may discover and order ExecutionInterceptor beans. SQL providers, parameter binders, type handlers, and row mappers remain compile-time-selected generated dependencies rather than global runtime registries.

See the Extension contracts for exact validation, lifecycle, and ownership rules.