Skip to content
This repository has been archived by the owner on Feb 23, 2023. It is now read-only.

Commit

Permalink
Merge pull request #1059 from christophstrobl
Browse files Browse the repository at this point in the history
* pr/1059:
  Invoke init and destroy callbacks
  Polish "Handle bean initialization methods"
  Handle bean initialization methods

Closes gh-1059
  • Loading branch information
snicoll committed Sep 27, 2021
2 parents 425d6e1 + 677f44f commit 1f96ae9
Show file tree
Hide file tree
Showing 13 changed files with 1,051 additions and 2 deletions.
5 changes: 5 additions & 0 deletions spring-aot/pom.xml
Expand Up @@ -38,6 +38,11 @@
<artifactId>picocli</artifactId>
</dependency>

<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
Expand Down
@@ -0,0 +1,70 @@
/*
* Copyright 2019-2021 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.context.annotation;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.ResolvableType;
import org.springframework.util.ClassUtils;

/**
* {@link BeanDefinitionPostProcessor} that makes sure to identify custom init and destroy
* methods.
*
* @author Christoph Strobl
* @author Stephane Nicoll
*/
class CommonAnnotationBeanDefinitionPostProcessor implements BeanDefinitionPostProcessor, BeanFactoryAware {

private final CommonAnnotationBeanPostProcessor postProcessor = new CommonAnnotationBeanPostProcessor();

private ClassLoader classLoader;

@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.postProcessor.setBeanFactory(beanFactory);
this.classLoader = ((ConfigurableBeanFactory) beanFactory).getBeanClassLoader();
}

@Override
public void postProcessBeanDefinition(String beanName, RootBeanDefinition beanDefinition) {
postProcessor.postProcessMergedBeanDefinition(beanDefinition, getBeanType(beanDefinition), beanName);
}

private Class<?> getBeanType(RootBeanDefinition beanDefinition) {
ResolvableType resolvableType = beanDefinition.getResolvableType();
if (resolvableType != ResolvableType.NONE) {
return resolvableType.toClass();
}
if (beanDefinition.getBeanClassName() != null) {
return loadBeanClassName(beanDefinition.getBeanClassName());
}
return Object.class;
}

private Class<?> loadBeanClassName(String className) {
try {
return ClassUtils.forName(className, this.classLoader);
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Bean definition refers to invalid class '" + className + "'", ex);
}
}

}
Expand Up @@ -16,21 +16,28 @@

package org.springframework.context.bootstrap.generator.infrastructure;

import java.lang.reflect.Method;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.lang.model.element.Modifier;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.CodeBlock.Builder;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;

import org.springframework.aot.context.annotation.ImportAwareInvoker;
import org.springframework.aot.context.annotation.InitDestroyBeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver;
import org.springframework.context.bootstrap.generator.bean.support.ParameterWriter;
import org.springframework.context.origin.BeanFactoryStructure;
import org.springframework.context.origin.BeanFactoryStructureAnalyzer;
import org.springframework.core.ResolvableType;

/**
* Write the necessary code to prepare the infrastructure so that the application
Expand All @@ -44,9 +51,12 @@ public class BootstrapInfrastructureWriter {

private final BootstrapWriterContext writerContext;

private final ParameterWriter parameterWriter;

public BootstrapInfrastructureWriter(ConfigurableListableBeanFactory beanFactory, BootstrapWriterContext writerContext) {
this.beanFactory = beanFactory;
this.writerContext = writerContext;
this.parameterWriter = new ParameterWriter();
}

public void writeInfrastructure(CodeBlock.Builder code) {
Expand All @@ -57,9 +67,14 @@ public void writeInfrastructure(CodeBlock.Builder code) {
this.writerContext.getBootstrapClass(this.writerContext.getPackageName()).addMethod(importAwareInvokerMethod);
code.addStatement("$T.register(context, this::createImportAwareInvoker)", ImportAwareInvoker.class);
}
MethodSpec initDestroyBeanPostProcessorMethod = handleInitDestroyBeanPostProcessor();
if (initDestroyBeanPostProcessorMethod != null) {
this.writerContext.getBootstrapClass(this.writerContext.getPackageName()).addMethod(initDestroyBeanPostProcessorMethod);
code.addStatement("context.getBeanFactory().addBeanPostProcessor($N())", initDestroyBeanPostProcessorMethod);
}
}

MethodSpec handleImportAwareInvoker() {
private MethodSpec handleImportAwareInvoker() {
BeanFactoryStructure structure = createBeanFactoryStructure();
Map<String, Class<?>> importLinks = new ImportAwareLinksDiscoverer(structure, this.beanFactory.getBeanClassLoader())
.buildImportAwareLinks(writerContext.getNativeConfigurationRegistry());
Expand All @@ -76,6 +91,32 @@ MethodSpec handleImportAwareInvoker() {
.addModifiers(Modifier.PRIVATE).addCode(code.build()).build();
}

private MethodSpec handleInitDestroyBeanPostProcessor() {
InitDestroyMethodsDiscoverer initDestroyMethodsDiscoverer = new InitDestroyMethodsDiscoverer(this.beanFactory);
Map<String, List<Method>> initMethods = initDestroyMethodsDiscoverer.registerInitMethods(
this.writerContext.getNativeConfigurationRegistry());
Map<String, List<Method>> destroyMethods = initDestroyMethodsDiscoverer.registerDestroyMethods(
this.writerContext.getNativeConfigurationRegistry());
if (initMethods.isEmpty() && destroyMethods.isEmpty()) {
return null;
}
Builder code = CodeBlock.builder();
writeLifecycleMethods(code, initMethods, "initMethods");
writeLifecycleMethods(code, destroyMethods, "destroyMethods");
code.addStatement("return new $T($L, $L)", InitDestroyBeanPostProcessor.class, "initMethods", "destroyMethods");
return MethodSpec.methodBuilder("createInitDestroyBeanPostProcessor").returns(InitDestroyBeanPostProcessor.class)
.addModifiers(Modifier.PRIVATE).addCode(code.build()).build();
}

private void writeLifecycleMethods(Builder code, Map<String, List<Method>> lifecycleMethods, String variableName) {
code.addStatement("$T $L = new $T<>()", ParameterizedTypeName.get(ClassName.get(Map.class),
ClassName.get(String.class), ParameterizedTypeName.get(List.class, String.class)), variableName, LinkedHashMap.class);
lifecycleMethods.forEach((key, value) -> {
code.addStatement("$L.put($S, $L)", variableName, key, this.parameterWriter.writeParameterValue(
value.stream().map(Method::getName).collect(Collectors.toList()), ResolvableType.forClassWithGenerics(List.class, String.class)));
});
}

private BeanFactoryStructure createBeanFactoryStructure() {
BeanFactoryStructureAnalyzer analyzer = new BeanFactoryStructureAnalyzer(
this.beanFactory.getBeanClassLoader());
Expand Down
@@ -0,0 +1,196 @@
/*
* Copyright 2019-2021 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.context.bootstrap.generator.infrastructure;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.bootstrap.generator.infrastructure.nativex.NativeConfigurationRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

/**
* Extract init and destroy methods from {@link BeanDefinition}.
*
* @author Stephane Nicoll
*/
class InitDestroyMethodsDiscoverer {

private static final String CLOSE_METHOD_NAME = "close";

private static final String SHUTDOWN_METHOD_NAME = "shutdown";

private final ConfigurableListableBeanFactory beanFactory;

private final Field externallyManagedInitMethods;

private final Field externallyManagedDestroyMethods;

public InitDestroyMethodsDiscoverer(ConfigurableListableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
this.externallyManagedInitMethods = makeAccessible("externallyManagedInitMethods");
this.externallyManagedDestroyMethods = makeAccessible("externallyManagedDestroyMethods");
}

private static Field makeAccessible(String fieldName) {
Field field = ReflectionUtils.findField(RootBeanDefinition.class, fieldName);
ReflectionUtils.makeAccessible(field);
return field;
}

/**
* Identify the beans that have init methods and return a mapping from bean name to
* init method names
* @param nativeConfiguration the registry to use to declare the init methods that
* should be available by reflection
* @return the mapping
*/
Map<String, List<Method>> registerInitMethods(NativeConfigurationRegistry nativeConfiguration) {
return processLifecycleMethods(nativeConfiguration, this::processInitMethods);
}

private Set<Method> processInitMethods(BeanDefinition beanDefinition, Class<?> beanType) {
Set<Method> methods = new LinkedHashSet<>();
String initMethodName = beanDefinition.getInitMethodName();
if (StringUtils.hasText(initMethodName)) {
methods.add(findMethod(beanType, initMethodName));
}
for (String methodName : getExternallyManagedLifecycleMethodNames(beanDefinition, this.externallyManagedInitMethods)) {
methods.add(findMethod(beanType, methodName));
}
return methods;
}

/**
* Identify the beans that have destroy methods and return a mapping from bean name to
* destroy method names
* @param nativeConfiguration the registry to use to declare the destroy methods that
* should be available by reflection
* @return the mapping
*/
Map<String, List<Method>> registerDestroyMethods(NativeConfigurationRegistry nativeConfiguration) {
return processLifecycleMethods(nativeConfiguration, this::processDestroyMethods);
}

Map<String, List<Method>> processLifecycleMethods(NativeConfigurationRegistry registry, BiFunction<BeanDefinition, Class<?>, Set<Method>> lifecycleMethodsFactory) {
Map<String, List<Method>> lifecycleMethods = new LinkedHashMap<>();
for (String beanName : this.beanFactory.getBeanDefinitionNames()) {
BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition(beanName);
Class<?> beanType = getBeanType(beanDefinition);
Set<Method> methods = lifecycleMethodsFactory.apply(beanDefinition, beanType);
if (!ObjectUtils.isEmpty(methods)) {
lifecycleMethods.put(beanName, new ArrayList<>(methods));
registry.reflection().forType(beanType).withMethods(methods.toArray(new Method[0]));
}
}
return lifecycleMethods;
}

private Set<Method> processDestroyMethods(BeanDefinition beanDefinition, Class<?> beanType) {
Set<Method> methods = new LinkedHashSet<>();
String destroyMethodName = beanDefinition.getDestroyMethodName();
if (AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName) ||
(destroyMethodName == null && AutoCloseable.class.isAssignableFrom(beanType))) {
Method method = detectInferredDestroyMethod(beanType);
if (method != null) {
methods.add(method);
}
}
else if (StringUtils.hasText(destroyMethodName)) {
methods.add(findMethod(beanType, destroyMethodName));
}
for (String methodName : getExternallyManagedLifecycleMethodNames(beanDefinition, this.externallyManagedDestroyMethods)) {
methods.add(findMethod(beanType, methodName));
}
return methods;
}

// TODO: remove once https://github.com/spring-projects/spring-framework/issues/27449 is resolved
@SuppressWarnings("unchecked")
private Set<String> getExternallyManagedLifecycleMethodNames(BeanDefinition beanDefinition, Field field) {
if (beanDefinition instanceof RootBeanDefinition) {
Object value = ReflectionUtils.getField(field, beanDefinition);
if (!ObjectUtils.isEmpty(value)) {
return (Set<String>) value;
}
}
return Collections.emptySet();
}

private Method detectInferredDestroyMethod(Class<?> beanType) {
if (!DisposableBean.class.isAssignableFrom(beanType)) {
try {
return beanType.getMethod(CLOSE_METHOD_NAME);
}
catch (NoSuchMethodException ex) {
try {
return beanType.getMethod(SHUTDOWN_METHOD_NAME);
}
catch (NoSuchMethodException ex2) {
// no candidate destroy method found
}
}
}
return null;
}

private Method findMethod(Class<?> beanType, String methodName) {
Method method = ReflectionUtils.findMethod(beanType, methodName);
if (method == null) {
throw new IllegalStateException("Lifecycle method annotation '" + methodName + "' not found on: " + beanType);
}
return method;
}

private Class<?> getBeanType(BeanDefinition beanDefinition) {
ResolvableType resolvableType = beanDefinition.getResolvableType();
if (resolvableType != ResolvableType.NONE) {
return resolvableType.toClass();
}
if (beanDefinition.getBeanClassName() != null) {
return loadBeanClassName(beanDefinition.getBeanClassName());
}
return Object.class;
}

private Class<?> loadBeanClassName(String className) {
try {
return ClassUtils.forName(className, this.beanFactory.getBeanClassLoader());
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Bean definition refers to invalid class '" + className + "'", ex);
}
}

}
@@ -1,3 +1,19 @@
/*
* Copyright 2019-2021 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.context.bootstrap.generator.nativex;

import java.lang.reflect.Executable;
Expand Down
3 changes: 2 additions & 1 deletion spring-aot/src/main/resources/META-INF/spring.factories
@@ -1,6 +1,7 @@
org.springframework.context.annotation.BeanDefinitionPostProcessor=\
org.springframework.data.RepositoryFactoryBeanPostProcessor,\
org.springframework.plugin.PluginRegistryFactoryBeanPostProcessor
org.springframework.plugin.PluginRegistryFactoryBeanPostProcessor,\
org.springframework.context.annotation.CommonAnnotationBeanDefinitionPostProcessor

org.springframework.context.bootstrap.generator.bean.BeanRegistrationWriterSupplier=\
org.springframework.boot.autoconfigure.AutoConfigurationPackagesBeanRegistrationWriterSupplier,\
Expand Down

0 comments on commit 1f96ae9

Please sign in to comment.