Nacos Integration & Deep Dive: OpenFeign Usage, Client Setup, and Cluster Architecture
OpenFeign Integration with Nacos
1. Dependencies & Initial Setup
First, add required dependencies to your Maven pom.xml:
<!-- OpenFeign core for remote service calls -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Jackson codec for Feign request/response handling -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jackson</artifactId>
</dependency>
<!-- Nacos service discovery client -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring Cloud LoadBalancer for instance routing -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
</dependency>
Enable Feign client scanning with @EnableFeignClients in your application entry point:
@SpringBootApplication
@EnableFeignClients(basePackages = "com.example.feign.clients")
public class UserServiceFeignApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceFeignApplication.class, args);
}
}
2. Define Feign Client Interface
Create a Feign client to interact with the order service. Note: Always use explicit parameter annotations to avoid default @RequestBody behavior for multiple parameters.
@FeignClient(value = "user-order-service", path = "/api/orders")
public interface UserOrderFeignClient {
/**
* Fetch orders for a given user ID using RESTful path parameter
* @param userId ID of the user to query
* @return Wrapped response with order list
*/
@GetMapping("/getOrdersByUserId/{userId}")
ApiResponse<List<OrderDto>> getOrdersByUserId(@PathVariable("userId") Integer userId);
/**
* Fetch order count as string response for a user
* @param userId ID of the user to query
* @return String representation of order count
*/
@GetMapping("/getOrderCountByUserId/{userId}")
String getOrderCountByUserId(@PathVariable("userId") Integer userId);
}
3. Integrate Feign Client in Controller
Inject the Feign client into a controller to expose query endpoints:
@RestController
@RequestMapping("/api/users")
public class OrderQueryController {
private final UserOrderFeignClient orderFeignClient;
// Constructor injection for better testability
public OrderQueryController(UserOrderFeignClient orderFeignClient) {
this.orderFeignClient = orderFeignClient;
}
@GetMapping("/{userId}/orders")
public ApiResponse<List<OrderDto>> getOrdersForUser(@PathVariable("userId") Integer userId) {
return orderFeignClient.getOrdersByUserId(userId);
}
}
Nacos Client Configuration
1. Nacos Server Setup
- Download Nacos 1.4.1 from GitHub and extract the archive.
- Start in standalone mode with JVM parameter:
-Dnacos.standalone=true - Verify server is running at
http://localhost:8848/nacos(default credentials: nacos/nacos).
2. Client Registration Configuration
Add Nacos discovery settings to your application.yml:
server:
port: 8081
spring:
application:
name: user-service-nacos-client
cloud:
nacos:
discovery:
server-addr: localhost:8848
cluster-name: BJ
namespace: 3a2d8f7c-1b9e-4c5d-8e7f-6a5b4c3d2e1f
username: nacos
password: nacos
ephemeral: true
3. RestTemplate with Load Balancing
Configure RestTemplate with @LoadBalanced to enable service discovery-aware routing:
@Configuration
public class AppConfig {
@Bean
@LoadBalanced
public RestTemplate createRestTemplate() {
return new RestTemplate();
}
}
Use RestTemplate to call the order service:
@RestController
@RequestMapping("/api/users")
public class UserOrderController {
private static final Logger log = LoggerFactory.getLogger(UserOrderController.class);
private final RestTemplate restTemplate;
public UserOrderController(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@GetMapping("/{userId}/orders")
public ApiResponse<List<OrderDto>> getOrdersForUser(@PathVariable("userId") Integer userId) {
log.info("Fetching orders for user ID: {}", userId);
String serviceUrl = "http://user-order-service/api/orders/getOrdersByUserId/" + userId;
return restTemplate.getForObject(serviceUrl, ApiResponse.class);
}
}
4. Cluster-Preferential Routing
Configure load balancing to prioritize instances in the same cluster:
@SpringBootApplication
@LoadBalancerClient(value = "user-order-service", configuration = ClusterPreferenceLoadBalancerConfig.class)
public class UserServiceNacosApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceNacosApplication.class, args);
}
}
Custom load balancer configuration:
public class ClusterPreferenceLoadBalancerConfig {
@Bean
ReactorLoadBalancer<ServiceInstance> nacosPreferredLoadBalancer(Environment env,
LoadBalancerClientFactory clientFactory,
NacosDiscoveryProperties nacosProps) {
String serviceId = env.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new NacosClusterPreferenceLoadBalancer(
clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class),
serviceId,
nacosProps
);
}
}
Core cluster selection logic:
private Response<ServiceInstance> selectTargetInstance(List<ServiceInstance> allInstances) {
if (allInstances.isEmpty()) {
log.warn("No available instances for service: {}", this.serviceId);
return new EmptyResponse();
}
try {
String currentCluster = this.nacosProps.getClusterName();
List<ServiceInstance> targetInstances = allInstances;
if (StringUtils.isNotBlank(currentCluster)) {
List<ServiceInstance> localClusterInstances = allInstances.stream()
.filter(instance -> {
String instanceCluster = instance.getMetadata().get("nacos.cluster");
return StringUtils.equals(instanceCluster, currentCluster);
})
.collect(Collectors.toList());
if (!CollectionUtils.isEmpty(localClusterInstances)) {
targetInstances = localClusterInstances;
log.info("Using local cluster instances for service: {}, cluster: {}", serviceId, currentCluster);
}
}
targetInstances = filterHealthyInstances(targetInstances);
ServiceInstance selectedInstance = NacosInstanceSelector.pickRandomWeightedInstance(targetInstances);
return new DefaultResponse(selectedInstance);
} catch (Exception e) {
log.error("Error selecting instance for service: {}", serviceId, e);
return null;
}
}
Nacos Deep Dive
1. Client Registration Flow
Nacos client registration is triggered via NacosAutoServiceRegistration, which implements ApplicationListener. This bean is initialized by NacosDiscoveryAutoConfiguration, loaded from spring.factories. When the Spring application reaches the ApplicationReadyEvent phase, the onApplicationEvent method sends an HTTP POST request to /nacos/v1/ns/instance to register the client.
2. Service Health Check
Nacos server runs InstanceHealthCheckTask scheduled tasks (every 5 seconds, initial delay 5s) to monitor instance health:
- If an instance's last heartbeat is >15 seconds old, it's marked unhealthy.
- If >30 seconds old, it's removed from the service registry.
3. Cluster Node Synchronization
ClusterNodeSyncManager maintains cluster node status by periodically sending HTTP GET requests to /operator/server/status on all other nodes. This ensures the cluster has an up-to-date view of active nodes.
4. Cluster Data Replication
When an instance is registered, ClusterDataSyncService triggers cross-node synchronization:
distroProtocol.sync(
new DistroKey(key, KeyBuilder.INSTANCE_LIST_KEY_PREFIX),
DataOperation.CHANGE,
globalConfig.getTaskDispatchPeriod() / 2
);
The sync method sends data change events to all other cluster nodes via delayed tasks, ensuring consistent service instance data across the cluster.
Key Questions
- What is the purpose of
@EnableFeignClientsand how does it work? - Why must parameter annotations be explicitly defined in Feign client methods?
- How does
@LoadBalancedenable service discovery for RestTemplate? - How does Nacos prioritize same-cluster instances in load balancing?
- What mechanisms ensure data consistency across Nacos cluster nodes?