Kotlin+SpringBoot與Minio存儲(chǔ)整合詳解

1地梨、Minio安裝

請(qǐng)看我之前撰寫的 Docker安裝Minio存儲(chǔ)服務(wù)器詳解

2、工程架構(gòu)模式

請(qǐng)看我之前撰寫的 Kotlin +SpringBoot + MyBatis完美搭建最簡(jiǎn)潔最酷的前后端分離框架進(jìn)行完善

3 、依賴jar

<dependency>
    <groupId>io.minio</groupId>
    <artifactId>minio</artifactId>
    <version>3.0.7</version>
</dependency>

4蔓姚、自動(dòng)裝配

  • 在resource在源文件里面添加一個(gè)spring.factories配置文件,配置代碼如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.flong.kotlin.core.minio.MinioAutoConfiguration

5、properties配置minio信息

# minio服務(wù)器url
minio.url=http://192.168.1.133:9000
# minio安裝指定的訪問(wèn)key
minio.accessKey=admin
# minio安裝指定的訪問(wèn)秘鑰
minio.secretKey=admin123456
# minio的啟用配置
minio.endpoint.enable=true

6、工程核心代碼如下

  • MinioAutoConfiguration 自動(dòng)裝配類

@EnableConfigurationProperties(MinioProperties::class)
open class MinioAutoConfiguration {
    @Autowired lateinit var minioProperties: MinioProperties;
    
    @Bean
    @ConditionalOnMissingBean(MinioTemplate::class)
    @ConditionalOnProperty(name = arrayOf("minio.url"))
    open fun minioTemplate():MinioTemplate {
        return MinioTemplate(minioProperties.url,
                minioProperties.accessKey,
                minioProperties.secretKey)
    }
}
  • MinioProperties 配置屬性類
/*
 * Value注解的使用
 * https://segmentfault.com/a/1190000010978025
 * https://blog.jetbrains.com/idea/2018/10/spring-kotlin-references-in-value-annotation/
 */
@Configuration
@ConfigurationProperties(prefix = "minio")
open class MinioProperties {
    /**
     * minio 服務(wù)地址 http://ip:port
     */
    var url:String=""
    /**
     * 用戶名
     */
    var accessKey:String=""
    /**
     * 密碼
     */
    var secretKey:String =""
}
  • MinioTemplate 連接minio服務(wù)器模板工具類
/**
 * MinioTemplate連接工具
 * Linux的時(shí)區(qū)與minio容器的時(shí)區(qū)不一致導(dǎo)致的異常
 * ErrorResponse(code=RequestTimeTooSkewed, message=The difference between the request time and the server's time is too large.,
 * bucketName=null, objectName=null, resource=/kotlin, requestId=null, hostId=null)
 */
@Component
@Configuration
@EnableConfigurationProperties(MinioProperties::class)
open class MinioTemplate  {
    var url:String       = ""
    var accessKey:String = ""
    var secretKey:String = ""
    //constructor無(wú)參數(shù)構(gòu)造方法是為了解決以下錯(cuò)誤:
    //Unsatisfied dependency expressed through constructor parameter 0; nested exception is
    //org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.lang.String'
    //available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
    constructor()
    
    //有參數(shù)的構(gòu)造方法
    constructor(url:String , accessKey:String, secretKey:String){
        this.url = url
        this.accessKey = accessKey
        this.secretKey = secretKey
    }
    
    @Autowired lateinit var minioProperties: MinioProperties
    
    open fun getMinioClient(): MinioClient {
        return MinioClient(minioProperties.url,minioProperties.accessKey,minioProperties.secretKey)
    }
    
    /**
     * 創(chuàng)建bucket
     * @param bucketName bucket名稱
     * @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#makeBucket
     */
    fun createBucket(bucketName: String) {
        var client = getMinioClient()
        if (!client.bucketExists(bucketName)) {
            client.makeBucket(bucketName)
        }
    }

    /**
     * 獲取全部bucket
     * <p>
     * https://docs.minio.io/cn/java-client-api-reference.html#listBuckets
     */
    fun getAllBuckets(): List<Bucket> {
        return getMinioClient().listBuckets()
    }

    /**
     * @param bucketName bucket名稱
     * @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#listBuckets
     */
    fun getBucket(bucketName: String): Optional<Bucket> {
        return getMinioClient().listBuckets().stream().filter({ b -> b.name().equals(bucketName) }).findFirst()
    }
    /**
     * @param bucketName bucket名稱
     * @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#removeBucket
     */
    fun removeBucket(bucketName: String) {
        getMinioClient().removeBucket(bucketName)
    }
    /**
     * 根據(jù)文件前置查詢文件
     * @param bucketName bucket名稱
     * @param prefix     前綴
     * @param recursive  是否遞歸查詢
     * @return
     * @throws Exception
     */
    fun getAllObjectsByPrefix(bucketName: String, prefix: String, recursive: Boolean): List<Item> {
        var objectList = ArrayList<Item>()
        var objectsIterator = getMinioClient().listObjects(bucketName, prefix, recursive)
        while (objectsIterator.iterator().hasNext()) {
            objectList.add(objectsIterator.iterator().next().get())
        }
        return objectList
    }
    
    /**
     * 獲取文件外鏈
     * @param bucketName bucket名稱
     * @param objectName 文件名稱
     * @param expires    過(guò)期時(shí)間 <=7
     * @return url
     * @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#getObject
     */
    fun getObjectURL(bucketName:String , objectName:String , expires:Int ) :String {
        return getMinioClient().presignedGetObject(bucketName, objectName, expires)
    }
    
    /**
     * 獲取文件
     * @param bucketName bucket名稱
     * @param objectName 文件名稱
     * @return 二進(jìn)制流
     * @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#getObject
     */
    fun getObject(bucketName:String , objectName:String ) :InputStream{
        return getMinioClient().getObject(bucketName, objectName)
    }
    
    /**
     * 上傳文件
     * @param bucketName bucket名稱
     * @param objectName 文件名稱
     * @param stream     文件流
     * @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#putObject
     */
    fun putObject(bucketName:String , objectName:String , stream:InputStream)  {
        //假設(shè)不存在 bucket名稱轧拄,程序創(chuàng)建 bucket名稱,不需要手動(dòng)創(chuàng)建
        createBucket(bucketName)
        getMinioClient().putObject(bucketName, objectName, stream, stream.available().toLong(), "application/octet-stream");
    }
    /**
     * 上傳文件
     * @param bucketName  bucket名稱
     * @param objectName  文件名稱
     * @param stream      文件流
     * @param size        大小
     * @param contextType 類型
     * @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#putObject
     */
    fun putObject(bucketName:String, objectName:String, stream:InputStream, size:Long,contextType:String ):Unit {
        //假設(shè)不存在 bucket名稱,程序創(chuàng)建 bucket名稱,不需要手動(dòng)創(chuàng)建
        createBucket(bucketName)
        getMinioClient().putObject(bucketName, objectName, stream, size, contextType)
    }
    
    /**
     * 獲取文件信息
     * @param bucketName bucket名稱
     * @param objectName 文件名稱
     * @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#statObject
     */
    fun getObjectInfo(bucketName:String , objectName:String ) : ObjectStat {
        return getMinioClient().statObject(bucketName, objectName)
    }
    
    /**
     * 刪除文件
     * @param bucketName bucket名稱
     * @param objectName 文件名稱
     * @throws Exception https://docs.minio.io/cn/java-client-api-reference.html#removeObject
     */
    fun removeObject(bucketName:String ,objectName : String ) :Unit {
        getMinioClient().removeObject(bucketName, objectName)
    }

}

注意點(diǎn)1: Linux的時(shí)區(qū)與minio容器的時(shí)區(qū)不一致導(dǎo)致的異常
ErrorResponse(code=RequestTimeTooSkewed, message=The difference between the request time and the server's time is too large., bucketName=null, objectName=null, resource=/kotlin, requestId=null, hostId=null)


【特別說(shuō)明】解決Linux的時(shí)區(qū)與minio容器的時(shí)區(qū)不一致

yum -y install utp ntpdate
  • 設(shè)置系統(tǒng)時(shí)間與網(wǎng)絡(luò)時(shí)間同步
ntpdate cn.pool.ntp.org
  • 將系統(tǒng)時(shí)間寫入硬件時(shí)間
hwclock --systohc
  • 設(shè)置系統(tǒng)時(shí)區(qū)為上海
timedatectl set-timezone Asia/Shanghai 

注意點(diǎn)2 constructor無(wú)參數(shù)構(gòu)造方法是為了解決以下錯(cuò)誤:
Unsatisfied dependency expressed through constructor parameter 0; nested exception is
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.lang.String' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

constructor()
  • Spring控制層測(cè)試類
package com.flong.kotlin.modules.controller
@ConditionalOnProperty(name = arrayOf("minio.endpoint.enable"), havingValue = "true")
@RestController
@RequestMapping("/rest")
class MinioController : BaseController() {
    @Autowired private lateinit var template: MinioTemplate

    //常量
    companion object MinioConstant {
        const val fileName = "kotlin.xls"
        const val bucketName = "kotlin"
    }

    /**
     * Bucket Endpoints
     */
    @PostMapping("/bucket/{bucketName}")
    fun createBucker(@PathVariable bucketName: String): Bucket {
        print("bucketName=" + bucketName)
        template.createBucket(bucketName)
        return template.getBucket(bucketName).get()
    }

    @GetMapping("/bucket")
    fun getBuckets(): List<Bucket> {
        return template.getAllBuckets()
    }
    @GetMapping("/bucket/{bucketName}")
    fun getBucket(@PathVariable bucketName: String): Bucket {
        return template.getBucket(bucketName).orElseThrow({ IllegalStateException("Bucket Name not found!") })
    }

    @DeleteMapping("/bucket/{bucketName}")
    @ResponseStatus(HttpStatus.ACCEPTED)
    fun deleteBucket(@PathVariable bucketName: String) {
        template.removeBucket(bucketName)
    }

    /**
     * Object Endpoints
     */
    @PostMapping("/object/{bucketName}")
    fun createObject(@RequestBody file: MultipartFile, @PathVariable bucketName: String): MinioObject {
        var name = file.getOriginalFilename()
        template.putObject(bucketName, name, file.getInputStream(), file.getSize(), file.getContentType())
        return MinioObject(template.getObjectInfo(bucketName, name))

    }

    @PostMapping("/object/{bucketName}/{objectName}")
    fun createObject(@RequestBody file: MultipartFile, @PathVariable bucketName: String, @PathVariable objectName: String): MinioObject {
        template.putObject(bucketName, objectName, file.getInputStream(), file.getSize(), file.getContentType())
        return MinioObject(template.getObjectInfo(bucketName, objectName))
    }

    @GetMapping("/object/{bucketName}/{objectName}")
    fun filterObject(@PathVariable bucketName: String, @PathVariable objectName: String): List<Item> {
        return template.getAllObjectsByPrefix(bucketName, objectName, true)
    }

    @GetMapping("/object/{bucketName}/{objectName}/{expires}")
    fun getObject(@PathVariable bucketName: String, @PathVariable objectName: String, @PathVariable expires: Int): Map<String, Any> {
        var responseBody = HashMap<String, Any>()
        // Put Object info
        responseBody.put("bucket", bucketName)
        responseBody.put("object", objectName)
        responseBody.put("url", template.getObjectURL(bucketName, objectName, expires))
        responseBody.put("expires", expires)
        return responseBody
    }

    @DeleteMapping("/object/{bucketName}/{objectName}/")
    @ResponseStatus(HttpStatus.ACCEPTED)
    fun deleteObject(@PathVariable bucketName: String, @PathVariable objectName: String) {
        template.removeObject(bucketName, objectName)
    }

    /*
     *下載文件
     */
    @GetMapping("/downloadFile")
    fun downloadFile(response: HttpServletResponse) {
        var fileName = MinioConstant.fileName
        var bucketName = MinioConstant.bucketName
        var inputStream = template.getObject(bucketName, fileName)
        IoUtils.downloadFile(response, fileName, "UTF-8", inputStream) //從minio下載
    }

    /*
     *上傳文件
     */
    @PostMapping("/upload")
    fun upload(@RequestParam("file") file: MultipartFile) : LiveResp<Any>{
        var originalFilename = file.getOriginalFilename();
        //隨機(jī)數(shù)和源文件名稱
        var newFilename = UUID.randomUUID().toString().replace("-", "") + originalFilename

        var resultMap = HashMap<String, String>(4);
        resultMap.put("bucketName", MinioConstant.bucketName);
        resultMap.put("fileName", newFilename);
        template.putObject(MinioConstant.bucketName, newFilename, file.getInputStream())
        return LiveResp(resultMap);
        
    }

}

常量值可以根據(jù)自己實(shí)際情況進(jìn)行修改測(cè)試讽膏,這里以一個(gè)圖片進(jìn)行測(cè)試檩电。

companion object MinioConstant {
    const val fileName = "kotlin.xls"
    const val bucketName = "kotlin"
}
  • 運(yùn)行Application.kt并訪問(wèn)測(cè)試
訪問(wèn)路徑:http://localhost:8080/rest/downloadFile
  • 運(yùn)行結(jié)果

7、工程源代碼

工程源代碼在dev-minio分支上

8 府树、總結(jié)與建議

1 俐末、以上問(wèn)題根據(jù)搭建 kotlin與Minio 實(shí)際情況進(jìn)行總結(jié)整理,除了技術(shù)問(wèn)題查很多網(wǎng)上資料,通過(guò)自身進(jìn)行學(xué)習(xí)之后梳理與分享奄侠。

2卓箫、 在學(xué)習(xí)過(guò)程中也遇到很多困難和疑點(diǎn),如有問(wèn)題或誤點(diǎn)垄潮,望各位老司機(jī)多多指出或者提出建議烹卒。本人會(huì)采納各種好建議和正確方式不斷完善現(xiàn)況,人在成長(zhǎng)過(guò)程中的需要優(yōu)質(zhì)的養(yǎng)料弯洗。

3旅急、 希望此文章能幫助各位老鐵們更好去了解如何在 kotlin上搭建Minio,也希望也希望您看了此文檔或者通過(guò)找資料親身經(jīng)歷實(shí)操學(xué)習(xí)效果會(huì)更好牡整。

備注:此文章屬于本人原創(chuàng),歡迎轉(zhuǎn)載和收藏.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末藐吮,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子果正,更是在濱河造成了極大的恐慌炎码,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秋泳,死亡現(xiàn)場(chǎng)離奇詭異潦闲,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)迫皱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門歉闰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)辖众,“玉大人,你說(shuō)我怎么就攤上這事和敬“颊ǎ” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵昼弟,是天一觀的道長(zhǎng)啤它。 經(jīng)常有香客問(wèn)我,道長(zhǎng)舱痘,這世上最難降的妖魔是什么变骡? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮芭逝,結(jié)果婚禮上塌碌,老公的妹妹穿的比我還像新娘。我一直安慰自己旬盯,他們只是感情好台妆,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著胖翰,像睡著了一般接剩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上泡态,一...
    開(kāi)封第一講書(shū)人閱讀 49,950評(píng)論 1 291
  • 那天搂漠,我揣著相機(jī)與錄音,去河邊找鬼某弦。 笑死,一個(gè)胖子當(dāng)著我的面吹牛而克,可吹牛的內(nèi)容都是我干的靶壮。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼员萍,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼腾降!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起碎绎,我...
    開(kāi)封第一講書(shū)人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤螃壤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后筋帖,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體奸晴,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年日麸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了寄啼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖墩划,靈堂內(nèi)的尸體忽然破棺而出涕刚,到底是詐尸還是另有隱情,我是刑警寧澤乙帮,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布杜漠,位于F島的核電站,受9級(jí)特大地震影響察净,放射性物質(zhì)發(fā)生泄漏碑幅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一塞绿、第九天 我趴在偏房一處隱蔽的房頂上張望沟涨。 院中可真熱鬧,春花似錦异吻、人聲如沸裹赴。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)棋返。三九已至,卻和暖如春雷猪,著一層夾襖步出監(jiān)牢的瞬間睛竣,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工求摇, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留射沟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓与境,卻偏偏與公主長(zhǎng)得像验夯,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子摔刁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

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