SpringBoot實(shí)現(xiàn)azure blob的文件上傳

Azure Blob 存儲(chǔ)是 Microsoft 提供的適用于云的對(duì)象存儲(chǔ)解決方案。 Blob 存儲(chǔ)最適合存儲(chǔ)巨量的非結(jié)構(gòu)化數(shù)據(jù)

準(zhǔn)備

Azure 訂閱

點(diǎn)擊創(chuàng)建免費(fèi)帳戶,選擇免費(fèi)開始薯演,使用微軟賬戶注冊(cè)訂閱后即可試用12個(gè)月

Azure 存儲(chǔ)帳戶

點(diǎn)擊創(chuàng)建存儲(chǔ)帳戶毯侦,根據(jù)教程即可創(chuàng)建一個(gè)存儲(chǔ)賬戶,若沒有安裝azure cli砖茸,推薦直接參考【門戶網(wǎng)站】一欄

Azure門戶憑據(jù)

  1. 登錄到 Azure 門戶酵镜。
  2. 找到自己的存儲(chǔ)帳戶碉碉。
  3. 在存儲(chǔ)帳戶概述的“設(shè)置”部分,選擇“訪問密鑰”淮韭。 在這里垢粮,可以查看你的帳戶訪問密鑰以及每個(gè)密鑰的完整連接字符串。
  4. 找到“密鑰 1”下面的“連接字符串”值靠粪,選擇“復(fù)制”按鈕復(fù)制該連接字符串蜡吧。 下一步需將此連接字符串值添加到某個(gè)環(huán)境變量。
image

開發(fā)步驟

配置

  • 引入依賴
<!--lombok -->
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <optional>true</optional>
</dependency>

<!--azure storage -->
<dependency>
    <groupId>com.microsoft.azure</groupId>
    <artifactId>azure-storage-spring-boot-starter</artifactId>
    <version>0.2.0</version>
</dependency>
  • 屬性配置
spring:
  servlet:      
    multipart:  # spring mvc文件上傳
      max-request-size: 10MB
      max-file-size: 1MB

azure:
  storage: # azure儲(chǔ)存配置
    default-endpoints-protocol: https
    account-name: [account-name]
    account-key: [account-key]
    endpoint-suffix: [endpoint-suffix]
    container-reference: [container-reference]  # 容器名稱
    generate-thumbnail: false  # 生成縮略圖

代碼編寫

  • 根據(jù)屬性編寫對(duì)應(yīng)參數(shù)類
@Data
@Component
public class AzureStorageParam {

    @Value("${azure.storage.default-endpoints-protocol}")
    private String defaultEndpointsProtocol;

    @Value("${azure.storage.account-name}")
    private String accountName;

    @Value("${azure.storage.account-key}")
    private String accountKey;

    @Value("${azure.storage.endpoint-suffix}")
    private String endpointSuffix;

    @Value("${azure.storage.container-reference}")
    private String containerReference;

    /**
     * 拼接連接字符串
     */
    public String getStorageConnectionString() {
        String storageConnectionString =
            String.format("DefaultEndpointsProtocol=%s;AccountName=%s;AccountKey=%s;EndpointSuffix=%s",
                defaultEndpointsProtocol, accountName, accountKey, endpointSuffix);
        return storageConnectionString;
    }
}
  • 編寫文件上傳的返回模型
@Data
@Accessors(chain = true)
public class BlobUpload {
    // 文件名
    private String fileName;

    // 原文件
    private String fileUrl;

    // 縮略圖
    private String thumbnailUrl;
}
  • 工具類
/**
 * 獲取blob container
 * 
 * @param storageConnectionString
 * @param containerReference
 * @return
 */
public static CloudBlobContainer getAzureContainer(String storageConnectionString, String containerReference) {
    CloudStorageAccount storageAccount;
    CloudBlobClient blobClient = null;
    CloudBlobContainer container = null;
    try {
        storageAccount = CloudStorageAccount.parse(storageConnectionString);
        blobClient = storageAccount.createCloudBlobClient();
        container = blobClient.getContainerReference(containerReference);

        container.createIfNotExists(BlobContainerPublicAccessType.CONTAINER, new BlobRequestOptions(),
            new OperationContext());
        return container;
    } catch (Exception e) {
        logger.error("獲取azure container異常: [{}]" , e.getMessage());
    }
    return null;
}
  • 編寫文件上傳的業(yè)務(wù)層接口
public interface IAzureStorageService {

    /**
     * 上傳文件(圖片)
     * @param type 文件類型
     * @param multipartFiles 文件
     * @return
     */
    BaseResult<Object> uploadFile(String type, MultipartFile[] multipartFiles);
}
  • 實(shí)現(xiàn)類
@Service
public class AzureStorageServiceImpl implements IAzureStorageService {
    // 設(shè)置縮略圖的寬高
    private static int thumbnailWidth = 150;
    private static int thumbnailHeight = 100;
    private static String thumbnailPrefix = "mini_";
    private static String originPrefix = "FAQ_";
    private final Logger logger = LoggerFactory.getLogger(AzureStorageServiceImpl.class);

    @Value("{azure.storage.generate-thumbnail}")
    private String generateThumbnail;

    @Autowired
    private AzureStorageParam azureStorageParam;

    @Override
    public BaseResult<Object> uploadFile(String type, MultipartFile[] multipartFiles) {
        // 校驗(yàn)圖片
        if (hasInvalidPic(multipartFiles)) {
            return BaseResult.error("包含非法圖片格式");
        }

        List<BlobUpload> blobUploadEntities = new ArrayList<>();

        // 獲取blob容器
        CloudBlobContainer container = AzureStorageUtil.getAzureContainer(
            azureStorageParam.getStorageConnectionString(), azureStorageParam.getContainerReference());
        if (container == null) {
            logger.error("獲取azure container異常");
            return BaseResult.error("獲取容器失敗");
        }
        try {
            for (MultipartFile tempMultipartFile : multipartFiles) {
                try {
                    // 將 blob 上傳到容器
                    String contentType = tempMultipartFile.getContentType().toLowerCase();
                    if (!contentType.equals("image/jpg") && !contentType.equals("image/jpeg")
                        && !contentType.equals("image/png")) {
                        return BaseResult.error("not pic");
                    }
                    // 時(shí)間+隨機(jī)數(shù)+文件擴(kuò)展名
                    String picType = contentType.split("/")[1];
                    String timeStamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
                    int number = (int)((Math.random() * 9) * 1000);
                    String referenceName = originPrefix + timeStamp + number + "." + picType;
                    CloudBlockBlob blob = container.getBlockBlobReference(referenceName);
                    blob.getProperties().setContentType(tempMultipartFile.getContentType());
                    blob.upload(tempMultipartFile.getInputStream(), tempMultipartFile.getSize());

                    // 返回圖片URL
                    BlobUpload blobUploadEntity = new BlobUpload();
                    blobUploadEntity.setFileName(tempMultipartFile.getOriginalFilename())
                        .setFileUrl(blob.getUri().toString());

                    // 生成縮略圖
                    if ("true".equalsIgnoreCase(generateThumbnail)) {
                        BufferedImage img =
                            new BufferedImage(thumbnailWidth, thumbnailHeight, BufferedImage.TYPE_INT_RGB);
                        BufferedImage read = ImageIO.read(tempMultipartFile.getInputStream());
                        img.createGraphics().drawImage(
                            read.getScaledInstance(thumbnailWidth, thumbnailHeight, Image.SCALE_SMOOTH), 0, 0, null);
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        ImageIO.write(img, "jpg", baos);
                        InputStream bais = new ByteArrayInputStream(baos.toByteArray());

                        String blobThumbnail = originPrefix + thumbnailPrefix + timeStamp + number + ".jpg";
                        CloudBlockBlob thumbnailBlob = container.getBlockBlobReference(blobThumbnail);
                        thumbnailBlob.getProperties().setContentType("image/jpeg");
                        thumbnailBlob.upload(bais, baos.toByteArray().length);

                        blobUploadEntity.setFileUrl(blob.getUri().toString())
                            .setThumbnailUrl(thumbnailBlob.getUri().toString());

                        // 關(guān)閉流
                        baos.close();
                        bais.close();
                    }
                    blobUploadEntities.add(blobUploadEntity);
                } catch (Exception e) {
                    logger.error("上傳[{}]時(shí)出現(xiàn)異常:[{}]", tempMultipartFile.getOriginalFilename(), e.getMessage());
                    return BaseResult.error("上傳出現(xiàn)異常占键,請(qǐng)稍后再試");
                }
            }
            return BaseResult.success(blobUploadEntities);
        } catch (Exception e) {
            logger.error("上傳文件出現(xiàn)異常: [{}]", e.getMessage());
        }
        return BaseResult.error("上傳出現(xiàn)異常昔善,請(qǐng)稍后再試");
    }

    /**
     * 判斷批量文件中是否都為圖片
     */
    private boolean hasInvalidPic(MultipartFile[] multipartFiles) {
        List<String> picTypeList = Arrays.asList("image/jpg", "image/jpeg", "image/png");
        return Arrays.stream(multipartFiles).anyMatch(i -> !picTypeList.contains(i.getContentType().toLowerCase()));
    }
}
  • mvc控制器
@RestController
public class UploadController {
    private static final Logger logger = LoggerFactory.getLogger(UploadController.class);

    @Autowired
    AzureStorageServiceImpl azureStorageService;

    /**
     * 文件上傳(圖片)
     * 
     * @param multipartFiles
     * @return
     */
    @PostMapping("/upload")
    public BaseResult<Object> upload(@RequestPart("file") MultipartFile[] multipartFiles) {
        logger.info("開始文件上傳...");
        if (multipartFiles == null || multipartFiles.length == 0) {
            return BaseResult.error("上傳失敗,請(qǐng)選擇文件");
        }

        return azureStorageService.uploadFile("PICTURE", multipartFiles);
    }
}

實(shí)列測(cè)試

使用postman測(cè)試

【請(qǐng)求體】-【form-data】-【key=file】畔乙,然后從本地選擇若干圖片

image
  • 復(fù)制圖片url即可查看圖片
image
  • 查看blob容器,即可以看到最新上傳的文件
image

使用表單提交測(cè)試

為方便測(cè)試君仆,直接使用thymeleaf模板進(jìn)行頁(yè)面上傳

  • 引入依賴
<!-- thymeleaf -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  • 簡(jiǎn)易頁(yè)面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>多文件上傳</title>
</head>
<body>
<form method="post" action="/upload" enctype="multipart/form-data">
    <input type="file" name="file"><br>
    <input type="file" name="file"><br>
    <input type="file" name="file"><br>
    <input type="submit" value="提交">
</form>
</body>
</html>
  • 控制器添加跳轉(zhuǎn)請(qǐng)求
@Controller
public class UploadController {
    private static final Logger logger = LoggerFactory.getLogger(UploadController.class);

    @Autowired
    AzureStorageServiceImpl azureStorageService;

    /**
     * 文件上傳,跳轉(zhuǎn)使用
     */
    @GetMapping("/upload")
    public String upload() {
        return "upload";
    }

    /**
     * 文件上傳(圖片)
     */
    @PostMapping("/upload")
    @ResponseBody
    public BaseResult<Object> upload(@RequestPart("file") MultipartFile[] multipartFiles) {
        logger.info("開始文件上傳...");
        if (multipartFiles == null || multipartFiles.length == 0) {
            return BaseResult.error("上傳失敗,請(qǐng)選擇文件");
        }

        return azureStorageService.uploadFile("PICTURE", multipartFiles);
    }
}
image
  • 上傳成功
image

詳細(xì)過程返咱,可參考源代碼:https://github.com/chetwhy/cloud-flow


參考文章:

Azure Blob 存儲(chǔ)文檔

Spring Boot實(shí)戰(zhàn)之文件上傳存入Azure Storage

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市牍鞠,隨后出現(xiàn)的幾起案子咖摹,更是在濱河造成了極大的恐慌,老刑警劉巖难述,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件萤晴,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡龄广,警方通過查閱死者的電腦和手機(jī)硫眯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來择同,“玉大人,你說我怎么就攤上這事净宵∏貌牛” “怎么了裹纳?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)紧武。 經(jīng)常有香客問我剃氧,道長(zhǎng),這世上最難降的妖魔是什么阻星? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任朋鞍,我火速辦了婚禮,結(jié)果婚禮上妥箕,老公的妹妹穿的比我還像新娘滥酥。我一直安慰自己,他們只是感情好畦幢,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布坎吻。 她就那樣靜靜地躺著,像睡著了一般宇葱。 火紅的嫁衣襯著肌膚如雪瘦真。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天黍瞧,我揣著相機(jī)與錄音诸尽,去河邊找鬼。 笑死印颤,一個(gè)胖子當(dāng)著我的面吹牛您机,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播膀哲,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼往产,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了某宪?” 一聲冷哼從身側(cè)響起仿村,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎兴喂,沒想到半個(gè)月后蔼囊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡衣迷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年畏鼓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片壶谒。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡云矫,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出汗菜,到底是詐尸還是另有隱情让禀,我是刑警寧澤挑社,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站巡揍,受9級(jí)特大地震影響痛阻,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜腮敌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一阱当、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧糜工,春花似錦弊添、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至钮莲,卻和暖如春免钻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背崔拥。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工极舔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人链瓦。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓拆魏,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親慈俯。 傳聞我的和親對(duì)象是個(gè)殘疾皇子渤刃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容