AotDialectFactory.java
/*
* Copyright 2026-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.jpa.repository.aot;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Set;
import org.hibernate.dialect.DatabaseVersion;
import org.hibernate.dialect.Dialect;
import org.hibernate.query.sqm.mutation.spi.MultiTableHandler;
import org.hibernate.query.sqm.mutation.spi.MultiTableHandlerBuildResult;
import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy;
import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy;
import org.hibernate.sql.exec.spi.JdbcParameterBindings;
import org.jspecify.annotations.Nullable;
import org.springframework.cglib.core.Predicate;
import org.springframework.cglib.proxy.Callback;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.cglib.proxy.NoOp;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* Builds the {@link Dialect} used to satisfy Hibernate's bootstrap requirements
* ({@link org.hibernate.cfg.JdbcSettings#DIALECT}) during the AOT phase.
* <p>
* Hibernate 8 relocated and reshaped {@link Dialect} compared to Hibernate 7. {@link #INSTANCE} provides an on-the-fly
* generated {@link Dialect} implementation that is compatible with Hibernate 7 and 8.
*
* @author Christoph Strobl
* @since 4.2
*/
class AotDialectFactory {
/**
* Flag if {@code Dialect#getMultiTableMutationSupport()} exists so we need to prevent Hibernate 8 from creating an
* implementation that we cannot tolerate during AOT.
*/
private static final boolean NEEDS_IDENTITY_COLUMN_SUPPORT_OVERRIDE = ClassUtils.hasMethod(Dialect.class,
"getMultiTableMutationSupport");
/**
* {@link org.hibernate.engine.jdbc.connections.internal.DatabaseConnectionInfoImpl#toInfoString()} captures
* {@link Dialect#getClass()} for rendering the dialect name, and CGLIB...something does not read well.
*/
private static final String DIALECT_NAME = "SpringDataJpaAotDialect";
/**
* Methods that {@link AotDialectInterceptor} needs to answer itself. Other calls are routed to {@link NoOp} doing
* things lazily to work around potential {@code --add-opens}.
*/
private static final Set<String> INTERCEPTED_METHODS = Set.of("getLimitHandler", "getSequenceSupport",
"getSqlAstTranslatorFactory", "getFallbackSqmInsertStrategy", "getFallbackSqmMutationStrategy",
"isCurrentTimestampSelectStringCallable", "getCurrentTimestampSelectString", "timestampdiffPattern");
static final Dialect INSTANCE = create();
static Dialect create() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Dialect.class);
enhancer.setClassLoader(Dialect.class.getClassLoader());
enhancer.setCallbackFilter(method -> isIntercepted(method) ? 0 : 1);
enhancer.setCallbacks(new Callback[] { new AotDialectInterceptor(), NoOp.INSTANCE });
enhancer.setNamingPolicy(AotDialectFactory::proxyNamingPolicy);
return (Dialect) enhancer.create(new Class<?>[] { DatabaseVersion.class },
new Object[] { DatabaseVersion.make(1, 0) });
}
private static boolean isIntercepted(Method method) {
if (method.getName().equals("getIdentityColumnSupport")) {
return NEEDS_IDENTITY_COLUMN_SUPPORT_OVERRIDE;
}
return INTERCEPTED_METHODS.contains(method.getName());
}
/**
* Resolve the class named {@code simpleName} living next to {@code type} - in its own package (Hibernate 7), or the
* {@code .spi} sub-package (Hibernate 8).
*/
@SuppressWarnings("NullAway")
private static Object getAvailableInstanceFor(Class<?> type, String simpleName) {
Class<?> implementation = resolveAvailableType(type.getPackageName(), simpleName, type.getClassLoader());
Field instanceField = ReflectionUtils.findField(implementation, "INSTANCE");
if (instanceField != null) {
ReflectionUtils.makeAccessible(instanceField);
return ReflectionUtils.getField(instanceField, null);
}
try {
Constructor<?> constructor = implementation.getDeclaredConstructor();
ReflectionUtils.makeAccessible(constructor);
return constructor.newInstance();
} catch (ReflectiveOperationException ex) {
throw new IllegalStateException("Cannot instantiate %s".formatted(implementation.getName()), ex);
}
}
private static Class<?> resolveAvailableType(String packageName, String simpleName, ClassLoader classLoader) {
String h7className = packageName + "." + simpleName;
if (ClassUtils.isPresent(h7className, classLoader)) {
return ClassUtils.resolveClassName(h7className, classLoader);
}
if (!packageName.endsWith(".spi")) {
String h8className = packageName + ".spi." + simpleName;
if (ClassUtils.isPresent(h8className, classLoader)) {
return ClassUtils.resolveClassName(h8className, classLoader);
}
}
throw new IllegalStateException("Unable to resolve [%s] next via [%s]".formatted(simpleName, packageName));
}
/**
* A generic no-op {@link Proxy} for a pure-behavioral extension point interface where AOT bootstrap only needs
* construction not to fail - the returned values are never actually used, since AOT never executes SQL.
*/
private static Object noOpProxy(Class<?> interfaceType) {
InvocationHandler handler = (proxy, method, args) -> {
Class<?> returnType = method.getReturnType();
if (returnType.equals(void.class)) {
return null;
}
if (returnType.isPrimitive()) {
return org.springframework.data.util.ReflectionUtils.getPrimitiveDefault(returnType);
}
// just use any value for the enum to satisfy the contract
if (returnType.isEnum()) {
return returnType.getEnumConstants()[0];
}
return null;
};
return Proxy.newProxyInstance(interfaceType.getClassLoader(), new Class<?>[] { interfaceType }, handler);
}
private static String proxyNamingPolicy(String prefix, String source, Object key, Predicate names) {
String base = Dialect.class.getPackageName() + "." + DIALECT_NAME;
String attempt = base;
int index = 2;
while (names.evaluate(attempt)) {
attempt = base + "_" + index++;
}
return attempt;
}
/**
* {@link MethodInterceptor} to handle {@link Dialect} changes between Hibernate 7 and 8 by opting for the most
* appropriate implementation based on return type rather than method name since Hiberate 8 keeps moving isntances
* around.
*/
private static class AotDialectInterceptor implements MethodInterceptor {
@Override
public @Nullable Object intercept(Object obj, Method method, Object @Nullable [] args, MethodProxy proxy)
throws Throwable {
return switch (method.getName()) {
case "getLimitHandler" -> getAvailableInstanceFor(method.getReturnType(), "OffsetFetchLimitHandler");
case "getSequenceSupport" -> getAvailableInstanceFor(method.getReturnType(), "ANSISequenceSupport");
case "getSqlAstTranslatorFactory" ->
getAvailableInstanceFor(method.getReturnType(), "StandardSqlAstTranslatorFactory");
case "getFallbackSqmInsertStrategy" -> FallbackMultiTableStrategies.INSERT;
case "getFallbackSqmMutationStrategy" -> FallbackMultiTableStrategies.MUTATION;
// Hibernate 8's getMultiTableMutationSupport() defaults to PERSISTENT_TABLE, which - unlike the
// getFallbackSqmInsertStrategy/getFallbackSqmMutationStrategy hooks above - always eagerly builds a
// real strategy at boot, hitting the identity-column support this fake Dialect does not implement.
case "getIdentityColumnSupport" -> noOpProxy(method.getReturnType());
case "isCurrentTimestampSelectStringCallable" -> false;
case "getCurrentTimestampSelectString" -> "call current_timestamp()";
// unit == null, kept as reflection-friendly positional check to avoid importing TemporalUnit
case "timestampdiffPattern" -> args[0] == null ? "(?3-?2)" : "datediff(?1,?2,?3)";
// should not be reachable -> if we get to this point something is clearly off
default -> throw new IllegalStateException("Unexpected method " + method + " routed to AotDialectInterceptor");
};
}
}
/**
* No-op fallback multi-table mutation strategies for AOT.
*/
private static class FallbackMultiTableStrategies {
static final MultiTableHandler HANDLER = (MultiTableHandler) Proxy.newProxyInstance(
AotDialectFactory.class.getClassLoader(), new Class[] { MultiTableHandler.class }, (proxy, method, args) -> {
if (method.getName().equals("createJdbcParameterBindings")) {
return JdbcParameterBindings.NO_BINDINGS;
}
if (method.getReturnType().isPrimitive()) {
return org.springframework.data.util.ReflectionUtils.getPrimitiveDefault(method.getReturnType());
}
return null;
});
static final SqmMultiTableInsertStrategy INSERT = (SqmMultiTableInsertStrategy) Proxy.newProxyInstance(
AotDialectFactory.class.getClassLoader(), new Class[] { SqmMultiTableInsertStrategy.class },
(proxy, method, args) -> fallbackHandlerResult(method));
static final SqmMultiTableMutationStrategy MUTATION = (SqmMultiTableMutationStrategy) Proxy.newProxyInstance(
AotDialectFactory.class.getClassLoader(), new Class[] { SqmMultiTableMutationStrategy.class },
(proxy, method, args) -> fallbackHandlerResult(method));
private static @Nullable Object fallbackHandlerResult(Method method) {
if (method.getName().equals("buildHandler")) {
return new MultiTableHandlerBuildResult(HANDLER, JdbcParameterBindings.NO_BINDINGS);
}
if (method.getReturnType().equals(void.class)) {
return null;
}
if (method.getReturnType().isPrimitive()) {
return org.springframework.data.util.ReflectionUtils.getPrimitiveDefault(method.getReturnType());
}
return null;
}
}
}