diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachingBeanRegistrationAotProcessor.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachingBeanRegistrationAotProcessor.java new file mode 100644 index 0000000000..ba0028f879 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachingBeanRegistrationAotProcessor.java @@ -0,0 +1,93 @@ +/* + * 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.cache.annotation; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.aop.config.AopConfigUtils; +import org.springframework.aop.framework.AopProxyUtils; +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.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * {@link BeanRegistrationAotProcessor} to register runtime hints for beans that use caching annotations to + * enable JDK proxy creation when needed. + * + * @author Sebastien Deleuze + * @since 6.0 + */ +public class CachingBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { + + private static final Set> CACHE_OPERATION_ANNOTATIONS = new LinkedHashSet<>(8); + + static { + CACHE_OPERATION_ANNOTATIONS.add(Cacheable.class); + CACHE_OPERATION_ANNOTATIONS.add(CacheEvict.class); + CACHE_OPERATION_ANNOTATIONS.add(CachePut.class); + CACHE_OPERATION_ANNOTATIONS.add(Caching.class); + } + + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + if (isCaching(registeredBean.getBeanClass()) && !isClassProxyingForced(registeredBean.getBeanFactory())) { + return (generationContext, beanRegistrationCode) -> registerSpringProxy(registeredBean.getBeanClass(), + generationContext.getRuntimeHints()); + } + return null; + } + + private static boolean isClassProxyingForced(ConfigurableListableBeanFactory beanFactory) { + return beanFactory.containsBean(AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME) && + Boolean.TRUE.equals(beanFactory.getBeanDefinition(AopConfigUtils.AUTO_PROXY_CREATOR_BEAN_NAME) + .getPropertyValues().get("proxyTargetClass")); + } + + private boolean isCaching(Class beanClass) { + if (!AnnotationUtils.isCandidateClass(beanClass, CACHE_OPERATION_ANNOTATIONS)) { + return false; + } + 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 CACHE_OPERATION_ANNOTATIONS.stream().anyMatch(mergedAnnotations::isPresent); + }); + } + + private static void registerSpringProxy(Class type, RuntimeHints runtimeHints) { + Class[] proxyInterfaces = ClassUtils.getAllInterfacesForClass(type); + if (proxyInterfaces.length == 0) { + return; + } + runtimeHints.proxies().registerJdkProxy(AopProxyUtils.completeJdkProxyInterfaces(proxyInterfaces)); + } +} diff --git a/spring-context/src/main/resources/META-INF/spring/aot.factories b/spring-context/src/main/resources/META-INF/spring/aot.factories index cce5c2019c..8b49207ec2 100644 --- a/spring-context/src/main/resources/META-INF/spring/aot.factories +++ b/spring-context/src/main/resources/META-INF/spring/aot.factories @@ -1,2 +1,3 @@ org.springframework.beans.factory.aot.BeanRegistrationAotProcessor= \ -org.springframework.context.aot.ReflectiveProcessorBeanRegistrationAotProcessor +org.springframework.context.aot.ReflectiveProcessorBeanRegistrationAotProcessor,\ +org.springframework.cache.annotation.CachingBeanRegistrationAotProcessor diff --git a/spring-context/src/test/java/org/springframework/cache/annotation/CachingBeanRegistrationAotProcessorTests.java b/spring-context/src/test/java/org/springframework/cache/annotation/CachingBeanRegistrationAotProcessorTests.java new file mode 100644 index 0000000000..b72adad5f1 --- /dev/null +++ b/spring-context/src/test/java/org/springframework/cache/annotation/CachingBeanRegistrationAotProcessorTests.java @@ -0,0 +1,111 @@ +/* + * 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.cache.annotation; + +import org.junit.jupiter.api.Test; + +import org.springframework.aop.config.AopConfigUtils; +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; +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.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.beans.factory.support.RootBeanDefinition; +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 CachingBeanRegistrationAotProcessor}. + * + * @author Sebastien Deleuze + */ +public class CachingBeanRegistrationAotProcessorTests { + + BeanRegistrationAotProcessor processor = new CachingBeanRegistrationAotProcessor(); + + GenerationContext generationContext = new TestGenerationContext(); + + + @Test + void ignoresNonCachingBean() { + assertThat(createContribution(NonCaching.class, false)).isNull(); + } + + @Test + void contributesProxyForCacheableInterface() { + process(CacheableServiceImpl.class, false); + RuntimeHints runtimeHints = this.generationContext.getRuntimeHints(); + assertThat(RuntimeHintsPredicates.proxies().forInterfaces(AopProxyUtils.completeJdkProxyInterfaces(CacheableServiceInterface.class))).accepts(runtimeHints); + } + + @Test + void ignoresProxyForCacheableInterfaceWithClassProxying() { + assertThat(createContribution(CacheableServiceImpl.class, true)).isNull(); + } + + @Test + void ignoresProxyForCacheableClass() { + assertThat(createContribution(CacheableService.class, true)).isNull(); + } + + @Nullable + private BeanRegistrationAotContribution createContribution(Class beanClass, boolean forceClassProxying) { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + if (forceClassProxying) { + AopConfigUtils.registerAutoProxyCreatorIfNecessary(beanFactory); + AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(beanFactory); + } + beanFactory.registerBeanDefinition(beanClass.getName(), new RootBeanDefinition(beanClass)); + return this.processor.processAheadOfTime(RegisteredBean.of(beanFactory, beanClass.getName())); + } + + private void process(Class beanClass, boolean forceClassProxying) { + BeanRegistrationAotContribution contribution = createContribution(beanClass, forceClassProxying); + assertThat(contribution).isNotNull(); + contribution.applyTo(this.generationContext, mock(BeanRegistrationCode.class)); + } + + static class NonCaching { + } + + interface CacheableServiceInterface { + + @Cacheable + void invoke(); + } + + class CacheableServiceImpl implements CacheableServiceInterface { + + @Override + public void invoke() { + } + } + + class CacheableService { + + @Cacheable + public void invoke() { + } + } +}