【小家Spring】Spring中@Value注解有多强大?从原理层面去剖析为何它有如此大的“能耐“

2019-09-03 15:51:08 浏览数 (1)

前言

@Value注解是Spring3.0后提出来的一个注解。注解内容本身非常之简单,但是它提供的功能却非常之强大。

首先从它的注解本身定义知道,它能使用在:

  1. 字段上
  2. set方法上
  3. 方法入参上
  4. 当作元注解

它的功能大致可归类为:

  1. 注入普通字符串
  2. 书写SpEL表达式(功能强大包括:获取系统属性、调用静态方法、计算、注入bean、调用bean的方法等等~~~)
  3. 注入Resource。如:@Value("classpath:com/demo/config.txt") 使用Resource类型接收
  4. 注入URL资源。如:@Value("http://www.baidu.com") 使用Resource类型接收

这里面不得不提的,就是它对强大的SpEL的支持,这是我们感叹@Value非常之强大的核心原因。关于SpEL的使用和原理浅析,请参见:

【小家Spring】SpEL你感兴趣的实现原理浅析spring-expression~(SpelExpressionParser、EvaluationContext、rootObject)

平时我们依赖注入你可能只会使用@Autowired等注解,但其实@Value也有注入bean的功能哦~

@Value

首先看看@Value注解本身,简单到令人发指有木有

代码语言:javascript复制
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Value {
	String value(); // 没有默认值 所以是必填的
}

接下来以一个具体实例,跟踪分析它的执行原理:

代码语言:javascript复制
@Configuration
public class RootConfig {

    @Value("#{person.name}")
    private String personName;

    @Bean
    public Person person() {
        return new Person("fsx", 18);
    }
}

之前我们已经讲过,当Bean进行初始化完成之后会populateBean()对它的属性进行赋值,这个时候AutowiredAnnotationBeanPostProcessor这个后置处理器生效,从而对属性进行依赖注入赋值。

AutowiredAnnotationBeanPostProcessor它能够处理@Autowired和@Value注解~ 注意:因为@ValueBeanPostProcessor来解析的,所以具有容器隔离性(本容器内的Bean使用@Value只能引用到本容器内的值哦~,因为BeanPostProcessor是具有隔离性的) 推荐:所有的@Value都写在根容器(也就是我们常说的Service容器)内,请不要放在web容器里。也就是说,请尽量不要在controller从使用@Value注解,因为业务我们都要求放在service层 三层架构:Controller、Service、Repository务必做到职责分离和松耦合~

若对Bean的初始化、实例化原理有不是非常清楚的,可抽空参见:

【小家Spring】细说Spring IOC容器的自动装配(@Autowired),以及Spring4.0新特性之【泛型依赖注入】的源码级解析

有了上面基础,所以我们直接从AutowiredAnnotationBeanPostProcessor#postProcessPropertyValues()方法入手:(Spring5.1后为postProcessProperties方法,方法的语义更加清晰些了~)

代码语言:javascript复制
public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
		implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
		
	// 这个方法是InstantiationAwareBeanPostProcessor的,它在给属性赋值的时候会被调用~~
	@Override
	public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
		
		// InjectionMetadata 里包含private final Collection<InjectedElement> injectedElements;表示所有需要注入处理的属性们~~~
		// 所以最终都是InjectionMetadata去处理~
		InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
		try {
			metadata.inject(bean, beanName, pvs);
		} ...
		return pvs;
	}
	
}
InjectionMetadata

用于管理注入元数据的内部类。不建议直接在应用程序中使用。AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor以及org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor这几个哥们最终的注入都是靠它~

本处直接看它的inject方法:

代码语言:javascript复制
public class InjectionMetadata {
	public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
		Collection<InjectedElement> checkedElements = this.checkedElements;
		Collection<InjectedElement> elementsToIterate = (checkedElements != null ? checkedElements : this.injectedElements);
	
		// 遍历,一个一个处理
		if (!elementsToIterate.isEmpty()) {
			for (InjectedElement element : elementsToIterate) {
				element.inject(target, beanName, pvs);
			}
		}
	}	
}

总而言之,最终是委托给了InjectedElement 。它有两个内部类的实现:AutowiredFieldElementAutowiredMethodElement 此处我们只关注字段注入的。

InjectedElement是InjectionMetadata的一个public内部类,并且是抽象的。AutowiredFieldElement和AutowiredMethodElement都是AutowiredAnnotationBeanPostProcessor的private内部类。 另外:CommonAnnotationBeanPostProcessor中有InjectedElement的实现类:LookupElementResourceElementEjbRefElementWebServiceRefElement等来辅助完成注入~

AutowiredFieldElement
代码语言:javascript复制
private class AutowiredFieldElement extends InjectionMetadata.InjectedElement {
	private final boolean required;
	private volatile boolean cached = false;
	@Nullable
	private volatile Object cachedFieldValue;

	public AutowiredFieldElement(Field field, boolean required) {
		super(field, null);
		this.required = required;
	}

	@Override
	protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
		// member在抽象父类:InjectedElement中定义~~~
		Field field = (Field) this.member;
		Object value;
		if (this.cached) {
			value = resolvedCachedArgument(beanName, this.cachedFieldValue);
		}
		else {
			DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
			desc.setContainingClass(bean.getClass());
			Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
			Assert.state(beanFactory != null, "No BeanFactory available");

			// 此处一般为SimpleTypeConverter,它registerDefaultEditors=true,所以普通类型大都能能通过属性编辑器实现转换的
			TypeConverter typeConverter = beanFactory.getTypeConverter();
			try {
				// 最最最根本的原理,其实在resolveDependency这个方法里,它最终返回的就是一个具体的值,这个value是个Object~
				value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
			} catch (BeansException ex) {
				throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
			}
			synchronized (this) {
				if (!this.cached) {
					if (value != null || this.required) {
						this.cachedFieldValue = desc;
						registerDependentBeans(beanName, autowiredBeanNames);
						if (autowiredBeanNames.size() == 1) {
							String autowiredBeanName = autowiredBeanNames.iterator().next();
							if (beanFactory.containsBean(autowiredBeanName) &&
									beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
								this.cachedFieldValue = new ShortcutDependencyDescriptor(
										desc, autowiredBeanName, field.getType());
							}
						}
					} else {
						this.cachedFieldValue = null;
					}
					this.cached = true;
				}
			}
		}
		if (value != null) {
			ReflectionUtils.makeAccessible(field);
			field.set(bean, value);
		}
	}
}
DefaultListableBeanFactory#resolveDependency

它是Spring容器整个体系里实现依赖查找的心脏~

代码语言:javascript复制
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
		implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
	...
	@Override
	@Nullable
	public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
			@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

		descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
		if (Optional.class == descriptor.getDependencyType()) {
			// 最终它也会走到:doResolveDependency
			return createOptionalDependency(descriptor, requestingBeanName);
		}
		else if (ObjectFactory.class == descriptor.getDependencyType() ||
				ObjectProvider.class == descriptor.getDependencyType()) {
			// 直接new一个Provider返回出去~
			return new DependencyObjectProvider(descriptor, requestingBeanName);
		}
		// 兼容jsr330的javax.inject.Provider
		else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
			return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName);
		}
		else {
		// ContextAnnotationAutowireCandidateResolver-> QualifierAnnotationAutowireCandidateResolver 他们能够解决  就直接返回 否则交给doResolveDependency
			Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
					descriptor, requestingBeanName);
			if (result == null) {
				result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
			}
			return result;
		}
	}
	...

	// 咱们此处descriptor:field 'personName'
	// beanName:rootConfig
	// autowiredBeanNames:[]
	// typeConverter:SimpleTypeConverter
	@Nullable
	public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
			@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

		InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
		try {
			// 若你的despriptor是ShortcutDependencyDescriptor,这个就会直接去beanFactory.getBean(this.shortcut, this.requiredType)  提前返回  主要作用是缓存的效果~
			Object shortcut = descriptor.resolveShortcut(this);
			if (shortcut != null) {
				return shortcut;
			}

			// public final class String
			Class<?> type = descriptor.getDependencyType();
			// 备注:QualifierAnnotationAutowireCandidateResolver会处理@Qualifier和@Value
			//QualifierAnnotationAutowireCandidateResolver#getSuggestedValue()
			//先拿出@Value注解的值  如果为null再去拿Method里这个注解的值~~~ 最终返回~  所以@Value也是可以标注在方法上的
			// 注意此处:若是@Value  这里返回值肯定是String  但是若是@Autowired此处返回值就可能是对象了~
			Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
			if (value != null) {
			
				// 说明这个注入是@Value  因为它是String
				if (value instanceof String) {
					// 使用StringValueResolver处理${}占位符~
					// 所以我们常用的只使用@Value("${xxx}")这样来注入值或者你就是个字面量值,到这一步就已经完事了~解析完成  
					// 若你是个el表达式  或者文件资源Resource啥的,会继续交给下面的beanExpressionResolver处理,所以它是处理复杂类型的核心~
					String strVal = resolveEmbeddedValue((String) value);
					BeanDefinition bd = (beanName != null && containsBean(beanName) ?
							getMergedBeanDefinition(beanName) : null);

					// 此处注意:处理器是BeanExpressionResolver~~~~它是处理@Value表达式的核心方法
					// 它的默认值是:StandardBeanExpressionResolver#evaluate
					// 这里面就会解析
					value = evaluateBeanDefinitionString(strVal, bd);
				}

		
				// 若我们没有定制,此处为SimpleTypeConverter... 值已经拿到手了,经由转换器以转换 就可以测地的返回喽~~~解析结束
				TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
				try {
					return converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());
				}
				catch (UnsupportedOperationException ex) {
					// A custom TypeConverter which does not support TypeDescriptor resolution...
					return (descriptor.getField() != null ?
							converter.convertIfNecessary(value, type, descriptor.getField()) :
							converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
				}
			}

			// ====================下面就不解释了,多bean和required的解释  前面已经分析过了=======================
			Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
			if (multipleBeans != null) {
				return multipleBeans;
			}

			Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
			if (matchingBeans.isEmpty()) {
				if (isRequired(descriptor)) {
					raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
				}
				return null;
			}

			String autowiredBeanName;
			Object instanceCandidate;

			if (matchingBeans.size() > 1) {
				autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
				if (autowiredBeanName == null) {
					if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
						return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);
					}
					else {
						// In case of an optional Collection/Map, silently ignore a non-unique case:
						// possibly it was meant to be an empty collection of multiple regular beans
						// (before 4.3 in particular when we didn't even look for collection beans).
						return null;
					}
				}
				instanceCandidate = matchingBeans.get(autowiredBeanName);
			}
			else {
				// We have exactly one match.
				Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();
				autowiredBeanName = entry.getKey();
				instanceCandidate = entry.getValue();
			}

			if (autowiredBeanNames != null) {
				autowiredBeanNames.add(autowiredBeanName);
			}
			if (instanceCandidate instanceof Class) {
				instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);
			}
			Object result = instanceCandidate;
			if (result instanceof NullBean) {
				if (isRequired(descriptor)) {
					raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
				}
				result = null;
			}
			if (!ClassUtils.isAssignableValue(type, result)) {
				throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass());
			}
			return result;
		}
		finally {
			ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);
		}
	}
}

QualifierAnnotationAutowireCandidateResolver实现了AutowireCandidateResolver,对要自动绑定的field或者参数和bean definition根据@qualifier注解进行匹配,当然也支持javax.inject.Qualifier同时也支持通过@value注解来绑定表达式的值。

从上面分析知道,当把@Value的占位符替换完成后,最终都会交给beanExpressionResolver由它来统一处理:包括根据beanName获取bean、SpEL计算等等~~~



BeanExpressionResolver

策略接口,用于通过将值作为表达式进行评估来解析值(如果适用)。它持有Bean工厂~

代码语言:javascript复制
// @since 3.0
public interface BeanExpressionResolver {
	// value此时还是复杂类型,比如本例的#{person.name}
	// BeanExpressionContext:持有beanFactory和scope的引用而已~
	@Nullable
	Object evaluate(@Nullable String value, BeanExpressionContext evalContext) throws BeansException;

}

它的唯一实现类StandardBeanExpressionResolver(当然我们是可以自己实现的,比如后面我们自定义@Value功能,通过继承StandardBeanExpressionResolver来扩展实现~毕竟Spring很暖心的给我们开了口)

StandardBeanExpressionResolver

语言解析器的标准实现,支持解析SpEL语言。

代码语言:javascript复制
public class StandardBeanExpressionResolver implements BeanExpressionResolver {

	// 因为SpEL是支持自定义前缀、后缀的   此处保持了和SpEL默认值的统一
	// 它的属性值事public的   so你可以自定义~
	/** Default expression prefix: "#{". */
	public static final String DEFAULT_EXPRESSION_PREFIX = "#{";
	/** Default expression suffix: "}". */
	public static final String DEFAULT_EXPRESSION_SUFFIX = "}";
	private String expressionPrefix = DEFAULT_EXPRESSION_PREFIX;
	private String expressionSuffix = DEFAULT_EXPRESSION_SUFFIX;
	
	private ExpressionParser expressionParser; // 它的最终值是SpelExpressionParser

	// 每个表达式都对应一个Expression,这样可以不用重复解析了~~~
	private final Map<String, Expression> expressionCache = new ConcurrentHashMap<>(256);
	// 每个BeanExpressionContex都对应着一个取值上下文~~~
	private final Map<BeanExpressionContext, StandardEvaluationContext> evaluationCache = new ConcurrentHashMap<>(8);
	
	// 匿名内部类   解析上下文。  和TemplateParserContext的实现一样。个人觉得直接使用它更优雅
	// 和ParserContext.TEMPLATE_EXPRESSION 这个常量也一毛一样
	private final ParserContext beanExpressionParserContext = new ParserContext() {
		@Override
		public boolean isTemplate() {
			return true;
		}
		@Override
		public String getExpressionPrefix() {
			return expressionPrefix;
		}
		@Override
		public String getExpressionSuffix() {
			return expressionSuffix;
		}
	};
	

	// 空构造函数:默认就是使用的SpelExpressionParser  下面你也可以自己set你自己的实现~
	public StandardBeanExpressionResolver() {
		this.expressionParser = new SpelExpressionParser();
	}
	public void setExpressionParser(ExpressionParser expressionParser) {
		Assert.notNull(expressionParser, "ExpressionParser must not be null");
		this.expressionParser = expressionParser;
	}

	// 解析代码相对来说还是比较简单的,毕竟复杂的解析逻辑都是SpEL里边~  这里只是使用一下而已~
	@Override
	@Nullable
	public Object evaluate(@Nullable String value, BeanExpressionContext evalContext) throws BeansException {
		if (!StringUtils.hasLength(value)) {
			return value;
		}
		try {
			Expression expr = this.expressionCache.get(value);
			if (expr == null) {
				// 注意:此处isTemplte=true
				expr = this.expressionParser.parseExpression(value, this.beanExpressionParserContext);
				this.expressionCache.put(value, expr);
			}

			// 构建getValue计算时的执行上下文~~~
			// 做种解析BeanName的ast为;org.springframework.expression.spel.ast.PropertyOrFieldReference
			StandardEvaluationContext sec = this.evaluationCache.get(evalContext);
			if (sec == null) {
				// 此处指定的rootObject为:evalContext   --> BeanExpressionContext 
				sec = new StandardEvaluationContext(evalContext);
				// 此处新增了4个,加上一个默认的   所以一共就有5个属性访问器了
				// 这样我们的SpEL就能访问BeanFactory、Map、Enviroment等组件了~
				// BeanExpressionContextAccessor表示调用bean的方法~~~~(比如我们此处就是使用的它)  最终执行者为;BeanExpressionContext   它持有BeanFactory的引用嘛~
				// 如果是单村的Bean注入,最终使用的也是BeanExpressionContextAccessor 目前没有找到BeanFactoryAccessor的用于之地~~~
				// addPropertyAccessor只是:addBeforeDefault 所以只是把default的放在了最后,我们手动add的还是保持着顺序的~
				// 注意:这些属性访问器是有先后顺序的,具体看下面~~~
				sec.addPropertyAccessor(new BeanExpressionContextAccessor());
				sec.addPropertyAccessor(new BeanFactoryAccessor());
				sec.addPropertyAccessor(new MapAccessor());
				sec.addPropertyAccessor(new EnvironmentAccessor());

				// setBeanResolver不是接口方法,仅仅辅助StandardEvaluationContext 去获取Bean
				sec.setBeanResolver(new BeanFactoryResolver(evalContext.getBeanFactory()));
				sec.setTypeLocator(new StandardTypeLocator(evalContext.getBeanFactory().getBeanClassLoader()));

				// 若conversionService不为null,就使用工厂的。否则就使用SpEL里默认的DefaultConverterService那个  
				// 最后包装成TypeConverter给set进去~~~
				ConversionService conversionService = evalContext.getBeanFactory().getConversionService();
				if (conversionService != null) {
					sec.setTypeConverter(new StandardTypeConverter(conversionService));
				}

				// 这个很有意思,是一个protected的空方法,因此我们发现若我们自己要自定义BeanExpressionResolver,完全可以继承自StandardBeanExpressionResolver
				// 因为我们绝大多数情况下,只需要提供更多的计算环境即可~~~~~
				customizeEvaluationContext(sec);
				this.evaluationCache.put(evalContext, sec);
			}
			return expr.getValue(sec);
		} catch (Throwable ex) {
			throw new BeanExpressionException("Expression parsing failed", ex);
		}
	}
	
	//Spring留给我们扩展的SPI	
	protected void customizeEvaluationContext(StandardEvaluationContext evalContext) {
	}
}

如上,整个@Value的解析过程至此就全部完成了。可能有小伙伴会问:怎么不见Resource这种注入呢?其实,从上面不难看出,这个是ConversionService去做的事,它能够把一个字符串转换成Resource对象,仅此而已

总得来说@Value它自己做的事本身还是非常单一的:依赖注入,只是它把众多功能都很好的像插件一样插拔进来了,从而对用户很友好的显示了显它的神通广大~

需要注意的是,在整个依赖的解析过程中,有两个非常重要的接口:BeanExpressionResolverAutowireCandidateResolver都扮演着重要角色,有兴趣的可以深入继续了解~

SpEL中PropertyAccessor的匹配规则和执行顺序

本来这个内容应该放在SpEL章节更为合适,但是就着这个Spring的环境,就放在此处了。

如上StandardEvaluationContext中默认就会给放在上4 1PropertyAccessor,他们的匹配规则和执行顺序势必会影响到最终的取值,因此这部分就看看他们的决策原理。

备注:这部分的理解对平时的使用几乎没有关系,但对你自定义扩展功能有较大的影响~

默认注册后,他们的顺序如下:

它选择的代码在此处(SpEL章节说到了,目前只有PropertyOrFieldReference这个AST才会涉及到PropertyAccessor的决策):

代码语言:javascript复制
// 它是一个AST~
public class PropertyOrFieldReference extends SpelNodeImpl {
	...
	// 按照本例而言,contextObject就为BeanExpressionContext
	private List<PropertyAccessor> getPropertyAccessorsToTry(@Nullable Object contextObject, List<PropertyAccessor> propertyAccessors) {

		Class<?> targetType = (contextObject != null ? contextObject.getClass() : null);

		List<PropertyAccessor> specificAccessors = new ArrayList<>();
		List<PropertyAccessor> generalAccessors = new ArrayList<>();
	
		// 此处propertyAccessors默认会有5个值,如上截图
		for (PropertyAccessor resolver : propertyAccessors) {
			Class<?>[] targets = resolver.getSpecificTargetClasses();
			if (targets == null) {
				// generic resolver that says it can be used for any type
				// 如果为null没指定类型,那就相当于这个处理器可议处理任意type
				generalAccessors.add(resolver);
			}
			else if (targetType != null) {
				for (Class<?> clazz : targets) {
		
					//你的处理器指定的targetType和我的完全一样,我才选中这个处理器,加入到specificAccessors
					// 找到了就立马break~~~ 所以我说  他们顺序是很重要的
					// specificAccessors里的值要么为[]  要么只有一个
					if (clazz == targetType) {
						specificAccessors.add(resolver);
						break;
					} 
					// targetType若是clazz的子类,加入到generalAccessors 作为通用的处理器
					else if (clazz.isAssignableFrom(targetType)) {
						generalAccessors.add(resolver);
					}
				}
			}
		}
	
		// 这里有些神操作~~~resolvers 作为最终返回  
		// 1、 先从generalAccessors里面移除掉了所有已匹配上的~~~比如此处generalAccessors的size为0~
		// 2、移除好后把它在添加进resolvers里面去~~~~
		List<PropertyAccessor> resolvers = new ArrayList<>(specificAccessors);
		generalAccessors.removeAll(specificAccessors);
		resolvers.addAll(generalAccessors);
		return resolvers;
	}
	...
}

如此,我们每一种待处理的类型,一般最终得到的都是唯一一个resolver,从而完成对该引用的取值。

自定义扩展@Value的功能

既然了解了一项技术的工作原理,那么接下里就是定制化、自己扩展自己玩了。

备注:由于本人今日身体欠佳,甚是乏累。并且我个人认为如果你对这个基本原理了解了之后,并且还对SpEL中的PropertyAccessor决策原理了解后,自己扩展@Value注解的功能并不是难事,so,I am 准备go sleep去了~

提示:因为Spring上下文默认是这么注册的beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));,所以我们的思路应该是替换掉它~

@Value中使用${}读取不存在的key时,不报错而是原样输出的问题

这个问题我觉得也是比较重要的。

如题,我们通用的一个观念是这样的:若使用@Value("${app.full2}")给字段赋值,若key不存在启动应该报错:

代码语言:javascript复制
Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'app.full2' in value "${app.full2}"
	at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:174)
	at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:126)
	at org.springframework.core.env.AbstractPropertyResolver.doResolvePlaceholders(AbstractPropertyResolver.java:236)
	at org.springframework.core.env.AbstractPropertyResolver.resolveRequiredPlaceholders(AbstractPropertyResolver.java:210)
	at org.springframework.context.support.PropertySourcesPlaceholderConfigurer$2.resolveStringValue(PropertySourcesPlaceholderConfigurer.java:172)

这样才符合我们预期。但是我今天自己在单元测试的时候发现我误打误撞写了一个不存在的key,但是,但是启动并没有报错,而且给我原样输出了。

这个现象引发了我的兴趣,必须调查清楚啊~~~

我们上面(包括推荐博文)已经花了长篇大论知道了,解析占位符这款发生在:DefaultListableBeanFactory#resolveDependency中,里面有一句代码是:

代码语言:javascript复制
	@Override
	public String resolveEmbeddedValue(String value) {
		if (value == null) {
			return null;
		}
		String result = value;
		for (StringValueResolver resolver : this.embeddedValueResolvers) {
			result = resolver.resolveStringValue(result);
			if (result == null) {
				return null;
			}
		}
		return result;
	}

同样的代码不同的现象,问题就出现在这里resolver.resolveStringValue(result)。这里总结一下

AbstractBeanFactory设置处理器的地方有两个:

第一处:

代码语言:javascript复制
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext, DisposableBean {

	protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
		...
		// 若没有指定EmbeddedValueResolver, 这里就用个匿名函数实现  做个保底嘛~~~
		if (!beanFactory.hasEmbeddedValueResolver()) {
			beanFactory.addEmbeddedValueResolver(new StringValueResolver() {
				@Override
				public String resolveStringValue(String strVal) {
					return getEnvironment().resolvePlaceholders(strVal);
				}
			});
		}
		...
	}
	
}

第二处:

代码语言:javascript复制
//@since 3.1
public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfigurer implements BeanNameAware, BeanFactoryAware {
	protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess, StringValueResolver valueResolver) {
		...
		// Spring3.0后  加载完配置文件后,会把这个处理器放进Bean工厂里面去。
		// New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes.
		beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
	}
}

它有两个子类:PropertyPlaceholderConfigurer,它的StringValueResolver实现为一个内部类实现的。

另一个子类PropertySourcesPlaceholderConfigurer,它的实现StringValueResolver为一个lambda表达式。

他俩有个共同点最终的解析都依赖于**PropertyPlaceholderHelper**并且,并且**ignoreUnresolvablePlaceholders**属性均为默认的fasle。

所以得出结论:若你手动配置过上面两个PlaceholderConfigurerSupport子类Bean,并且没有改变过ignoreUnresolvablePlaceholders这个值,那你最终会使用它们去解析${}占位符,从而如果找不到key就启动报错了。

但是若你没有手动配置过,那将最终交给AbstractBeanFactory的那个内部类处理,也就是这句话:return getEnvironment().resolvePlaceholders(strVal);而它最终解析如下:

代码语言:javascript复制
public abstract class AbstractEnvironment implements ConfigurableEnvironment {

	// 两个接口方法。显然AbstractBeanFactory默认实现为这个方法,而并非Required的~~~
	@Override
	public String resolvePlaceholders(String text) {
		return this.propertyResolver.resolvePlaceholders(text);
	}
	@Override
	public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
		return this.propertyResolver.resolveRequiredPlaceholders(text);
	}
}

public abstract class AbstractPropertyResolver implements ConfigurablePropertyResolver {
	// 调用的resolvePlaceholders这个方法,所以默认情况下  即使key不存在  也是没关系的
	@Override
	public String resolvePlaceholders(String text) {
		if (this.nonStrictHelper == null) {
			this.nonStrictHelper = createPlaceholderHelper(true);
		}
		return doResolvePlaceholders(text, this.nonStrictHelper);
	}
	@Override
	public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
		if (this.strictHelper == null) {
			this.strictHelper = createPlaceholderHelper(false);
		}
		return doResolvePlaceholders(text, this.strictHelper);
	}
}

如上,就能解释了为何有时候你使用@Value找不到key就启动报错,有时候却原样输出呢? 这就是其根本原因喽~

备注:此处都只指的纯Spring环境,而非Boot环境~~~

关于SpringBoot环境下,默认情况下都是key必须存在的,否则启动报错。原因如下:

代码语言:javascript复制
@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
public class PropertyPlaceholderAutoConfiguration {

	@Bean
	@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
	public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
		return new PropertySourcesPlaceholderConfigurer();
	}

}

而在spring.factories文件里配置有它:

代码语言:javascript复制
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration

总之:使用@Value注入时请保证key是存在的,否则建议请使用defaultValue语法处理

总结

相信我们都有一个直观感受:@Value注解非常之简单,但功能非常之强大。

同时Spring的知识点都非常具有层次性,正所谓下层基础决定了你的上层建筑。Spring能够这么高的扩展性,得益于它根基的牢固。

本文@Value的能力,绝大部分其实都是SpEL的能力。换句话说:你对SpEL有多了解,决定了你对@Value注解的使用有多了解

Spring工程非常优秀和快速流行的原因之一也是因为如此:屏蔽了巨多复杂实现,并且对developer提供简单易用的API,从而上手非常之容易(潜台词:精通非常的难)

0 人点赞