Spring Cloud Alibaba/Spring Boot整合華為云存儲實例(REST API方式)

一個小作業,初次嘗試華為云存儲,一點分享

原項目采用Spring Cloud Alibaba微服務技術、Spring Boot框架技術、VueJS前端框架開發技術,nacos注冊中心,數據庫為mysql

下面看一下沒有運用云存儲的原項目(可跳過):

原項目概覽:

關鍵代碼

application.yaml

server:port: 8007service:ipAddr: http://localhost
download:address: F:\file\spring:application:name: uploadService8007servlet:multipart:max-file-size: 20MBmax-request-size: 30MBresources:static-locations: file:f:/file/mvc:static-path-pattern: /file/**cloud:nacos:discovery:server-addr: localhost:8848
management:endpoints:web:exposure:include: '*'

其中定義了網關地址(在編碼過程中為“http://localhost”地址),文件請求最大大小為20MB,以及最大請求大小為30MB,文件映射地址,此處將/file/**映射為本地F盤下的file文件(此處可自定義本地盤路徑,自定義后將項目圖片素材解壓并存放至對應路徑中

啟動類

在src/main/Java目錄下添加com.qst.upload包,并創建啟動類port8007_upload,并添加啟動方法

@Import(value = WebMvcConfig.class)
@EnableDiscoveryClient@SpringBootApplication
public class port8007_upload {public static void main(String[] args) {SpringApplication.run(port8007_upload.class,args);}}

文件上傳控制器

在com.qst.upload包下創建文件上傳控制器UploadController,并添加圖片上傳,音頻文件上傳以及歌詞文件上傳接口方法

@RestController@RequestMapping("/upload")
public class UploadController {@AutowiredUploadService uploadService;@PostMapping("upImage")public Mess upLoadImage(MultipartFile file){if(CheckUtil.isImage(file.getOriginalFilename())) {String url = uploadService.uploadImage(file);return Mess.success().mess("文件上傳成功").data("url", url);}return Mess.fail().mess("文件格式錯誤");}@PostMapping("upLyric")public Mess upLoadLyric(MultipartFile file){if(CheckUtil.isLyric(file.getOriginalFilename())) {String url = uploadService.uploadLyric(file);return Mess.success().mess("文件上傳成功").data("url", url);}return Mess.fail().mess("文件格式錯誤");}@PostMapping("upMusic")public Mess upLoadMusic(MultipartFile file){if (CheckUtil.isMusic(file.getOriginalFilename())){Integer length= MusicUtil.getDuration(file);String url=uploadService.uploadMusic(file);return Mess.success().mess("上傳成功").data("url",url).data("timelength",length);}return Mess.fail().mess("文件格式錯誤");}}

實現文件上傳服務類

在com.qst.upload中創建文件上傳服務類UploadService,并添加生成文件名方法,將文件保存到本地方法,上傳歌曲方法,上傳歌詞方法以及上傳圖片方法

@Service
public class UploadService {@Value("${download.address}")String address;@Value("${service.ipAddr}")String ip;public String uploadImage(MultipartFile file){return upLoadFile(file,"image");}public String uploadLyric(MultipartFile file){return upLoadFile(file,"lyric");}public String uploadMusic(MultipartFile file){return upLoadFile(file,"music");}public String upLoadFile(MultipartFile file,String type){OutputStream os = null;InputStream is = null;String newName=getNewName(file);try {is=file.getInputStream();} catch (IOException e) {e.printStackTrace();}try {byte[] bs=new byte[1024];int length;File tempFile = new File(address.concat(type));if (!tempFile.exists()){tempFile.mkdirs();}os = new FileOutputStream(tempFile.getPath().concat(File.separator).concat(newName));while ((length = is.read(bs)) != -1) {os.write(bs, 0, length);}return ip.concat("/file/").concat(type).concat("/").concat(newName);}catch (Exception e){System.out.println(e.getMessage());}finally {try {os.close();is.close();}catch (Exception e){}}return null;}public String getNewName(MultipartFile file){String uuid= UUID.randomUUID().toString();String filename=file.getOriginalFilename();String newName=uuid+filename.substring(filename.lastIndexOf("."));return newName;}}

修改網關配置

網關微服務中添加上傳微服務服務名。

添加路由轉發,首先將上傳請求轉發到文件上傳模塊微服務中,由于配置了文件映射,所以將請求一級路徑(文件訪問請求)為“file”的請求轉發到文件上傳模塊微服務中

service:upload: uploadService8007
- id: uploaduri: lb://${service.upload}predicates:- Path=/upload/**- id: fileuri: lb://${service.upload}predicates:- Path=/file/**

測試

圖片上傳接口

請求地址:“localhost/upload/upImage”

請求類型:Post

請求體:file:{上傳文件}

響應:

{"code":?20,"message":?"文件上傳成功","success":?true,"data":?{"url":?"http://localhost/file/image/a062734c-ce62-49c3-938c-2ecf9337a267.jpg"}
}

在postman中點擊地址url可直接發送get請求,獲取剛剛上傳的文件?

其他上傳功能同理

把官方文檔過一過腦子

官方開發流程概覽:

序號

任務

說明

1

創建項目

項目是您在AppGallery Connect(以下簡稱AGC)資源的組織實體。當您需要使用云存儲服務時,您需要先在AGC中創建您的項目。

說明

您可以通過創建不同的項目,實現分別在測試環境和開發環境使用云存儲。

2

開通云存儲服務

-

3

獲取API授權

在向AGC服務端發起REST API請求前,您需要先獲得AGC服務端的授權。

4

管理文件

通過調用云存儲REST API,您可以進行上傳文件、下載文件、刪除文件、文件元數據管理以及獲取云端某個目錄下的文件列表等操作。

其中第一步和第二步都是傻瓜式操作
其中第三步獲取API授權

創建API客戶端

API客戶端是AGC用于管理用戶訪問AppGallery Connect API的身份憑據。在訪問某個API前,必須創建有權訪問該API的API客戶端。

  1. 登錄AppGallery Connect,點擊“開發與服務”。
  2. 在項目列表中選擇需要獲取憑證的項目,在“項目設置”頁面點擊“Server SDK”頁簽。
  3. 點擊認證憑據區域內“API客戶端”旁的“創建”。
  4. 在彈出的提示框內點擊“確認”,完成認證憑據創建,點擊“下載認證憑據”下載json文件,獲取文件中的client_id和client_secret信息。?

將下載后的認證憑據文件agc-apiclient-*.json放置到您的Server服務器中指定路徑下,后續初始化SDK時將會使用到該文件?

獲取訪問API的Token

創建完API客戶端后需要到華為AGC平臺進行鑒權,鑒權通過后將獲得用于訪問AppGallery Connect API的Access Token。用戶憑借該Access Token即可訪問REST API。您可以調用獲取Token接口來獲取Access Token。

功能介紹

在使用API客戶端方式調用Connect API的接口前,需要通過華為開放平臺進行鑒權,并獲取認證通過后的Token。

接口原型

承載協議

HTTPS POST

接口方向

開發者服務器 -> 華為服務器

接口URL

https://{domain}/api/oauth2/v1/token

  • 中國站點的domain:connect-api.cloud.huawei.com
  • 德國站點的domain:connect-api-dre.cloud.huawei.com
  • 新加坡站點的domain:connect-api-dra.cloud.huawei.com
  • 俄羅斯站點的domain:connect-api-drru.cloud.huawei.com
    注意

    本接口使用的domain必須是項目設置的數據處理位置對應的domain,例如:項目設置數據處理位置為中國,那么本接口中的domain必須使用“connect-api.cloud.huawei.com”;

數據格式

請求:Content-Type: application/json

響應:Content-Type: application/json

請求參數

請求參數以JSON格式傳入,包含參數如下。

參數名稱

必選(M)/可選(O)

數據類型

參數說明

grant_type

M

String(256)

固定傳入“client_credentials”。

client_id

M

String(256)

客戶端ID,即下載項目級憑證agc-apiclient-*.json文件中的client_id。

client_secret

M

String(2048)

客戶端密鑰,即下載項目級憑證agc-apiclient-*.json文件中的client_secret。

請求示例

  1. POST /api/oauth2/v1/token
  2. Host: connect-api.cloud.huawei.com
  3. Content-Type: application/json
  4. {
  5. "grant_type":"client_credentials",
  6. "client_id":"26********20",
  7. "client_secret":"************************"
  8. }

響應參數

返回值為JSON格式的字符串,包含參數如下。

參數名稱

必選(M)/可選(O)

數據類型

參數說明

access_token

O

String

認證Token,用于AppGallery Connect API接口調用。

此參數只在獲取成功時返回。

expires_in

O

Long

access_token的有效期,單位秒。您需要在過期時間到達時重新調用本接口獲取新的access_token。

有效期為48小時,如果在有效期內再次調用接口獲取access_token時,新老access_token都是有效的。

此參數只在獲取成功時返回。

ret

O

String(100)

獲取Token失敗時的錯誤信息,包含錯誤碼及描述信息的JSON字符串,格式為{"code":retcode, "msg": "description"},retcode為錯誤碼,description為錯誤碼描述信息。

?postman在線調試接口(有助于理解截止目前的操作)

HMS Core | Postman API Network

下載文件接口:
?

功能介紹

此接口用于使用用戶身份認證方式下載文件。

接口原型

承載協議

HTTPS GET

接口方向

開發者服務器->華為服務器

接口URL

https://{domain}/{bucket_name}/{object_name}

注意

  • {bucket_name}為當前存儲實例名稱,{object_name}為需獲取的文件名稱。
  • {domain}為接口域名。
    • 中國站點的域名:ops-server-drcn.agcstorage.link/v0
    • 德國站點的域名:ops-server-dre.agcstorage.link/v0
    • 新加坡站點的域名:ops-server-dra.agcstorage.link/v0
    • 俄羅斯站點的域名:ops-server-drru.agcstorage.link/v0
  • 調用獲取Token接口時使用的站點必須與本接口使用的站點保持一致。例如本接口使用的站點是德國,即接口域名為“ops-server-dre.agcstorage.link/v0”,則調用獲取Token接口也必須使用德國站點,其接口域名需為“connect-api-dre.cloud.huawei.com”。

數據格式

請求:Content-Type: application/json

響應:Content-Type: application/json

請求參數

Header

參數

類型

必選(M)/可選(O)

說明

Authorization

String

M

認證信息,格式為“Authorization: Bearer ${access_token}”。access_token為獲取Token中獲取的access_token。

client_id

String

M

客戶端ID,獲取方法參考創建API客戶端。

productId

String

M

項目ID,查詢方法可參見查詢項目ID。

Range

String

O

HTTP標準協議頭,用于指定第一個字節和最后一個字節的位置,告訴服務器想獲取的文件范圍。如未指定,則獲取整個文件。

例如一個文件有900個字節,其范圍為0-899,則:

Range: bytes=500- 表示讀取該文件的500-899字節。

Range: bytes=500-699 表示讀取該文件的500-699字節。

If-Modified-Since

String

O

一個條件式請求首部。

服務器只在所請求的資源在給定的時間之后對內容進行過修改的情況下才會將資源返回,狀態碼為200 。如果請求的資源在給定的時間之后未經修改,則返回一個不帶消息主體的304響應。

格式:<day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT

舉例:Mon, 10 Apr 2023 07:28:00 GMT

說明

If-Modified-Since只可以用在GET或HEAD請求中。當與If-None-Match 一同出現時,If-Modified-Since會被忽略,除非服務器不支持If-None-Match。

If-None-Match

String

O

一個條件式請求首部。

對于GET和HEAD方法,當且僅當服務器上沒有任何資源的ETag屬性值與這個首部中列出的相匹配時,服務器端會才返回所請求的資源,響應碼為200 ,否則返回304。其他方法暫不實現。

格式:If-None-Match: "<etag_value>"

不支持通配符*,不支持弱比較算法。

X-Agc-Trace-Id

String

O

單個文件請求的Traceid。可不填或者隨機生成,用于日志打印跟蹤。

請求示例

  1. GET /v0/testagc-02reg/test3.jpg
  2. client_id: 8490****0232064
  3. productId: 7364****4461998
  4. Content-Type: application/json
  5. X-Agc-Trace-Id: 123456789
  6. Authorization: Bearer ****
  7. User-Agent: PostmanRuntime/7.6.0
  8. Accept: */*
  9. Host: ops-server-drcn.agcstorage.link
  10. accept-encoding: gzip, deflate

響應參數

參數

類型

必選(M)/可選(O)

說明

status

Integer

M

HTTP響應碼,200表示成功。

X-Agc-Trace-Id

String

O

單個文件請求的Traceid,與請求參數X-Agc-Trace-Id保持一致。

Last-Modified

String

O

文件的最近修改時間,配合If-Modified-Since使用。

格式:<day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT

舉例:Wed, 21 Oct 2015 07:28:00 GMT

Etag

String

O

HTTP標準Header,配合If-None-Match使用。

格式:"<etag_value>"

舉例:"33a64df551425fcc55e4d42a148795d9f25f89d4"

Cache-Control

String

O

HTTP標準Header,通過指定指令來實現緩存機制。

代碼實現/項目實踐

開始之前需要導入需要的依賴(集成Java Server SDK)

云存儲Java Server SDK發布在Maven倉庫,您需要在pom.xml文件中添加Maven倉庫地址和云存儲服務SDK依賴。

  1. 在項目中找到pom.xml文件,并添加Maven倉庫地址。

    1. <repositories>
    2. <repository>
    3. <id>sz-maven-public</id>
    4. <name>sz-maven-public</name>
    5. <url>https://developer.huawei.com/repo/</url>
    6. </repository>
    7. </repositories>
  1. 添加云存儲服務SDK的依賴。
    1. <dependency>
    2. <groupId>com.huawei.agconnect.server</groupId>
    3. <artifactId>agconnect-storage</artifactId>
    4. <version>1.2.0.101</version>
    5. </dependency>

上傳控制器不用修改

修改配置文件

server:port: 8007
service:ipAddr: http://localhost
download:address: D:\file\
spring:application:name: uploadService8007servlet:multipart:max-file-size: 20MBmax-request-size: 30MBcloud:nacos:discovery:server-addr: localhost:8848
management:endpoints:web:exposure:include: '*'huawei:agc:credential-path: D:\Download\agc-apiclient-*** # 憑據文件路徑bucket-name: ***  # 存儲桶名稱region: CN  # 區域標識(CN/DE/SG/RU)storage-url: https://ops-server-drcn.agcstorage.link/v0/  # 存儲服務URLclient-id: 17233***93184  # 客戶端ID(從憑據文件獲取)project-id: 46132***67964  # 項目ID(從憑據文件獲取)

修改上傳UploadService ,仍不是重點

package com.qst.upload;import com.qst.domain.entity.Log;
import com.qst.upload.huawei.HuaweiStorageUploadService;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.UUID;@Service
public class UploadService {@Value("${huawei.agc.bucket-name}")private String bucketName;@Autowiredprivate HuaweiStorageUploadService huaweiStorageUploadService;public String uploadImage(MultipartFile file) {return upLoadFile(file, "image");}public String uploadLyric(MultipartFile file) {return upLoadFile(file, "lyric");}public String uploadMusic(MultipartFile file) {return upLoadFile(file, "music");}public String upLoadFile(MultipartFile file, String type) {try {// 生成新文件名String newFilename = getNewName(file);String objectPath = type + "/" + newFilename;// 上傳到華為云存儲huaweiStorageUploadService.uploadFile(file, objectPath);// 返回本地代理下載URLreturn "/upload/download/" + type + "/" + newFilename;} catch (Exception e) {throw new RuntimeException("文件上傳失敗", e);}}public String getNewName(MultipartFile file) {String uuid = UUID.randomUUID().toString();String filename = file.getOriginalFilename();return uuid + filename.substring(filename.lastIndexOf("."));}}

新增HuaweiStorageUploadService(重點)

package com.qst.upload.huawei;import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
//
///**
// * 華為云存儲文件上傳服務
// */
@Service
public class HuaweiStorageUploadService {private static final Logger log = LoggerFactory.getLogger(HuaweiStorageUploadService.class);@Value("${huawei.agc.region}")private String region;@Value("${huawei.agc.client-id}")private String clientId;@Value("${huawei.agc.project-id}")private String projectId;@Value("${huawei.agc.credential-path}")private String credentialPath;@Value("${huawei.agc.bucket-name}")private String bucketName;@Autowiredprivate RestTemplate restTemplate;@Autowiredprivate ObjectMapper objectMapper;/*** 上傳文件到華為云存儲*/public String uploadFile(MultipartFile file, String objectPath) {try {// 構建上傳URLString url = buildUploadUrl(objectPath);log.info("華為云文件上傳URL: {}", url);// 準備請求頭HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);headers.set("client_id", clientId);headers.set("productId", projectId);headers.set("Authorization", "Bearer " + getAccessToken());headers.set("X-Agc-File-Size", String.valueOf(file.getSize()));headers.set("X-Agc-Trace-Id", UUID.randomUUID().toString());// 使用字節數組創建請求實體HttpEntity<byte[]> requestEntity = new HttpEntity<>(file.getBytes(), headers);// 執行PUT請求ResponseEntity<String> response = restTemplate.exchange(url,HttpMethod.PUT,requestEntity,String.class);// 處理響應if (response.getStatusCode() == HttpStatus.OK) {log.info("文件上傳成功,華為云URL: {}", url);return url;} else {log.error("華為云文件上傳失敗,狀態碼: {}", response.getStatusCodeValue());throw new RuntimeException("文件上傳失敗");}} catch (Exception e) {log.error("華為云文件上傳異常", e);throw new RuntimeException("文件上傳失敗", e);}}/*** 構建上傳URL*/private String buildUploadUrl(String objectPath) {String domain = getDomainByRegion(region);return "https://" + domain + "/" + bucketName + "/" + objectPath;}/*** 根據區域獲取域名*/private String getDomainByRegion(String region) {Map<String, String> domainMap = Map.of("CN", "ops-server-drcn.agcstorage.link/v0","DE", "ops-server-dre.agcstorage.link/v0","SG", "ops-server-dra.agcstorage.link/v0","RU", "ops-server-drru.agcstorage.link/v0");String domain = domainMap.get(region.toUpperCase());if (domain == null) {throw new IllegalArgumentException("不支持的區域: " + region);}return domain;}/*** 獲取訪問令牌*/private String getAccessToken() {try {String authUrl = getAuthUrlByRegion(region);log.info("獲取華為云Token URL: {}", authUrl);// 讀取憑據文件String content = new String(Files.readAllBytes(Paths.get(credentialPath)));Map<String, String> credential = objectMapper.readValue(content, new TypeReference<Map<String, String>>() {});// 準備請求頭HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));// 準備請求體Map<String, String> requestBody = new HashMap<>();requestBody.put("grant_type", "client_credentials");requestBody.put("client_id", credential.get("client_id"));requestBody.put("client_secret", credential.get("client_secret"));// 創建請求實體HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(requestBody, headers);// 發送請求(使用exchange方法)ResponseEntity<Map> response = restTemplate.exchange(authUrl,HttpMethod.POST,requestEntity,Map.class);// 處理響應if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {String token = (String) response.getBody().get("access_token");log.info("成功獲取華為云Token");return token;} else {log.error("獲取華為云Token失敗,狀態碼: {}", response.getStatusCodeValue());throw new RuntimeException("獲取華為云Token失敗");}} catch (Exception e) {log.error("獲取華為云Token異常", e);throw new RuntimeException("獲取Token失敗", e);}}/*** 根據區域獲取Token接口URL*/private String getAuthUrlByRegion(String region) {Map<String, String> authUrlMap = Map.of("CN", "https://connect-api.cloud.huawei.com/api/oauth2/v1/token","DE", "https://connect-api-dre.cloud.huawei.com/api/oauth2/v1/token","SG", "https://connect-api-dra.cloud.huawei.com/api/oauth2/v1/token","RU", "https://connect-api-drru.cloud.huawei.com/api/oauth2/v1/token");String url = authUrlMap.get(region.toUpperCase());if (url == null) {throw new IllegalArgumentException("不支持的區域: " + region);}return url;}private String buildDownloadUrl(String objectPath) {String domain = getDomainByRegion(region);return "https://" + domain + "/" + bucketName + "/" + objectPath;}
}

HuaweiStorageUploadService解析(重點)

  1. getDomainByRegion()根據不同的地區返回不同的華為服務器接口url。一般都是CN,也可不要這個方法
  2. buildUploadUrl()根據官方文檔的指導,拼接接口URL:https://{domain}/{bucket_name}/{object_name}
  3. getAccessToken()獲取訪問API的Token

????????需要訪問https://{domain}/api/oauth2/v1/token

????????傳入合適的參數給該接口即可得到access_token,即認證Token,用于AppGallery Connect API接口調用

? ? ? ? 從下載的這個文件讀取并解析client_secret和client_id等

// 準備請求頭HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));// 準備請求體Map<String, String> requestBody = new HashMap<>();requestBody.put("grant_type", "client_credentials");requestBody.put("client_id", credential.get("client_id"));requestBody.put("client_secret", credential.get("client_secret"));// 創建請求實體HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(requestBody, headers);

然后準備http請求,之后獲取token

之后在uploadFile()方法

調用buildUploadUrl構建上傳URL

準備請求頭,其中調用剛剛說到的getAccessToken方法獲取token

?然后執行put請求并處理響應

新增HuaweiStorageDownloadService

package com.qst.upload.huawei;import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.Resource;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;@Service
public class HuaweiStorageDownloadService {private static final Logger log = LoggerFactory.getLogger(HuaweiStorageDownloadService.class);@Value("${huawei.agc.region}")private String region;@Value("${huawei.agc.client-id}")private String clientId;@Value("${huawei.agc.project-id}")private String projectId;@Value("${huawei.agc.bucket-name}")private String bucketName;@Autowiredprivate RestTemplate restTemplate;@Autowiredprivate ObjectMapper objectMapper;@Value("${huawei.agc.credential-path}")private String credentialPath;public ResponseEntity<StreamingResponseBody> downloadFile(String type,String filename,HttpHeaders originalHeaders) {try {// 1. 構建華為云下載URLString downloadUrl = buildDownloadUrl(type, filename);System.out.println("華為云下載URL: "+downloadUrl);log.info("代理下載華為云文件: {}", downloadUrl);// 2. 創建流式響應體StreamingResponseBody stream = outputStream -> {HttpURLConnection connection = null;InputStream input = null;try {// 3. 創建HTTP連接URL url = new URL(downloadUrl);connection = (HttpURLConnection) url.openConnection();connection.setRequestMethod("GET");// 4. 設置請求頭connection.setRequestProperty("client_id", clientId);connection.setRequestProperty("productId", projectId);connection.setRequestProperty("Authorization", "Bearer " + getAccessToken());connection.setRequestProperty("X-Agc-Trace-Id", UUID.randomUUID().toString());// 5. 設置Range頭部(如果存在)if (originalHeaders.containsKey(HttpHeaders.RANGE)) {String range = originalHeaders.getRange().get(0).toString();connection.setRequestProperty("Range", range);}// 6. 連接并檢查響應connection.connect();int status = connection.getResponseCode();if (status >= 300) {log.error("華為云下載失敗: {}", status);throw new RuntimeException("下載失敗,狀態碼: " + status);}// 7. 獲取輸入流input = connection.getInputStream();// 8. 流式傳輸數據byte[] buffer = new byte[8192];int bytesRead;while ((bytesRead = input.read(buffer)) != -1) {try {outputStream.write(buffer, 0, bytesRead);} catch (IOException e) {// 處理客戶端斷開連接log.warn("客戶端可能已斷開連接: {}", e.getMessage());break;}}outputStream.flush();} catch (Exception e) {log.error("下載處理失敗", e);throw new RuntimeException(e);} finally {// 9. 確保關閉資源if (input != null) {try {input.close();} catch (IOException e) {log.warn("關閉輸入流失敗", e);}}if (connection != null) {connection.disconnect();}}};// 10. 設置響應頭HttpHeaders responseHeaders = new HttpHeaders();responseHeaders.setContentType(MediaType.APPLICATION_OCTET_STREAM);responseHeaders.setContentDispositionFormData("attachment", filename);// 11. 返回響應return ResponseEntity.ok().headers(responseHeaders).body(stream);} catch (Exception e) {log.error("代理下載失敗", e);return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();}}// 構建下載URLprivate String buildDownloadUrl(String type, String filename) {String domain = getDomainByRegion(region);return "https://" + domain + "/" + bucketName + "/" + type + "/" + filename;}// 獲取區域域名(與上傳服務相同)private String getDomainByRegion(String region) {Map<String, String> domainMap = Map.of("CN", "ops-server-drcn.agcstorage.link/v0","DE", "ops-server-dre.agcstorage.link/v0","SG", "ops-server-dra.agcstorage.link/v0","RU", "ops-server-drru.agcstorage.link/v0");String domain = domainMap.get(region.toUpperCase());if (domain == null) {throw new IllegalArgumentException("不支持的區域: " + region);}return domain;}// 獲取訪問令牌(與上傳服務相同)private String getAccessToken() {try {String authUrl = getAuthUrlByRegion(region);log.info("獲取華為云Token URL: {}", authUrl);// 讀取憑據文件String content = new String(Files.readAllBytes(Paths.get(credentialPath)));Map<String, String> credential = objectMapper.readValue(content, new TypeReference<Map<String, String>>() {});// 準備請求頭HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));// 準備請求體Map<String, String> requestBody = new HashMap<>();requestBody.put("grant_type", "client_credentials");requestBody.put("client_id", credential.get("client_id"));requestBody.put("client_secret", credential.get("client_secret"));// 創建請求實體HttpEntity<Map<String, String>> requestEntity = new HttpEntity<>(requestBody, headers);// 發送請求(使用exchange方法)ResponseEntity<Map> response = restTemplate.exchange(authUrl,HttpMethod.POST,requestEntity,Map.class);// 處理響應if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {String token = (String) response.getBody().get("access_token");log.info("成功獲取華為云Token");return token;} else {log.error("獲取華為云Token失敗,狀態碼: {}", response.getStatusCodeValue());throw new RuntimeException("獲取華為云Token失敗");}} catch (Exception e) {log.error("獲取華為云Token異常", e);throw new RuntimeException("獲取Token失敗", e);}}private String getAuthUrlByRegion(String region) {Map<String, String> authUrlMap = Map.of("CN", "https://connect-api.cloud.huawei.com/api/oauth2/v1/token","DE", "https://connect-api-dre.cloud.huawei.com/api/oauth2/v1/token","SG", "https://connect-api-dra.cloud.huawei.com/api/oauth2/v1/token","RU", "https://connect-api-drru.cloud.huawei.com/api/oauth2/v1/token");String url = authUrlMap.get(region.toUpperCase());if (url == null) {throw new IllegalArgumentException("不支持的區域: " + region);}return url;}
}

訪問云存儲與上傳service類似,主要在于下載的代碼,換了幾個傳輸流,都有bug。

圖文結合詳解(重點)

最后這個流式傳輸才沒有沖突,具體沒有細究。


?數據庫存儲的歌曲信息中包含了音頻url,封面url等

url為/upload/download/music/9fb58a26-a351-4d78-9d72-66f255d80f74.mp3,是用來請求后端的

再由后端進行訪問華為云

那為什么不把華為云接口直接存數據庫,這樣前端只要得到musicDetail就能直接去華為云拿數據了,不用經過后端來回倒騰了

因為華為云安全機制很嚴格,訪問接口需要攜帶三個必須請求頭:

Authorization

String

M

認證信息,格式為“Authorization: Bearer ${access_token}”。access_token為獲取Token中獲取的access_token。

client_id

String

M

客戶端ID,獲取方法參考創建API客戶端。

productId

String

M

項目ID,查詢方法可參見查詢項目ID。

只能通過后端添加請求頭來進行訪問。

一個小問題

開發途中又發現拖動播放進度條的功能受限。原因是

音頻文件不支持流式播放的進度跳轉(seekable)。當通過代理服務提供音頻流時,瀏覽器無法直接跳轉到音頻的中間位置,因為代理服務沒有正確支持 HTTP Range 請求(即支持部分內容請求)。解決方案

修改?HuaweiStorageDownloadService,添加對 Range 請求的完整支持

public ResponseEntity<StreamingResponseBody> downloadFile(String type,String filename,HttpHeaders originalHeaders) {try {// 1. 構建華為云下載URLString downloadUrl = buildDownloadUrl(type, filename);log.info("代理下載華為云文件: {}", downloadUrl);// 2. 創建HTTP連接URL url = new URL(downloadUrl);HttpURLConnection connection = (HttpURLConnection) url.openConnection();connection.setRequestMethod("GET");// 3. 設置認證頭connection.setRequestProperty("client_id", clientId);connection.setRequestProperty("productId", projectId);connection.setRequestProperty("Authorization", "Bearer " + getAccessToken());connection.setRequestProperty("X-Agc-Trace-Id", UUID.randomUUID().toString());// 4. 處理Range請求if (originalHeaders.containsKey(HttpHeaders.RANGE)) {String rangeHeader = originalHeaders.getFirst(HttpHeaders.RANGE);connection.setRequestProperty("Range", rangeHeader);}// 5. 連接服務器connection.connect();// 6. 獲取響應狀態碼int statusCode = connection.getResponseCode();// 7. 準備響應頭HttpHeaders responseHeaders = new HttpHeaders();// 8. 添加內容類型(根據文件類型)String contentType = getContentType(filename);responseHeaders.setContentType(MediaType.parseMediaType(contentType));// 9. 添加支持Range的響應頭responseHeaders.set("Accept-Ranges", "bytes");// 10. 處理部分內容響應(206)if (statusCode == HttpURLConnection.HTTP_PARTIAL) {String contentRange = connection.getHeaderField("Content-Range");responseHeaders.set("Content-Range", contentRange);}// 11. 添加內容長度(如果可用)String contentLength = connection.getHeaderField("Content-Length");if (contentLength != null) {responseHeaders.setContentLength(Long.parseLong(contentLength));}// 12. 創建流式響應體InputStream inputStream = connection.getInputStream();StreamingResponseBody stream = outputStream -> {try (inputStream) {byte[] buffer = new byte[8192];int bytesRead;while ((bytesRead = inputStream.read(buffer)) != -1) {outputStream.write(buffer, 0, bytesRead);}} catch (IOException e) {log.warn("客戶端可能已斷開連接: {}", e.getMessage());} finally {connection.disconnect();}};// 13. 返回響應return ResponseEntity.status(statusCode).headers(responseHeaders).body(stream);} catch (Exception e) {log.error("代理下載失敗", e);return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();}
}// 根據文件擴展名獲取內容類型private String getContentType(String filename) {String extension = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();switch (extension) {case "mp3": return "audio/mpeg";case "wav": return "audio/wav";case "ogg": return "audio/ogg";case "flac": return "audio/flac";case "m4a": return "audio/mp4";default: return "application/octet-stream";}}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/87823.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/87823.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/87823.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Petalinux工程如何離線編譯

目錄 一.下載離線包 1.1 共享狀態緩存包&#xff1a;sstate-cache 1.1.1 進入官網打開Petalinux工具網頁 1.1.2 找到相應的Petalinux版本 1.1.3 根據平臺下載 1.2 下載downloads源碼包 1.3 open_components源碼包 二.解壓 2.1 sstate-cache 2.2 downloads源碼包 2.3…

w446數字化農家樂管理平臺的設計與實現

&#x1f64a;作者簡介&#xff1a;多年一線開發工作經驗&#xff0c;原創團隊&#xff0c;分享技術代碼幫助學生學習&#xff0c;獨立完成自己的網站項目。 代碼可以查看文章末尾??聯系方式獲取&#xff0c;記得注明來意哦~&#x1f339;贈送計算機畢業設計600個選題excel文…

AWS WebRTC:通過shell分析viewer端日志文件

在并發過程中,每個viewer會產生一個對應的日志文件,日志文件名為: viewer_channel_index_20250626_030943_145.logviewer端日志比master端日志文件數量多,比例大概是5:1,有1個master就會有5個viewer,每個viewer對應一個日志文件。 我要統計的是從啟動viewer到出第一幀視…

時間轉換——借助時間模塊time

兩種時間戳類型 例如s11704879917000 1、13位的時間戳&#xff1a;單位&#xff08;毫秒&#xff09; &#xff08;1&#xff09;毫秒變成秒&#xff0c;1s1000ms&#xff0c;s1/1000&#xff08;秒&#xff09; &#xff08;2&#xff09;加載時間 times time.localtime(…

LabVIEW MathScript薄板熱流模擬

熱流模擬是熱設計關鍵環節&#xff0c;傳統工具精準但開發周期長&#xff0c;本 VI 利用 LabVIEW 優勢&#xff0c;面向工程師快速驗證需求&#xff0c;在初步方案迭代、教學演示等場景更具效率&#xff0c;為熱分析提供輕量化替代路徑&#xff0c;后續可結合專業工具&#xff…

為什么大語言模型訓練和推理中越來越多地使用 bfloat16?

隨著大語言模型&#xff08;LLM&#xff09;的參數規模從幾十億&#xff08;B&#xff09;飆升到千億&#xff08;T&#xff09;級別&#xff0c;模型的訓練與推理效率變得尤為關鍵。為了在保證精度的同時節省顯存、加快運算&#xff0c;混合精度訓練&#xff08;Mixed Precisi…

暴力破解漏洞與命令執行漏洞

在當今的互聯網世界中&#xff0c;網絡安全威脅無處不在。對于Java后端開發者而言&#xff0c;了解常見的Web漏洞及其防護措施至關重要。本文將探討兩種常見的安全漏洞&#xff1a;暴力破解漏洞&#xff08;Brute Force&#xff09;和命令執行漏洞&#xff08;Command Injectio…

HDFS Java API 開發指南:從基礎操作到高級應用

HDFS (Hadoop Distributed File System) 作為大數據生態的核心存儲系統&#xff0c;提供了分布式、高容錯、高吞吐量的數據存儲能力。通過 Java API 操作 HDFS 是開發大數據應用的基礎技能。本文將基于你的筆記&#xff0c;詳細解析 HDFS Java API 的使用方法&#xff0c;并提供…

區塊鏈技術核心組件及應用架構的全面解析

區塊鏈技術是一套融合密碼學、分布式系統與經濟激勵的復合型技術體系&#xff0c;以下是其核心組件及應用架構的全面解析&#xff1a;一、區塊鏈核心技術棧 1. 分布式賬本技術&#xff08;DLT&#xff09; 核心原理&#xff1a;多節點共同維護不可篡改的數據鏈數據結構&#xf…

golang 協程 如何中斷和恢復

Go語言通知協程退出(取消)的幾種方式 - 知乎 GoLang之goroutine底層系列二(goroutine的創建、讓出、恢復)_golang goroutine-CSDN博客 在 Go 語言中&#xff0c;協程&#xff08;也稱為 goroutine&#xff09;是通過 go 關鍵字啟動的輕量級線程。由于 goroutine 的調度是由 Go…

ARMv8 創建3級頁表示例

最近在研究arm v8頁表創建過程&#xff0c;順帶做了一個如下形式的頁表&#xff0c; // level 1 table, 4 entries: // 0000 0000 - 3FFF FFFF, 1GB block, DDR // 4000 0000 - 7FFF FFFF, 1GB block, DDR // 8000 0000 - BFFF FFFF, 1GB block, DDR // C000 0000 - FFFF FFFF…

遷港戰平 精神可勝國足

遷港戰平可勝國足 江蘇省城市足球聯賽第6輪&#xff0c;宿遷隊主場迎戰連云港隊。比賽中&#xff0c;宿遷隊由張棟和高馳各入一球&#xff0c;連云港隊則憑借穆家鑫與李團杰的進球連扳兩城。最終雙方以2比2握手言和。 第38分鐘&#xff0c;張棟角球進攻中無人盯防推射破門&…

408第三季part2 - 計算機網絡 - ip分布首部格式與分片

理解 好好看一下這個圖 每行是4B&#xff0c;首部也不一定是20B&#xff0c;還有可選字段&#xff0c;可以變的更大 然后我們先看一下概念 然后這個生存時間每路過一個路由器就會扣1滴血 比如一開始是13&#xff0c;經過r1r2r3到B會變成10 但如果是2&#xff0c;經過第二個路…

詳解String類不可變的底層原理

String類 String的基本特性 不可變性: String 對象一旦創建就不能被修改&#xff0c;所有看似修改的操作實際上都是創建新的 String 對象final類: String 類被聲明為 final&#xff0c;不能被繼承基于字符數組: 內部使用final char value[]存儲字符數據(Java9以后改為byte[] …

GIT: 一個用于視覺與語言的生成式圖像到文本轉換 Transformer

摘要 在本文中&#xff0c;我們設計并訓練了一個生成式圖像到文本轉換 Transformer——GIT&#xff0c;以統一視覺-語言任務&#xff0c;如圖像/視頻字幕生成和問答。雖然生成式模型在預訓練和微調之間提供了一致的網絡架構&#xff0c;但現有工作通常包含復雜的結構&#xff…

20250706-9-Docker快速入門(下)-Docker在線答疑_筆記

一、Kubernetes核心概念與集群搭建 1. 在線答疑 &#xfeff; 1&#xff09;答疑Docker需要掌握到什么程度 學習目標&#xff1a;達到入門水平即可&#xff0c;重點掌握第一章Docker入門視頻內容學習建議&#xff1a;預習時間約3-4小時&#xff0c;建議吸收視頻內容的80%學…

Node.js-http模塊

HTTP 協議 概念 HTTP&#xff08;hypertext transport protocol&#xff09;協議&#xff1b;中文叫超文本傳輸協議,是一種基于TCP/IP的應用層通信協議這個協議詳細規定了 瀏覽器 和萬維網 服務器 之間互相通信的規則。協議中主要規定了兩個方面的內容 客戶端&#xff1a;用來…

Java JDBC的初步了解

文章目錄 基本流程注冊驅動的兩種方法DriverManagerDriverManager 的核心作用核心原理自動注冊驅動的機制關鍵方法 示例代碼: 連接Mysql數據庫StatementPreparedStatement JDBC全稱Java DataBase Connectivity。 定義: JDBC 是 Java 語言中用于連接和執行 SQL 操作的標準接口。…

[netty5: ChunkedInput ChunkedWriteHandler]-源碼分析

ChunkedInput ChunkedInput<B> 是 Netty 中用于按塊讀取不定長數據流的接口&#xff0c;常配合 ChunkedWriteHandler 實現流式寫入&#xff0c;支持如文件、流、HTTP 和 WebSocket 等多種數據源。 實現類簡要說明ChunkedFile用于將常規文件按塊傳輸&#xff08;使用傳統…

QT 第十二講 --- 控件篇 LineEdit,TextEdit與ComboBox

前言&#xff1a;歡迎進入 QT 控件世界的第十二講&#xff01;在上一講《QT 第十一講 --- 控件篇 LCDnumber&#xff0c;ProgressBar與CalenderWidget》中&#xff0c;我們探索了用于信息展示和狀態反饋的控件&#xff1a;精準的數字顯示器 LCD Number、直觀的進度指示器 Progr…