Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Nacos Integration & Deep Dive: OpenFeign Usage, Client Setup, and Cluster Architecture

Tech 1

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

  1. Download Nacos 1.4.1 from GitHub and extract the archive.
  2. Start in standalone mode with JVM parameter: -Dnacos.standalone=true
  3. 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

  1. What is the purpose of @EnableFeignClients and how does it work?
  2. Why must parameter annotations be explicitly defined in Feign client methods?
  3. How does @LoadBalanced enable service discovery for RestTemplate?
  4. How does Nacos prioritize same-cluster instances in load balancing?
  5. What mechanisms ensure data consistency across Nacos cluster nodes?

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.