一、接口选型与适用场景
亚马逊为开发者提供两套官方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 安全规范
-
密钥管理:使用AWS Secrets Manager或Spring Cloud Config加密存储密钥
-
HTTPS强制:所有请求必须使用TLS 1.2+
-
日志脱敏 :禁止在日志中打印
SecretKey、RefreshToken等敏感信息