跳到主要内容

最佳实践

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


目录


1. 客户端管理

1.1 使用单例模式

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

✅ 推荐做法

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;
}
}

❌ 不推荐做法

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

1.2 Spring Boot 环境下的配置

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

@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);
}
}
@Service
public class DataService {

private final LovrabetSDKClient sdkClient;

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

2. 安全性

2.1 AccessKey 管理

✅ 推荐做法

方式 1: 使用环境变量

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

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

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

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

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

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

❌ 不推荐做法

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

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

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

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

2.2 数据传输安全

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

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

2.3 敏感数据处理

// 在日志中脱敏敏感信息
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

// ✅ 推荐:根据实际需求选择合适的 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);

避免深度分页

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

3.2 批量操作优化

使用线程池并发处理

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());
}

控制并发数量

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

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

3.3 缓存策略

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

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 连接池,无需额外配置。但要注意:

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

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

4. 错误处理

4.1 统一错误处理

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

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;
}
}

使用示例:

// 方式 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 重试机制

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

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;
}
}

使用示例:

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

4.3 超时处理

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

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 结构化日志

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

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 日志级别

合理使用日志级别:

// 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 敏感信息脱敏

/**
* 日志脱敏工具
*/
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) + "***";
}
}

使用示例:

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

6. 测试策略

6.1 单元测试

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

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 集成测试

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 测试数据隔离

/**
* 测试数据管理
*/
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 环境配置

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

# 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 健康检查

实现健康检查端点:

@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 收集监控指标:

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 示例:

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 部署配置:

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