Skip to content

Bad property binding performance while use binder with an un enumerable property source #45970

@tanruian

Description

@tanruian
Contributor

Upgrade spring-boot to 3.5.0, an issue where a property source binds to java objects slowly, for deep nested collection (consists of 4 depth), around 1 minutes. Number of scans of property with exponential growth.
Because the ManagementContextAutoConfiguration automatically adds an un enumerable property source to the environment,resulting in the inability to confirm whether the property source contains bindable property, requiring guessing and traversing property one by one based on the index 0-12

Root cause is changes in 93113a4

import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.boot.context.properties.bind.AbstractBindHandler;
import org.springframework.boot.context.properties.bind.BindContext;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.core.env.PropertySource;
import org.springframework.lang.NonNull;

import java.util.List;

@Slf4j
public class SpringBindIssue {

    public static void main(String[] args) {

        var propertySources = List.of(ConfigurationPropertySource.from(new PropertySource<>("xxxx") {
            @Override
            public Object getProperty(@NonNull String name) {
                if (name.equals("xxxx")) {
                    return "xxx";
                }
                return null;
            }
        }));

        AbstractBindHandler abstractBindHandler = new AbstractBindHandler() {

            @Override
            public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target, BindContext context) {
                log.info("Start bind [{}] [{}]", target.getType(), name);
                return super.onStart(name, target, context);
            }

        };
        new Binder(propertySources, null, new ApplicationConversionService(), null, abstractBindHandler).bind("", NestedObject1.class);
    }


    @Setter
    @Getter
    public static class NestedObject1 {

        @NestedConfigurationProperty
        private List<NestedObject2> children;

    }

    @Setter
    @Getter
    public static class NestedObject2 {

        @NestedConfigurationProperty
        private List<NestedObject3> children;

    }

    @Setter
    @Getter
    public static class NestedObject3 {

        @NestedConfigurationProperty
        private List<NestedObject4> children;

    }

    @Setter
    @Getter
    public static class NestedObject4 {

        @NestedConfigurationProperty
        private List<String> children;

    }


}
Start bind [SpringBindIssue$NestedObject1] []
Start bind [java.util.List<SpringBindIssue$NestedObject2>] [children]
Start bind [SpringBindIssue$NestedObject2] [children[0]]
Start bind [java.util.List<SpringBindIssue$NestedObject3>] [children[0].children]
Start bind [SpringBindIssue$NestedObject3] [children[0].children[0]]
Start bind [java.util.List<SpringBindIssue$NestedObject4>] [children[0].children[0].children]
Start bind [SpringBindIssue$NestedObject4] [children[0].children[0].children[0]]
Start bind [java.util.List<java.lang.String>] [children[0].children[0].children[0].children]
Start bind [java.lang.String] [children[0].children[0].children[0].children[0]]
Start bind [java.lang.String] [children[0].children[0].children[0].children[1]]
Start bind [java.lang.String] [children[0].children[0].children[0].children[2]]
Start bind [java.lang.String] [children[0].children[0].children[0].children[3]]
Start bind [java.lang.String] [children[0].children[0].children[0].children[4]]
Start bind [java.lang.String] [children[0].children[0].children[0].children[5]]
Start bind [java.lang.String] [children[0].children[0].children[0].children[6]]
Start bind [java.lang.String] [children[0].children[0].children[0].children[7]]
Start bind [java.lang.String] [children[0].children[0].children[0].children[8]]
Start bind [java.lang.String] [children[0].children[0].children[0].children[9]]
Start bind [java.lang.String] [children[0].children[0].children[0].children[10]]
Start bind [java.lang.String] [children[0].children[0].children[0].children[11]]
Start bind [java.lang.String] [children[0].children[0].children[0].children[12]]
Start bind [SpringBindIssue$NestedObject4] [children[0].children[0].children[1]]
Start bind [java.util.List<java.lang.String>] [children[0].children[0].children[1].children]
Start bind [java.lang.String] [children[0].children[0].children[1].children[0]]
Start bind [java.lang.String] [children[0].children[0].children[1].children[1]]
...
...
...
...

Activity

wilkinsona

wilkinsona commented on Jun 16, 2025

@wilkinsona
Member

Here's the reproducer without Lombok:

package gh45970;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.boot.context.properties.bind.AbstractBindHandler;
import org.springframework.boot.context.properties.bind.BindContext;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.core.env.PropertySource;
import org.springframework.lang.NonNull;

public class SpringBindIssue {

	private static final Logger log = LoggerFactory.getLogger(SpringBindIssue.class);

	public static void main(String[] args) {

		var propertySources = List.of(ConfigurationPropertySource.from(new PropertySource<>("xxxx") {
			@Override
			public Object getProperty(@NonNull String name) {
				if (name.equals("xxxx")) {
					return "xxx";
				}
				return null;
			}
		}));

		AbstractBindHandler abstractBindHandler = new AbstractBindHandler() {

			@Override
			public <T> Bindable<T> onStart(ConfigurationPropertyName name, Bindable<T> target, BindContext context) {
				log.info("Start bind [{}] [{}]", target.getType(), name);
				return super.onStart(name, target, context);
			}

		};
		new Binder(propertySources, null, new ApplicationConversionService(), null, abstractBindHandler).bind("",
				NestedObject1.class);
	}

	public static class NestedObject1 {

		@NestedConfigurationProperty
		private List<NestedObject2> children;

		public List<NestedObject2> getChildren() {
			return this.children;
		}

		public void setChildren(List<NestedObject2> children) {
			this.children = children;
		}

	}

	public static class NestedObject2 {

		@NestedConfigurationProperty
		private List<NestedObject3> children;

		public List<NestedObject3> getChildren() {
			return this.children;
		}

		public void setChildren(List<NestedObject3> children) {
			this.children = children;
		}

	}

	public static class NestedObject3 {

		@NestedConfigurationProperty
		private List<NestedObject4> children;

		public List<NestedObject4> getChildren() {
			return this.children;
		}

		public void setChildren(List<NestedObject4> children) {
			this.children = children;
		}

	}

	public static class NestedObject4 {

		@NestedConfigurationProperty
		private List<String> children;

		public List<String> getChildren() {
			return this.children;
		}

		public void setChildren(List<String> children) {
			this.children = children;
		}

	}

}

The logging with 3.4 is minimal:

06:37:35.041 [main] INFO temp.SpringBindIssue -- Start bind [temp.SpringBindIssue$NestedObject1] []
06:37:35.061 [main] INFO temp.SpringBindIssue -- Start bind [java.util.List<temp.SpringBindIssue$NestedObject2>] [children]
06:37:35.064 [main] INFO temp.SpringBindIssue -- Start bind [temp.SpringBindIssue$NestedObject2] [children[0]]
06:37:35.065 [main] INFO temp.SpringBindIssue -- Start bind [java.util.List<temp.SpringBindIssue$NestedObject3>] [children[0].children]
06:37:35.065 [main] INFO temp.SpringBindIssue -- Start bind [temp.SpringBindIssue$NestedObject3] [children[0].children[0]]
06:37:35.066 [main] INFO temp.SpringBindIssue -- Start bind [java.util.List<temp.SpringBindIssue$NestedObject4>] [children[0].children[0].children]
06:37:35.066 [main] INFO temp.SpringBindIssue -- Start bind [temp.SpringBindIssue$NestedObject4] [children[0].children[0].children[0]]
06:37:35.066 [main] INFO temp.SpringBindIssue -- Start bind [java.util.List<java.lang.String>] [children[0].children[0].children[0].children]
06:37:35.066 [main] INFO temp.SpringBindIssue -- Start bind [java.lang.String] [children[0].children[0].children[0].children[0]]

With 3.5, it's over 33000 lines.

added this to the 3.5.x milestone on Jun 16, 2025
self-assigned this
on Jun 17, 2025
philwebb

philwebb commented on Jun 17, 2025

@philwebb
Member

Closing in favor of PR #45968. Thanks @tanruian!

added
status: supersededAn issue that has been superseded by another
and removed
type: regressionA regression from a previous release
on Jun 17, 2025
removed this from the 3.5.x milestone on Jun 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

status: supersededAn issue that has been superseded by another

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

    Development

    No branches or pull requests

      Participants

      @philwebb@wilkinsona@spring-projects-issues@tanruian

      Issue actions

        Bad property binding performance while use binder with an un enumerable property source · Issue #45970 · spring-projects/spring-boot