springboot @ConfigurationProperties注解源码解析(含松散绑定)

springboot @ConfigurationProperties注解源码解析(含松散绑定)

1.概要

属性绑定实现逻辑:
由springboot编写的org.springframework.boot.context.properties.bind.Binder工具类实现,中间过程涉及多层递归,建议debug调试跟踪查看。
@ConfigurationProperties自动绑定实现逻辑:
org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor BeanPostProcessor后置处理器实现。
松散绑定的实现逻辑:
①通过反射根据bean对象中的方法特别是setter,获取属性名称(统一为短横线形式),详见:org.springframework.boot.context.properties.bind.DataObjectPropertyName#toDashedForm
②根据@ConfigurationProperties注解prefix匹配属性组(其数据结构为Map<ConfigurationPropertyName, Set<String>>)。
③根据属性名称匹配属性组中的值,注意ConfigurationPropertyName重写了hashcodeequals驼峰下划线短横线格式互认。

2.测试代码

  • 示例代码1
    @Test
    public void test() {
        final MockEnvironment environment = new MockEnvironment();
        environment.setProperty("rocketmq.name-server", "localhost:9876");
        RocketMQProperties rocketMQProperties = Binder.get(environment).bindOrCreate( "rocketmq", RocketMQProperties.class);
        System.out.println(rocketMQProperties.getNameServer());
    }
  • 示例代码2
    @Test
    public void test() {
        final Map<String, Object> map = new HashMap<>(16);
        map.putIfAbsent("rocketmq.nameServer", "localhost:9876");
        final Properties properties = new Properties();
        properties.putIfAbsent("rocketmq.nameServer", "localhost:9876");
        final ConfigurableEnvironment environment = new StandardEnvironment();
        final MutablePropertySources propertySources = environment.getPropertySources();
        // 增加map属性组
        propertySources.addLast(new MapPropertySource("messageQueueProperties", map));
        // 增加properties属性组
//        propertySources.addLast(new PropertiesPropertySource("messageQueueProperties", properties));
        RocketMQProperties rocketMQProperties = Binder.get(environment).bindOrCreate( "rocketmq", RocketMQProperties.class);
        System.out.println(rocketMQProperties.getNameServer());
    }

3.调用链

由于以下调用过程存在递归调用存在,源代码分析过程比较乱,建议直接debug调试阅读源码,以下内容仅供参考!
org.springframework.boot.context.properties.ConfigurationProperties @ConfigurationProperties注解
=>
org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor#postProcessBeforeInitialization BeanPostProcessor bean后置处理器
=>
org.springframework.boot.context.properties.ConfigurationPropertiesBinder#bind 读取@ConfigurationProperties注解,将前缀作为ConfigurationPropertiesBean对象的bind name
=>
org.springframework.boot.context.properties.bind.Binder#bind(java.lang.String, org.springframework.boot.context.properties.bind.Bindable<T>, org.springframework.boot.context.properties.bind.BindHandler)
=>
org.springframework.boot.context.properties.bind.Binder#bindDataObject
=>
org.springframework.boot.context.properties.bind.JavaBeanBinder.Bean#get 通过反射实例化目标类
……递归调用
=>
org.springframework.boot.context.properties.bind.Binder#bindObject
=>
org.springframework.boot.context.properties.source.SpringIterableConfigurationPropertySource#getConfigurationProperty
=>
org.springframework.boot.context.properties.source.SpringIterableConfigurationPropertySource.Mappings#getMapped 重点:特别注意此处,从map对象中匹配属性值时,驼峰、下划线、短横线均可以取到。(原因是源码中重写了org.springframework.boot.context.properties.source.ConfigurationPropertyName类的hashcodeequals方法)。

4.@ConfigurationProperties注解处理

完整路径org.springframework.boot.context.properties.ConfigurationPropertiesBinder#bind
读取@ConfigurationProperties注解,将前缀作为ConfigurationPropertiesBean对象的bind name

	BindResult<?> bind(ConfigurationPropertiesBean propertiesBean) {
		Bindable<?> target = propertiesBean.asBindTarget();
		ConfigurationProperties annotation = propertiesBean.getAnnotation();
		BindHandler bindHandler = getBindHandler(target, annotation);
		return getBinder().bind(annotation.prefix(), target, bindHandler);
	}

5.属性绑定

完整路径org.springframework.boot.context.properties.bind.Binder
用途:将Spring容器Environment中的变量绑定到指定对象(除了支持String、Integer、Enum同样支持Map、List)。

有了此工具类,不使用@ConfigurationProperties注解也可以使用松散绑定的特性,享受springboot带来的便利!

  • org.springframework.boot.context.properties.bind.Binder#bindObject
private <T> Object bindObject(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler,
			Context context, boolean allowRecursiveBinding) {
		// 重点 根据名称获取属性值
		ConfigurationProperty property = findProperty(name, context);
		if (property == null && context.depth != 0 && containsNoDescendantOf(context.getSources(), name)) {
			return null;
		}
		AggregateBinder<?> aggregateBinder = getAggregateBinder(target, context);
		if (aggregateBinder != null) {
			return bindAggregate(name, target, handler, context, aggregateBinder);
		}
		if (property != null) {
			try {
				return bindProperty(target, context, property);
			}
			catch (ConverterNotFoundException ex) {
				// We might still be able to bind it using the recursive binders
				Object instance = bindDataObject(name, target, handler, context, allowRecursiveBinding);
				if (instance != null) {
					return instance;
				}
				throw ex;
			}
		}
		return bindDataObject(name, target, handler, context, allowRecursiveBinding);
	}
  • org.springframework.boot.context.properties.bind.JavaBeanBinder源码
/*
 * Copyright 2012-2020 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.boot.context.properties.bind;

import java.beans.Introspector;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.springframework.beans.BeanUtils;
import org.springframework.boot.context.properties.bind.Binder.Context;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.ConfigurationPropertyState;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;

/**
 * {@link DataObjectBinder} for mutable Java Beans.
 *
 * @author Phillip Webb
 * @author Madhura Bhave
 */
class JavaBeanBinder implements DataObjectBinder {

	static final JavaBeanBinder INSTANCE = new JavaBeanBinder();

	@Override
	public <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Context context,
			DataObjectPropertyBinder propertyBinder) {
		boolean hasKnownBindableProperties = target.getValue() != null && hasKnownBindableProperties(name, context);
		Bean<T> bean = Bean.get(target, hasKnownBindableProperties);
		if (bean == null) {
			return null;
		}
		BeanSupplier<T> beanSupplier = bean.getSupplier(target);
		boolean bound = bind(propertyBinder, bean, beanSupplier, context);
		return (bound ? beanSupplier.get() : null);
	}

	@Override
	@SuppressWarnings("unchecked")
	public <T> T create(Bindable<T> target, Context context) {
		Class<T> type = (Class<T>) target.getType().resolve();
		return (type != null) ? BeanUtils.instantiateClass(type) : null;
	}

	private boolean hasKnownBindableProperties(ConfigurationPropertyName name, Context context) {
		for (ConfigurationPropertySource source : context.getSources()) {
			if (source.containsDescendantOf(name) == ConfigurationPropertyState.PRESENT) {
				return true;
			}
		}
		return false;
	}

	private <T> boolean bind(DataObjectPropertyBinder propertyBinder, Bean<T> bean, BeanSupplier<T> beanSupplier,
			Context context) {
		boolean bound = false;
		for (BeanProperty beanProperty : bean.getProperties().values()) {
			bound |= bind(beanSupplier, propertyBinder, beanProperty);
			context.clearConfigurationProperty();
		}
		return bound;
	}

	private <T> boolean bind(BeanSupplier<T> beanSupplier, DataObjectPropertyBinder propertyBinder,
			BeanProperty property) {
		String propertyName = property.getName();
		ResolvableType type = property.getType();
		Supplier<Object> value = property.getValue(beanSupplier);
		Annotation[] annotations = property.getAnnotations();
		Object bound = propertyBinder.bindProperty(propertyName,
				Bindable.of(type).withSuppliedValue(value).withAnnotations(annotations));
		if (bound == null) {
			return false;
		}
		if (property.isSettable()) {
			property.setValue(beanSupplier, bound);
		}
		else if (value == null || !bound.equals(value.get())) {
			throw new IllegalStateException("No setter found for property: " + property.getName());
		}
		return true;
	}

	/**
	 * The bean being bound.
	 *
	 * @param <T> the bean type
	 */
	static class Bean<T> {

		private static Bean<?> cached;

		private final ResolvableType type;

		private final Class<?> resolvedType;

		private final Map<String, BeanProperty> properties = new LinkedHashMap<>();

		Bean(ResolvableType type, Class<?> resolvedType) {
			this.type = type;
			this.resolvedType = resolvedType;
			addProperties(resolvedType);
		}

		private void addProperties(Class<?> type) {
			while (type != null && !Object.class.equals(type)) {
				Method[] declaredMethods = getSorted(type, Class::getDeclaredMethods, Method::getName);
				Field[] declaredFields = getSorted(type, Class::getDeclaredFields, Field::getName);
				addProperties(declaredMethods, declaredFields);
				type = type.getSuperclass();
			}
		}

		private <S, E> E[] getSorted(S source, Function<S, E[]> elements, Function<E, String> name) {
			E[] result = elements.apply(source);
			Arrays.sort(result, Comparator.comparing(name));
			return result;
		}

		protected void addProperties(Method[] declaredMethods, Field[] declaredFields) {
			for (int i = 0; i < declaredMethods.length; i++) {
				if (!isCandidate(declaredMethods[i])) {
					declaredMethods[i] = null;
				}
			}
			for (Method method : declaredMethods) {
				addMethodIfPossible(method, "get", 0, BeanProperty::addGetter);
				addMethodIfPossible(method, "is", 0, BeanProperty::addGetter);
			}
			for (Method method : declaredMethods) {
				addMethodIfPossible(method, "set", 1, BeanProperty::addSetter);
			}
			for (Field field : declaredFields) {
				addField(field);
			}
		}

		private boolean isCandidate(Method method) {
			int modifiers = method.getModifiers();
			return !Modifier.isPrivate(modifiers) && !Modifier.isProtected(modifiers) && !Modifier.isAbstract(modifiers)
					&& !Modifier.isStatic(modifiers) && !Object.class.equals(method.getDeclaringClass())
					&& !Class.class.equals(method.getDeclaringClass()) && method.getName().indexOf('$') == -1;
		}

		private void addMethodIfPossible(Method method, String prefix, int parameterCount,
				BiConsumer<BeanProperty, Method> consumer) {
			if (method != null && method.getParameterCount() == parameterCount && method.getName().startsWith(prefix)
					&& method.getName().length() > prefix.length()) {
				String propertyName = Introspector.decapitalize(method.getName().substring(prefix.length()));
				consumer.accept(this.properties.computeIfAbsent(propertyName, this::getBeanProperty), method);
			}
		}

		private BeanProperty getBeanProperty(String name) {
			return new BeanProperty(name, this.type);
		}

		private void addField(Field field) {
			BeanProperty property = this.properties.get(field.getName());
			if (property != null) {
				property.addField(field);
			}
		}

		Map<String, BeanProperty> getProperties() {
			return this.properties;
		}

		@SuppressWarnings("unchecked")
		BeanSupplier<T> getSupplier(Bindable<T> target) {
			return new BeanSupplier<>(() -> {
				T instance = null;
				if (target.getValue() != null) {
					instance = target.getValue().get();
				}
				if (instance == null) {
					instance = (T) BeanUtils.instantiateClass(this.resolvedType);
				}
				return instance;
			});
		}

		@SuppressWarnings("unchecked")
		static <T> Bean<T> get(Bindable<T> bindable, boolean canCallGetValue) {
			ResolvableType type = bindable.getType();
			Class<?> resolvedType = type.resolve(Object.class);
			Supplier<T> value = bindable.getValue();
			T instance = null;
			if (canCallGetValue && value != null) {
				instance = value.get();
				resolvedType = (instance != null) ? instance.getClass() : resolvedType;
			}
			if (instance == null && !isInstantiable(resolvedType)) {
				return null;
			}
			Bean<?> bean = Bean.cached;
			if (bean == null || !bean.isOfType(type, resolvedType)) {
				bean = new Bean<>(type, resolvedType);
				cached = bean;
			}
			return (Bean<T>) bean;
		}

		private static boolean isInstantiable(Class<?> type) {
			if (type.isInterface()) {
				return false;
			}
			try {
				type.getDeclaredConstructor();
				return true;
			}
			catch (Exception ex) {
				return false;
			}
		}

		private boolean isOfType(ResolvableType type, Class<?> resolvedType) {
			if (this.type.hasGenerics() || type.hasGenerics()) {
				return this.type.equals(type);
			}
			return this.resolvedType != null && this.resolvedType.equals(resolvedType);
		}

	}

	private static class BeanSupplier<T> implements Supplier<T> {

		private final Supplier<T> factory;

		private T instance;

		BeanSupplier(Supplier<T> factory) {
			this.factory = factory;
		}

		@Override
		public T get() {
			if (this.instance == null) {
				this.instance = this.factory.get();
			}
			return this.instance;
		}

	}

	/**
	 * A bean property being bound.
	 */
	static class BeanProperty {

		private final String name;

		private final ResolvableType declaringClassType;

		private Method getter;

		private Method setter;

		private Field field;

		BeanProperty(String name, ResolvableType declaringClassType) {
		// 重点:将驼峰、下划线统一转换为短横线的格式
			this.name = DataObjectPropertyName.toDashedForm(name);
			this.declaringClassType = declaringClassType;
		}

		void addGetter(Method getter) {
			if (this.getter == null) {
				this.getter = getter;
			}
		}

		void addSetter(Method setter) {
			if (this.setter == null || isBetterSetter(setter)) {
				this.setter = setter;
			}
		}

		private boolean isBetterSetter(Method setter) {
			return this.getter != null && this.getter.getReturnType().equals(setter.getParameterTypes()[0]);
		}

		void addField(Field field) {
			if (this.field == null) {
				this.field = field;
			}
		}

		String getName() {
			return this.name;
		}

		ResolvableType getType() {
			if (this.setter != null) {
				MethodParameter methodParameter = new MethodParameter(this.setter, 0);
				return ResolvableType.forMethodParameter(methodParameter, this.declaringClassType);
			}
			MethodParameter methodParameter = new MethodParameter(this.getter, -1);
			return ResolvableType.forMethodParameter(methodParameter, this.declaringClassType);
		}

		Annotation[] getAnnotations() {
			try {
				return (this.field != null) ? this.field.getDeclaredAnnotations() : null;
			}
			catch (Exception ex) {
				return null;
			}
		}

		Supplier<Object> getValue(Supplier<?> instance) {
			if (this.getter == null) {
				return null;
			}
			return () -> {
				try {
					this.getter.setAccessible(true);
					return this.getter.invoke(instance.get());
				}
				catch (Exception ex) {
					throw new IllegalStateException("Unable to get value for property " + this.name, ex);
				}
			};
		}

		boolean isSettable() {
			return this.setter != null;
		}

		void setValue(Supplier<?> instance, Object value) {
			try {
				this.setter.setAccessible(true);
				this.setter.invoke(instance.get(), value);
			}
			catch (Exception ex) {
				throw new IllegalStateException("Unable to set value for property " + this.name, ex);
			}
		}

	}

}

6.松散绑定

完整路径org.springframework.boot.context.properties.bind.DataObjectPropertyName#toDashedForm
用途:将驼峰下划线统一转换为短横线的格式,这也是为什么springboot推送用户将属性书写为-

/*
 * Copyright 2012-2020 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.boot.context.properties.bind;

/**
 * Internal utility to help when dealing with data object property names.
 *
 * @author Phillip Webb
 * @author Madhura Bhave
 * @since 2.2.3
 * @see DataObjectBinder
 */
public abstract class DataObjectPropertyName {

	private DataObjectPropertyName() {
	}

	/**
	 * Return the specified Java Bean property name in dashed form.
	 * @param name the source name
	 * @return the dashed from
	 */
	public static String toDashedForm(String name) {
		StringBuilder result = new StringBuilder(name.length());
		boolean inIndex = false;
		for (int i = 0; i < name.length(); i++) {
			char ch = name.charAt(i);
			if (inIndex) {
				result.append(ch);
				if (ch == ']') {
					inIndex = false;
				}
			}
			else {
				if (ch == '[') {
					inIndex = true;
					result.append(ch);
				}
				else {
					ch = (ch != '_') ? ch : '-';
					if (Character.isUpperCase(ch) && result.length() > 0 && result.charAt(result.length() - 1) != '-') {
						result.append('-');
					}
					result.append(Character.toLowerCase(ch));
				}
			}
		}
		return result.toString();
	}

}

完整路径org.springframework.boot.context.properties.source.SpringIterableConfigurationPropertySource#getConfigurationProperty
用途:特别注意此处,从map对象中匹配属性值时,驼峰、下划线、短横线均可以取到。(原因是源码中重写了org.springframework.boot.context.properties.source.ConfigurationPropertyName类的hashcodeequals方法)。
丫的!此处打断点看了4、5遍才搞明白,为什么明明字符串不一样也可以匹配成功,以为其它地方做了映射处理!!!

@Override
	public ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) {
		if (name == null) {
			return null;
		}
		ConfigurationProperty configurationProperty = super.getConfigurationProperty(name);
		if (configurationProperty != null) {
			return configurationProperty;
		}
		// 根据bean对象转换的短横线式的属性名称,匹配Environment中的属性值
		for (String candidate : getMappings().getMapped(name)) {
			Object value = getPropertySource().getProperty(candidate);
			if (value != null) {
				Origin origin = PropertySourceOrigin.get(getPropertySource(), candidate);
				return ConfigurationProperty.of(name, value, origin);
			}
		}
		return null;
	}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

搬山境KL攻城狮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值