Apache HttpClient5 之順豐快遞
Apache HttpClient5 之順豐快遞
概要
-
由于順豐Java Sdk使用Apache HttpClient版本較低,且不支持maven引用——需要手工導入maven,
以下使用Java 17、Apache HttpClient5進行重構、加密算法保留與順豐Sdk一致。 -
Java 17:代碼運行環境是Java 17
-
maven:Apache HttpClient5 具體版本如下
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
<version>5.3.5</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.25</version>
</dependency>
代碼
- 順豐下單接口(預下單、下單、注冊路由)
import com.alibaba.fastjson2.JSON;
import com.yyds.common.controller.BaseController;
import com.yyds.common.vo.AjaxResult;
import com.yyds.config.SfConfig;
import com.yyds.module.sf.dto.ScheduleACourierDto;
import com.yyds.module.sf.dto.SfPreOrderDto;
import com.yyds.module.sf.service.SfService;
import com.yyds.module.sf.vo.SfExpCreateOrderVo;
import com.yyds.module.sf.vo.BusinessResultVo;
import com.yyds.utils.HttpClientService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/sf")
@RequiredArgsConstructor
public class SfController extends BaseController {
// 預下單Code
private static final String EXP_RECE_PRE_ORDER = "EXP_RECE_PRE_ORDER";
// 下單Code
private static final String EXP_RECE_CREATE_ORDER = "EXP_RECE_CREATE_ORDER";
// 注冊路由Code
private static final String EXP_RECE_REGISTER_ROUTE = "EXP_RECE_REGISTER_ROUTE";
private final SfConfig sfConfig;
private final SfService sfService;
private final HttpClientService httpClientService;
/**
* 順豐生成訂單接口
*
* @param dto 下單參數
* @return
*/
@PostMapping("expCreateOrder")
public AjaxResult expCreateOrder(@RequestBody ScheduleACourierDto dto) {
try {
// 預下單-校驗是否可以下單
SfPreOrderDto preDto = sfService.buildSfPreOrderParam(dto);
String preMsgData = JSON.toJSONString(preDto);
Map<String, String> preParams = sfService.buildCommonReqParams(EXP_RECE_PRE_ORDER, preMsgData);
String preRespResult = httpClientService.post(sfConfig.getCallUrlProd(), preParams);
BusinessResultVo preApiResult = sfService.getCommonRespResult(preRespResult);
if (!preApiResult.getStatus()) {
return error(preApiResult.getMsg());
}
// 下單
String createMsgData = sfService.buildSfCreateOrderParam(preDto, dto.getSendStartTm());
Map<String, String> createParams = sfService.buildCommonReqParams(EXP_RECE_CREATE_ORDER, createMsgData);
String createRespResult = httpClientService.post(sfConfig.getCallUrlProd(), createParams);
BusinessResultVo orderVo = sfService.getCreatRespResult(createRespResult);
if (!orderVo.getStatus()) {
return error(orderVo.getMsg());
}
// 注冊路由
String routeMsgData = sfService.buildSfRegRouteParam(orderVo.getWaybillNo());
Map<String, String> routeParams = sfService.buildCommonReqParams(EXP_RECE_REGISTER_ROUTE, routeMsgData);
String routeRespResult = httpClientService.post(sfConfig.getCallUrlProd(), routeParams);
BusinessResultVo routeApiResult = sfService.getCommonRespResult(routeRespResult);
if (routeApiResult.getStatus()) {
return success(SfExpCreateOrderVo.builder().sfWaybillNo(orderVo.getWaybillNo()).build());
}
return error();
} catch (Exception e) {
log.error("順豐-expCreateOrder: {}", e.getMessage(),e);
throw new RuntimeException(e);
}
}
}
- 順豐下單服務層(參數構造、結果處理、業務處理)具體參數可看順豐官方文檔
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.yyds.config.SfConfig;
import com.yyds.module.sf.dto.*;
import com.yyds.module.sf.util.VerifyCodeUtil;
import com.yyds.module.sf.vo.BusinessResultVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
@Slf4j
@Service
@RequiredArgsConstructor
public class SfService {
private final SfConfig sfConfig;
/**
* 構造請求順豐的公共參數
*
* @param serviceCode 業務類型
* @param msgDataJson 具體參數(json格式)
* @return 公共參數
*
*/
public Map<String, String> buildCommonReqParams(String serviceCode, String msgDataJson) {
log.info("順豐-服務Code:{}-請求參數:{}", serviceCode, msgDataJson);
Map<String, String> params = new HashMap<>();
String timeStamp = String.valueOf(System.currentTimeMillis());
params.put("partnerID", sfConfig.getClientCode()); // 顧客編碼 ,對應豐橋上獲取的clientCode
params.put("requestID", UUID.randomUUID().toString().replace("-", ""));
params.put("serviceCode",serviceCode);// 接口服務碼
params.put("timestamp", timeStamp);
params.put("msgData", msgDataJson);
String md5key = msgDataJson + timeStamp + sfConfig.getCheckWord();
params.put("msgDigest", VerifyCodeUtil.md5EncryptAndBase64(md5key));
return params;
}
/**
* 處理公共響應結果
*
* @param result 響應結果(json)
* @return
*/
public BusinessResultVo getCommonRespResult(String result) {
JSONObject jsonObject = JSON.parseObject(result);
log.info("順豐響應結果:{}", jsonObject);
if (jsonObject.get("apiResultCode") != null && !"A1000".equals(jsonObject.getString("apiResultCode"))) {
return BusinessResultVo.builder().status(false).msg("請求順豐失敗!").build();
}
JSONObject apiResultData = jsonObject.getJSONObject("apiResultData");
String errCode = apiResultData.get("errorCode") != null ? apiResultData.getString("errorCode") : "";
if (apiResultData.getBoolean("success")) {
if ("S0000".equals(errCode)) {
return BusinessResultVo.builder().status(true).build();
}
} else {
if ("S0000".equals(errCode)) {
return BusinessResultVo.builder().status(false).msg(apiResultData.getString("errorMsg")).build();
}
}
return BusinessResultVo.builder().status(false).build();
}
/**
* 處理下單的響應結果
*
* @param result 響應結果
* @return 順豐單號
*/
public BusinessResultVo getCreatRespResult(String result) {
BusinessResultVo resultVo = this.getCommonRespResult(result);
if (!resultVo.getStatus()) {
return resultVo;
}
BusinessResultVo paramResultVo = BusinessResultVo.builder().status(false).msg("請求順豐結果異常!").build();
JSONObject jsonObject = JSON.parseObject(result);
JSONObject apiResultData = jsonObject.getJSONObject("apiResultData");
JSONObject msgData = apiResultData.getJSONObject("msgData");
if (msgData == null) {
return paramResultVo;
}
JSONArray waybillNoList = msgData.getJSONArray("waybillNoInfoList");
if (waybillNoList == null || waybillNoList.isEmpty()) {
return paramResultVo;
}
// 獲取單號列表,waybillType = 1 ,為母單,暫時不需要子單
Optional<JSONObject> waybillNoOptional = waybillNoList.stream().filter(i -> i instanceof JSONObject)
.map(i -> (JSONObject) i)
.filter(item -> "1".equals(item.getString("waybillType")))
.findFirst();
if (waybillNoOptional.isEmpty()) {
return paramResultVo;
}
// 獲取母單的單號
String waybillNo = waybillNoOptional.get().getString("waybillNo");
if (waybillNo.isEmpty()) {
return paramResultVo;
}
// 保存順豐相關信息
// 業務數據相關邏輯省略
return BusinessResultVo.builder().status(true).waybillNo(waybillNo).build();
}
/**
* 創建下單的請求參數
*
* @param preDto
* @return
*/
public String buildSfCreateOrderParam(SfPreOrderDto preDto, String sendStartTm) {
SfCreateOrderDto orderDto = new SfCreateOrderDto();
BeanUtils.copyProperties(preDto, orderDto);
orderDto.setLanguage("zh-CN");
orderDto.setIsReturnRoutelabel("1");
orderDto.setIsDocall("1");
orderDto.setSendStartTm(sendStartTm);
CargoDetails cargoDetails = new CargoDetails();
cargoDetails.setSourceArea("CHN");
cargoDetails.setName("化妝品");
cargoDetails.setCurrency("CNY");
orderDto.setCargoDetails(List.of(cargoDetails));
return JSON.toJSONString(orderDto);
}
/**
* 預下單 -校驗是否可以下單
* @param courierDto 下單參數
* @return
*/
public SfPreOrderDto buildSfPreOrderParam(ScheduleACourierDto courierDto) {
SfPreOrderDto dto = new SfPreOrderDto();
dto.setOrderId(this.getOrderId());
// 綁定順豐月卡
dto.setMonthlyCard(sfConfig.getMonthlyCard());
// 1:順豐特快、2:順豐標快、6:順豐即日
dto.setExpressTypeId("2");
String[] region = courierDto.getRegion().split("/");
// 拼接區:如果詳細地址包含則不拼接
if(!courierDto.getAddress().contains(region[2])){
courierDto.setAddress(region[2]+courierDto.getAddress());
}
// 拼接市:如果詳細地址包含則不拼接
if(!courierDto.getAddress().contains(region[1])){
courierDto.setAddress(region[1]+courierDto.getAddress());
}
// 拼接省:如果詳細地址包含則不拼接
if(!courierDto.getAddress().contains(region[0])){
courierDto.setAddress(region[0]+courierDto.getAddress());
}
// 寄件人相關
ContactInfo contactSend = new ContactInfo();
contactSend.setContactType(1);
contactSend.setTel(courierDto.getTel());
contactSend.setMobile(courierDto.getTel());
contactSend.setProvince(region[0]);
contactSend.setCity(region[1]);
contactSend.setCounty(region[2]);
contactSend.setAddress(courierDto.getAddress());
contactSend.setContact(courierDto.getCustomerName());
// 收件人相關
ContactInfo contactReceiveItems = new ContactInfo();
contactReceiveItems.setContactType(2);
contactReceiveItems.setTel(sfConfig.getTel());
contactReceiveItems.setMobile(sfConfig.getTel());
contactReceiveItems.setProvince(sfConfig.getProvince());
contactReceiveItems.setCity(sfConfig.getCity());
contactReceiveItems.setCounty(sfConfig.getCounty());
contactReceiveItems.setAddress(sfConfig.getAddress());
contactReceiveItems.setContact(sfConfig.getContact());
contactReceiveItems.setCompany("YYDS有限公司");
List<ContactInfo> contactInfoList = List.of(contactSend,contactReceiveItems);
dto.setContactInfoList(contactInfoList);
return dto;
}
/**
* 路由注冊
*
* @param waybillNo 順豐單號
* @return
*/
public String buildSfRegRouteParam(String waybillNo) {
SfRegisterRouteDto dto = new SfRegisterRouteDto();
dto.setType("2");
dto.setAttributeNo(waybillNo);
dto.setLanguage("zh-CN");
dto.setCountry("CN");
return JSON.toJSONString(dto);
}
/**
* 自定義-生成單號
*
* @return
*/
public String getOrderId() {
String formattedDateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
// 前綴-日期
return "YYDS-" + formattedDateTime;
}
}
- VerifyCodeUtil 加密工具類(順豐數據通訊使用)
import lombok.AllArgsConstructor;
import org.apache.commons.codec.binary.Base64;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
@AllArgsConstructor
public class VerifyCodeUtil {
public static String md5EncryptAndBase64(String str) {
return encodeBase64(md5Encrypt(str));
}
private static byte[] md5Encrypt(String encryptStr) {
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(encryptStr.getBytes(StandardCharsets.UTF_8));
return md5.digest();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static String encodeBase64(byte[] b) {
return (new Base64()).encodeAsString(b);
}
}
- HttpClientConfig :Http-配置連接池、請求級別、連接資源等
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.DefaultHttpRequestRetryStrategy;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner;
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.ProxySelector;
import java.time.Duration;
@Configuration
public class HttpClientConfig {
// 連接池最大連接數 - 控制整個連接池允許的最大并發連接數
private final static int MAX_TOTAL = 100;
// 每個路由(目標主機)的默認最大連接數 - 限制到單個主機的并發連接數
private final static int DEFAULT_MAX_PER_ROUTE = 50;
// 連接建立超時時間(毫秒)- 建立TCP連接的最大等待時間
private final static Duration CONNECT_TIMEOUT = Duration.ofMillis(5000);
// 從連接池獲取連接的超時時間(秒)- 等待連接池分配連接的最大時間
private final static Duration CONNECTION_REQUEST_TIMEOUT = Duration.ofSeconds(2);
// 響應超時時間(秒)- 等待服務器響應的最大時間
private final static Duration RESPONSE_TIMEOUT = Duration.ofSeconds(30);
// Socket超時時間(毫秒)- 兩次數據包之間的最大間隔時間
private final static Duration SOCKET_TIMEOUT = Duration.ofMillis(10000);
// 空閑連接最大存活時間(毫秒)- 超過此時間的空閑連接將被驅逐
private final static Duration IDLE_EVICT_TIME = Duration.ofMillis(120000);
/**
* 配置HTTP連接池管理器 (核心Bean)
* 使用@Bean使其由Spring容器管理單例生命周期
* Spring容器關閉時自動調用close()釋放連接
*/
@Bean(destroyMethod = "close")
public HttpClientConnectionManager httpClientConnectionManager() throws Exception {
// 創建SSL socket工廠
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(
SSLContexts.custom().build(),
new String[]{"TLSv1.2", "TLSv1.3"}, // 支持的TLS協議版本
null, // 支持的密碼套件(null表示使用默認值)
new DefaultHostnameVerifier()); // 主機名驗證器
// 配置默認連接配置
ConnectionConfig connectionConfig = ConnectionConfig.custom()
.setConnectTimeout(Timeout.of(CONNECT_TIMEOUT)) // 連接建立超時
.setSocketTimeout(Timeout.of(SOCKET_TIMEOUT)) // Socket讀寫超時
.build();
// 創建連接池管理器(HTTP 連接會自動使用默認的普通 Socket 工廠(PlainConnectionSocketFactory),無需顯式設置)
PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(sslSocketFactory) // 設置SSL socket工廠
.setDefaultConnectionConfig(connectionConfig) // 設置默認連接配置
.build();
// 設置連接池參數
connectionManager.setMaxTotal(MAX_TOTAL); // 設置整個連接池的最大連接數
connectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_PER_ROUTE); // 設置每個路由的默認最大連接數
return connectionManager;
}
/**
* 配置請求級別的參數
* 主要設置從連接池獲取連接的超時時間和響應超時時間
*/
@Bean
public RequestConfig requestConfig() {
return RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.of(CONNECTION_REQUEST_TIMEOUT))
// 從連接池獲取連接的超時時間
.setResponseTimeout(Timeout.of(RESPONSE_TIMEOUT))
// 等待服務器響應的超時時間
.build();
}
/**
* 創建并配置HttpClient實例
* 使用@Bean注解使其成為Spring管理的Bean
* destroyMethod = "close"確保Spring容器關閉時自動關閉HttpClient釋放資源
*/
@Bean(destroyMethod = "close")
public CloseableHttpClient httpClient(HttpClientConnectionManager manager, RequestConfig reqConfig) {
return HttpClients.custom()
.setConnectionManager(manager)
// 設置連接池
.setDefaultRequestConfig(reqConfig)
// 設置請求配置
.setRoutePlanner(new SystemDefaultRoutePlanner(ProxySelector.getDefault()))
// 使用系統默認代理
.setRetryStrategy(new DefaultHttpRequestRetryStrategy(3, TimeValue.ofSeconds(1)))
// 使用內置的重試策略
.evictExpiredConnections()
// 驅逐過期連接
.evictIdleConnections(TimeValue.of(IDLE_EVICT_TIME))
// 驅逐空閑連接
.build();
}
}
- HttpClientUtil http工具類
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class HttpClientUtil {
private final RequestConfig requestConfig;
private final HttpClient httpClient;
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS").withZone(ZoneId.systemDefault());
public String post(String url, StringEntity entity) {
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(entity);
return invoke(httpClient, httpPost);
}
public String post(String url, List<NameValuePair> parameters) {
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8));
return invoke(httpClient, httpPost);
}
public String postSFAPI(String url, String xml, String verifyCode) {
List<NameValuePair> parameters = List.of(
new BasicNameValuePair("xml", xml),
new BasicNameValuePair("verifyCode", verifyCode));
HttpPost httpPost = new HttpPost(url);
httpPost.setEntity(new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8));
return invoke(httpClient, httpPost);
}
public String get(String url) {
HttpGet get = new HttpGet(url);
return invoke(httpClient, get);
}
public String delete(String url) {
HttpDelete delete = new HttpDelete(url);
return invoke(httpClient, delete);
}
public String post(String url, Map<String, String> params) {
HttpPost httpPost = new HttpPost(url);
httpPost.setConfig(requestConfig);
httpPost.addHeader("appCode", params.get("partnerID"));
httpPost.addHeader("timestamp", FORMATTER.format(Instant.now()));
httpPost.addHeader("Content-Type", ContentType.APPLICATION_FORM_URLENCODED.withCharset(StandardCharsets.UTF_8).toString());
List<NameValuePair> paramsList = params.entrySet().stream()
.map(i->new BasicNameValuePair(i.getKey(),i.getValue()))
.collect(Collectors.toList());
httpPost.setEntity(new UrlEncodedFormEntity(paramsList, StandardCharsets.UTF_8));
return invoke(httpClient, httpPost);
}
public static String invoke(HttpClient httpclient, HttpUriRequest httpRequest) {
try {
// 創建一個響應處理器
HttpClientResponseHandler<String> responseHandler = resp -> {
int statusCode = resp.getCode();
HttpEntity entity = resp.getEntity();
String body = entity != null ? EntityUtils.toString(entity) : "";
if (statusCode >= 200 && statusCode < 300) {
return body;
}else if (statusCode >= 400 && statusCode < 500) {
// 客戶端錯誤
log.warn("客戶端錯誤 {}: {}", statusCode, body);
throw new RuntimeException("客戶端錯誤: " + statusCode);
} else if (statusCode >= 500) {
// 服務器錯誤
log.error("服務器錯誤 {}: {}", statusCode, body);
throw new RuntimeException("服務器錯誤: " + statusCode);
} else {
// 其他狀態碼
log.info("非標準狀態碼 {}: {}", statusCode, body);
return body;
}
};
return httpclient.execute(httpRequest, responseHandler);
} catch (IOException | RuntimeException e) {
log.error("HttpClient請求執行失敗: {} {}", httpRequest.getMethod(), httpRequest.getRequestUri(), e);
throw new RuntimeException("HTTP請求執行失敗", e);
}
}
}

浙公網安備 33010602011771號