Sfoglia il codice sorgente

Avoid initializing raw bean during runtime in native-images

Closes gh-14825
Marcus Hert Da Coregio 1 anno fa
parent
commit
472c9f8275

+ 35 - 3
config/src/main/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessor.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -22,11 +22,14 @@ import java.util.List;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 
+import org.springframework.aop.support.AopUtils;
 import org.springframework.beans.factory.Aware;
 import org.springframework.beans.factory.DisposableBean;
 import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.ObjectProvider;
 import org.springframework.beans.factory.SmartInitializingSingleton;
 import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
+import org.springframework.core.NativeDetector;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
 import org.springframework.util.Assert;
 
@@ -55,14 +58,13 @@ final class AutowireBeanFactoryObjectPostProcessor
 	}
 
 	@Override
-	@SuppressWarnings("unchecked")
 	public <T> T postProcess(T object) {
 		if (object == null) {
 			return null;
 		}
 		T result = null;
 		try {
-			result = (T) this.autowireBeanFactory.initializeBean(object, object.toString());
+			result = initializeBeanIfNeeded(object);
 		}
 		catch (RuntimeException ex) {
 			Class<?> type = object.getClass();
@@ -78,6 +80,36 @@ final class AutowireBeanFactoryObjectPostProcessor
 		return result;
 	}
 
+	/**
+	 * Invokes {@link AutowireCapableBeanFactory#initializeBean(Object, String)} only if
+	 * needed, i.e when the application is not a native image or the object is not a CGLIB
+	 * proxy.
+	 * @param object the object to initialize
+	 * @param <T> the type of the object
+	 * @return the initialized bean or an existing bean if the object is a CGLIB proxy and
+	 * the application is a native image
+	 * @see <a href=
+	 * "https://github.com/spring-projects/spring-security/issues/14825">Issue
+	 * gh-14825</a>
+	 */
+	@SuppressWarnings("unchecked")
+	private <T> T initializeBeanIfNeeded(T object) {
+		if (!NativeDetector.inNativeImage() || !AopUtils.isCglibProxy(object)) {
+			return (T) this.autowireBeanFactory.initializeBean(object, object.toString());
+		}
+		ObjectProvider<?> provider = this.autowireBeanFactory.getBeanProvider(object.getClass());
+		Object bean = provider.getIfUnique();
+		if (bean == null) {
+			String msg = """
+					Failed to resolve an unique bean (single or primary) of type [%s] from the BeanFactory.
+					Because the object is a CGLIB Proxy, a raw bean cannot be initialized during runtime in a native image.
+					"""
+				.formatted(object.getClass());
+			throw new IllegalStateException(msg);
+		}
+		return (T) bean;
+	}
+
 	@Override
 	public void afterSingletonsInstantiated() {
 		for (SmartInitializingSingleton singleton : this.smartSingletons) {

+ 61 - 1
config/src/test/java/org/springframework/security/config/annotation/configuration/AutowireBeanFactoryObjectPostProcessorTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -16,9 +16,13 @@
 
 package org.springframework.security.config.annotation.configuration;
 
+import java.lang.reflect.Modifier;
+
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mockito;
 
+import org.springframework.aop.framework.ProxyFactory;
 import org.springframework.beans.factory.BeanClassLoaderAware;
 import org.springframework.beans.factory.BeanFactoryAware;
 import org.springframework.beans.factory.DisposableBean;
@@ -31,13 +35,16 @@ import org.springframework.context.EnvironmentAware;
 import org.springframework.context.MessageSourceAware;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.core.NativeDetector;
 import org.springframework.security.config.annotation.ObjectPostProcessor;
 import org.springframework.security.config.test.SpringTestContext;
 import org.springframework.security.config.test.SpringTestContextExtension;
 import org.springframework.web.context.ServletContextAware;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatException;
 import static org.mockito.ArgumentMatchers.isNotNull;
+import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 
@@ -132,6 +139,59 @@ public class AutowireBeanFactoryObjectPostProcessorTests {
 		assertThat(bean.doStuff()).isEqualTo("null");
 	}
 
+	@Test
+	void postProcessWhenObjectIsCgLibProxyAndInNativeImageThenUseExistingBean() {
+		try (var detector = Mockito.mockStatic(NativeDetector.class)) {
+			given(NativeDetector.inNativeImage()).willReturn(true);
+
+			ProxyFactory proxyFactory = new ProxyFactory(new MyClass());
+			proxyFactory.setProxyTargetClass(!Modifier.isFinal(MyClass.class.getModifiers()));
+			MyClass myClass = (MyClass) proxyFactory.getProxy();
+
+			this.spring.register(Config.class, myClass.getClass()).autowire();
+			this.spring.getContext().getBean(myClass.getClass()).setIdentifier("0000");
+
+			MyClass postProcessed = this.objectObjectPostProcessor.postProcess(myClass);
+			assertThat(postProcessed.getIdentifier()).isEqualTo("0000");
+		}
+	}
+
+	@Test
+	void postProcessWhenObjectIsCgLibProxyAndInNativeImageAndBeanDoesNotExistsThenIllegalStateException() {
+		try (var detector = Mockito.mockStatic(NativeDetector.class)) {
+			given(NativeDetector.inNativeImage()).willReturn(true);
+
+			ProxyFactory proxyFactory = new ProxyFactory(new MyClass());
+			proxyFactory.setProxyTargetClass(!Modifier.isFinal(MyClass.class.getModifiers()));
+			MyClass myClass = (MyClass) proxyFactory.getProxy();
+
+			this.spring.register(Config.class).autowire();
+
+			assertThatException().isThrownBy(() -> this.objectObjectPostProcessor.postProcess(myClass))
+				.havingRootCause()
+				.isInstanceOf(IllegalStateException.class)
+				.withMessage(
+						"""
+								Failed to resolve an unique bean (single or primary) of type [class org.springframework.security.config.annotation.configuration.AutowireBeanFactoryObjectPostProcessorTests$MyClass$$SpringCGLIB$$0] from the BeanFactory.
+								Because the object is a CGLIB Proxy, a raw bean cannot be initialized during runtime in a native image.
+								""");
+		}
+	}
+
+	static class MyClass {
+
+		private String identifier = "1234";
+
+		String getIdentifier() {
+			return this.identifier;
+		}
+
+		void setIdentifier(String identifier) {
+			this.identifier = identifier;
+		}
+
+	}
+
 	@Configuration
 	static class Config {