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

相关推荐
沐知全栈开发4 小时前
ionic 手势事件详解
开发语言
用户8356290780514 小时前
用 Python 轻松在 Excel 工作表中应用条件格式
后端·python
red1giant_star4 小时前
Python根据文件后缀统计文件大小、找出文件位置(仿Everything)
后端·python
雷欧力4 小时前
如何使用 Claude API?3 种接入方案实测,附完整代码(2026)
python·claude
lsx2024064 小时前
Bootstrap 按钮
开发语言
qinqinzhang4 小时前
Java 中的 IoC、AOP、MVC
java
神仙别闹4 小时前
基于 Python 实现 BERT 的情感分析模型
开发语言·python·bert
禾叙_4 小时前
【langchain4j】结构化输出(六)
java·开发语言
NQBJT4 小时前
VS Code配置Python人工智能开发环境
开发语言·人工智能·vscode·python
浮游本尊4 小时前
一文讲透巡检链路:采集程序 → 上传数据包 → 后端解析入库 → 分析出报告
python