Skip to content

Commit

Permalink
Ensure package-private init/destroy methods are always invoked
Browse files Browse the repository at this point in the history
Prior to this commit, if an init/destroy method was package-private and
declared in a superclass in a package different from the package in
which the registered bean resided, a local init/destroy method with the
same name would effectively "shadow" the method from the different
package, resulting in only the local init/destroy method being invoked.

This commit addresses this issue by tracking package-private init
methods from different packages using their fully-qualified method
names, analogous to the existing support for private init/destroy
methods.

Closes gh-30718
  • Loading branch information
sbrannen committed Jun 21, 2023
1 parent f67f98a commit 0eb33d0
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 29 deletions.
Expand Up @@ -269,13 +269,13 @@ private LifecycleMetadata buildLifecycleMetadata(final Class<?> beanClass) {

ReflectionUtils.doWithLocalMethods(currentClass, method -> {
if (this.initAnnotationType != null && method.isAnnotationPresent(this.initAnnotationType)) {
currInitMethods.add(new LifecycleMethod(method));
currInitMethods.add(new LifecycleMethod(method, beanClass));
if (logger.isTraceEnabled()) {
logger.trace("Found init method on class [" + beanClass.getName() + "]: " + method);
}
}
if (this.destroyAnnotationType != null && method.isAnnotationPresent(this.destroyAnnotationType)) {
currDestroyMethods.add(new LifecycleMethod(method));
currDestroyMethods.add(new LifecycleMethod(method, beanClass));
if (logger.isTraceEnabled()) {
logger.trace("Found destroy method on class [" + beanClass.getName() + "]: " + method);
}
Expand Down Expand Up @@ -404,12 +404,12 @@ private static class LifecycleMethod {

private final String identifier;

public LifecycleMethod(Method method) {
public LifecycleMethod(Method method, Class<?> beanClass) {
if (method.getParameterCount() != 0) {
throw new IllegalStateException("Lifecycle annotation requires a no-arg method: " + method);
}
this.method = method;
this.identifier = (Modifier.isPrivate(method.getModifiers()) ?
this.identifier = (isPrivateOrNotVisible(method, beanClass) ?
ClassUtils.getQualifiedMethodName(method) : method.getName());
}

Expand All @@ -436,6 +436,23 @@ public boolean equals(@Nullable Object other) {
public int hashCode() {
return this.identifier.hashCode();
}

/**
* Determine if the supplied lifecycle {@link Method} is private or not
* visible to the supplied bean {@link Class}.
* @since 6.0.11
*/
private static boolean isPrivateOrNotVisible(Method method, Class<?> beanClass) {
int modifiers = method.getModifiers();
if (Modifier.isPrivate(modifiers)) {
return true;
}
// Method is declared in a class that resides in a different package
// than the bean class and the method is neither public nor protected?
return (!method.getDeclaringClass().getPackageName().equals(beanClass.getPackageName()) &&
!(Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers)));
}

}

}
Expand Up @@ -497,14 +497,15 @@ public Set<Member> getExternallyManagedConfigMembers() {

/**
* Register an externally managed configuration initialization method &mdash;
* for example, a method annotated with JSR-250's
* {@link jakarta.annotation.PostConstruct} annotation.
* <p>The supplied {@code initMethod} may be the
* {@linkplain Method#getName() simple method name} for non-private methods or the
* for example, a method annotated with JSR-250's {@link javax.annotation.PostConstruct}
* or Jakarta's {@link jakarta.annotation.PostConstruct} annotation.
* <p>The supplied {@code initMethod} may be a
* {@linkplain Method#getName() simple method name} or a
* {@linkplain org.springframework.util.ClassUtils#getQualifiedMethodName(Method)
* qualified method name} for {@code private} methods. A qualified name is
* necessary for {@code private} methods in order to disambiguate between
* multiple private methods with the same name within a class hierarchy.
* qualified method name} for package-private and {@code private} methods.
* A qualified name is necessary for package-private and {@code private} methods
* in order to disambiguate between multiple such methods with the same name
* within a type hierarchy.
*/
public void registerExternallyManagedInitMethod(String initMethod) {
synchronized (this.postProcessingLock) {
Expand Down
Expand Up @@ -28,6 +28,8 @@
import org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.lifecyclemethods.InitDestroyBean;
import org.springframework.context.annotation.lifecyclemethods.PackagePrivateInitDestroyBean;

import static org.assertj.core.api.Assertions.assertThat;

Expand All @@ -53,11 +55,11 @@ class InitDestroyMethodLifecycleTests {
@Test
void initDestroyMethods() {
Class<?> beanClass = InitDestroyBean.class;
DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "afterPropertiesSet", "destroy");
DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "initMethod", "destroyMethod");
InitDestroyBean bean = beanFactory.getBean(InitDestroyBean.class);
assertThat(bean.initMethods).as("init-methods").containsExactly("afterPropertiesSet");
assertThat(bean.initMethods).as("init-methods").containsExactly("initMethod");
beanFactory.destroySingletons();
assertThat(bean.destroyMethods).as("destroy-methods").containsExactly("destroy");
assertThat(bean.destroyMethods).as("destroy-methods").containsExactly("destroyMethod");
}

@Test
Expand Down Expand Up @@ -132,6 +134,26 @@ void jakartaAnnotationsCustomPrivateInitDestroyMethodsWithTheSameMethodNames() {
);
}

@Test
void jakartaAnnotationsCustomPackagePrivateInitDestroyMethodsWithTheSameMethodNames() {
Class<?> beanClass = SubPackagePrivateInitDestroyBean.class;
DefaultListableBeanFactory beanFactory = createBeanFactoryAndRegisterBean(beanClass, "initMethod", "destroyMethod");
SubPackagePrivateInitDestroyBean bean = beanFactory.getBean(SubPackagePrivateInitDestroyBean.class);

assertThat(bean.initMethods).as("init-methods").containsExactly(
"PackagePrivateInitDestroyBean.postConstruct",
"SubPackagePrivateInitDestroyBean.postConstruct",
"initMethod"
);

beanFactory.destroySingletons();
assertThat(bean.destroyMethods).as("destroy-methods").containsExactly(
"SubPackagePrivateInitDestroyBean.preDestroy",
"PackagePrivateInitDestroyBean.preDestroy",
"destroyMethod"
);
}

@Test
void allLifecycleMechanismsAtOnce() {
Class<?> beanClass = AllInOneBean.class;
Expand Down Expand Up @@ -165,21 +187,6 @@ private static DefaultListableBeanFactory createBeanFactoryAndRegisterBean(Class
}


static class InitDestroyBean {

final List<String> initMethods = new ArrayList<>();
final List<String> destroyMethods = new ArrayList<>();


public void afterPropertiesSet() throws Exception {
this.initMethods.add("afterPropertiesSet");
}

public void destroy() throws Exception {
this.destroyMethods.add("destroy");
}
}

static class InitializingDisposableWithShadowedMethodsBean extends InitDestroyBean implements
InitializingBean, DisposableBean {

Expand Down Expand Up @@ -296,4 +303,18 @@ public void destroy() throws Exception {
}
}

static class SubPackagePrivateInitDestroyBean extends PackagePrivateInitDestroyBean {

@PostConstruct
void postConstruct() {
this.initMethods.add("SubPackagePrivateInitDestroyBean.postConstruct");
}

@PreDestroy
void preDestroy() {
this.destroyMethods.add("SubPackagePrivateInitDestroyBean.preDestroy");
}

}

}
@@ -0,0 +1,36 @@
/*
* Copyright 2002-2023 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.lifecyclemethods;

import java.util.ArrayList;
import java.util.List;

public class InitDestroyBean {

public final List<String> initMethods = new ArrayList<>();
public final List<String> destroyMethods = new ArrayList<>();


public void initMethod() {
this.initMethods.add("initMethod");
}

public void destroyMethod() {
this.destroyMethods.add("destroyMethod");
}

}
@@ -0,0 +1,34 @@
/*
* Copyright 2002-2023 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.lifecyclemethods;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

public class PackagePrivateInitDestroyBean extends InitDestroyBean {

@PostConstruct
void postConstruct() {
this.initMethods.add("PackagePrivateInitDestroyBean.postConstruct");
}

@PreDestroy
void preDestroy() {
this.destroyMethods.add("PackagePrivateInitDestroyBean.preDestroy");
}

}

0 comments on commit 0eb33d0

Please sign in to comment.