codeconsole commented on code in PR #16709: URL: https://github.com/apache/grails-core/pull/16709#discussion_r4339994222
########## 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 in 508579a9e2 instead of 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. Both processors now handle a bean whose class is a strict subclass of its generated definition's declared type in three steps: 1. Ask a plain Spring processor of the same kind for the declared type's members. It is configured the way the code was generated and is given a definition of its own, so this processor's caches and the bean's lifecycle registrations are untouched. 2. Register those members on the bean's definition, together with each method's override in the bean's class, because the generated code called the override. 3. Process the bean as usual, which now injects only the members the subclass adds. Your fixture is now the third feature of `GrailsApplicationPostProcessorAnnotationConfigSpec`, with an `@Resource` setter and an `@Autowired` override in the subclass added. - Before the change it counted `collaborator: 2, overridableCollaborator: 2, resourceCollaborator: 2, initialize: 1`. - After it, each is 1, and the subclass's own `@Autowired` member is still injected once. - Dropping just the override registration brings `overridableCollaborator` back to 2. An interface has no members to mark, so a bean declared as one, like the link generator, is still processed in full. The javadoc paragraph now describes all of this, and `GeneratedBeanDefinitionsSpec` covers the marking directly, including the cases where it marks nothing. ########## 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: Added in 8aaa181574, in the shape you suggested. `InjectedFixture` has a `@PreDestroy shutdown()`, and the generated definition records `destroyMethodNames = ['shutdown']`. After `context.close()` it has run once for the generated bean and once for the bean declared later. With the override reduced to a plain `super` call it runs twice for the generated bean. The fixture also gained a `@Resource` setter, so this processor's `postProcessProperties` skip is covered too. ########## 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: Reworded in ad17e6a383 to the condition you describe: - Boot adds the generated initializer ahead of the application's initializers and sorts them stably, so it runs before this unordered one but after any initializer that has an order. - The record relies on none of those registering a bean definition. Boot's own only set the parent context and register a singleton. The PR description notes the consequence. A definition registered that early would be taken for a generated one, and a bean of exactly the type it declares would not be injected. That is also what happens to it in a plain Spring Boot application running on generated artifacts, which has no injection processors at runtime. ########## grails-core/src/main/groovy/grails/boot/config/GrailsApplicationPostProcessor.groovy: ########## @@ -344,23 +346,33 @@ class GrailsApplicationPostProcessor implements BeanDefinitionRegistryPostProces * * <p>They are registered under the names Spring uses itself, so a context that already has them * keeps what it has, and a context running without generated artifacts is untouched.</p> + * + * <p>They leave alone the beans the generated code created, which it has already injected: injected a Review Comment: Updated in 4281e587c4. The two processors are off the list of what an AOT-processed application lacks. A new paragraph says they are registered again as the application starts, for the annotated members the generated code cannot see: those of a bean a plugin registers at runtime, and those of a bean declared as an interface and built by a supplier, like the link generator. It also says that a bean the generated code created is still injected and initialized once, and that the processors inject it only through the members its definition's declared type does not have. -- 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]
