diff --git a/nacos-spring-context/src/main/java/com/alibaba/nacos/spring/context/annotation/config/SpringValueAnnotationBeanPostProcessor.java b/nacos-spring-context/src/main/java/com/alibaba/nacos/spring/context/annotation/config/SpringValueAnnotationBeanPostProcessor.java new file mode 100644 index 0000000..a7392f3 --- /dev/null +++ b/nacos-spring-context/src/main/java/com/alibaba/nacos/spring/context/annotation/config/SpringValueAnnotationBeanPostProcessor.java @@ -0,0 +1,332 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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 com.alibaba.nacos.spring.context.annotation.config; + +import com.alibaba.nacos.common.utils.MD5Utils; +import com.alibaba.nacos.spring.context.event.config.NacosConfigReceivedEvent; +import com.alibaba.nacos.spring.util.NacosUtils; +import com.alibaba.nacos.spring.util.PlaceholderHelper; +import com.alibaba.spring.beans.factory.annotation.AbstractAnnotationBeanPostProcessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.TypeConverter; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.annotation.InjectionMetadata; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.context.EnvironmentAware; +import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.env.Environment; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; + +import static org.springframework.core.annotation.AnnotationUtils.getAnnotation; + +/** + * @author wuhaoqiang + * @since 1.1.2 + * @desc: spring @Value Processor + **/ +public class SpringValueAnnotationBeanPostProcessor + extends AbstractAnnotationBeanPostProcessor implements BeanFactoryAware, + EnvironmentAware, ApplicationListener { + + /** + * The name of {@link SpringValueAnnotationBeanPostProcessor} bean. + */ + public static final String BEAN_NAME = "StringValueAnnotationBeanPostProcessor"; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + /** + * placeholder, valueTarget. + */ + private Map> placeholderStringValueTargetMap = new HashMap>(); + + private ConfigurableListableBeanFactory beanFactory; + + private Environment environment; + + private BeanExpressionResolver exprResolver; + + private BeanExpressionContext exprContext; + + public SpringValueAnnotationBeanPostProcessor() { + super(Value.class); + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + if (!(beanFactory instanceof ConfigurableListableBeanFactory)) { + throw new IllegalArgumentException( + "StringValueAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory"); + } + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + this.exprResolver = ((ConfigurableListableBeanFactory) beanFactory).getBeanExpressionResolver(); + this.exprContext = new BeanExpressionContext((ConfigurableListableBeanFactory) beanFactory, null); + } + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @Override + protected Object doGetInjectedBean(AnnotationAttributes attributes, Object bean, + String beanName, Class injectedType, + InjectionMetadata.InjectedElement injectedElement) throws Exception { + Object value = resolveStringValue(attributes.getString("value")); + Member member = injectedElement.getMember(); + if (member instanceof Field) { + return convertIfNecessary((Field) member, value); + } + + if (member instanceof Method) { + return convertIfNecessary((Method) member, value); + } + + return null; + } + + @Override + protected String buildInjectedObjectCacheKey(AnnotationAttributes attributes, + Object bean, String beanName, Class injectedType, + InjectionMetadata.InjectedElement injectedElement) { + return bean.getClass().getName() + attributes; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, final String beanName) + throws BeansException { + + doWithFields(bean, beanName); + + doWithMethods(bean, beanName); + + return super.postProcessBeforeInitialization(bean, beanName); + } + + @Override + public void onApplicationEvent(NacosConfigReceivedEvent event) { + if (StringUtils.isEmpty(event)) { + return; + } + Map updateProperties = NacosUtils.toProperties(event.getContent(), event.getType()); + + for (Map.Entry> entry : placeholderStringValueTargetMap + .entrySet()) { + + String key = environment.resolvePlaceholders(entry.getKey()); + // Process modified keys, excluding deleted keys + if (!updateProperties.containsKey(key)) { + continue; + } + String newValue = environment.getProperty(key); + + if (newValue == null) { + continue; + } + List beanPropertyList = entry.getValue(); + for (StringValueTarget target : beanPropertyList) { + String md5String = MD5Utils.md5Hex(newValue, "UTF-8"); + boolean isUpdate = !target.lastMD5.equals(md5String); + if (isUpdate) { + target.updateLastMD5(md5String); + Object evaluatedValue = resolveStringValue(target.stringValueExpr); + if (target.method == null) { + setField(target, evaluatedValue); + } else { + setMethod(target, evaluatedValue); + } + } + } + } + } + + private Object resolveStringValue(String strVal) { + String value = beanFactory.resolveEmbeddedValue(strVal); + if (exprResolver != null && value != null) { + return exprResolver.evaluate(value, exprContext); + } + return value; + } + + private Object convertIfNecessary(Field field, Object value) { + TypeConverter converter = beanFactory.getTypeConverter(); + return converter.convertIfNecessary(value, field.getType(), field); + } + + private Object convertIfNecessary(Method method, Object value) { + Class[] paramTypes = method.getParameterTypes(); + Object[] arguments = new Object[paramTypes.length]; + + TypeConverter converter = beanFactory.getTypeConverter(); + + if (arguments.length == 1) { + return converter.convertIfNecessary(value, paramTypes[0], + new MethodParameter(method, 0)); + } + + for (int i = 0; i < arguments.length; i++) { + arguments[i] = converter.convertIfNecessary(value, paramTypes[i], + new MethodParameter(method, i)); + } + + return arguments; + } + + private void doWithFields(final Object bean, final String beanName) { + ReflectionUtils.doWithFields(bean.getClass(), + new ReflectionUtils.FieldCallback() { + @Override + public void doWith(Field field) throws IllegalArgumentException { + Value annotation = getAnnotation(field, Value.class); + doWithAnnotation(beanName, bean, annotation, field.getModifiers(), + null, field); + } + }); + } + + private void doWithMethods(final Object bean, final String beanName) { + ReflectionUtils.doWithMethods(bean.getClass(), + new ReflectionUtils.MethodCallback() { + @Override + public void doWith(Method method) throws IllegalArgumentException { + Value annotation = getAnnotation(method, Value.class); + doWithAnnotation(beanName, bean, annotation, + method.getModifiers(), method, null); + } + }); + } + + private void doWithAnnotation(String beanName, Object bean, Value annotation, + int modifiers, Method method, Field field) { + if (annotation != null) { + if (Modifier.isStatic(modifiers)) { + return; + } + + Set placeholderList = PlaceholderHelper.findPlaceholderKeys(annotation.value()); + for (String placeholder : placeholderList) { + StringValueTarget stringValueTarget = new StringValueTarget(bean, beanName, + method, field, annotation.value()); + put2ListMap(placeholderStringValueTargetMap, placeholder, + stringValueTarget); + } + } + } + + private void put2ListMap(Map> map, K key, V value) { + List valueList = map.get(key); + if (valueList == null) { + valueList = new ArrayList(); + } + valueList.add(value); + map.put(key, valueList); + } + + private void setMethod(StringValueTarget stringValueTarget, Object propertyValue) { + Method method = stringValueTarget.method; + ReflectionUtils.makeAccessible(method); + try { + method.invoke(stringValueTarget.bean, + convertIfNecessary(method, propertyValue)); + + if (logger.isDebugEnabled()) { + logger.debug("Update value with {} (method) in {} (bean) with {}", + method.getName(), stringValueTarget.beanName, propertyValue); + } + } catch (Throwable e) { + if (logger.isErrorEnabled()) { + logger.error("Can't update value with " + method.getName() + + " (method) in " + stringValueTarget.beanName + " (bean)", e); + } + } + } + + private void setField(final StringValueTarget stringValueTarget, + final Object propertyValue) { + final Object bean = stringValueTarget.bean; + + Field field = stringValueTarget.field; + + String fieldName = field.getName(); + + try { + ReflectionUtils.makeAccessible(field); + field.set(bean, convertIfNecessary(field, propertyValue)); + + if (logger.isDebugEnabled()) { + logger.debug("Update value of the {}" + " (field) in {} (bean) with {}", + fieldName, stringValueTarget.beanName, propertyValue); + } + } catch (Throwable e) { + if (logger.isErrorEnabled()) { + logger.error("Can't update value of the " + fieldName + " (field) in " + + stringValueTarget.beanName + " (bean)", e); + } + } + } + + private static class StringValueTarget { + + private final Object bean; + + private final String beanName; + + private final Method method; + + private final Field field; + + private String lastMD5; + + private final String stringValueExpr; + + StringValueTarget(Object bean, String beanName, Method method, Field field, String stringValueExpr) { + this.bean = bean; + + this.beanName = beanName; + + this.method = method; + + this.field = field; + + this.lastMD5 = ""; + + this.stringValueExpr = stringValueExpr; + } + + protected void updateLastMD5(String newMD5) { + this.lastMD5 = newMD5; + } + + } + +} diff --git a/nacos-spring-context/src/main/java/com/alibaba/nacos/spring/util/NacosBeanUtils.java b/nacos-spring-context/src/main/java/com/alibaba/nacos/spring/util/NacosBeanUtils.java index 9eeac6b..f903cef 100644 --- a/nacos-spring-context/src/main/java/com/alibaba/nacos/spring/util/NacosBeanUtils.java +++ b/nacos-spring-context/src/main/java/com/alibaba/nacos/spring/util/NacosBeanUtils.java @@ -29,6 +29,7 @@ import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; +import com.alibaba.nacos.spring.context.annotation.config.SpringValueAnnotationBeanPostProcessor; import org.apache.commons.lang3.ArrayUtils; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ListableBeanFactory; @@ -362,6 +363,13 @@ public static void registerNacosValueAnnotationBeanPostProcessor( NacosValueAnnotationBeanPostProcessor.class); } + public static void registerStringValueAnnotationBeanPostProcessor( + BeanDefinitionRegistry registry) { + registerInfrastructureBeanIfAbsent(registry, + SpringValueAnnotationBeanPostProcessor.BEAN_NAME, + SpringValueAnnotationBeanPostProcessor.class); + } + /** * Register Nacos Common Beans * @@ -398,6 +406,8 @@ public static void registerNacosConfigBeans(BeanDefinitionRegistry registry, registerNacosValueAnnotationBeanPostProcessor(registry); + registerStringValueAnnotationBeanPostProcessor(registry); + registerConfigServiceBeanBuilder(registry); registerLoggingNacosConfigMetadataEventListener(registry); diff --git a/nacos-spring-context/src/main/java/com/alibaba/nacos/spring/util/PlaceholderHelper.java b/nacos-spring-context/src/main/java/com/alibaba/nacos/spring/util/PlaceholderHelper.java new file mode 100644 index 0000000..ba291f2 --- /dev/null +++ b/nacos-spring-context/src/main/java/com/alibaba/nacos/spring/util/PlaceholderHelper.java @@ -0,0 +1,126 @@ +package com.alibaba.nacos.spring.util; + +import com.google.common.base.Strings; +import com.google.common.collect.Sets; +import org.springframework.util.StringUtils; + +import java.util.Set; +import java.util.Stack; + +/** + * @author wuhaoqiang + * @since 1.1.2 + **/ +public class PlaceholderHelper { + + private static final String PLACEHOLDER_PREFIX = "${"; + private static final String PLACEHOLDER_SUFFIX = "}"; + private static final String VALUE_SEPARATOR = ":"; + private static final String SIMPLE_PLACEHOLDER_PREFIX = "{"; + + /** + * find keys from placeholder + * ${key} -> "key" + * xxx${key}yyy -> "key" + * ${key:${key2:1}} -> "key", "key2" + * ${${key}} -> "key" + * ${${key:100}} -> "key" + * ${${key}:${key2}} -> "key", "key2" + * @param propertyString ${key} + * @return key + */ + public static Set findPlaceholderKeys(String propertyString) { + Set placeholderKeys = Sets.newHashSet(); + + if (Strings.isNullOrEmpty(propertyString) || + !(propertyString.contains(PLACEHOLDER_PREFIX) && propertyString.contains(PLACEHOLDER_SUFFIX))) { + return placeholderKeys; + } + // handle xxx${yyy}zzz -> ${yyy}zzz + propertyString = propertyString.substring(propertyString.indexOf(PLACEHOLDER_PREFIX)); + + Stack stack = new Stack(); + stack.push(propertyString); + + while (!stack.isEmpty()) { + String strVal = stack.pop(); + int startIndex = strVal.indexOf(PLACEHOLDER_PREFIX); + if (startIndex == -1) { + placeholderKeys.add(strVal); + continue; + } + int endIndex = findPlaceholderEndIndex(strVal, startIndex); + if (endIndex == -1) { + // invalid placeholder + continue; + } + + String placeholderCandidate = strVal.substring(startIndex + PLACEHOLDER_PREFIX.length(), endIndex); + + // ${key} + // startsWith '${' continue + if (placeholderCandidate.startsWith(PLACEHOLDER_PREFIX)) { + stack.push(placeholderCandidate); + } else { + // exist ':' -> key:${key2:2} + int separatorIndex = placeholderCandidate.indexOf(VALUE_SEPARATOR); + + if (separatorIndex == -1) { + stack.push(placeholderCandidate); + } else { + stack.push(placeholderCandidate.substring(0, separatorIndex)); + String defaultValuePart = + normalizeToPlaceholder(placeholderCandidate.substring(separatorIndex + VALUE_SEPARATOR.length())); + if (!Strings.isNullOrEmpty(defaultValuePart)) { + stack.push(defaultValuePart); + } + } + } + + // has remaining part, e.g. ${a}.${b} + if (endIndex + PLACEHOLDER_SUFFIX.length() < strVal.length() - 1) { + String remainingPart = normalizeToPlaceholder(strVal.substring(endIndex + PLACEHOLDER_SUFFIX.length())); + if (!Strings.isNullOrEmpty(remainingPart)) { + stack.push(remainingPart); + } + } + } + + return placeholderKeys; + } + + private static String normalizeToPlaceholder(String strVal) { + int startIndex = strVal.indexOf(PLACEHOLDER_PREFIX); + if (startIndex == -1) { + return null; + } + int endIndex = strVal.lastIndexOf(PLACEHOLDER_SUFFIX); + if (endIndex == -1) { + return null; + } + + return strVal.substring(startIndex, endIndex + PLACEHOLDER_SUFFIX.length()); + } + + private static int findPlaceholderEndIndex(CharSequence buf, int startIndex) { + int index = startIndex + PLACEHOLDER_PREFIX.length(); + int withinNestedPlaceholder = 0; + while (index < buf.length()) { + if (StringUtils.substringMatch(buf, index, PLACEHOLDER_SUFFIX)) { + if (withinNestedPlaceholder > 0) { + withinNestedPlaceholder--; + index = index + PLACEHOLDER_SUFFIX.length(); + } else { + return index; + } + } else if (StringUtils.substringMatch(buf, index, SIMPLE_PLACEHOLDER_PREFIX)) { + withinNestedPlaceholder++; + index = index + SIMPLE_PLACEHOLDER_PREFIX.length(); + } else { + index++; + } + } + return -1; + } +} + diff --git a/nacos-spring-context/src/test/java/com/alibaba/nacos/spring/util/PlaceholderHelperTest.java b/nacos-spring-context/src/test/java/com/alibaba/nacos/spring/util/PlaceholderHelperTest.java new file mode 100644 index 0000000..82d9fb0 --- /dev/null +++ b/nacos-spring-context/src/test/java/com/alibaba/nacos/spring/util/PlaceholderHelperTest.java @@ -0,0 +1,33 @@ +package com.alibaba.nacos.spring.util; + +import com.google.common.collect.Sets; +import org.junit.Assert; +import org.junit.Test; + +/** + * @author whq + * @since 1.1.2 + */ +public class PlaceholderHelperTest { + + @Test + public void testFindPlaceholderKeys() { + /** + * find keys from placeholder + * ${key} => "key" + * xxx${key}yyy => "key" + * ${key:${key2:1}} => "key", "key2" + * ${${key}} => "key" + * ${${key:100}} => "key" + * ${${key}:${key2}} => "key", "key2" + */ + Assert.assertEquals(PlaceholderHelper.findPlaceholderKeys("${key}"), Sets.newHashSet("key")); + Assert.assertEquals(PlaceholderHelper.findPlaceholderKeys("xxx${key}yyy"), Sets.newHashSet("key")); + Assert.assertEquals(PlaceholderHelper.findPlaceholderKeys("${key:${key2:1}}"), Sets.newHashSet("key", "key2")); + Assert.assertEquals(PlaceholderHelper.findPlaceholderKeys("${${key}}"), Sets.newHashSet("key")); + Assert.assertEquals(PlaceholderHelper.findPlaceholderKeys("${${key:100}}"), Sets.newHashSet("key")); + Assert.assertEquals(PlaceholderHelper.findPlaceholderKeys("${${key}:${key2}}"), Sets.newHashSet("key", "key2")); + + } + +}