codeconsole commented on code in PR #16709: URL: https://github.com/apache/grails-core/pull/16709#discussion_r4341266959
########## grails-core/src/main/groovy/org/apache/grails/core/aot/GeneratedBeanDefinitions.java: ########## @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.grails.core.aot; + +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.IdentityHashMap; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.util.ClassUtils; + +/** + * The bean definitions the code generated ahead of time contributed, recorded before anything else is + * declared, with the type each one declares. + * + * <p>An application running on generated artifacts has the processors that inject through annotations + * registered again, for the beans the generated code could not see into. The generated code injects a bean + * through the annotated members of the type its definition declares, and records that type's initialization + * and destruction methods, so those processors leave a bean of exactly that type alone; one injected twice + * runs a side-effecting {@code @Autowired} method twice, which registered every executor's metrics a second + * time.</p> + * + * <p>A bean of a subclass of that type has members the generated code never saw, so those processors inject it + * too, but only through them: the members the generated code injected are first marked on the bean's + * definition as configured from outside, the way Spring keeps two processors from injecting one member. A + * factory method declaring an interface, as the link generator's does, leaves every annotated member of the + * class it returns to the processors, and a bean whose class could not be told at all is processed as + * usual.</p> + * + * <p>The definitions are recorded by identity as the context is initialized, after the generated initializer + * has run and before any plugin declares a bean. A definition a plugin declares later, or declares again in + * place of a generated one, is a different object and is processed as usual.</p> + * + * @since 8.0.1 + */ +public final class GeneratedBeanDefinitions { + + /** The singleton the definitions are recorded in. */ + public static final String BEAN_NAME = "grailsGeneratedBeanDefinitions"; + + /** Each generated definition, and the type it declares. */ + private final Map<BeanDefinition, Class<?>> declaredTypes = new IdentityHashMap<>(); + + private GeneratedBeanDefinitions() { + } + + /** + * Records every bean definition the bean factory holds as generated, with the type it declares. + * + * @param beanFactory a bean factory the generated code has just contributed its definitions to + */ + public static void record(ConfigurableListableBeanFactory beanFactory) { + if (beanFactory.containsSingleton(BEAN_NAME)) { + return; + } + GeneratedBeanDefinitions generated = new GeneratedBeanDefinitions(); + for (String name : beanFactory.getBeanDefinitionNames()) { + BeanDefinition definition = beanFactory.getBeanDefinition(name); + Class<?> declaredType = definition.getResolvableType().resolve(); + if (declaredType != null) { + generated.declaredTypes.put(definition, declaredType); Review Comment: Fixed in 1d6f73b3ed. The snapshot is gone. The code generated for each definition now marks the definition itself: `GeneratedInjectionBeanRegistrationAotProcessor` has it call `GeneratedBeanDefinitions.markGenerated(beanDefinition, "<declared type>")` just before `return beanDefinition`. So a definition counts as generated exactly when the generated code built it, whenever and wherever anything else registers one. `GrailsPluginLifecycleInitializer` is back to what 8.0.x has. The regression you asked for is in the new `GeneratedArtifactsInjectionSpec`. It generates the code for a context with `ApplicationContextAotGenerator`, compiles it with javac, and starts a context from it with the processors registered. An earlier initializer registers one bean by class and one with a supplier, then the generated initializer runs. Each bean is injected, initialized and destroyed once. I also ran the same spec against the previous design: there, `declaredEarly` got `[:]`, meaning nothing ran. ########## grails-core/src/main/groovy/org/apache/grails/core/aot/GeneratedBeanDefinitions.java: ########## @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.grails.core.aot; + +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.IdentityHashMap; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.util.ClassUtils; + +/** + * The bean definitions the code generated ahead of time contributed, recorded before anything else is + * declared, with the type each one declares. + * + * <p>An application running on generated artifacts has the processors that inject through annotations + * registered again, for the beans the generated code could not see into. The generated code injects a bean + * through the annotated members of the type its definition declares, and records that type's initialization + * and destruction methods, so those processors leave a bean of exactly that type alone; one injected twice + * runs a side-effecting {@code @Autowired} method twice, which registered every executor's metrics a second + * time.</p> + * + * <p>A bean of a subclass of that type has members the generated code never saw, so those processors inject it + * too, but only through them: the members the generated code injected are first marked on the bean's + * definition as configured from outside, the way Spring keeps two processors from injecting one member. A + * factory method declaring an interface, as the link generator's does, leaves every annotated member of the + * class it returns to the processors, and a bean whose class could not be told at all is processed as + * usual.</p> + * + * <p>The definitions are recorded by identity as the context is initialized, after the generated initializer + * has run and before any plugin declares a bean. A definition a plugin declares later, or declares again in + * place of a generated one, is a different object and is processed as usual.</p> + * + * @since 8.0.1 + */ +public final class GeneratedBeanDefinitions { + + /** The singleton the definitions are recorded in. */ + public static final String BEAN_NAME = "grailsGeneratedBeanDefinitions"; + + /** Each generated definition, and the type it declares. */ + private final Map<BeanDefinition, Class<?>> declaredTypes = new IdentityHashMap<>(); + + private GeneratedBeanDefinitions() { + } + + /** + * Records every bean definition the bean factory holds as generated, with the type it declares. + * + * @param beanFactory a bean factory the generated code has just contributed its definitions to + */ + public static void record(ConfigurableListableBeanFactory beanFactory) { + if (beanFactory.containsSingleton(BEAN_NAME)) { + return; + } + GeneratedBeanDefinitions generated = new GeneratedBeanDefinitions(); + for (String name : beanFactory.getBeanDefinitionNames()) { + BeanDefinition definition = beanFactory.getBeanDefinition(name); + Class<?> declaredType = definition.getResolvableType().resolve(); + if (declaredType != null) { + generated.declaredTypes.put(definition, declaredType); + } + } + beanFactory.registerSingleton(BEAN_NAME, generated); + } + + /** + * Whether the generated code has already injected the bean: its definition is still the one the generated + * code contributed, and the bean is of the type that definition declares -- or a subclass Spring generated + * for it, as it does for a configuration class. + * + * @param beanFactory the bean factory the bean is in + * @param beanName the bean's name + * @param beanClass the class of the bean + * @return {@code true} when generated definitions were recorded, the bean's definition is one of them and + * the bean is of the type it declares + */ + public static boolean isInjectedByGeneratedCode(@Nullable BeanFactory beanFactory, String beanName, + Class<?> beanClass) { + Class<?> declaredType = getDeclaredType(beanFactory, beanName); + return declaredType != null && declaredType == ClassUtils.getUserClass(beanClass); Review Comment: Fixed in 1d6f73b3ed, keyed on the instance as you suggest. `markGenerated` wraps the generated instance supplier, using Spring's own `andThen` so `getFactoryMethod()` is kept, and records each instance it creates, weakly and by identity. The processors mark the generated members on the merged definition, so the instance that supplier created is injected only through what its class adds. Any other instance under that name goes to a plain processor and is injected in full. Lifecycle methods are no longer skipped. The common processor calls the annotated ones for every instance, and the names the generated code recorded for them are taken off the generated definitions when the processors are registered. While I was in there: with those names left on, `configureBean` ran `@PostConstruct` twice. It works on a copy of the definition, which keeps the recorded name but loses the record that the processor already called the method. 8.0.x does this today too. The round trip covers three paths, and each now runs every member once: - `configureBean` on another instance. The previous design injected nothing: `[initialize:1]`. - `configureBean` on a prototype template that was never created, as `@Configurable` uses. The previous design gave the same result. - The Hibernate bean container's sequence: `autowireBeanProperties`, `applyBeanPropertyValues`, `initializeBean`, then `destroyBean`. The previous design never ran `@PostConstruct`. ########## grails-core/src/main/groovy/org/apache/grails/core/aot/GeneratedBeanDefinitions.java: ########## @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.grails.core.aot; + +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.IdentityHashMap; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.util.ClassUtils; + +/** + * The bean definitions the code generated ahead of time contributed, recorded before anything else is + * declared, with the type each one declares. + * + * <p>An application running on generated artifacts has the processors that inject through annotations + * registered again, for the beans the generated code could not see into. The generated code injects a bean + * through the annotated members of the type its definition declares, and records that type's initialization + * and destruction methods, so those processors leave a bean of exactly that type alone; one injected twice + * runs a side-effecting {@code @Autowired} method twice, which registered every executor's metrics a second + * time.</p> + * + * <p>A bean of a subclass of that type has members the generated code never saw, so those processors inject it + * too, but only through them: the members the generated code injected are first marked on the bean's + * definition as configured from outside, the way Spring keeps two processors from injecting one member. A + * factory method declaring an interface, as the link generator's does, leaves every annotated member of the + * class it returns to the processors, and a bean whose class could not be told at all is processed as + * usual.</p> + * + * <p>The definitions are recorded by identity as the context is initialized, after the generated initializer + * has run and before any plugin declares a bean. A definition a plugin declares later, or declares again in + * place of a generated one, is a different object and is processed as usual.</p> + * + * @since 8.0.1 + */ +public final class GeneratedBeanDefinitions { + + /** The singleton the definitions are recorded in. */ + public static final String BEAN_NAME = "grailsGeneratedBeanDefinitions"; + + /** Each generated definition, and the type it declares. */ + private final Map<BeanDefinition, Class<?>> declaredTypes = new IdentityHashMap<>(); + + private GeneratedBeanDefinitions() { + } + + /** + * Records every bean definition the bean factory holds as generated, with the type it declares. + * + * @param beanFactory a bean factory the generated code has just contributed its definitions to + */ + public static void record(ConfigurableListableBeanFactory beanFactory) { + if (beanFactory.containsSingleton(BEAN_NAME)) { + return; + } + GeneratedBeanDefinitions generated = new GeneratedBeanDefinitions(); + for (String name : beanFactory.getBeanDefinitionNames()) { + BeanDefinition definition = beanFactory.getBeanDefinition(name); + Class<?> declaredType = definition.getResolvableType().resolve(); + if (declaredType != null) { + generated.declaredTypes.put(definition, declaredType); + } + } + beanFactory.registerSingleton(BEAN_NAME, generated); + } + + /** + * Whether the generated code has already injected the bean: its definition is still the one the generated + * code contributed, and the bean is of the type that definition declares -- or a subclass Spring generated + * for it, as it does for a configuration class. + * + * @param beanFactory the bean factory the bean is in + * @param beanName the bean's name + * @param beanClass the class of the bean + * @return {@code true} when generated definitions were recorded, the bean's definition is one of them and + * the bean is of the type it declares + */ + public static boolean isInjectedByGeneratedCode(@Nullable BeanFactory beanFactory, String beanName, + Class<?> beanClass) { + Class<?> declaredType = getDeclaredType(beanFactory, beanName); + return declaredType != null && declaredType == ClassUtils.getUserClass(beanClass); + } + + /** + * Marks the members the generated code injected as configured from outside, on the definition of a bean it + * created of a subclass of the type that definition declares: the members the given processor finds on the + * declared type, and the methods of the bean's class overriding them, which are the ones the generated code + * called. A processor of that kind then injects the bean only through the members its class adds. + * + * @param beanFactory the bean factory the bean is in + * @param beanName the bean's name + * @param beanType the class of the bean + * @param beanDefinition the bean's merged definition, before the processor of that kind processes it + * @param generatingProcessor a processor of the kind and configuration the code was generated with, which + * is given a definition of its own to process + */ + public static void markMembersInjectedByGeneratedCode(@Nullable BeanFactory beanFactory, String beanName, + Class<?> beanType, RootBeanDefinition beanDefinition, MergedBeanDefinitionPostProcessor generatingProcessor) { + Class<?> declaredType = getDeclaredType(beanFactory, beanName); + Class<?> beanClass = ClassUtils.getUserClass(beanType); + if (declaredType == null || declaredType == beanClass || !declaredType.isAssignableFrom(beanClass)) { + return; + } + RootBeanDefinition generated = new RootBeanDefinition(declaredType); + generatingProcessor.postProcessMergedBeanDefinition(generated, declaredType, beanName); + for (Member member : generated.getExternallyManagedConfigMembers()) { + beanDefinition.registerExternallyManagedConfigMember(member); + if (member instanceof Method method) { + beanDefinition.registerExternallyManagedConfigMember(ClassUtils.getMostSpecificMethod(method, beanClass)); + } + } + } + + @Nullable + private static Class<?> getDeclaredType(@Nullable BeanFactory beanFactory, String beanName) { + if (!(beanFactory instanceof ConfigurableListableBeanFactory listable) || + !listable.containsSingleton(BEAN_NAME) || !listable.containsBeanDefinition(beanName)) { Review Comment: Fixed in 1d6f73b3ed, as you suggest. Spring runs the registration AOT processors for inner beans too, so the code generated for an inner definition carries the same mark. The processors read the mark from the merged definition they're given, not from the registry by name. The recorded lifecycle names are also taken off inner definitions, however deeply they sit in property values or constructor arguments. The round trip has an outer bean holding an inner `InjectedFixture` as a property. The inner bean's `@Autowired`, `@Resource` and `@PostConstruct` members each run once. Against the previous design, each injected member ran twice. ########## grails-core/src/main/groovy/org/apache/grails/core/aot/GeneratedBeanDefinitions.java: ########## @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.grails.core.aot; + +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.IdentityHashMap; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.util.ClassUtils; + +/** + * The bean definitions the code generated ahead of time contributed, recorded before anything else is + * declared, with the type each one declares. + * + * <p>An application running on generated artifacts has the processors that inject through annotations + * registered again, for the beans the generated code could not see into. The generated code injects a bean + * through the annotated members of the type its definition declares, and records that type's initialization + * and destruction methods, so those processors leave a bean of exactly that type alone; one injected twice + * runs a side-effecting {@code @Autowired} method twice, which registered every executor's metrics a second + * time.</p> + * + * <p>A bean of a subclass of that type has members the generated code never saw, so those processors inject it + * too, but only through them: the members the generated code injected are first marked on the bean's + * definition as configured from outside, the way Spring keeps two processors from injecting one member. A + * factory method declaring an interface, as the link generator's does, leaves every annotated member of the + * class it returns to the processors, and a bean whose class could not be told at all is processed as + * usual.</p> + * + * <p>The definitions are recorded by identity as the context is initialized, after the generated initializer + * has run and before any plugin declares a bean. A definition a plugin declares later, or declares again in + * place of a generated one, is a different object and is processed as usual.</p> + * + * @since 8.0.1 + */ +public final class GeneratedBeanDefinitions { + + /** The singleton the definitions are recorded in. */ + public static final String BEAN_NAME = "grailsGeneratedBeanDefinitions"; + + /** Each generated definition, and the type it declares. */ + private final Map<BeanDefinition, Class<?>> declaredTypes = new IdentityHashMap<>(); + + private GeneratedBeanDefinitions() { + } + + /** + * Records every bean definition the bean factory holds as generated, with the type it declares. + * + * @param beanFactory a bean factory the generated code has just contributed its definitions to + */ + public static void record(ConfigurableListableBeanFactory beanFactory) { + if (beanFactory.containsSingleton(BEAN_NAME)) { + return; + } + GeneratedBeanDefinitions generated = new GeneratedBeanDefinitions(); + for (String name : beanFactory.getBeanDefinitionNames()) { + BeanDefinition definition = beanFactory.getBeanDefinition(name); + Class<?> declaredType = definition.getResolvableType().resolve(); + if (declaredType != null) { + generated.declaredTypes.put(definition, declaredType); + } + } + beanFactory.registerSingleton(BEAN_NAME, generated); + } + + /** + * Whether the generated code has already injected the bean: its definition is still the one the generated + * code contributed, and the bean is of the type that definition declares -- or a subclass Spring generated + * for it, as it does for a configuration class. + * + * @param beanFactory the bean factory the bean is in + * @param beanName the bean's name + * @param beanClass the class of the bean + * @return {@code true} when generated definitions were recorded, the bean's definition is one of them and + * the bean is of the type it declares + */ + public static boolean isInjectedByGeneratedCode(@Nullable BeanFactory beanFactory, String beanName, + Class<?> beanClass) { + Class<?> declaredType = getDeclaredType(beanFactory, beanName); + return declaredType != null && declaredType == ClassUtils.getUserClass(beanClass); + } + + /** + * Marks the members the generated code injected as configured from outside, on the definition of a bean it + * created of a subclass of the type that definition declares: the members the given processor finds on the + * declared type, and the methods of the bean's class overriding them, which are the ones the generated code + * called. A processor of that kind then injects the bean only through the members its class adds. + * + * @param beanFactory the bean factory the bean is in + * @param beanName the bean's name + * @param beanType the class of the bean + * @param beanDefinition the bean's merged definition, before the processor of that kind processes it + * @param generatingProcessor a processor of the kind and configuration the code was generated with, which + * is given a definition of its own to process + */ + public static void markMembersInjectedByGeneratedCode(@Nullable BeanFactory beanFactory, String beanName, + Class<?> beanType, RootBeanDefinition beanDefinition, MergedBeanDefinitionPostProcessor generatingProcessor) { + Class<?> declaredType = getDeclaredType(beanFactory, beanName); + Class<?> beanClass = ClassUtils.getUserClass(beanType); + if (declaredType == null || declaredType == beanClass || !declaredType.isAssignableFrom(beanClass)) { + return; + } + RootBeanDefinition generated = new RootBeanDefinition(declaredType); + generatingProcessor.postProcessMergedBeanDefinition(generated, declaredType, beanName); + for (Member member : generated.getExternallyManagedConfigMembers()) { + beanDefinition.registerExternallyManagedConfigMember(member); + if (member instanceof Method method) { + beanDefinition.registerExternallyManagedConfigMember(ClassUtils.getMostSpecificMethod(method, beanClass)); Review Comment: Fixed in 38d55f3361 with `BridgeMethodResolver.getMostSpecificMethod`. `GeneratedBeanDefinitionsSpec` has a generic superclass and subclass pair, and asserts that the concrete override is marked, not the bridge. `GeneratedArtifactsInjectionSpec` asserts that the override runs once through generated code. One difference from your shape: the round-trip fixture bounds the type parameter (`T extends Dependency`). Spring's generated code resolves the dependency by the erased parameter type. With an unbounded `T`, the generated code itself fails with `No qualifying bean of type '?'` before any processor runs. ########## grails-core/src/main/groovy/org/apache/grails/core/aot/GeneratedBeanDefinitions.java: ########## @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.grails.core.aot; + +import java.util.IdentityHashMap; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.util.ClassUtils; + +/** + * The bean definitions the code generated ahead of time contributed, recorded before anything else is + * declared, with the type each one declares. + * + * <p>An application running on generated artifacts has the processors that inject through annotations + * registered again, for the beans the generated code could not see into. The generated code injects a bean + * through the annotated members of the type its definition declares, and records that type's initialization + * and destruction methods, so those processors leave a bean of exactly that type alone; one injected twice + * runs a side-effecting {@code @Autowired} method twice, which registered every executor's metrics a second + * time.</p> + * + * <p>A bean of any other class has members the generated code never saw: a factory method declaring an Review Comment: I fixed this rather than documenting it. Spring already has the mechanism: `InjectionMetadata.checkConfigMembers` skips any member already registered on the merged definition as an externally managed config member. That is how it keeps two processors from injecting the same member. Since James's review, the generated code marks each definition with the type it injects through, `registeredBean.getBeanClass()`, the type Spring's own processors used. Before processing a bean, both processors: 1. Ask a plain Spring processor, configured as the code was generated and given a definition of its own, for that type's members. 2. Register those members on the merged definition, together with each method's override in the bean's class, resolved through generic bridges, because the generated code called the override. The instance the generated code created is then injected only through what its class adds. `GeneratedArtifactsInjectionSpec` runs your fixture through generated code, with an `@Resource` setter and an `@Autowired` override added. Each member runs once, and the subclass's own member is still injected. An interface has no members to mark, so the link generator's bean is processed in full, as before. ########## grails-core/src/main/groovy/org/apache/grails/core/aot/GeneratedArtifactsCommonAnnotationBeanPostProcessor.java: ########## @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.grails.core.aot; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.BeansException; +import org.springframework.beans.PropertyValues; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor; + +/** + * Injects through {@code @Resource}, and calls {@code @PostConstruct} and {@code @PreDestroy} methods on, the + * beans the code generated ahead of time did not handle, and leaves alone the ones it did -- the beans it + * created that are of the type their definitions declare: it has injected those and recorded their + * initialization and destruction methods on their definitions. + * + * @since 8.0.1 + * @see GeneratedBeanDefinitions + */ +public class GeneratedArtifactsCommonAnnotationBeanPostProcessor extends CommonAnnotationBeanPostProcessor { + + @Nullable + private BeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) { + super.setBeanFactory(beanFactory); + this.beanFactory = beanFactory; + } + + @Override + public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) { + if (!GeneratedBeanDefinitions.isInjectedByGeneratedCode(beanFactory, beanName, beanType)) { + super.postProcessMergedBeanDefinition(beanDefinition, beanType, beanName); + } + } + + @Override + public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException { + if (GeneratedBeanDefinitions.isInjectedByGeneratedCode(beanFactory, beanName, bean.getClass())) { + return pvs; + } + return super.postProcessProperties(pvs, bean, beanName); + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (GeneratedBeanDefinitions.isInjectedByGeneratedCode(beanFactory, beanName, bean.getClass())) { + return bean; + } + return super.postProcessBeforeInitialization(bean, beanName); + } + + @Override + public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException { Review Comment: Covered, though since James's review the override itself is gone. The common processor now calls `@PreDestroy` for every instance, as Spring's own does, and the destruction names the generated code recorded from annotations are taken off its definitions. `GeneratedArtifactsInjectionSpec` checks that `shutdown()` runs once on `close()` for each bean the generated code created, and once on `destroyBean` for another instance. ########## grails-core/src/main/groovy/grails/boot/config/GrailsPluginLifecycleInitializer.java: ########## @@ -29,12 +32,19 @@ * {@code doWithSpring} be present in the registry before Boot's {@code @ConditionalOnMissingBean} * guards are evaluated, so auto-configured defaults back off in favour of plugin beans. * + * <p>On generated artifacts it also records the bean definitions the generated code contributed. Spring Boot + * applies the generated initializer before any other, and no plugin has declared a bean yet, so what the Review Comment: This is gone now. After James's review, the snapshot was replaced by marks the generated code writes onto each definition, so nothing depends on initializer order any more, and `GrailsPluginLifecycleInitializer` is back to what 8.0.x has. A bean that an initializer ordered ahead of the generated one declares is processed as usual, and `GeneratedArtifactsInjectionSpec` covers that case. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
