From 1458d5f39fef06d01348352fc67d5bc2cbe812e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 8 Jul 2022 15:43:14 +0200 Subject: [PATCH] Add support for @Transactional in native images This commit introduces a TransactionBeanRegistrationAotProcessor in charge of creating the required proxy and reflection hints when @Transactional is detected on beans. It also refines DefaultAopProxyFactory to throw an exception when a subclass-based proxy is created in native images since that's unsupported for now (see gh-28115 related issue). Closes gh-28717 --- .../aop/framework/DefaultAopProxyFactory.java | 6 +- ...ansactionBeanRegistrationAotProcessor.java | 99 ++++++++++ .../TransactionRuntimeHintsRegistrar.java | 1 + .../resources/META-INF/spring/aot.factories | 2 + ...tionBeanRegistrationAotProcessorTests.java | 177 ++++++++++++++++++ 5 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionBeanRegistrationAotProcessor.java create mode 100644 spring-tx/src/main/resources/META-INF/spring/aot.factories create mode 100644 spring-tx/src/test/java/org/springframework/transaction/annotation/TransactionBeanRegistrationAotProcessorTests.java diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java index e63e1721232..75d547d6db4 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAopProxyFactory.java @@ -54,8 +54,7 @@ public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { @Override public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { - if (!NativeDetector.inNativeImage() && - (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) { + if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { Class targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class: " + @@ -64,6 +63,9 @@ public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) { return new JdkDynamicAopProxy(config); } + if (NativeDetector.inNativeImage()) { + throw new AopConfigException("Subclass-based proxies are not support yet in native images"); + } return new ObjenesisCglibAopProxy(config); } else { diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionBeanRegistrationAotProcessor.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionBeanRegistrationAotProcessor.java new file mode 100644 index 00000000000..617a57e51d4 --- /dev/null +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionBeanRegistrationAotProcessor.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2022 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.transaction.annotation; + +import java.lang.reflect.AnnotatedElement; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.aop.SpringProxy; +import org.springframework.aop.framework.Advised; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.core.DecoratingProxy; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * AOT {@code BeanRegistrationAotProcessor} that detects the presence of + * {@link Transactional @Transactional} on annotated elements and creates + * the required proxy and reflection hints. + * + * @author Sebastien Deleuze + * @since 6.0 + * @see TransactionRuntimeHintsRegistrar + */ +public class TransactionBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { + + private final static String JAKARTA_TRANSACTIONAL_CLASS_NAME = "jakarta.transaction.Transactional"; + + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + Class beanClass = registeredBean.getBeanClass(); + if (isTransactional(beanClass)) { + return new TransactionBeanRegistrationAotContribution(beanClass); + } + return null; + } + + private boolean isTransactional(Class beanClass) { + Set elements = new LinkedHashSet<>(); + elements.add(beanClass); + ReflectionUtils.doWithMethods(beanClass, elements::add); + for (Class interfaceClass : ClassUtils.getAllInterfacesForClass(beanClass)) { + elements.add(interfaceClass); + ReflectionUtils.doWithMethods(interfaceClass, elements::add); + } + return elements.stream().anyMatch(element -> { + MergedAnnotations mergedAnnotations = MergedAnnotations.from(element, MergedAnnotations.SearchStrategy.TYPE_HIERARCHY); + return mergedAnnotations.isPresent(Transactional.class) || mergedAnnotations.isPresent(JAKARTA_TRANSACTIONAL_CLASS_NAME); + }); + } + + private static class TransactionBeanRegistrationAotContribution implements BeanRegistrationAotContribution { + + private Class beanClass; + + public TransactionBeanRegistrationAotContribution(Class beanClass) { + this.beanClass = beanClass; + } + + @Override + public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { + RuntimeHints runtimeHints = generationContext.getRuntimeHints(); + LinkedHashSet> interfaces = new LinkedHashSet<>(); + Class[] proxyInterfaces = ClassUtils.getAllInterfacesForClass(this.beanClass); + if (proxyInterfaces.length == 0) { + return; + } + for (Class proxyInterface : proxyInterfaces) { + interfaces.add(proxyInterface); + runtimeHints.reflection().registerType(proxyInterface, builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_METHODS)); + } + interfaces.add(SpringProxy.class); + interfaces.add(Advised.class); + interfaces.add(DecoratingProxy.class); + runtimeHints.proxies().registerJdkProxy(interfaces.toArray(Class[]::new)); + } + } +} diff --git a/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionRuntimeHintsRegistrar.java b/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionRuntimeHintsRegistrar.java index bf32a734d7b..477fbf82c4c 100644 --- a/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionRuntimeHintsRegistrar.java +++ b/spring-tx/src/main/java/org/springframework/transaction/annotation/TransactionRuntimeHintsRegistrar.java @@ -30,6 +30,7 @@ import static java.util.Arrays.asList; * * @author Sebastien Deleuze * @since 6.0 + * @see TransactionBeanRegistrationAotProcessor */ public class TransactionRuntimeHintsRegistrar implements RuntimeHintsRegistrar { diff --git a/spring-tx/src/main/resources/META-INF/spring/aot.factories b/spring-tx/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 00000000000..7a80630cd32 --- /dev/null +++ b/spring-tx/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.beans.factory.aot.BeanRegistrationAotProcessor=\ +org.springframework.transaction.annotation.TransactionBeanRegistrationAotProcessor \ No newline at end of file diff --git a/spring-tx/src/test/java/org/springframework/transaction/annotation/TransactionBeanRegistrationAotProcessorTests.java b/spring-tx/src/test/java/org/springframework/transaction/annotation/TransactionBeanRegistrationAotProcessorTests.java new file mode 100644 index 00000000000..92fabbf2cb4 --- /dev/null +++ b/spring-tx/src/test/java/org/springframework/transaction/annotation/TransactionBeanRegistrationAotProcessorTests.java @@ -0,0 +1,177 @@ +/* + * Copyright 2002-2022 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.transaction.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.SpringProxy; +import org.springframework.aop.framework.Advised; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHintsPredicates; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.DecoratingProxy; +import org.springframework.core.testfixture.aot.generate.TestGenerationContext; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link TransactionBeanRegistrationAotProcessor}. + * + * @author Sebastien Deleuze + */ +public class TransactionBeanRegistrationAotProcessorTests { + + private final TransactionBeanRegistrationAotProcessor processor = new TransactionBeanRegistrationAotProcessor(); + + private final GenerationContext generationContext = new TestGenerationContext(); + + @Test + void shouldSkipNonAnnotatedType() { + process(NonAnnotatedBean.class); + assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).isEmpty(); + assertThat(this.generationContext.getRuntimeHints().proxies().jdkProxies()).isEmpty(); + } + + @Test + void shouldSkipAnnotatedTypeWithNoInterface() { + process(NoInterfaceBean.class); + assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).isEmpty(); + assertThat(this.generationContext.getRuntimeHints().proxies().jdkProxies()).isEmpty(); + } + + @Test + void shouldProcessTransactionalOnClass() { + process(TransactionalOnTypeBean.class); + assertThat(RuntimeHintsPredicates.reflection().onType(NonAnnotatedTransactionalInterface.class) + .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS)).accepts(this.generationContext.getRuntimeHints()); + assertThat(RuntimeHintsPredicates.proxies().forInterfaces(NonAnnotatedTransactionalInterface.class, SpringProxy.class, Advised.class, DecoratingProxy.class)).accepts(this.generationContext.getRuntimeHints()); + } + + @Test + void shouldProcessJakartaTransactionalOnClass() { + process(JakartaTransactionalOnTypeBean.class); + assertThat(RuntimeHintsPredicates.reflection().onType(NonAnnotatedTransactionalInterface.class) + .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS)).accepts(this.generationContext.getRuntimeHints()); + assertThat(RuntimeHintsPredicates.proxies().forInterfaces(NonAnnotatedTransactionalInterface.class, SpringProxy.class, Advised.class, DecoratingProxy.class)).accepts(this.generationContext.getRuntimeHints()); + } + + @Test + void shouldProcessTransactionalOnInterface() { + process(TransactionalOnTypeInterface.class); + assertThat(RuntimeHintsPredicates.reflection().onType(TransactionalOnTypeInterface.class) + .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS)).accepts(this.generationContext.getRuntimeHints()); + assertThat(RuntimeHintsPredicates.proxies().forInterfaces(TransactionalOnTypeInterface.class, SpringProxy.class, Advised.class, DecoratingProxy.class)).accepts(this.generationContext.getRuntimeHints()); + } + + @Test + void shouldProcessTransactionalOnClassMethod() { + process(TransactionalOnClassMethodBean.class); + assertThat(RuntimeHintsPredicates.reflection().onType(NonAnnotatedTransactionalInterface.class) + .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS)).accepts(this.generationContext.getRuntimeHints()); + assertThat(RuntimeHintsPredicates.proxies().forInterfaces(NonAnnotatedTransactionalInterface.class, SpringProxy.class, Advised.class, DecoratingProxy.class)).accepts(this.generationContext.getRuntimeHints()); + } + + @Test + void shouldProcessTransactionalOnInterfaceMethod() { + process(TransactionalOnInterfaceMethodBean.class); + assertThat(RuntimeHintsPredicates.reflection().onType(TransactionalOnMethodInterface.class) + .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS)).accepts(this.generationContext.getRuntimeHints()); + assertThat(RuntimeHintsPredicates.proxies().forInterfaces(TransactionalOnMethodInterface.class, SpringProxy.class, Advised.class, DecoratingProxy.class)).accepts(this.generationContext.getRuntimeHints()); + } + + private void process(Class beanClass) { + BeanRegistrationAotContribution contribution = createContribution(beanClass); + if (contribution != null) { + contribution.applyTo(this.generationContext, mock(BeanRegistrationCode.class)); + } + } + + @Nullable + private BeanRegistrationAotContribution createContribution(Class beanClass) { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition(beanClass.getName(), new RootBeanDefinition(beanClass)); + return this.processor.processAheadOfTime(RegisteredBean.of(beanFactory, beanClass.getName())); + } + + + @SuppressWarnings("unused") + static class NonAnnotatedBean { + + public void notTransactional() { + } + } + + @SuppressWarnings("unused") + @Transactional + static class NoInterfaceBean { + + public void notTransactional() { + } + } + + @Transactional + static class TransactionalOnTypeBean implements NonAnnotatedTransactionalInterface { + + public void transactional() { + } + } + + @jakarta.transaction.Transactional + static class JakartaTransactionalOnTypeBean implements NonAnnotatedTransactionalInterface { + + public void transactional() { + } + } + + interface NonAnnotatedTransactionalInterface { + + void transactional(); + } + + @Transactional + interface TransactionalOnTypeInterface { + + void transactional(); + } + + static class TransactionalOnClassMethodBean implements NonAnnotatedTransactionalInterface { + + @Transactional + public void transactional() { + } + } + + interface TransactionalOnMethodInterface { + + @Transactional + void transactional(); + } + + static class TransactionalOnInterfaceMethodBean implements TransactionalOnMethodInterface { + + @Transactional + public void transactional() { + } + } +}