Skip to content

最佳实践

本文档介绍在生产环境中使用 Lovrabet Java OpenSDK 的最佳实践,帮助您构建高性能、高可用的应用。


目录

    1. 客户端管理
    1. 安全性
    1. 性能优化
    1. 错误处理
    1. 日志管理
    1. 测试策略
    1. 部署建议

1. 客户端管理

1.1 使用单例模式

LovrabetSDKClient 是线程安全的,建议在应用中创建单例复用,避免频繁创建实例。

✅ 推荐做法

Java
package com.example.config;

import com.lovrabet.runtime.opensdk.client.LovrabetSDKClient;

/**
 * SDK 客户端单例管理器
 */
public class SdkClientManager {
    private static volatile LovrabetSDKClient instance;

    private SdkClientManager() {
        // 私有构造函数,防止外部实例化
    }

    public static LovrabetSDKClient getInstance() {
        if (instance == null) {
            synchronized (SdkClientManager.class) {
                if (instance == null) {
                    String accessKey = ConfigManager.getAccessKey();
                    String baseUrl = ConfigManager.getBaseUrl();
                    instance = new LovrabetSDKClient(accessKey, baseUrl);
                }
            }
        }
        return instance;
    }
}

❌ 不推荐做法

Java
// 不推荐:每次调用都创建新实例
public void someMethod() {
    LovrabetSDKClient client = new LovrabetSDKClient(accessKey, baseUrl);
    // ...
}

1.2 Spring Boot 环境下的配置

使用 Spring 的依赖注入管理 SDK 客户端:

Java
@Configuration
public class LovrabetConfig {

    @Value("${lovrabet.access-key}")
    private String accessKey;

    @Value("${lovrabet.base-url}")
    private String baseUrl;

    @Bean
    public LovrabetSDKClient lovrabetSDKClient() {
        return new LovrabetSDKClient(accessKey, baseUrl);
    }
}
Java
@Service
public class DataService {

    private final LovrabetSDKClient sdkClient;

    // 通过构造函数注入
    public DataService(LovrabetSDKClient sdkClient) {
        this.sdkClient = sdkClient;
    }
}

2. 安全性

2.1 AccessKey 管理

✅ 推荐做法

方式 1: 使用环境变量

Java
String accessKey = System.getenv("LOVRABET_ACCESS_KEY");
if (accessKey == null || accessKey.isEmpty()) {
    throw new IllegalStateException("LOVRABET_ACCESS_KEY 环境变量未设置");
}
Bash
# 在系统环境中设置
export LOVRABET_ACCESS_KEY="ak-your-access-key"

# 或者在启动 Java 应用时传递
java -jar app.jar

方式 2: 使用配置文件 + 配置中心

YAML
# application.yml
lovrabet:
  access-key: ${LOVRABET_ACCESS_KEY:}
  base-url: https://runtime.lovrabet.com/openapi
Java
@Value("${lovrabet.access-key}")
private String accessKey;

方式 3: 使用密钥管理服务

Java
// 使用云服务商的密钥管理服务
String accessKey = secretManager.getSecret("lovrabet-access-key");

❌ 不推荐做法

Java
// ❌ 不要硬编码在代码中
String accessKey = "ak-OD8xWhMOsL5ChQ3Akhv4uYYiu1fPOFQGVF9BULIeov8";

// ❌ 不要提交到代码仓库
// git add config.properties  (包含明文 accessKey)

// ❌ 不要在日志中打印
logger.info("AccessKey: " + accessKey);  // 危险!

// ❌ 不要在异常信息中暴露
throw new Exception("AccessKey " + accessKey + " 无效");

2.2 数据传输安全

Java
// ✅ 始终使用 HTTPS
String baseUrl = "https://runtime.lovrabet.com/openapi";

// ❌ 不要使用 HTTP
// String baseUrl = "http://runtime.lovrabet.com/openapi";

2.3 敏感数据处理

Java
// 在日志中脱敏敏感信息
public void logCustomerInfo(Map<String, Object> customer) {
    String phone = (String) customer.get("TEXT_2");
    // 脱敏手机号:138****1234
    String maskedPhone = phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
    logger.info("客户电话: {}", maskedPhone);
}

3. 性能优化

3.1 分页查询优化

选择合适的 pageSize

Java
// ✅ 推荐:根据实际需求选择合适的 pageSize
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("pageSize", 50);   // 20-100 之间较为合理
paramMap.put("currentPage", 1);

// ❌ 不推荐:pageSize 过小,导致请求次数过多
paramMap.put("pageSize", 5);

// ❌ 不推荐:pageSize 过大,导致单次响应慢、内存占用高
paramMap.put("pageSize", 10000);

避免深度分页

Java
// ❌ 避免查询过深的页码
if (currentPage > 100) {
    logger.warn("页码过深可能影响性能: {}", currentPage);
    // 考虑使用其他方式,如基于游标的分页
}

3.2 批量操作优化

使用线程池并发处理

Java
import java.util.concurrent.*;

/**
 * 批量创建数据 - 使用线程池
 */
public void batchCreate(List<Map<String, Object>> dataList) {
    // 创建固定大小的线程池
    ExecutorService executor = Executors.newFixedThreadPool(10);
    List<Future<Boolean>> futures = new ArrayList<>();

    for (Map<String, Object> data : dataList) {
        Future<Boolean> future = executor.submit(() -> {
            try {
                LovrabetRequest request = buildRequest(data);
                LovrabetResult<String> result = sdkClient.create(request);
                return result.isSuccess();
            } catch (Exception e) {
                logger.error("创建数据失败", e);
                return false;
            }
        });
        futures.add(future);
    }

    // 等待所有任务完成
    int successCount = 0;
    for (Future<Boolean> future : futures) {
        try {
            if (future.get(30, TimeUnit.SECONDS)) {
                successCount++;
            }
        } catch (Exception e) {
            logger.error("获取任务结果失败", e);
        }
    }

    executor.shutdown();
    logger.info("批量创建完成,成功: {}/{}", successCount, dataList.size());
}

控制并发数量

Java
// ✅ 推荐:控制并发数避免过载服务器
ExecutorService executor = Executors.newFixedThreadPool(10);  // 限制 10 个并发

// ❌ 不推荐:无限制并发可能导致服务器过载
// ExecutorService executor = Executors.newCachedThreadPool();

3.3 缓存策略

对于不常变化的数据,使用本地缓存减少 API 调用:

Java
import com.github.benmanes.caffeine.cache.*;

/**
 * 带缓存的数据服务
 */
public class CachedDataService {

    private final LovrabetSDKClient sdkClient;

    // 使用 Caffeine 缓存
    private final Cache<String, Map<String, Object>> cache = Caffeine.newBuilder()
        .maximumSize(1000)                 // 最大缓存 1000 条
        .expireAfterWrite(5, TimeUnit.MINUTES)  // 5 分钟后过期
        .build();

    public Map<String, Object> getData(String id) {
        // 先从缓存获取
        return cache.get(id, key -> {
            // 缓存未命中,从 API 获取
            logger.debug("缓存未命中,从 API 获取数据: {}", id);
            return fetchFromApi(id);
        });
    }

    private Map<String, Object> fetchFromApi(String id) {
        LovrabetRequest request = new LovrabetRequest();
        request.setAppCode(appCode);
        request.setModelCode(modelCode);
        request.setId(Long.parseLong(id));

        LovrabetResult<?> result = sdkClient.getOne(request);

        if (result.isSuccess()) {
            return (Map<String, Object>) result.getData();
        }
        return null;
    }

    // 更新数据时清除缓存
    public void updateData(String id, Map<String, Object> data) {
        // 更新 API
        updateApi(id, data);

        // 清除缓存
        cache.invalidate(id);
    }
}

3.4 连接复用

SDK 内部已经使用 HTTP 连接池,无需额外配置。但要注意:

Java
// ✅ 推荐:复用 SDK 客户端实例
LovrabetSDKClient client = SdkClientManager.getInstance();

// ❌ 不推荐:频繁创建和销毁客户端
for (int i = 0; i < 1000; i++) {
    LovrabetSDKClient client = new LovrabetSDKClient(accessKey, baseUrl);
    // ...
}

4. 错误处理

4.1 统一错误处理

创建统一的错误处理工具类:

Java
package com.example.util;

import com.lovrabet.runtime.opensdk.model.LovrabetResult;

/**
 * SDK 结果处理工具
 */
public class ResultHandler {

    /**
     * 处理结果,失败时抛出异常
     */
    public static <T> T unwrap(LovrabetResult<T> result) {
        if (result == null) {
            throw new LovrabetException("API 响应为空");
        }

        if (result.isSuccess()) {
            return result.getData();
        } else {
            throw new LovrabetException(
                result.getResultCode(),
                result.getResultMsg()
            );
        }
    }

    /**
     * 处理结果,失败时返回默认值
     */
    public static <T> T unwrapOrDefault(LovrabetResult<T> result, T defaultValue) {
        if (result == null || !result.isSuccess()) {
            return defaultValue;
        }
        return result.getData();
    }

    /**
     * 处理结果,失败时返回 Optional
     */
    public static <T> Optional<T> unwrapOptional(LovrabetResult<T> result) {
        if (result != null && result.isSuccess()) {
            return Optional.ofNullable(result.getData());
        }
        return Optional.empty();
    }
}

/**
 * 自定义异常类
 */
public class LovrabetException extends RuntimeException {
    private final String errorCode;

    public LovrabetException(String message) {
        super(message);
        this.errorCode = "UNKNOWN";
    }

    public LovrabetException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }

    public String getErrorCode() {
        return errorCode;
    }
}

使用示例:

Java
// 方式 1: 失败时抛出异常
try {
    Map<String, Object> data = ResultHandler.unwrap(result);
    // 处理数据
} catch (LovrabetException e) {
    logger.error("API 调用失败 [{}]: {}", e.getErrorCode(), e.getMessage());
}

// 方式 2: 失败时使用默认值
List<Map<String, Object>> dataList =
    ResultHandler.unwrapOrDefault(result, Collections.emptyList());

// 方式 3: 使用 Optional
ResultHandler.unwrapOptional(result)
    .ifPresent(data -> processData(data));

4.2 重试机制

对于临时性错误(如网络抖动),实现自动重试:

Java
package com.example.util;

import java.util.function.Supplier;

/**
 * 重试工具类
 */
public class RetryHelper {

    /**
     * 带指数退避的重试
     */
    public static <T> LovrabetResult<T> executeWithRetry(
            Supplier<LovrabetResult<T>> operation,
            int maxRetries,
            long initialDelayMs) {

        int attempt = 0;
        long delay = initialDelayMs;

        while (attempt <= maxRetries) {
            try {
                LovrabetResult<T> result = operation.get();

                // 成功则返回
                if (result != null && result.isSuccess()) {
                    if (attempt > 0) {
                        logger.info("重试成功,尝试次数: {}", attempt + 1);
                    }
                    return result;
                }

                // 已达最大重试次数
                if (attempt == maxRetries) {
                    logger.error("达到最大重试次数 {},操作失败", maxRetries);
                    return result;
                }

                // 判断是否需要重试
                if (!shouldRetry(result)) {
                    logger.warn("错误不可重试,直接返回");
                    return result;
                }

                // 等待后重试
                logger.warn("操作失败,{} ms 后进行第 {} 次重试", delay, attempt + 2);
                Thread.sleep(delay);

                // 指数退避:每次重试间隔翻倍
                delay *= 2;
                attempt++;

            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("重试被中断", e);
            } catch (Exception e) {
                logger.error("执行操作时发生异常", e);
                if (attempt == maxRetries) {
                    throw new RuntimeException("重试后仍然失败", e);
                }
                attempt++;
            }
        }

        throw new RuntimeException("不应该到达这里");
    }

    /**
     * 判断错误是否可重试
     */
    private static <T> boolean shouldRetry(LovrabetResult<T> result) {
        if (result == null) {
            return true;  // 空结果可重试
        }

        String errorCode = result.getResultCode();

        // 以下错误码不应重试
        if ("INVALID_PARAM".equals(errorCode) ||
            "ACCESS_DENIED".equals(errorCode) ||
            "RESOURCE_NOT_FOUND".equals(errorCode)) {
            return false;
        }

        // 其他错误可重试
        return true;
    }
}

使用示例:

Java
// 最多重试 3 次,初始间隔 1000ms,使用指数退避
LovrabetResult<?> result = RetryHelper.executeWithRetry(
    () -> sdkClient.getOne(request),
    3,      // 最多重试 3 次
    1000L   // 初始延迟 1 秒
);

4.3 超时处理

对于长时间运行的操作,设置超时机制:

Java
import java.util.concurrent.*;

/**
 * 带超时的操作执行
 */
public <T> LovrabetResult<T> executeWithTimeout(
        Supplier<LovrabetResult<T>> operation,
        long timeoutSeconds) {

    ExecutorService executor = Executors.newSingleThreadExecutor();

    try {
        Future<LovrabetResult<T>> future = executor.submit(operation::get);
        return future.get(timeoutSeconds, TimeUnit.SECONDS);
    } catch (TimeoutException e) {
        logger.error("操作超时,超过 {} 秒", timeoutSeconds);
        throw new RuntimeException("操作超时", e);
    } catch (Exception e) {
        logger.error("操作执行失败", e);
        throw new RuntimeException("操作失败", e);
    } finally {
        executor.shutdown();
    }
}

5. 日志管理

5.1 结构化日志

使用结构化日志便于后续分析:

Java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DataService {
    private static final Logger logger = LoggerFactory.getLogger(DataService.class);

    public void createData(Map<String, Object> data) {
        long startTime = System.currentTimeMillis();
        String operation = "createData";

        try {
            logger.info("开始操作 - operation={}, dataSize={}",
                operation, data.size());

            LovrabetResult<String> result = sdkClient.create(request);
            long duration = System.currentTimeMillis() - startTime;

            if (result.isSuccess()) {
                logger.info("操作成功 - operation={}, id={}, duration={}ms",
                    operation, result.getData(), duration);
            } else {
                logger.error("操作失败 - operation={}, errorCode={}, errorMsg={}, duration={}ms",
                    operation, result.getResultCode(), result.getResultMsg(), duration);
            }

        } catch (Exception e) {
            long duration = System.currentTimeMillis() - startTime;
            logger.error("操作异常 - operation={}, duration={}ms",
                operation, duration, e);
            throw e;
        }
    }
}

5.2 日志级别

合理使用日志级别:

Java
// DEBUG: 详细的调试信息
logger.debug("请求参数: {}", paramMap);

// INFO: 重要的业务流程信息
logger.info("开始同步客户数据,数量: {}", customers.size());

// WARN: 警告信息,不影响主流程
logger.warn("数据字段 {} 为空,将使用默认值", fieldName);

// ERROR: 错误信息,需要关注
logger.error("创建数据失败: {}", result.getResultMsg());

// ERROR with exception: 异常堆栈
logger.error("处理数据时发生异常", e);

5.3 敏感信息脱敏

Java
/**
 * 日志脱敏工具
 */
public class LogMaskUtil {

    /**
     * 脱敏手机号
     */
    public static String maskPhone(String phone) {
        if (phone == null || phone.length() < 11) {
            return phone;
        }
        return phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
    }

    /**
     * 脱敏邮箱
     */
    public static String maskEmail(String email) {
        if (email == null || !email.contains("@")) {
            return email;
        }
        String[] parts = email.split("@");
        String username = parts[0];
        if (username.length() <= 2) {
            return email;
        }
        String masked = username.substring(0, 2) + "***" +
            username.substring(username.length() - 1);
        return masked + "@" + parts[1];
    }

    /**
     * 脱敏 AccessKey
     */
    public static String maskAccessKey(String accessKey) {
        if (accessKey == null || accessKey.length() < 20) {
            return "***";
        }
        return accessKey.substring(0, 10) + "***";
    }
}

使用示例:

Java
logger.info("客户信息 - 姓名: {}, 电话: {}, 邮箱: {}",
    customer.getName(),
    LogMaskUtil.maskPhone(customer.getPhone()),
    LogMaskUtil.maskEmail(customer.getEmail())
);

6. 测试策略

6.1 单元测试

使用 Mock 框架进行单元测试:

Java
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

public class CustomerServiceTest {

    @Test
    public void testCreateCustomer() {
        // 创建 Mock 对象
        LovrabetSDKClient mockClient = Mockito.mock(LovrabetSDKClient.class);

        // 设置 Mock 行为
        LovrabetResult<String> mockResult = new LovrabetResult<>();
        mockResult.setSuccess(true);
        mockResult.setData("12345");

        Mockito.when(mockClient.create(Mockito.any()))
            .thenReturn(mockResult);

        // 测试
        CustomerService service = new CustomerService(mockClient);
        String id = service.createCustomer(buildTestCustomer());

        // 验证
        assertEquals("12345", id);
        Mockito.verify(mockClient, Mockito.times(1)).create(Mockito.any());
    }
}

6.2 集成测试

Java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class DataServiceIntegrationTest {

    @Autowired
    private DataService dataService;

    @Test
    public void testGetCustomers() {
        List<Map<String, Object>> customers = dataService.getCustomers(1, 10);

        assertNotNull(customers);
        assertTrue(customers.size() > 0);
    }

    @Test
    public void testCreateAndQuery() {
        // 创建测试数据
        Map<String, Object> customerData = buildTestData();
        String id = dataService.createCustomer(customerData);

        assertNotNull(id);

        // 查询验证
        Map<String, Object> retrieved = dataService.getCustomer(id);
        assertEquals(customerData.get("TEXT_1"), retrieved.get("TEXT_1"));
    }
}

6.3 测试数据隔离

Java
/**
 * 测试数据管理
 */
public class TestDataManager {

    private List<String> createdIds = new ArrayList<>();

    /**
     * 创建测试数据
     */
    public String createTestData(LovrabetSDKClient sdkClient) {
        LovrabetRequest request = new LovrabetRequest();
        // 设置测试数据标识
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("TEXT_1", "TEST_" + System.currentTimeMillis());
        request.setParamMap(paramMap);

        LovrabetResult<String> result = sdkClient.create(request);
        if (result.isSuccess()) {
            String id = result.getData();
            createdIds.add(id);  // 记录创建的 ID
            return id;
        }
        return null;
    }

    /**
     * 清理测试数据
     */
    public void cleanup(LovrabetSDKClient sdkClient) {
        for (String id : createdIds) {
            // 清理测试数据的逻辑
            logger.debug("清理测试数据: {}", id);
        }
        createdIds.clear();
    }
}

7. 部署建议

7.1 环境配置

使用不同的配置文件管理不同环境:

YAML
# application-dev.yml (开发环境)
lovrabet:
  access-key: ${LOVRABET_ACCESS_KEY}
  base-url: https://runtime.lovrabet.com/openapi
  app-code: app-dev-xxx

---
# application-prod.yml (生产环境)
lovrabet:
  access-key: ${LOVRABET_ACCESS_KEY}
  base-url: https://runtime.lovrabet.com/openapi
  app-code: app-prod-xxx

7.2 健康检查

实现健康检查端点:

Java
@RestController
public class HealthController {

    private final LovrabetSDKClient sdkClient;

    @GetMapping("/health/lovrabet")
    public Map<String, Object> checkLovrabetHealth() {
        Map<String, Object> health = new HashMap<>();

        try {
            // 尝试调用一个简单的 API
            LovrabetRequest request = new LovrabetRequest();
            request.setAppCode(appCode);

            long startTime = System.currentTimeMillis();
            LovrabetResult<?> result = sdkClient.getDatasetCodeList(request);
            long duration = System.currentTimeMillis() - startTime;

            health.put("status", result.isSuccess() ? "UP" : "DOWN");
            health.put("responseTime", duration + "ms");
            health.put("message", result.getResultMsg());

        } catch (Exception e) {
            health.put("status", "DOWN");
            health.put("error", e.getMessage());
        }

        return health;
    }
}

7.3 监控指标

使用 Micrometer 收集监控指标:

Java
import io.micrometer.core.instrument.*;

@Service
public class MonitoredDataService {

    private final LovrabetSDKClient sdkClient;
    private final MeterRegistry meterRegistry;
    private final Counter successCounter;
    private final Counter failureCounter;
    private final Timer responseTimer;

    public MonitoredDataService(
            LovrabetSDKClient sdkClient,
            MeterRegistry meterRegistry) {
        this.sdkClient = sdkClient;
        this.meterRegistry = meterRegistry;

        // 注册指标
        this.successCounter = Counter.builder("lovrabet.api.success")
            .description("API 调用成功次数")
            .register(meterRegistry);

        this.failureCounter = Counter.builder("lovrabet.api.failure")
            .description("API 调用失败次数")
            .register(meterRegistry);

        this.responseTimer = Timer.builder("lovrabet.api.response.time")
            .description("API 响应时间")
            .register(meterRegistry);
    }

    public Map<String, Object> getData(String id) {
        return responseTimer.record(() -> {
            LovrabetResult<?> result = sdkClient.getOne(buildRequest(id));

            if (result.isSuccess()) {
                successCounter.increment();
                return (Map<String, Object>) result.getData();
            } else {
                failureCounter.increment();
                return null;
            }
        });
    }
}

7.4 容器化部署

Dockerfile 示例:

Dockerfile
FROM openjdk:11-jre-slim

# 设置工作目录
WORKDIR /app

# 复制 jar 文件
COPY target/myapp.jar app.jar

# 设置环境变量(生产环境应通过 Secret 管理)
ENV LOVRABET_ACCESS_KEY=""

# 暴露端口
EXPOSE 8080

# 启动应用
ENTRYPOINT ["java", "-jar", "app.jar"]

Kubernetes 部署配置:

YAML
apiVersion: v1
kind: Secret
metadata:
  name: lovrabet-secret
type: Opaque
data:
  access-key: <base64-encoded-access-key>

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:latest
        env:
        - name: LOVRABET_ACCESS_KEY
          valueFrom:
            secretKeyRef:
              name: lovrabet-secret
              key: access-key
        ports:
        - containerPort: 8080
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health/lovrabet
            port: 8080
          initialDelaySeconds: 20
          periodSeconds: 5

总结

遵循以上最佳实践,您可以构建高性能、高可用、安全的生产级应用:

  1. 客户端管理 - 使用单例模式复用客户端
  2. 安全性 - 妥善管理 AccessKey,避免硬编码
  3. 性能优化 - 合理分页、使用缓存、控制并发
  4. 错误处理 - 统一错误处理、实现重试机制
  5. 日志管理 - 结构化日志、敏感信息脱敏
  6. 测试策略 - 完善的单元测试和集成测试
  7. 部署建议 - 健康检查、监控指标、容器化部署

下一步

掌握最佳实践后,建议按以下顺序继续学习

  1. 📖 快速开始5 分钟完成第一个 CRUD 程序
  2. 📚 核心概念深入理解 SDK 工作原理
  3. 📋 API 参考查阅完整的接口文档
  4. 💡 业务示例学习 5 个真实场景的实现
  5. 🚀 最佳实践 ← 当前位置掌握生产环境优化技巧
  6. 常见问题 ← 推荐下一步解决使用中的疑问

需要帮助? 访问 常见问题 或联系 service@lovrabet.com

基于飞书知识库同步生成,内容以飞书源文档为准