Skip to content

Commit

Permalink
Apollo client integration with Spring
Browse files Browse the repository at this point in the history
  • Loading branch information
nobodyiam committed Feb 15, 2017
1 parent 87129ad commit ee51eff
Show file tree
Hide file tree
Showing 45 changed files with 1,869 additions and 8 deletions.
6 changes: 6 additions & 0 deletions apollo-client/pom.xml
Expand Up @@ -32,6 +32,12 @@
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!-- optional spring dependency -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<optional>true</optional>
</dependency>
<!-- test -->
<dependency>
<groupId>org.eclipse.jetty</groupId>
Expand Down
@@ -0,0 +1,83 @@
package com.ctrip.framework.apollo.spring.annotation;

import com.google.common.base.Preconditions;

import com.ctrip.framework.apollo.Config;
import com.ctrip.framework.apollo.ConfigChangeListener;
import com.ctrip.framework.apollo.ConfigService;
import com.ctrip.framework.apollo.model.ConfigChangeEvent;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
* Apollo Annotation Processor for Spring Application
*
* @author Jason Song(song_s@ctrip.com)
*/
public class ApolloAnnotationProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
Class clazz = bean.getClass();
processFields(bean, clazz.getDeclaredFields());
processMethods(bean, clazz.getDeclaredMethods());
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}

private void processFields(Object bean, Field[] declaredFields) {
for (Field field : declaredFields) {
ApolloConfig annotation = AnnotationUtils.getAnnotation(field, ApolloConfig.class);
if (annotation == null) {
continue;
}

Preconditions.checkArgument(Config.class.isAssignableFrom(field.getType()),
"Invalid type: %s for field: %s, should be Config", field.getType(), field);

String namespace = annotation.value();
Config config = ConfigService.getConfig(namespace);

ReflectionUtils.makeAccessible(field);
ReflectionUtils.setField(field, bean, config);
}
}

private void processMethods(final Object bean, Method[] declaredMethods) {
for (final Method method : declaredMethods) {
ApolloConfigChangeListener annotation = AnnotationUtils.findAnnotation(method, ApolloConfigChangeListener.class);
if (annotation == null) {
continue;
}

Class<?>[] parameterTypes = method.getParameterTypes();
Preconditions.checkArgument(parameterTypes.length == 1,
"Invalid number of parameters: %s for method: %s, should be 1", parameterTypes.length, method);
Preconditions.checkArgument(ConfigChangeEvent.class.isAssignableFrom(parameterTypes[0]),
"Invalid parameter type: %s for method: %s, should be ConfigChangeEvent", parameterTypes[0], method);

ReflectionUtils.makeAccessible(method);
String[] namespaces = annotation.value();
for (String namespace : namespaces) {
Config config = ConfigService.getConfig(namespace);

config.addChangeListener(new ConfigChangeListener() {
@Override
public void onChange(ConfigChangeEvent changeEvent) {
ReflectionUtils.invokeMethod(method, bean, changeEvent);
}
});
}
}
}

}
@@ -0,0 +1,31 @@
package com.ctrip.framework.apollo.spring.annotation;

import com.ctrip.framework.apollo.core.ConfigConsts;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Use this annotation to inject Apollo Config Instance.
*
* <p>Usage example:</p>
* <pre class="code">
* //Inject the config for "someNamespace"
* &#064;ApolloConfig("someNamespace")
* private Config config;
* </pre>
*
* @author Jason Song(song_s@ctrip.com)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface ApolloConfig {
/**
* Apollo namespace for the config, if not specified then default to application
*/
String value() default ConfigConsts.NAMESPACE_APPLICATION;
}
@@ -0,0 +1,33 @@
package com.ctrip.framework.apollo.spring.annotation;

import com.ctrip.framework.apollo.core.ConfigConsts;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Use this annotation to register Apollo ConfigChangeListener.
*
* <p>Usage example:</p>
* <pre class="code">
* //Listener on namespaces of "someNamespace" and "anotherNamespace"
* &#064;ApolloConfigChangeListener({"someNamespace","anotherNamespace"})
* private void onChange(ConfigChangeEvent changeEvent) {
* //handle change event
* }
* </pre>
*
* @author Jason Song(song_s@ctrip.com)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface ApolloConfigChangeListener {
/**
* Apollo namespace for the config, if not specified then default to application
*/
String[] value() default {ConfigConsts.NAMESPACE_APPLICATION};
}
@@ -0,0 +1,35 @@
package com.ctrip.framework.apollo.spring.annotation;

import com.google.common.collect.Lists;

import com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor;
import com.ctrip.framework.apollo.spring.util.BeanRegistrationUtil;

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;

/**
* @author Jason Song(song_s@ctrip.com)
*/
public class ApolloConfigRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
AnnotationAttributes attributes = AnnotationAttributes.fromMap(importingClassMetadata
.getAnnotationAttributes(EnableApolloConfig.class.getName()));
String[] namespaces = attributes.getStringArray("value");
int order = attributes.getNumber("order");
PropertySourcesProcessor.addNamespaces(Lists.newArrayList(namespaces), order);

BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesPlaceholderConfigurer.class.getName(),
PropertySourcesPlaceholderConfigurer.class);

BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesProcessor.class.getName(),
PropertySourcesProcessor.class);

BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloAnnotationProcessor.class.getName(),
ApolloAnnotationProcessor.class);
}
}
@@ -0,0 +1,44 @@
package com.ctrip.framework.apollo.spring.annotation;

import com.ctrip.framework.apollo.core.ConfigConsts;

import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Use this annotation to register Apollo property sources when using Java Config.
*
* <p>Configuration example:</p>
* <pre class="code">
* &#064;Configuration
* &#064;EnableApolloConfig({"someNamespace","anotherNamespace"})
* public class AppConfig {
*
* }
* </pre>
*
* @author Jason Song(song_s@ctrip.com)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(ApolloConfigRegistrar.class)
public @interface EnableApolloConfig {
/**
* Apollo namespaces to inject configuration into Spring Property Sources.
*/
String[] value() default {ConfigConsts.NAMESPACE_APPLICATION};

/**
* The order of the apollo config, default is {@link Ordered#LOWEST_PRECEDENCE}, which is Integer.MAX_VALUE.
* If there are properties with the same name in different apollo configs, the apollo config with smaller order wins.
* @return
*/
int order() default Ordered.LOWEST_PRECEDENCE;
}
@@ -0,0 +1,21 @@
package com.ctrip.framework.apollo.spring.config;

import com.ctrip.framework.apollo.Config;

import org.springframework.core.env.PropertySource;

/**
* Property source wrapper for Config
*
* @author Jason Song(song_s@ctrip.com)
*/
public class ConfigPropertySource extends PropertySource<Config> {
public ConfigPropertySource(String name, Config source) {
super(name, source);
}

@Override
public Object getProperty(String name) {
return this.source.getProperty(name, null);
}
}
@@ -0,0 +1,26 @@
package com.ctrip.framework.apollo.spring.config;

import com.ctrip.framework.apollo.spring.annotation.ApolloAnnotationProcessor;
import com.ctrip.framework.apollo.spring.util.BeanRegistrationUtil;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;

/**
* Apollo Property Sources processor for Spring XML Based Application
*
* @author Jason Song(song_s@ctrip.com)
*/
public class ConfigPropertySourcesProcessor extends PropertySourcesProcessor
implements BeanDefinitionRegistryPostProcessor {

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesPlaceholderConfigurer.class.getName(),
PropertySourcesPlaceholderConfigurer.class);
BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloAnnotationProcessor.class.getName(),
ApolloAnnotationProcessor.class);
}
}
@@ -0,0 +1,58 @@
package com.ctrip.framework.apollo.spring.config;

import com.google.common.base.Splitter;
import com.google.common.base.Strings;

import com.ctrip.framework.apollo.core.ConfigConsts;

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
import org.springframework.core.Ordered;
import org.w3c.dom.Element;

/**
* @author Jason Song(song_s@ctrip.com)
*/
public class NamespaceHandler extends NamespaceHandlerSupport {
private static final Splitter NAMESPACE_SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults();

@Override
public void init() {
registerBeanDefinitionParser("config", new BeanParser());
}

static class BeanParser extends AbstractSingleBeanDefinitionParser {
@Override
protected Class<?> getBeanClass(Element element) {
return ConfigPropertySourcesProcessor.class;
}

@Override
protected boolean shouldGenerateId() {
return true;
}

@Override
protected void doParse(Element element, BeanDefinitionBuilder builder) {
String namespaces = element.getAttribute("namespaces");
//default to application
if (Strings.isNullOrEmpty(namespaces)) {
namespaces = ConfigConsts.NAMESPACE_APPLICATION;
}

int order = Ordered.LOWEST_PRECEDENCE;
String orderAttribute = element.getAttribute("order");

if (!Strings.isNullOrEmpty(orderAttribute)) {
try {
order = Integer.parseInt(orderAttribute);
} catch (Throwable ex) {
throw new IllegalArgumentException(
String.format("Invalid order: %s for namespaces: %s", orderAttribute, namespaces));
}
}
PropertySourcesProcessor.addNamespaces(NAMESPACE_SPLITTER.splitToList(namespaces), order);
}
}
}

0 comments on commit ee51eff

Please sign in to comment.