# Java 语言实现示例

本文档提供一个完整的 Java 语言实现示例，演示支付宝订阅产品的接入。

## 项目结构

```
src/main/java/com/alipay/subscription/demo/
├── AlipaySubscriptionDemo.java    # 主程序入口
├── client/
│   └── AlipayClient.java          # API客户端（签名、请求）
├── config/
│   └── AlipayConfig.java          # 配置类
├── model/
│   ├── ApiResponse.java           # API响应
│   ├── Customer.java              # 客户模型
│   ├── Price.java                 # 价格模型
│   ├── Product.java               # 商品模型
│   └── Subscription.java          # 订阅模型
├── service/
│   └── SubscriptionService.java   # 订阅服务
└── util/
    └── SignUtil.java              # RSA2签名工具
```

## 核心代码

### 1. Maven依赖

```xml
<dependencies>
    <!-- JSON处理 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.2</version>
    </dependency>

    <!-- HTTP客户端 -->
    <dependency>
        <groupId>com.squareup.okhttp3</groupId>
        <artifactId>okhttp</artifactId>
        <version>4.11.0</version>
    </dependency>
</dependencies>
```

### 2. 支付宝客户端

```java
public class AlipayClient {

    private final AlipayConfig config;
    private final OkHttpClient httpClient;
    private final ObjectMapper objectMapper;

    public AlipayClient(AlipayConfig config) {
        this.config = config;
        this.httpClient = new OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .build();
        this.objectMapper = new ObjectMapper();
        this.objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
        this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 关键配置：忽略null值，避免支付宝API报参数类型错误
        this.objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    }

    /**
     * 执行API请求
     * 重要：公共参数必须放在 URL Query 中，biz_content 放在 POST Body 中
     */
    public <T> ApiResponse<T> execute(String method, Object bizContent,
                                       String responseKey, Class<T> responseType) {
        try {
            // 1. 构建公共请求参数（放在URL Query中）
            Map<String, String> publicParams = new HashMap<>();
            publicParams.put("app_id", config.getAppId());
            publicParams.put("method", method);
            publicParams.put("format", "JSON");
            publicParams.put("charset", "UTF-8");  // 必须在URL中！
            publicParams.put("sign_type", "RSA2");
            publicParams.put("timestamp", LocalDateTime.now()
                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            publicParams.put("version", "1.0");

            // 2. 序列化业务参数
            String bizContentJson = objectMapper.writeValueAsString(bizContent);

            // 3. 构建完整参数用于签名（包含biz_content）
            Map<String, String> allParams = new HashMap<>(publicParams);
            allParams.put("biz_content", bizContentJson);

            // 4. 生成签名
            String sign = SignUtil.generateSign(allParams, config.getPrivateKey());
            publicParams.put("sign", sign);

            // 5. 构建URL（公共参数在Query中）
            HttpUrl.Builder urlBuilder = HttpUrl.parse(config.getGatewayUrl()).newBuilder();
            for (Map.Entry<String, String> entry : publicParams.entrySet()) {
                urlBuilder.addQueryParameter(entry.getKey(), entry.getValue());
            }
            String url = urlBuilder.build().toString();

            // 6. 构建POST Body（仅包含biz_content）
            FormBody.Builder formBuilder = new FormBody.Builder();
            formBuilder.add("biz_content", bizContentJson);

            // 7. 发送请求
            Request request = new Request.Builder()
                    .url(url)
                    .post(formBuilder.build())
                    .build();

            try (Response response = httpClient.newCall(request).execute()) {
                String responseBody = response.body().string();
                return parseResponse(responseBody, responseKey, responseType);
            }
        } catch (Exception e) {
            throw new RuntimeException("API调用失败: " + e.getMessage(), e);
        }
    }
}
```

### 3. RSA2 签名实现

```java
public class SignUtil {

    private static final String SIGN_ALGORITHM = "SHA256withRSA";
    private static final String KEY_ALGORITHM = "RSA";

    /**
     * 生成签名
     */
    public static String generateSign(Map<String, String> params, String privateKeyPem) {
        try {
            // 1. 构建待签名字符串（排除sign参数，按key ASCII排序）
            String signContent = params.entrySet().stream()
                    .filter(e -> e.getKey() != null && e.getValue() != null)
                    .filter(e -> !"sign".equals(e.getKey()))
                    .filter(e -> !e.getValue().isEmpty())
                    .sorted(Map.Entry.comparingByKey())
                    .map(e -> e.getKey() + "=" + e.getValue())
                    .collect(Collectors.joining("&"));

            // 2. 使用私钥签名
            PrivateKey key = parsePrivateKey(privateKeyPem);
            Signature signature = Signature.getInstance(SIGN_ALGORITHM);
            signature.initSign(key);
            signature.update(signContent.getBytes("UTF-8"));
            byte[] signBytes = signature.sign();

            // 3. Base64编码
            return Base64.getEncoder().encodeToString(signBytes);
        } catch (Exception e) {
            throw new RuntimeException("生成签名失败: " + e.getMessage(), e);
        }
    }

    /**
     * 解析PEM格式私钥
     */
    private static PrivateKey parsePrivateKey(String privateKeyPem) throws Exception {
        String cleanKey = privateKeyPem
                .replace("-----BEGIN RSA PRIVATE KEY-----", "")
                .replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END RSA PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replaceAll("\\s+", "");

        byte[] keyBytes = Base64.getDecoder().decode(cleanKey);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
        return keyFactory.generatePrivate(keySpec);
    }
}
```

### 4. 商品模型（注意null值处理）

```java
public class Product {

    private String name;
    private String description;

    @JsonProperty("marketing_features")
    private String marketingFeatures;

    // 响应字段
    @JsonProperty("product_id")
    private String productId;

    // 注意：不要同时定义 marketingFeatures 和 marketingFeaturesJson
    // 会导致Jackson序列化时属性冲突
}
```

### 5. 订阅服务

```java
public class SubscriptionService {

    private final AlipayClient alipayClient;

    /**
     * 创建商品
     */
    public String createProduct(String name, String description) {
        Product product = new Product(name, description);

        ApiResponse<ProductCreateResponse> response = alipayClient.execute(
                "alipay.trade.product.create",
                product,
                "alipay_trade_product_create_response",
                ProductCreateResponse.class
        );

        if (response.isSuccess()) {
            return response.getData().getProductId();
        }
        throw new RuntimeException("商品创建失败: " + response.getSubMsg());
    }

    /**
     * 创建价格
     */
    public String createPrice(String productId, long amount, String interval, int intervalCount) {
        Price price = new Price(productId, amount, new Price.Recurring(interval, intervalCount));

        ApiResponse<PriceCreateResponse> response = alipayClient.execute(
                "alipay.trade.price.create",
                price,
                "alipay_trade_price_create_response",
                PriceCreateResponse.class
        );

        if (response.isSuccess()) {
            return response.getData().getPriceId();
        }
        throw new RuntimeException("价格创建失败: " + response.getSubMsg());
    }

    /**
     * 创建客户
     */
    public String createCustomer(String name, String phone, String email) {
        Customer customer = new Customer(name, phone, email);

        ApiResponse<CustomerCreateResponse> response = alipayClient.execute(
                "alipay.trade.customer.create",
                customer,
                "alipay_trade_customer_create_response",
                CustomerCreateResponse.class
        );

        if (response.isSuccess()) {
            return response.getData().getCustomerId();
        }
        throw new RuntimeException("客户创建失败: " + response.getSubMsg());
    }

    /**
     * 创建订阅
     */
    public SubscriptionInfo createSubscription(String customerId, String priceId, String title) {
        Subscription subscription = new Subscription(
                customerId,
                Collections.singletonList(new Subscription.SubscriptionItem(priceId)),
                title
        );

        ApiResponse<SubscriptionCreateResponse> response = alipayClient.execute(
                "alipay.trade.subscription.create",
                subscription,
                "alipay_trade_subscription_create_response",
                SubscriptionCreateResponse.class
        );

        if (response.isSuccess()) {
            SubscriptionCreateResponse data = response.getData();
            return new SubscriptionInfo(
                    data.getSubscriptionId(),
                    data.getPayAmount(),
                    data.getAlipaySchema()
            );
        }
        throw new RuntimeException("订阅创建失败: " + response.getSubMsg());
    }
}
```

## 环境配置

### 预发环境

```java
AlipayConfig config = new AlipayConfig();
config.setAppId("2021005159652075");
config.setPrivateKey("MIIEvgIBADANBgkqhkiG9w0BAQEFA...");
config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...");
config.setGatewayUrl("https://openapipre.alipay.com/gateway.do");
config.setMockMode(false);
```

### Dev环境

```java
AlipayConfig config = new AlipayConfig();
config.setAppId("2021008156661106");
config.setPrivateKey("MIIEvwIBADANBgkqhkiG9w0BAQEFA...");
config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...");
config.setGatewayUrl("http://openapi.stable.alipay.net/gateway.do");
config.setMockMode(false);
```

## 常见问题

### 1. null值导致参数类型错误

**错误**：`参数[metadata]错误, 原因: 参数类型不匹配，期望类型为[string]，实际传入类型为[null]`

**解决**：配置ObjectMapper忽略null值
```java
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
```

### 2. Jackson属性冲突

**错误**：`Conflicting getter definitions for property "marketing_features"`

**解决**：不要同时定义多个字段映射到同一个JSON属性名

### 3. charset参数位置错误

**错误**：`签名不正确，charset参数应该在URL查询字符串中`

**解决**：公共参数放在URL Query中，biz_content放在POST Body中

## 运行示例

```bash
# 编译
mvn clean compile

# 运行
mvn exec:java -Dexec.mainClass="com.alipay.subscription.demo.AlipaySubscriptionDemo"
```

## 完整代码

完整代码示例请参考：[demo/java](https://github.com/example/alipay-subscription-demo/tree/main/java)