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]

Reply via email to