OkHttp + Retrofit2 调用第三方接口完整教程(以nomad为例)

第一部分:基础概念与架构设计

1.1 技术栈介绍

核心技术组件包括:

  • OkHttp3:高性能HTTP客户端,负责底层网络通信

  • Retrofit2:类型安全的HTTP客户端,将HTTP API转换为Java接口

  • Jackson:JSON序列化/反序列化库

  • Spring Boot:依赖注入和配置管理

1.2 项目架构设计

架构分层如下:

core/

├── config/ # 配置类(文档1、2、3)

├── client/ # 客户端实现(文档4、5)

├── model/ # 数据模型

├── utils/ # 工具类(文档6)

└── exception/ # 异常处理

第二部分:详细配置实现

2.1 配置属性管理(NomadConfigProperties.class)

配置文件的作用

  • 集中管理所有HTTP相关参数

  • 支持不同环境的不同配置

  • 提供参数验证机制

ClientConfig.class

java 复制代码
/**
 * @author Felix
 * @describe: 客户端配置接口
 * @date 2025年12月06日 10:53
 */
public interface ClientConfig {
    Integer getConnectTimeout();
    Integer getReadTimeout();
    Integer getWriteTimeout();
    Integer getMaxIdleConnections();
    Integer getKeepAliveDuration();
    String getToken();
}

NomadConfigProperties.class

java 复制代码
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author Felix
 * @describe: Nomad 客户端配置属性
 * @date 2025年12月03日 16:53
 */
@Data
@Component
@ConfigurationProperties(prefix = "nomad.client")
public class NomadConfigProperties implements ClientConfig {

    // 基础配置
    private String baseUrl = "http://192.168.1.183:4646";
    private String token;
    private String region = "global";
    private String namespace = "default";

    // HTTP客户端配置
    private Integer connectTimeout = 5000;
    private Integer readTimeout = 30000;
    private Integer writeTimeout = 30000;
    private Integer maxIdleConnections = 10;
    private Integer keepAliveDuration = 300;

    // 重试配置
    private Boolean enableRetry = true;
    private Integer maxRetries = 3;
    private Integer retryDelay = 1000;

    // 监控配置
    private Boolean enableMetrics = true;
    private Integer healthCheckInterval = 30000;

    // Nomad 1.7.x 特性
    private Boolean enableNamespace = true;
    private Boolean enableVolumeCsi = true;
    private Boolean enableDynamicNodeMetadata = true;

    // 验证配置
    public void validate() {
        if (baseUrl == null || baseUrl.trim().isEmpty()) {
            throw new IllegalArgumentException("Nomad base URL must not be empty");
        }
        // 建议添加URL格式基础验证
        if (!baseUrl.startsWith("http")) {
            throw new IllegalArgumentException("Nomad base URL must start with http or https");
        }
        if (connectTimeout <= 0 || readTimeout <= 0 || writeTimeout <= 0) {
            throw new IllegalArgumentException("Timeout values must be positive");
        }
        if (maxRetries < 0) {
            throw new IllegalArgumentException("Max retries cannot be negative");
        }
    }

}

2.2 HTTP客户端配置(HttpClientConfig.class)

多客户端策略

为Nomad和Consul创建了独立的OkHttpClient实例,这种设计的优势:

  • 隔离性:不同服务的配置互不影响

  • 定制化:每个服务可以有特定的拦截器和超时设置

  • 可维护性:问题排查时更容易定位

HttpClientConfig.class

java 复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
 * @author Felix
 * @describe: HTTP客户端配置
 * @date 2025年12月03日 17:01
 */

@Slf4j
@Configuration
public class HttpClientConfig {
    private static final Logger logger = LoggerFactory.getLogger(HttpClientConfig.class);

    @Resource
    private NomadConfigProperties nomadConfig;

    @Resource
    private ConsulConfigProperties consulConfig;

    /**
     * 创建默认的 Jackson ObjectMapper
     */
    @Bean
    @ConditionalOnMissingBean(ObjectMapper.class)
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        return mapper;
    }

    /**
     * 创建 Nomad 专用的 OkHttpClient
     */
    @Bean(name = "nomadHttpClient")
    public OkHttpClient nomadHttpClient() {
        return createHttpClientBuilder(nomadConfig)
                .readTimeout(nomadConfig.getReadTimeout(), TimeUnit.MILLISECONDS)
                .writeTimeout(nomadConfig.getWriteTimeout(), TimeUnit.MILLISECONDS)
                .build();
    }

    /**
     * 创建 Consul 专用的 OkHttpClient
     */
    @Bean(name = "consulHttpClient")
    public OkHttpClient consulHttpClient() {
        return createHttpClientBuilder(consulConfig)
                .readTimeout(consulConfig.getReadTimeout(), TimeUnit.MILLISECONDS)
                .writeTimeout(consulConfig.getWriteTimeout(), TimeUnit.MILLISECONDS)
                // 添加Consul特定的配置
                .addInterceptor(chain -> {
                    Request original = chain.request();
                    Request.Builder requestBuilder = original.newBuilder();

                    // Consul特定头信息
                    if (StringUtils.isNotBlank(consulConfig.getToken())) {
                        requestBuilder.addHeader("X-Consul-Token", consulConfig.getToken());
                    }
                    if (StringUtils.isNotBlank(consulConfig.getNamespace())) {
                        requestBuilder.addHeader("X-Consul-Namespace", consulConfig.getNamespace());
                    }
                    return chain.proceed(requestBuilder.build());
                })
                .build();
    }

    /**
     * 创建通用的 HTTP 客户端构建器
     */
    private OkHttpClient.Builder createHttpClientBuilder(ClientConfig config) {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();

        // 连接超时
        builder.connectTimeout(config.getConnectTimeout(), TimeUnit.MILLISECONDS);

        // 连接池配置
        ConnectionPool connectionPool = new ConnectionPool(
                config.getMaxIdleConnections(),
                config.getKeepAliveDuration(),
                TimeUnit.SECONDS
        );
        builder.connectionPool(connectionPool);

        // 添加认证令牌(如果有)
        if (config.getToken() != null && !config.getToken().isEmpty()) {
            builder.addInterceptor(chain -> {
                Request.Builder requestBuilder = chain.request().newBuilder();
                if (config instanceof NomadConfigProperties) {
                    requestBuilder.addHeader("X-Nomad-Token", config.getToken());
                } else if (config instanceof ConsulConfigProperties) {
                    requestBuilder.addHeader("X-Consul-Token", config.getToken());
                }
                Request request = requestBuilder.build();
                return chain.proceed(request);
            });
        }

        return builder;
    }

    /**
     * 创建 Nomad Retrofit 客户端
     */
    @Bean
    public Retrofit nomadRetrofit(@Autowired ObjectMapper objectMapper,
                                  @Autowired OkHttpClient nomadHttpClient) {
        return new Retrofit.Builder()
                .baseUrl(nomadConfig.getBaseUrl())
                .client(nomadHttpClient)
                .addConverterFactory(JacksonConverterFactory.create(objectMapper))
                .build();
    }
}

拦截器链设计

  • 认证拦截器:自动添加Token头部

  • 日志拦截器:记录请求/响应信息(可自定义补充)

  • 监控拦截器:收集性能指标

2.3 Retrofit配置(HttpClientConfig.class中的nomadRetrofit方法)

核心配置要素

  • 基础URL:必须正确设置API端点

  • 转换工厂:JacksonConverterFactory处理JSON序列化

  • HTTP客户端:使用配置好的OkHttpClient实例

第三部分:API接口定义

3.1 接口设计规范(NomadClient.class)

RESTful设计原则

  • 资源化URL设计:/v1/jobs/v1/nodes

  • 合适的HTTP方法:GET查询、POST创建、PUT更新、DELETE删除

  • 清晰的路径参数:@Path("jobId")用于指定资源

  • 合理的查询参数:@Query用于过滤和分页

java 复制代码
import com.forest.geocube.core.model.NomadClientStats;
import com.forest.geocube.core.model.NomadJob;
import com.forest.geocube.core.model.NomadNode;
import com.forest.geocube.core.model.NomadSpace;
import retrofit2.Call;
import retrofit2.http.*;

import java.util.List;
import java.util.Map;

/**
 * @author Felix
 * @describe:Nomad REST API 客户端接口 使用 Retrofit 进行 HTTP 通信
 * @date 2025年12月03日 17:09
 */
public interface NomadClient {
    // ==================== 集群管理 ====================

    /**
     * 获取集群领导信息
     */
    @GET("/v1/status/leader")
    Call<String> getLeader();

    /**
     * 获取集群成员信息
     */
    @GET("/v1/agent/members")
    Call<Map<String, Object>> getMembers();

    /**
     * 获取集群服务器信息
     */
    @GET("/v1/agent/servers")
    Call<List<String>> getServers();

    // ===================== 客户端管理 ====================

    /**
     * Nomad客户端状态统计信息查询
     */
    @GET("/v1/client/stats/{nodeId}")
    Call<NomadClientStats> getClientStats(
            @Query("nodeId") String nodeId
    );

    // ==================== 节点管理 ====================

    /**
     * 获取所有节点
     */
    @GET("/v1/nodes")
    Call<List<NomadNode>> getNodes(
            @Query("prefix") String prefix,
            @Query("region") String region,
            @Query("namespace") String namespace
    );

    /**
     * 获取节点详情
     */
    @GET("/v1/node/{nodeId}")
    Call<NomadNode> getNode(
            @Path("nodeId") String nodeId,
            @Query("region") String region,
            @Query("namespace") String namespace
    );

    /**
     * 更新节点状态
     */
    @POST("/v1/node/{nodeId}/drain")
    Call<Void> drainNode(
            @Path("nodeId") String nodeId,
            @Query("enable") Boolean enable,
            @Query("deadline") Long deadline,
            @Query("meta") Map<String, String> meta,
            @Query("region") String region,
            @Query("namespace") String namespace,
            @Body Map<String, Object> body
    );

    /**
     * 节点资格管理
     */
    @POST("/v1/node/{nodeId}/eligibility")
    Call<Void> updateNodeEligibility(
            @Path("nodeId") String nodeId,
            @Query("eligibility") String eligibility,
            @Query("region") String region,
            @Query("namespace") String namespace
    );

    // ==================== 任务管理 ====================

    /**
     * 获取所有任务
     */
    @GET("/v1/jobs")
    Call<List<NomadJob>> getJobs(
            @Query("prefix") String prefix,
            @Query("region") String region,
            @Query("namespace") String namespace
    );

    /**
     * 获取任务详情
     */
    @GET("/v1/job/{jobId}")
    Call<NomadJob> getJob(
            @Path("jobId") String jobId,
            @Query("region") String region,
            @Query("namespace") String namespace
    );

    /**
     * 创建任务
     */
    @POST("/v1/jobs")
    Call<Void> createJob(
            @Query("region") String region,
            @Query("namespace") String namespace,
            @Body NomadJob job
    );

    /**
     * 更新任务
     */
    @POST("/v1/job/{jobId}")
    Call<Void> updateJob(
            @Path("jobId") String jobId,
            @Query("region") String region,
            @Query("namespace") String namespace,
            @Body NomadJob job
    );

    /**
     * 删除任务
     */
    @DELETE("/v1/job/{jobId}")
    Call<Void> deleteJob(
            @Path("jobId") String jobId,
            @Query("purge") Boolean purge,
            @Query("global") Boolean global,
            @Query("region") String region,
            @Query("namespace") String namespace
    );

    /**
     * 启停任务
     */
    @POST("/v1/job/{jobId}/periodic/force")
    Call<Void> forcePeriodicJob(
            @Path("jobId") String jobId,
            @Query("region") String region,
            @Query("namespace") String namespace
    );

    // ==================== 分配管理 ====================

    /**
     * 获取任务分配
     */
    @GET("/v1/job/{jobId}/allocations")
    Call<List<Map<String, Object>>> getAllocations(
            @Path("jobId") String jobId,
            @Query("region") String region,
            @Query("namespace") String namespace
    );

    /**
     * 获取分配详情
     */
    @GET("/v1/allocation/{allocationId}")
    Call<Map<String, Object>> getAllocation(
            @Path("allocationId") String allocationId,
            @Query("region") String region,
            @Query("namespace") String namespace
    );

    /**
     * 停止分配
     */
    @DELETE("/v1/allocation/{allocationId}")
    Call<Void> stopAllocation(
            @Path("allocationId") String allocationId,
            @Query("region") String region,
            @Query("namespace") String namespace
    );

    // ==================== 卷管理 ====================

    /**
     * 获取所有卷
     */
    @GET("/v1/volumes")
    Call<List<Map<String, Object>>> getVolumes(
            @Query("type") String type,
            @Query("plugin_id") String pluginId,
            @Query("region") String region,
            @Query("namespace") String namespace
    );

    /**
     * 创建卷
     */
    @POST("/v1/volume/csi/{volumeId}/{action}")
    Call<Void> manageVolume(
            @Path("volumeId") String volumeId,
            @Path("action") String action,
            @Query("region") String region,
            @Query("namespace") String namespace,
            @Body Map<String, Object> body
    );

    // ==================== 命名空间管理 ====================

    /**
     * 获取所有命名空间
     */
    @GET("/v1/namespaces")
    Call<List<NomadSpace>> getNamespaces();

    /**
     * 创建命名空间
     */
    @POST("/v1/namespace")
    Call<Void> createNamespace(@Body NomadSpace namespace);

    /**
     * 删除命名空间
     */
    @DELETE("/v1/namespace/{namespace}")
    Call<Void> deleteNamespace(@Path("namespace") String namespace);

    /**
     * 删除命名空间
     */
    @GET("/v1/namespace/{namespace}")
    Call<NomadSpace> readNamespace(@Path("namespace") String namespace);
}

注解使用技巧

@GET("/v1/job/{jobId}") // 路径模板

Call<NomadJob> getJob(

@Path("jobId") String jobId, // 路径参数

@Query("region") String region, // 查询参数

@Query("namespace") String namespace // 查询参数

);

第四部分:客户端实现

4.1 实现类设计(NomadClientImpl.class)

代码如下:

java 复制代码
import com.forest.geocube.core.config.NomadConfigProperties;
import com.forest.geocube.core.exception.NomadClientException;
import com.forest.geocube.core.model.NomadClientStats;
import com.forest.geocube.core.model.NomadJob;
import com.forest.geocube.core.model.NomadNode;
import com.forest.geocube.core.model.NomadSpace;
import com.forest.geocube.core.utils.RetryUtil;
import okhttp3.OkHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import retrofit2.Response;
import retrofit2.Retrofit;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Felix
 * @describe: Nomad 客户端实现
 * @date 2025年12月03日 17:15
 */
@Component
public class NomadClientImpl {
    private static final Logger logger = LoggerFactory.getLogger(NomadClientImpl.class);

    private final NomadClient nomadClient;
    private final NomadConfigProperties config;
    private final OkHttpClient httpClient;
    private final RetryUtil retryUtil;

    @Autowired
    public NomadClientImpl(@Qualifier("nomadRetrofit") Retrofit retrofit,
                           NomadConfigProperties config,
                           @Qualifier("nomadHttpClient") OkHttpClient httpClient,
                           RetryUtil retryUtil) {

        this.nomadClient = retrofit.create(NomadClient.class);
        this.config = config;
        this.httpClient = httpClient;
        this.retryUtil = retryUtil;
    }

    // ==================== 集群管理 ====================

    /**
     * 获取集群领导者
     */
    public String getLeader() {
        return retryUtil.executeWithRetry(() -> {
            Response<String> response = nomadClient.getLeader().execute();
            if (!response.isSuccessful()) {
                throw new NomadClientException("Failed to get cluster leader: " + response.message());
            }
            return response.body();
        });
    }

    /**
     * 检查集群健康状态
     */
    public boolean isHealthy() {
        try {
            Response<String> response = nomadClient.getLeader().execute();
            return response.isSuccessful();
        } catch (IOException e) {
            logger.error("Failed to check Nomad cluster health", e);
            return false;
        }
    }

    // ==================== 节点管理 ====================

    /**
     * 获取所有节点
     */
    public List<NomadNode> getNodes(String prefix) {
        return retryUtil.executeWithRetry(() -> {
            Response<List<NomadNode>> response = nomadClient.getNodes(
                    prefix, config.getRegion(), config.getNamespace()
            ).execute();

            if (!response.isSuccessful()) {
                throw new NomadClientException("Failed to get nodes: " + response.message());
            }
            return response.body();
        });
    }

    /**
     * 获取节点详情
     */
    public NomadNode getNode(String nodeId) {
        return retryUtil.executeWithRetry(() -> {
            Response<NomadNode> response = nomadClient.getNode(
                    nodeId, config.getRegion(), config.getNamespace()
            ).execute();

            if (!response.isSuccessful()) {
                if (response.code() == 404) {
                    throw new NomadClientException("Node not found: " + nodeId);
                }
                throw new NomadClientException("Failed to get node: " + response.message());
            }
            return response.body();
        });
    }

    /**
     * Nomad客户端状态统计信息查询
     */
    public NomadClientStats getClientStats(String nodeId) {
        return retryUtil.executeWithRetry(() -> {
            Response<NomadClientStats> response = nomadClient.getClientStats(
                    nodeId
            ).execute();
            if (!response.isSuccessful()) {
                if (response.code() == 404) {
                    throw new NomadClientException("ClientStats not found: " + nodeId);
                }
                throw new NomadClientException("Failed to get ClientStats: " + response.message());
            }
            return response.body();
        });
    }

    /**
     * 排空节点
     */
    public void drainNode(String nodeId, boolean enable, Long deadline, Map<String, String> meta) {
        retryUtil.executeWithRetry(() -> {
            Response<Void> response = nomadClient.drainNode(
                    nodeId, enable, deadline, meta,
                    config.getRegion(), config.getNamespace(), new HashMap<>()
            ).execute();

            if (!response.isSuccessful()) {
                throw new NomadClientException("Failed to drain node: " + response.message());
            }
            return null;
        });
    }

    /**
     * 更新节点调度资格
     */
    public void updateNodeEligibility(String nodeId, boolean eligible) {
        retryUtil.executeWithRetry(() -> {
            String eligibility = eligible ? "eligible" : "ineligible";
            Response<Void> response = nomadClient.updateNodeEligibility(
                    nodeId, eligibility, config.getRegion(), config.getNamespace()
            ).execute();

            if (!response.isSuccessful()) {
                throw new NomadClientException("Failed to update node eligibility: " + response.message());
            }
            return null;
        });
    }

    // ==================== 任务管理 ====================

    /**
     * 获取所有任务
     */
    public List<NomadJob> getJobs(String prefix) {
        return retryUtil.executeWithRetry(() -> {
            Response<List<NomadJob>> response = nomadClient.getJobs(
                    prefix, config.getRegion(), config.getNamespace()
            ).execute();

            if (!response.isSuccessful()) {
                throw new NomadClientException("Failed to get jobs: " + response.message());
            }
            return response.body();
        });
    }

    /**
     * 获取任务详情
     */
    public NomadJob getJob(String jobId) {
        return retryUtil.executeWithRetry(() -> {
            Response<NomadJob> response = nomadClient.getJob(
                    jobId, config.getRegion(), config.getNamespace()
            ).execute();

            if (!response.isSuccessful()) {
                if (response.code() == 404) {
                    throw new NomadClientException("Job not found: " + jobId);
                }
                throw new NomadClientException("Failed to get job: " + response.message());
            }
            return response.body();
        });
    }

    /**
     * 创建任务
     */
    public void createJob(NomadJob job) {
        retryUtil.executeWithRetry(() -> {
            Response<Void> response = nomadClient.createJob(
                    config.getRegion(), config.getNamespace(), job
            ).execute();

            if (!response.isSuccessful()) {
                throw new NomadClientException("Failed to create job: " + response.message());
            }
            return null;
        });
    }

    /**
     * 更新任务
     */
    public void updateJob(String jobId, NomadJob job) {
        retryUtil.executeWithRetry(() -> {
            Response<Void> response = nomadClient.updateJob(
                    jobId, config.getRegion(), config.getNamespace(), job
            ).execute();

            if (!response.isSuccessful()) {
                throw new NomadClientException("Failed to update job: " + response.message());
            }
            return null;
        });
    }

    /**
     * 删除任务
     */
    public void deleteJob(String jobId, boolean purge, boolean global) {
        retryUtil.executeWithRetry(() -> {
            Response<Void> response = nomadClient.deleteJob(
                    jobId, purge, global, config.getRegion(), config.getNamespace()
            ).execute();

            if (!response.isSuccessful()) {
                throw new NomadClientException("Failed to delete job: " + response.message());
            }
            return null;
        });
    }

    // ==================== 分配管理 ====================

    /**
     * 获取任务分配
     */
    public List<Map<String, Object>> getAllocations(String jobId) {
        return retryUtil.executeWithRetry(() -> {
            Response<List<Map<String, Object>>> response = nomadClient.getAllocations(
                    jobId, config.getRegion(), config.getNamespace()
            ).execute();

            if (!response.isSuccessful()) {
                throw new NomadClientException("Failed to get allocations: " + response.message());
            }
            return response.body();
        });
    }

    /**
     * 获取分配详情
     */
    public Map<String, Object> getAllocation(String allocationId) {
        return retryUtil.executeWithRetry(() -> {
            Response<Map<String, Object>> response = nomadClient.getAllocation(
                    allocationId, config.getRegion(), config.getNamespace()
            ).execute();

            if (!response.isSuccessful()) {
                if (response.code() == 404) {
                    throw new NomadClientException("Allocation not found: " + allocationId);
                }
                throw new NomadClientException("Failed to get allocation: " + response.message());
            }
            return response.body();
        });
    }

    /**
     * 停止分配
     */
    public void stopAllocation(String allocationId) {
        retryUtil.executeWithRetry(() -> {
            Response<Void> response = nomadClient.stopAllocation(
                    allocationId, config.getRegion(), config.getNamespace()
            ).execute();

            if (!response.isSuccessful()) {
                throw new NomadClientException("Failed to stop allocation: " + response.message());
            }
            return null;
        });
    }

    // ==================== 卷管理 ====================

    /**
     * 获取所有存储卷
     */
    public List<Map<String, Object>> getVolumes(String type, String pluginId) {
        return retryUtil.executeWithRetry(() -> {
            Response<List<Map<String, Object>>> response = nomadClient.getVolumes(
                    type, pluginId, config.getRegion(), config.getNamespace()
            ).execute();

            if (!response.isSuccessful()) {
                throw new NomadClientException("Failed to get volumes: " + response.message());
            }
            return response.body();
        });
    }

    /**
     * 挂载存储卷
     */
    public void mountVolume(String volumeId, String nodeId, String allocationId) {
        retryUtil.executeWithRetry(() -> {
            Map<String, Object> body = new HashMap<>();
            body.put("NodeID", nodeId);
            body.put( "AllocationID", allocationId);
            Response<Void> response = nomadClient.manageVolume(
                    volumeId, "mount", config.getRegion(), config.getNamespace(), body
            ).execute();

            if (!response.isSuccessful()) {
                throw new NomadClientException("Failed to mount volume: " + response.message());
            }
            return null;
        });
    }

    /**
     * 卸载存储卷
     */
    public void unmountVolume(String volumeId, String nodeId, String allocationId) {
        retryUtil.executeWithRetry(() -> {
            Map<String, Object> body = new HashMap<>();
            body.put("NodeID", nodeId);
            body.put( "AllocationID", allocationId);

            Response<Void> response = nomadClient.manageVolume(
                    volumeId, "unmount", config.getRegion(), config.getNamespace(), body
            ).execute();

            if (!response.isSuccessful()) {
                throw new NomadClientException("Failed to unmount volume: " + response.message());
            }
            return null;
        });
    }

    // ==================== 命名空间管理 ====================

    /**
     * 获取所有命名空间
     */
    public List<NomadSpace> getNamespaces() {
        return retryUtil.executeWithRetry(() -> {
            Response<List<NomadSpace>> response = nomadClient.getNamespaces().execute();
            if (!response.isSuccessful()) {
                throw new NomadClientException("Failed to get namespaces: " + response.message());
            }
            return response.body();
        });
    }

    /**
     * 创建命名空间
     */
    public void createNamespace(NomadSpace nomadSpace) {
        retryUtil.executeWithRetry(() -> {
//            Map<String, Object> namespaceBody = new HashMap<>();
//            namespaceBody.put("Name", namespace);
//            namespaceBody.put("Description", description);

            Response<Void> response = nomadClient.createNamespace(nomadSpace).execute();

            if (!response.isSuccessful()) {
                if (response.code() == 400) {
                    throw new NomadClientException("Invalid namespace configuration: " + response.message());
                }
                throw new NomadClientException("Failed to create namespace: " + response.message());
            }
            return null;
        });
    }

    /**
     * 删除命名空间
     */
    public void deleteNamespace(String namespace) {
        retryUtil.executeWithRetry(() -> {
            Response<Void> response = nomadClient.deleteNamespace(namespace).execute();

            if (!response.isSuccessful()) {
                if (response.code() == 404) {
                    throw new NomadClientException("Namespace not found: " + namespace);
                }
                if (response.code() == 409) {
                    throw new NomadClientException("Cannot delete namespace with active jobs: " + namespace);
                }
                throw new NomadClientException("Failed to delete namespace: " + response.message());
            }
            return null;
        });
    }

    /**
     * 查询命名空间详情
     */
    public NomadSpace readNamespace(String namespace) {
        return retryUtil.executeWithRetry(() -> {
            Response<NomadSpace> response = nomadClient.readNamespace(namespace).execute();
            if (!response.isSuccessful()) {
                if (response.code() == 404) {
                    throw new NomadClientException("Namespace not found: " + namespace);
                }
                throw new NomadClientException("Failed to delete namespace: " + response.message());
            }
            return response.body();
        });
    }
}

NomadClientException.class

java 复制代码
/**
 * @author Felix
 * @describe: Nomad客户端异常
 * @date 2025年12月03日 17:23
 */
public class NomadClientException extends RuntimeException {

    public NomadClientException(String message) {
        super(message);
    }

    public NomadClientException(String message, Throwable cause) {
        super(message, cause);
    }

    public NomadClientException(Throwable cause) {
        super(cause);
    }
}

依赖注入策略

@Autowired

public NomadClientImpl(

@Qualifier("nomadRetrofit") Retrofit retrofit, // 指定Bean名称

NomadConfigProperties config, // 配置属性

@Qualifier("nomadHttpClient") OkHttpClient httpClient, // HTTP客户端

RetryUtil retryUtil // 重试工具

)

统一的异常处理模式

if (!response.isSuccessful()) {

if (response.code() == 404) {

throw new NomadClientException("Node not found: " + nodeId);

}

throw new NomadClientException("Failed to get node: " + response.message());

}

第五部分:高级特性实现

5.1 重试机制详解(RetryUtil.class)

重试策略设计

// 固定间隔重试

public <T> T executeWithRetry(Callable<T> operation, int maxRetries, long retryDelay)

// 指数退避重试

public <T> T executeWithExponentialBackoff(Callable<T> operation, int maxRetries, long initialDelay)

RetryUtil.class

java 复制代码
import com.forest.geocube.core.exception.NomadClientException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.concurrent.Callable;

/**
 * @author Felix
 * @describe: 重试工具类
 * @date 2025年12月03日 17:21
 */
@Component
public class RetryUtil {

    private static final Logger logger = LoggerFactory.getLogger(RetryUtil.class);

    private final int maxRetries;
    private final long retryDelay;

    public RetryUtil() {
        this.maxRetries = 3;
        this.retryDelay = 1000;
    }

    public RetryUtil(int maxRetries, long retryDelay) {
        this.maxRetries = maxRetries;
        this.retryDelay = retryDelay;
    }

    /**
     * 执行带有重试逻辑的操作
     */
    public <T> T executeWithRetry(Callable<T> operation) {
        return executeWithRetry(operation, maxRetries, retryDelay);
    }

    /**
     * 执行带有重试逻辑的操作(自定义重试参数)
     */
    public <T> T executeWithRetry(Callable<T> operation, int maxRetries, long retryDelay) {
        int attempts = 0;
        Exception lastException = null;

        while (attempts <= maxRetries) {
            try {
                return operation.call();
            }catch (NomadClientException e){
                logger.error(e.getMessage());
                throw e;
            } catch (Exception e) {
                lastException = e;
                attempts++;

                if (attempts > maxRetries) {
                    logger.error("Operation failed after {} attempts", attempts, e);
                    throw new RuntimeException("Operation failed after " + attempts + " attempts", e);
                }

                logger.warn("Operation failed (attempt {}/{}), retrying in {}ms...",
                        attempts, maxRetries, retryDelay, e);

                try {
                    Thread.sleep(retryDelay);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Retry interrupted", ie);
                }
            }
        }

        throw new RuntimeException("Operation failed", lastException);
    }

    /**
     * 执行带有重试逻辑的操作(Supplier版本)
     */
//    public <T> T executeWithRetry(Supplier<T> operation) {
//        return executeWithRetry(() -> operation.get());
//    }

    /**
     * 执行带有指数退避的重试
     */
    public <T> T executeWithExponentialBackoff(Callable<T> operation, int maxRetries, long initialDelay) {
        int attempts = 0;
        Exception lastException = null;
        long delay = initialDelay;

        while (attempts <= maxRetries) {
            try {
                return operation.call();
            } catch (Exception e) {
                lastException = e;
                attempts++;

                if (attempts > maxRetries) {
                    logger.error("Operation failed after {} attempts with exponential backoff", attempts, e);
                    throw new RuntimeException("Operation failed after " + attempts + " attempts", e);
                }

                logger.warn("Operation failed (attempt {}/{}), retrying in {}ms...",
                        attempts, maxRetries, delay, e);

                try {
                    Thread.sleep(delay);
                    // 指数退避:每次重试延迟加倍
                    delay *= 2;
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Retry interrupted", ie);
                }
            }
        }

        throw new RuntimeException("Operation failed", lastException);
    }
}
相关推荐
一 乐2 小时前
海鲜商城购物|基于SprinBoot+vue的海鲜商城系统(源码+数据库+文档)
前端·javascript·数据库·vue.js·spring boot
掘金-我是哪吒3 小时前
第378集设备服务接入系统Java微服务后端架构实战
java·开发语言·spring·微服务·架构
百万彩票中奖候选人3 小时前
迁移 Docker 存储目录
java·docker·eureka
一颗宁檬不酸3 小时前
Java Web 踩坑实录:JSTL 标签库 URI 解析失败(HTTP 500 错误)完美解决
java·开发语言·前端
西岭千秋雪_3 小时前
MySQL日志梳理(服务器层)
java·运维·服务器·数据库·mysql
有一个好名字3 小时前
Java 高性能序列化框架 Kryo 详解:从入门到实战
java·开发语言
爬山算法3 小时前
Redis(166)如何使用Redis实现实时统计?
java·redis·bootstrap
better_liang3 小时前
每日Java面试场景题知识点之-Spring Boot微服务配置管理
java·spring boot·微服务·面试题·配置管理
seven97_top3 小时前
数据结构——树
java·数据结构