Java调用亚马逊商品详情API接口完全指南

一、接口选型与适用场景

亚马逊为开发者提供两套官方API体系,Java开发者需根据业务场景选择:

API类型 认证方式 数据深度 适用对象 调用频率限制
Product Advertising API 5.0 (PA-API) AWS Signature V4 基础商品信息、价格、图片、推广链接 联盟营销伙伴、比价网站 免费层1000次/天
Selling Partner API (SP-API) OAuth 2.0 + AWS Signature V4 完整销售数据、库存、订单、COSMO意图标签 注册卖家、ERP开发商 动态限流(通常1-2秒/次)

本文重点:PA-API 5.0的Java原生实现与SP-API的Spring Boot集成方案。


二、PA-API 5.0 Java原生开发

2.1 环境准备

Maven依赖配置

XML 复制代码
<dependencies>
    <!-- AWS SDK for Signature V4 -->
    <dependency>
        <groupId>software.amazon.awssdk</groupId>
        <artifactId>auth</artifactId>
        <version>2.21.0</version>
    </dependency>
    
    <!-- HTTP客户端 -->
    <dependency>
        <groupId>org.apache.httpcomponents.client5</groupId>
        <artifactId>httpclient5</artifactId>
        <version>5.2.1</version>
    </dependency>
    
    <!-- JSON处理 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.2</version>
    </dependency>
    
    <!-- XML解析(PA-API默认返回XML) -->
    <dependency>
        <groupId>org.dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>2.1.4</version>
    </dependency>
</dependencies>

2.2 核心实现:AWS Signature V4签名

PA-API 5.0强制使用AWS Signature Version 4认证,以下是完整的Java签名实现:

java 复制代码
package com.example.amazon.api;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * AWS Signature V4 签名生成器
 * 适用于Amazon PA-API 5.0和SP-API
 */
public class AwsSignatureV4Generator {
    
    private static final String ALGORITHM = "AWS4-HMAC-SHA256";
    private static final String SERVICE = "ProductAdvertisingAPI"; // PA-API服务名
    
    /**
     * 生成完整签名信息
     */
    public static SignatureResult generateSignature(
            String method,           // HTTP方法: GET/POST
            String uri,              // 请求路径: /paapi5/searchitems
            Map<String, String> queryParams, // URL参数
            String body,             // 请求体(JSON)
            String accessKey,        // AWS Access Key
            String secretKey,        // AWS Secret Key
            String region,           // 区域: us-east-1
            String host              // 主机: webservices.amazon.com
    ) throws Exception {
        
        // 1. 生成时间戳
        ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
        String amzDate = now.format(DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'"));
        String dateStamp = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
        
        // 2. 创建规范请求(Canonical Request)
        String canonicalUri = uri;
        String canonicalQueryString = createCanonicalQueryString(queryParams);
        String canonicalHeaders = "host:" + host + "\n" + "x-amz-date:" + amzDate + "\n";
        String signedHeaders = "host;x-amz-date";
        
        // 计算请求体哈希
        String payloadHash = sha256Hash(body);
        
        String canonicalRequest = method + "\n" +
                canonicalUri + "\n" +
                canonicalQueryString + "\n" +
                canonicalHeaders + "\n" +
                signedHeaders + "\n" +
                payloadHash;
        
        // 3. 创建待签名字符串(String to Sign)
        String credentialScope = dateStamp + "/" + region + "/" + SERVICE + "/aws4_request";
        String stringToSign = ALGORITHM + "\n" +
                amzDate + "\n" +
                credentialScope + "\n" +
                sha256Hash(canonicalRequest);
        
        // 4. 计算签名
        byte[] signingKey = getSignatureKey(secretKey, dateStamp, region, SERVICE);
        String signature = hmacSha256Hex(signingKey, stringToSign);
        
        // 5. 构建Authorization头
        String authorizationHeader = ALGORITHM + " " +
                "Credential=" + accessKey + "/" + credentialScope + ", " +
                "SignedHeaders=" + signedHeaders + ", " +
                "Signature=" + signature;
        
        return new SignatureResult(authorizationHeader, amzDate, signature);
    }
    
    /**
     * 创建规范查询字符串
     */
    private static String createCanonicalQueryString(Map<String, String> params) {
        if (params == null || params.isEmpty()) return "";
        
        return params.entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .map(e -> urlEncode(e.getKey()) + "=" + urlEncode(e.getValue()))
                .collect(Collectors.joining("&"));
    }
    
    /**
     * URL编码(符合AWS规范)
     */
    private static String urlEncode(String value) {
        return java.net.URLEncoder.encode(value, StandardCharsets.UTF_8)
                .replace("+", "%20")
                .replace("*", "%2A")
                .replace("%7E", "~");
    }
    
    /**
     * SHA256哈希
     */
    private static String sha256Hash(String data) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hash = digest.digest(data.getBytes(StandardCharsets.UTF_8));
        return bytesToHex(hash);
    }
    
    /**
     * 获取签名密钥
     */
    private static byte[] getSignatureKey(String key, String dateStamp, String regionName, String serviceName) throws Exception {
        byte[] kSecret = ("AWS4" + key).getBytes(StandardCharsets.UTF_8);
        byte[] kDate = hmacSha256(kSecret, dateStamp);
        byte[] kRegion = hmacSha256(kDate, regionName);
        byte[] kService = hmacSha256(kRegion, serviceName);
        return hmacSha256(kService, "aws4_request");
    }
    
    /**
     * HMAC-SHA256计算
     */
    private static byte[] hmacSha256(byte[] key, String data) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(key, "HmacSHA256"));
        return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
    }
    
    private static String hmacSha256Hex(byte[] key, String data) throws Exception {
        return bytesToHex(hmacSha256(key, data));
    }
    
    private static String bytesToHex(byte[] bytes) {
        StringBuilder result = new StringBuilder();
        for (byte b : bytes) {
            result.append(String.format("%02x", b));
        }
        return result.toString();
    }
    
    /**
     * 签名结果封装
     */
    public static class SignatureResult {
        public final String authorizationHeader;
        public final String amzDate;
        public final String signature;
        
        public SignatureResult(String authHeader, String amzDate, String signature) {
            this.authorizationHeader = authHeader;
            this.amzDate = amzDate;
            this.signature = signature;
        }
    }
}

2.3 商品详情查询实现(GetItems)

java 复制代码
package com.example.amazon.api;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.io.entity.StringEntity;

import java.util.*;

/**
 * Amazon PA-API 5.0 商品详情客户端
 */
public class AmazonProductClient {
    
    private final String accessKey;
    private final String secretKey;
    private final String partnerTag;
    private final String marketplaceId;
    private final String region;
    private final String host;
    private final ObjectMapper objectMapper;
    
    public AmazonProductClient(String accessKey, String secretKey, String partnerTag, 
                               String marketplaceId, String region) {
        this.accessKey = accessKey;
        this.secretKey = secretKey;
        this.partnerTag = partnerTag;
        this.marketplaceId = marketplaceId;
        this.region = region;
        this.host = "webservices.amazon." + getDomainSuffix(marketplaceId);
        this.objectMapper = new ObjectMapper();
    }
    
    /**
     * 根据ASIN获取商品详情
     */
    public ProductDetail getProductByAsin(String asin) throws Exception {
        // 构建请求体
        ObjectNode requestBody = objectMapper.createObjectNode();
        requestBody.put("ItemIds", asin);
        requestBody.put("ItemIdType", "ASIN");
        requestBody.put("Resources", 
            "Images.Primary.Medium,Images.Variants.Small," +
            "ItemInfo.Title,ItemInfo.Features,ItemInfo.ProductInfo," +
            "Offers.Listings.Price,Offers.Listings.SavingBasis," +
            "CustomerReviews.StarRating");
        requestBody.put("PartnerTag", partnerTag);
        requestBody.put("PartnerType", "Associates");
        requestBody.put("Marketplace", marketplaceId);
        
        String jsonBody = requestBody.toString();
        
        // 生成签名
        AwsSignatureV4Generator.SignatureResult signature = 
            AwsSignatureV4Generator.generateSignature(
                "POST",
                "/paapi5/getitems",
                null,
                jsonBody,
                accessKey,
                secretKey,
                region,
                host
            );
        
        // 发送请求
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            HttpPost httpPost = new HttpPost("https://" + host + "/paapi5/getitems");
            
            // 设置请求头
            httpPost.setHeader("Content-Type", "application/json; charset=UTF-8");
            httpPost.setHeader("X-Amz-Date", signature.amzDate);
            httpPost.setHeader("Authorization", signature.authorizationHeader);
            httpPost.setHeader("X-Amz-Target", "com.amazon.paapi5.v1.ProductAdvertisingAPIv1.GetItems");
            
            // 设置请求体
            httpPost.setEntity(new StringEntity(jsonBody, ContentType.APPLICATION_JSON));
            
            try (CloseableHttpResponse response = client.execute(httpPost)) {
                int statusCode = response.getCode();
                String responseBody = new String(response.getEntity().getContent().readAllBytes());
                
                if (statusCode == 200) {
                    return parseProductDetail(responseBody, asin);
                } else {
                    throw new RuntimeException("API请求失败: " + statusCode + ", " + responseBody);
                }
            }
        }
    }
    
    /**
     * 解析商品详情
     */
    private ProductDetail parseProductDetail(String jsonResponse, String asin) throws Exception {
        JsonNode root = objectMapper.readTree(jsonResponse);
        JsonNode itemsResult = root.path("ItemsResult");
        JsonNode items = itemsResult.path("Items");
        
        if (items.isArray() && items.size() > 0) {
            JsonNode item = items.get(0);
            
            ProductDetail detail = new ProductDetail();
            detail.setAsin(asin);
            
            // 解析标题
            JsonNode titleNode = item.path("ItemInfo").path("Title").path("DisplayValue");
            detail.setTitle(titleNode.asText());
            
            // 解析价格
            JsonNode priceNode = item.path("Offers").path("Listings").get(0).path("Price").path("DisplayAmount");
            detail.setPrice(priceNode.asText());
            
            // 解析图片
            JsonNode imageNode = item.path("Images").path("Primary").path("Medium").path("URL");
            detail.setMainImage(imageNode.asText());
            
            // 解析评分
            JsonNode ratingNode = item.path("CustomerReviews").path("StarRating").path("Value");
            detail.setRating(ratingNode.asDouble());
            
            // 解析特性
            List<String> features = new ArrayList<>();
            JsonNode featuresNode = item.path("ItemInfo").path("Features").path("DisplayValues");
            if (featuresNode.isArray()) {
                featuresNode.forEach(f -> features.add(f.asText()));
            }
            detail.setFeatures(features);
            
            return detail;
        }
        
        throw new RuntimeException("未找到商品: " + asin);
    }
    
    private String getDomainSuffix(String marketplaceId) {
        Map<String, String> domainMap = new HashMap<>();
        domainMap.put("ATVPDKIKX0DER", "com");    // 美国
        domainMap.put("A1F83G8C2ARO7P", "co.uk");  // 英国
        domainMap.put("A1VC38T7YXB528", "co.jp");  // 日本
        domainMap.put("A13V1IB3VIYZZH", "fr");     // 法国
        // ... 其他站点
        
        return domainMap.getOrDefault(marketplaceId, "com");
    }
    
    /**
     * 商品详情实体类
     */
    public static class ProductDetail {
        private String asin;
        private String title;
        private String price;
        private String mainImage;
        private double rating;
        private List<String> features;
        
        // Getters and Setters
        public String getAsin() { return asin; }
        public void setAsin(String asin) { this.asin = asin; }
        public String getTitle() { return title; }
        public void setTitle(String title) { this.title = title; }
        public String getPrice() { return price; }
        public void setPrice(String price) { this.price = price; }
        public String getMainImage() { return mainImage; }
        public void setMainImage(String mainImage) { this.mainImage = mainImage; }
        public double getRating() { return rating; }
        public void setRating(double rating) { this.rating = rating; }
        public List<String> getFeatures() { return features; }
        public void setFeatures(List<String> features) { this.features = features; }
        
        @Override
        public String toString() {
            return String.format("ProductDetail{asin='%s', title='%s', price='%s', rating=%.1f}", 
                asin, title, price, rating);
        }
    }
}

2.4 关键词搜索实现(SearchItems)

java 复制代码
/**
 * 关键词搜索商品
 */
public List<ProductDetail> searchByKeyword(String keyword, String searchIndex, int itemCount) throws Exception {
    ObjectNode requestBody = objectMapper.createObjectNode();
    requestBody.put("Keywords", keyword);
    requestBody.put("SearchIndex", searchIndex != null ? searchIndex : "All");
    requestBody.put("ItemCount", Math.min(itemCount, 20)); // 最大20
    requestBody.put("PartnerTag", partnerTag);
    requestBody.put("PartnerType", "Associates");
    requestBody.put("Marketplace", marketplaceId);
    requestBody.put("Resources", 
        "Images.Primary.Medium,ItemInfo.Title,Offers.Listings.Price," +
        "CustomerReviews.StarRating,BrowseNodeInfo.BrowseNodes");
    
    // 排序方式:Relevance | PriceLowToHigh | PriceHighToLow | SalesRank
    requestBody.put("SortBy", "SalesRank");
    
    String jsonBody = requestBody.toString();
    
    AwsSignatureV4Generator.SignatureResult signature = 
        AwsSignatureV4Generator.generateSignature(
            "POST",
            "/paapi5/searchitems",
            null,
            jsonBody,
            accessKey,
            secretKey,
            region,
            host
        );
    
    // HTTP请求发送逻辑与GetItems类似...
    // 解析返回的Items数组
}

三、SP-API Java Spring Boot集成

SP-API面向注册卖家,使用OAuth 2.0 + AWS Signature V4双重认证。

3.1 Spring Boot Starter配置

java 复制代码
package com.example.amazon.spapi.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationProperties(prefix = "amazon.spapi")
public class SpApiConfig {
    private String clientId;
    private String clientSecret;
    private String refreshToken;
    private String awsAccessKey;
    private String awsSecretKey;
    private String region; // na/eu/fe
    private String marketplaceId;
    
    // Getters and Setters...
}

3.2 令牌管理服务

java 复制代码
package com.example.amazon.spapi.auth;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Instant;
import java.util.Base64;

@Service
public class SpApiTokenService {
    
    private final SpApiConfig config;
    private final ObjectMapper objectMapper;
    private String accessToken;
    private Instant tokenExpiry;
    
    public synchronized String getAccessToken() throws Exception {
        if (accessToken != null && Instant.now().isBefore(tokenExpiry.minusSeconds(300))) {
            return accessToken; // 提前5分钟刷新
        }
        
        // 构建OAuth请求
        String authString = config.getClientId() + ":" + config.getClientSecret();
        String encodedAuth = Base64.getEncoder().encodeToString(authString.getBytes());
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.amazon.com/auth/o2/token"))
            .header("Content-Type", "application/x-www-form-urlencoded")
            .header("Authorization", "Basic " + encodedAuth)
            .POST(HttpRequest.BodyPublishers.ofString(
                "grant_type=refresh_token&refresh_token=" + config.getRefreshToken()
            ))
            .build();
        
        HttpClient client = HttpClient.newHttpClient();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        
        JsonNode json = objectMapper.readTree(response.body());
        accessToken = json.get("access_token").asText();
        int expiresIn = json.get("expires_in").asInt();
        tokenExpiry = Instant.now().plusSeconds(expiresIn);
        
        return accessToken;
    }
}

3.3 SP-API商品目录客户端

java 复制代码
package com.example.amazon.spapi.client;

import com.example.amazon.spapi.auth.SpApiTokenService;
import com.example.amazon.spapi.config.SpApiConfig;
import org.springframework.stereotype.Component;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

@Component
public class SpApiCatalogClient {
    
    private final SpApiTokenService tokenService;
    private final SpApiConfig config;
    private final HttpClient httpClient;
    
    public String getCatalogItem(String asin) throws Exception {
        String accessToken = tokenService.getAccessToken();
        String endpoint = getEndpoint(config.getRegion());
        
        // 构建请求
        String path = "/catalog/2022-04-01/items/" + asin;
        String query = "marketplaceIds=" + config.getMarketplaceId() + 
                      "&includedData=summaries,images,salesRanks";
        
        // 生成AWS签名(使用访问令牌而非Refresh Token进行签名)
        String signedRequest = signRequest("GET", path + "?" + query, "", accessToken);
        
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://" + endpoint + path + "?" + query))
            .header("x-amz-access-token", accessToken)
            .header("x-amz-date", getAmzDate())
            .header("Authorization", signedRequest)
            .header("Content-Type", "application/json")
            .GET()
            .build();
        
        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        
        if (response.statusCode() == 200) {
            return response.body();
        } else {
            throw new RuntimeException("SP-API请求失败: " + response.statusCode() + " - " + response.body());
        }
    }
    
    private String signRequest(String method, String canonicalUri, String payload, String accessToken) throws Exception {
        // SP-API签名逻辑与PA-API类似,但需包含x-amz-access-token头
        // 具体实现参考AWS文档...
        return "AWS4-HMAC-SHA256 Credential=...";
    }
    
    private String getEndpoint(String region) {
        switch (region) {
            case "na": return "sellingpartnerapi-na.amazon.com";
            case "eu": return "sellingpartnerapi-eu.amazon.com";
            case "fe": return "sellingpartnerapi-fe.amazon.com";
            default: throw new IllegalArgumentException("未知区域: " + region);
        }
    }
    
    private String getAmzDate() {
        return ZonedDateTime.now(ZoneOffset.UTC)
            .format(DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'"));
    }
}

四、完整调用示例

java 复制代码
package com.example.amazon.demo;

import com.example.amazon.api.AmazonProductClient;
import com.example.amazon.api.AwsSignatureV4Generator;

public class AmazonApiDemo {
    public static void main(String[] args) {
        // PA-API配置
        String accessKey = "YOUR_AWS_ACCESS_KEY";
        String secretKey = "YOUR_AWS_SECRET_KEY";
        String partnerTag = "yourtag-20";
        String marketplaceId = "ATVPDKIKX0DER"; // 美国站
        
        AmazonProductClient client = new AmazonProductClient(
            accessKey, secretKey, partnerTag, marketplaceId, "us-east-1"
        );
        
        try {
            // 1. 查询单个商品详情
            System.out.println("=== 查询单个商品 ===");
            AmazonProductClient.ProductDetail product = 
                client.getProductByAsin("B08N5WRWNW");
            System.out.println(product);
            
            // 2. 关键词搜索
            System.out.println("\n=== 关键词搜索 ===");
            var products = client.searchByKeyword("wireless headphones", "Electronics", 10);
            products.forEach(p -> System.out.println(p.getTitle() + " - " + p.getPrice()));
            
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

五、异常处理与最佳实践

5.1 常见异常处理

复制代码
/**
 * 亚马逊API异常分类处理
 */
public class AmazonApiExceptionHandler {
    
    public static void handleException(Exception e) {
        if (e.getMessage().contains("InvalidSignature")) {
            // 签名错误:检查系统时间是否同步(AWS要求时间误差<5分钟)
            System.err.println("签名验证失败,请检查:1)AccessKey/SecretKey 2)系统时间同步");
        } else if (e.getMessage().contains("Throttling")) {
            // 限流错误:实现指数退避重试
            System.err.println("请求过于频繁,请降低调用频率");
        } else if (e.getMessage().contains("InvalidParameterValue")) {
            // 参数错误
            System.err.println("请求参数错误,请检查ASIN或关键词格式");
        } else if (e.getMessage().contains("AccessDenied")) {
            // 权限不足
            System.err.println("权限不足,请确认已申请相应API权限");
        }
    }
}

5.2 性能优化建议

优化项 实现方案
连接池 使用Apache HttpClient 5的连接池管理,避免频繁创建连接
异步调用 使用CompletableFuture实现批量ASIN并发查询
本地缓存 对类目数据、汇率等静态信息使用Caffeine缓存
请求合并 使用PA-API的批量查询接口(最多10个ASIN/次)减少API调用次数

5.3 安全规范

  1. 密钥管理:使用AWS Secrets Manager或Spring Cloud Config加密存储密钥

  2. HTTPS强制:所有请求必须使用TLS 1.2+

  3. 日志脱敏 :禁止在日志中打印SecretKeyRefreshToken等敏感信息

相关推荐
不光头强2 小时前
jwt学习
java·大数据·学习
lsx2024062 小时前
PostgreSQL中的NULL处理
开发语言
凸头2 小时前
美团Leaf发号器
java
是梦终空1162 小时前
模板编译期机器学习
开发语言·c++·算法
SmartBrain2 小时前
基于 Spring AI 构建多智能体协作系统(高级版)
java·人工智能·spring
艾莉丝努力练剑2 小时前
文件描述符fd:跨进程共享机制
java·linux·运维·服务器·开发语言·c++
工藤新一¹2 小时前
《操作系统》第一章(1)
java·服务器·前端
nimadan122 小时前
**豆包seed写剧本2025指南,AI编剧工具实战应用解析**
人工智能·python
沉下去,苦磨练!2 小时前
python的if __name__ == ‘__main__‘
python