Alibaba Nacos ships main core features of Cloud-Native application, including:
- Service Discovery and Service Health Check
- Dynamic Configuration Management
- Dynamic DNS Service
- Service and MetaData Management
Nacos Spring Project is based on it and embraces Spring ECO System so that developers could build Spring application rapidly. nacos-spring-context is a core module that fully expands modern Java programming models:
Those features strongly depends Spring Framework 3.2+ API and seamlessly integrates any Spring Stack.
We recommend developers to use annotation-driven programming, even though XML-based features also work.
-
Checkout
nacos-spring-projectSource Code$ git clone [email protected]:nacos-group/nacos-spring-project.git
-
Maven Build Source Code
$ mvn clean package
-
Run Spring Web MVC Samples
$ java -jar target/nacos-spring-webmvc-sample.war
| Dependencies | Compatibility |
|---|---|
| Java | 1.6+ |
| Spring Context | 3.2+ |
| Alibaba Spring Context Support | 1.0.1+ |
| Alibaba Nacos | 0.2.1+ |
First, you have to start a Nacos Server in backend , If you don't know steps, you can learn about quick start.
Suppose your Nacos Server is startup, you would add nacos-spring-context in your Spring application's dependencies :
<dependencies>
...
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-spring-context</artifactId>
<version>${latest.version}</version>
</dependency>
...
</dependencies>After that, you could annotate @EnableNacos in Spring @Configuration Class :
@Configuration
@EnableNacos(
globalProperties =
@NacosProperties(serverAddr = "${nacos.server-addr:localhost:12345}")
)
public class NacosConfiguration {
...
}
serverAddrattribute configures "${host}:${port}" of your Nacos Server
If you'd like to use "Distributed Configuration" features, ConfigService is a core service interface to get or publish config, you could use "Dependency Injection" to inject ConfigService instance in your Spring Beans when @EnableNacos is annotated:
@Service
public class ConfigServiceDemo {
@NacosInjected
private ConfigService configService;
public void demoGetConfig() {
try {
String dataId = "{dataId}";
String group = "{group}";
String content = configService.getConfig(dataId, groupId, 5000);
System.out.println(content);
} catch (NacosException e) {
e.printStackTrace();
}
}
...
}above code equals below one:
try {
// Initialize the configuration service, and the console automatically obtains the following parameters through the sample code.
String serverAddr = "{serverAddr}";
String dataId = "{dataId}";
String group = "{group}";
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
// Actively get the configuration.
String content = configService.getConfig(dataId, group, 5000);
System.out.println(content);
} catch (NacosException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}However, also can inject any NamingService instance for "Service Discovery" scenario:
@NacosInjected
private NamingService namingService;If you were not familiar with usages of ConfigService and NamingService, please learn about SDK first.
nacos-spring-context is a core module of Nacos integrating with Spring Framework, which provides much rich features around Spring Stack, including Spring Boot and Spring Cloud. Their core features include:
@EnableNacos is a modular-driven annotation that enables all features of Nacos Spring, mainly includes "Service Discovery" and "Distributed Configuration", it is equals to @EnableNacosDiscovery and
@EnableNacosConfig, they also could be configured separately, that means you could use them in different scenarios.
The globalProperties is a required attribute in any @EnableNacos, @EnableNacosDiscovery or @EnableNacosConfig,
whose type is @NacosProperties, which initializes "Global Nacos Properties" that will be used other annotations
and components, e,g @NacosInjected. In other words, Global Nacos Properties" that is a global or default properties
with lowest order can be override if need be. The precedence rules of override:
| Precedence Order | Nacos Annotation | Required |
|---|---|---|
| 1 | *.proeprties() |
N |
| 2 | @EnableNacosConfig.globalProperties() or @EnableNacosDiscovery.globalProperties() |
Y |
| 3 | @EnableNacos.globalProperties() |
Y |
*.proeprties() is a Special Nacos Properties that comes from one of them :
@NacosInjected.proeprties()@NacosConfigListener.proeprties()@NacosPropertySource.proeprties()@NacosConfigurationProperties.proeprties()
Special Nacos Properties is also configured by @NacosProperties, however it's optional and used to override Global
Nacos Properties for special scenarios, even though it is not required. If they are absent, The Nacos Properties will
try to retrieve from @EnableNacosConfig.globalProperties() or @EnableNacosDiscovery.globalProperties(), or
@EnableNacos.globalProperties().
@NacosProperties is an uniform annotation of Global and Special Nacos Properties, which plays a mediator between Java Properties and NacosFactory class creating the instances of ConfigService or NamingService.
@NacosProperties‘s attributes completely support placeholders whose source is all kinds of PropertySource in Spring Environment abstraction, typically Java System Properties and OS environment variables. The prefix of all placeholders are "nacos.", the mapping between @NacosProperties's attributes and Nacos Properties as below:
| Attribute | Property | Placeholder | Description | Required |
|---|---|---|---|---|
endpoint() |
endpoint |
${nacos.endpoint:} |
N | |
namespace() |
namespace |
${nacos.namespace:} |
N | |
accessKey() |
access-key |
${nacos.access-key:} |
N | |
secretKey() |
secret-key |
${nacos.secret-key:} |
N | |
serverAddr() |
server-addr |
${nacos.server-addr:} |
Y | |
contextPath() |
context-path |
${nacos.context-path:} |
N | |
clusterName() |
cluster-name |
${nacos.cluster-name:} |
N | |
encode() |
encode |
${nacos.encode:UTF-8} |
N |
There are some different in globalProperties()'s placeholders between @EnableNacosDiscovery and @EnableNacosConfig:
| Attribute | @EnableNacosDiscovery's Placeholder |
|---|---|
endpoint() |
${nacos.discovery.endpoint:${nacos.endpoint:}} |
namespace() |
${nacos.discovery.namespace:${nacos.namespace:}} |
accessKey() |
${nacos.discovery.access-key:${nacos.access-key:}} |
secretKey() |
${nacos.discovery.secret-key:${nacos.secret-key:}} |
serverAddr() |
${nacos.discovery.server-addr:${nacos.server-addr:}} |
contextPath() |
${nacos.discovery.context-path:${nacos.context-path:}} |
clusterName() |
${nacos.discovery.cluster-name:${nacos.cluster-name:}} |
encode() |
${nacos.discovery.encode:${nacos.encode:UTF-8}} |
| Attribute | @EnableNacosConfig's Placeholder |
|---|---|
endpoint() |
${nacos.config.endpoint:${nacos.endpoint:}} |
namespace() |
${nacos.config.namespace:${nacos.namespace:}} |
accessKey() |
${nacos.config.access-key:${nacos.access-key:}} |
secretKey() |
${nacos.config.secret-key:${nacos.secret-key:}} |
serverAddr() |
${nacos.config.server-addr:${nacos.server-addr:}} |
contextPath() |
${nacos.config.context-path:${nacos.context-path:}} |
clusterName() |
${nacos.config.cluster-name:${nacos.cluster-name:}} |
encode() |
${nacos.config.encode:${nacos.encode:UTF-8}} |
Such placeholders of @EnableNacosDiscovery and @EnableNacosConfig are designed to isolate different Nacos servers, maybe a lot of scenarios are unnecessary, as well as re-use general placeholders as default.
Suppose there was a config in Nacos Server whose dataId is "testDataId" and groupId is default group("DEFAULT_GROUP"), and then you'd like to change its' content by ConfigService#publishConfig method:
@NacosInjected
private ConfigService configService;
@Test
public void testPublishConfig() throws NacosException {
configService.publishConfig(DATA_ID, DEFAULT_GROUP, "9527");
}This new content should be notified in somewhere, and your could add a config change listener method into your Spring Beans:
@NacosConfigListener(dataId = DATA_ID)
public void onMessage(String config) {
assertEquals("mercyblitz", config); // asserts true
}It's equals to below code :
configService.addListener(DATA_ID, DEFAULT_GROUP, new AbstractListener() {
@Override
public void receiveConfigInfo(String config) {
assertEquals("9527", config); // asserts true
}
});Howerver @NacosConfigListener supports richer type-conversion feature.
@NacosConfigListener's type-conversion includes build-in and customized implementations. Default, build-in type-conversion is based on Spring DefaultFormattingConversionService, that means it had involved most general cases and the higher Spring framework , the richer features. Thus, the content "9527" in above example also be listened by a method with integer or Integer argument:
@NacosConfigListener(dataId = DATA_ID)
public void onInteger(Integer value) {
assertEquals(Integer.valueOf(9527), value); // asserts true
}
@NacosConfigListener(dataId = DATA_ID)
public void onInt(int value) {
assertEquals(9527, value); // asserts true
}Of couse, nacos-spring-context provides elastic extension for devlopers. If you define a named nacosConfigConversionService Spring Bean whose type is ConversionService , the DefaultFormattingConversionService will be ignored. What's more, you could customize NacosConfigConverter interface's implementation to specify a listener method for type conversion:
public class UserNacosConfigConverter implements NacosConfigConverter<User> {
@Override
public boolean canConvert(Class<User> targetType) {
return true;
}
@Override
public User convert(String source) {
return JSON.parseObject(source, User.class);
}
}UserNacosConfigConverter class binds @NacosConfigListener.converter() attribute:
@NacosInjected
private ConfigService configService;
@Test
public void testPublishUser() throws NacosException {
configService.publishConfig("user", DEFAULT_GROUP, "{\"id\":1,\"name\":\"mercyblitz\"}");
}
@NacosConfigListener(dataId = "user", converter = UserNacosConfigConverter.class)
public void onUser(User user) {
assertEquals(Long.valueOf(1L), user.getId());
assertEquals("mercyblitz", user.getName());
}Customized NacosConfigConverter may cost must time, thus @NacosConfigListener.timeout() attribute could be set timeout that limits max execution time to prevent blocking other listeners:
@Configuration
public class Listeners {
private Integer integerValue;
private Double doubleValue;
@NacosConfigListener(dataId = DATA_ID, timeout = 50)
public void onInteger(Integer value) throws Exception {
Thread.sleep(100); // timeout of execution
this.integerValue = value;
}
@NacosConfigListener(dataId = DATA_ID, timeout = 200)
public void onDouble(Double value) throws Exception {
Thread.sleep(100); // normal execution
this.doubleValue = value;
}
public Integer getIntegerValue() {
return integerValue;
}
public Double getDoubleValue() {
return doubleValue;
}
}Listeners Bean's integerValue will not be changed, always null, thus those asserts will be true:
@Autowired
private Listeners listeners;
@Test
public void testPublishConfig() throws NacosException {
configService.publishConfig(DATA_ID, DEFAULT_GROUP, "9527");
assertNull(listeners.getIntegerValue()); // asserts true
assertEquals(Double.valueOf(9527), listeners.getDoubleValue()); // asserts true
}@NacosInjected is a core annotation to injectConfigService or NamingService instance in your Spring Beans, which make those instances to be cacheable, that means they will be same if their @NacosProperties are equal whether they come from Global or Special Nacos Properties:
@NacosInjected
private ConfigService configService;
@NacosInjected(properties = @NacosProperties(encode = "UTF-8"))
private ConfigService configService2;
@NacosInjected(properties = @NacosProperties(encode = "GBK"))
private ConfigService configService3;
@NacosInjected
private NamingService namingService;
@NacosInjected(properties = @NacosProperties(encode = "UTF-8"))
private NamingService namingService2;
@NacosInjected(properties = @NacosProperties(encode = "GBK"))
private NamingService namingService3;
@Test
public void testInjection() {
Assert.assertEquals(configService, configService2);
Assert.assertNotEquals(configService2, configService3);
Assert.assertEquals(namingService, namingService2);
Assert.assertNotEquals(namingService2, namingService3);
}The property configService certainty uses @EnableNacos#globalProperties() or @EnableConfigNacos#globalProperties() , because the encode attribute's default value is “UTF-8”, thus configService2 annotated @NacosProperties(encode = "GBK") and configService are same. In same reason, namingService2 and namingService3 are same, and vice versa.
More powerfull feature that is @NacosInjected will enhance ConfigService instances that different from those created by NacosFactory.createConfigService() method, they support Nacos Spring events, for instance, there will be an NacosConfigPublishedEvent after an enhanced ConfigService invokes publishConfig() method, more details, please refer to Event/Listener Driven.
Externalized Configuration is a concept involved by Spring Boot, which allow application to receive external property sources controlling runtime behavior. Nacos Server as an isolation process outsize application that maintain applications' configuration, is also a external source, thus nacos-spring-context provides properties features including object binding, dynamic configuration(auto-refreshed) and so on, and it's required to depend on Spring Boot or Spring Cloud framework.
Here is a simple comparison between nacos-spring-context and Spring stack:
| Spring Stack | Nacos Spring | Highlight |
|---|---|---|
@Value |
@NacosValue |
auto-refreshed |
@ConfigurationProperties |
@NacosConfigurationProperties |
auto-refreshed,@NacosProperty,@NacosIgnore |
@PropertySource |
@NacosPropertySource |
auto-refreshed, precedence order control |
@PropertySources |
@NacosPropertySources |
Nacos Event/Listener Driven is based on standard Spring Event/Listener mechanism, Spring's ApplicationEvent is a abstract super class for all Nacos Spring Event:
| Nacos Spring Event | Trigger |
|---|---|
NacosConfigPublishedEvent |
After ConfigService.publishConfig() |
NacosConfigReceivedEvent |
AfterListener.receiveConfigInfo() |
NacosConfigRemovedEvent |
After configService.removeConfig() |
NacosConfigTimeoutEvent |
ConfigService.getConfig() on timeout |
NacosConfigListenerRegisteredEvent |
After ConfigService.addListner() or ConfigService.removeListener() |
NacosConfigurationPropertiesBeanBoundEvent |
After @NacosConfigurationProperties binding |
NacosConfigMetadataEvent |
After Nacos Config operations |