1. 程式人生 > 其它 >`spring-boot-data-mongodb` 動態建立 `bucketName`

`spring-boot-data-mongodb` 動態建立 `bucketName`

當專案使用MongoDB儲存檔案的時候,小於16M是可以直接存在集合裡面的,如果大於就需要用到GridFs來儲存了。

小於16M儲存的集合是可以執行時動態建立的,但是大於不可以。GridFs預設的儲存桶的名是fs來定義的。具體可參考GridFSBucketImpl實現類。這裡對這個類不做過多介紹。這裡主要介紹如何在程式執行時動態建立 bucketName

  1. 先看下spring-boot-data-mongodb提供的模板類GridFsTemplate中具體方法。這裡主要分析儲存和查詢的方法,該類的程式碼如下,該類有很多store過載方法

    public class GridFsTemplate extends GridFsOperationsSupport implements GridFsOperations, ResourcePatternResolver {
    
        private final MongoDbFactory dbFactory;
    
        private final @Nullable
        String bucket;
    
    
        public GridFsTemplate(MongoDbFactory dbFactory, MongoConverter converter) {
            this(dbFactory, converter, null);
        }
    
    
        public GridFsTemplate(MongoDbFactory dbFactory, MongoConverter converter, @Nullable String bucket) {
    
            super(converter);
    
            Assert.notNull(dbFactory, "MongoDbFactory must not be null!");
    
            this.dbFactory = dbFactory;
            this.bucket = bucket;
        }
    
    
        public ObjectId store(InputStream content, String filename) {
            return store(content, filename, (Object) null);
        }
    
    
        @Override
        public ObjectId store(InputStream content, @Nullable Object metadata) {
            return store(content, null, metadata);
        }
    
    
        @Override
        public ObjectId store(InputStream content, @Nullable Document metadata) {
            return store(content, null, metadata);
        }
    
    
        public ObjectId store(InputStream content, @Nullable String filename, @Nullable String contentType) {
            return store(content, filename, contentType, (Object) null);
        }
    
    
        public ObjectId store(InputStream content, @Nullable String filename, @Nullable Object metadata) {
            return store(content, filename, null, metadata);
        }
    
    
        public ObjectId store(InputStream content, @Nullable String filename, @Nullable String contentType,
                              @Nullable Object metadata) {
            return store(content, filename, contentType, toDocument(metadata));
        }
    
    
        public ObjectId store(InputStream content, @Nullable String filename, @Nullable Document metadata) {
            return this.store(content, filename, null, metadata);
        }
    
        /**
         * 主要儲存實現
         */
        public ObjectId store(InputStream content, @Nullable String filename, @Nullable String contentType,
                              @Nullable Document metadata) {
    
            Assert.notNull(content, "InputStream must not be null!");
            // 主要getGridFs 返回一個 GridFSBucket,這個接口裡面抽象大量的CURD的方法。具體你們可以看看
            return getGridFs().uploadFromStream(filename, content, computeUploadOptionsFor(contentType, metadata));
        }
    
    
        public GridFSFindIterable find(Query query) {
    
            Assert.notNull(query, "Query must not be null!");
    
            Document queryObject = getMappedQuery(query.getQueryObject());
            Document sortObject = getMappedQuery(query.getSortObject());
    
            return getGridFs().find(queryObject).sort(sortObject);
        }
    
       
        public GridFSFile findOne(Query query) {
            return find(query).first();
        }
    
    
        public void delete(Query query) {
    
            for (GridFSFile gridFSFile : find(query)) {
                getGridFs().delete(gridFSFile.getId());
            }
        }
    
    
        public ClassLoader getClassLoader() {
            return dbFactory.getClass().getClassLoader();
        }
    
     
        public GridFsResource getResource(String location) {
    
            return Optional.ofNullable(findOne(query(whereFilename().is(location)))) 
                    .map(this::getResource) 
                    .orElseGet(() -> GridFsResource.absent(location));
        }
    
        public GridFsResource getResource(GridFSFile file) {
    
            Assert.notNull(file, "GridFSFile must not be null!");
    
            return new GridFsResource(file, getGridFs().openDownloadStream(file.getId()));
        }
    
        public GridFsResource[] getResources(String locationPattern) {
    
            if (!StringUtils.hasText(locationPattern)) {
                return new GridFsResource[0];
            }
    
            AntPath path = new AntPath(locationPattern);
    
            if (path.isPattern()) {
    
                GridFSFindIterable files = find(query(whereFilename().regex(path.toRegex())));
                List<GridFsResource> resources = new ArrayList<>();
    
                for (GridFSFile file : files) {
                    resources.add(getResource(file));
                }
    
                return resources.toArray(new GridFsResource[0]);
            }
    
            return new GridFsResource[]{getResource(locationPattern)};
        }
    
        /**
         * 這個方法是最重要的
         */
        private GridFSBucket getGridFs() {
    
            MongoDatabase db = dbFactory.getDb();
            /* 
            * 判斷 bucket 是否為null,是建立預設,否則根據傳入bucket的建立GridFSBucket物件,預設建立的GridFsTemplate是不傳入bucket
            * 具體可以參考MongoDbFactoryDependentConfiguration該類的bean建立實現
            * 
            * */
            return bucket == null ? GridFSBuckets.create(db) : GridFSBuckets.create(db, bucket);
        }
    }
    
    

    可以看到GridFsTemplate核心是getGridFs()方法。該類裡面的所有操作都是依賴該方法的。我們主要針對這個就可以實現執行時動態建立bucketName

  2. 我們自定義一個類繼承 GridFsTemplate即可,程式碼如下

    /**
     * 自定義 {@link GridFsTemplate} 實現,用於實現動態建立 bucket
     *
     * @author: pinlin
     * @date: 2021/9/1 17:03
     */
    public class CustomGridFsTemplate extends GridFsTemplate {
    
        private final MongoDbFactory dbFactory;
    
        public CustomGridFsTemplate(MongoDbFactory dbFactory, MongoConverter converter) {
            super(dbFactory, converter);
            this.dbFactory = dbFactory;
        }
    
        public CustomGridFsTemplate(MongoDbFactory dbFactory, MongoConverter converter, String bucket) {
            super(dbFactory, converter, bucket);
            this.dbFactory = dbFactory;
        }
    
        /**
         * 校驗符合平臺預設
         *
         * @param bucketName MongoDB 桶的名字 具體參考 
         * @throws CustomSystemException 丟擲自定義異常
         * @author zpl
         * @date 2021/9/2 14:21
         */
        public static void isExist(String bucketName) throws CustomSystemException {
            if (StrUtil.isBlank(bucketName)) {
                throw new CustomSystemException("bucketName 名字不能為空");
            }
            // GridFsBucketEnum該列舉是我定義MongoDB的列舉類,主要用於校驗傳入bucketName是否是平臺預設的。安全著想
            if (Boolean.FALSE.equals(GridFsBucketEnum.isExclude(bucketName))) {
                throw new CustomSystemException("bucketName 名字不符合平臺預設");
            }
        }
    
        /**
         * 儲存檔案
         *
         * @param content     輸入檔案流
         * @param filename    檔名
         * @param contentType 檔案型別
         * @param bucketName  桶的名字 
         * @return {@link ObjectId}
         * @throws CustomSystemException 丟擲非受檢異常,外部注意捕獲
         * @author zpl
         * @date 2021/9/2 14:27
         */
        public ObjectId store(InputStream content, String filename, String contentType, String bucketName) throws CustomSystemException {
            return getGridFs(bucketName).uploadFromStream(filename, content, computeUploadOptionsFor(contentType, toDocument(null)));
        }
    
        /**
         * 建立 {@link GridFSBucket} 物件
         *
         * @param bucketName MongoDB的桶的名字
         * @return {@link GridFSBucket}
         * @author zpl
         * @date 2021/9/2 14:05
         */
        private GridFSBucket getGridFs(String bucketName) {
            isExist(bucketName);
            MongoDatabase db = dbFactory.getDb();
            if (StrUtil.isNotBlank(bucketName)) {
                return GridFSBuckets.create(db, bucketName);
            }
            return GridFSBuckets.create(db);
        }
    
        /**
         * 查詢多個 返回 {@link GridFSFindIterable} 物件
         *
         * @param query      查詢物件 {@link Query}
         * @param bucketName MongoDB桶的名字
         * @return {@link GridFSFindIterable}
         * @author zpl
         * @date 2021/9/2 14:17
         */
        public GridFSFindIterable find(Query query, String bucketName) {
            Assert.notNull(query, "Query 不能為空!");
            Assert.notNull(bucketName, "bucketName 不能為空!");
    
            Document queryObject = getMappedQuery(query.getQueryObject());
            Document sortObject = getMappedQuery(query.getSortObject());
    
            return getGridFs(bucketName).find(queryObject).sort(sortObject);
        }
    
        /**
         * 查詢單個
         *
         * @param query      查詢物件 {@link Query}
         * @param bucketName MongoDB桶的名字
         * @return {@link GridFSFile}
         * @throws CustomSystemException 丟擲非受檢異常,外部注意捕獲
         * @author zpl
         * @date 2021/9/2 14:18
         */
        public GridFSFile findOne(Query query, String bucketName) {
            return find(query, bucketName).first();
        }
    
        /**
         * 刪除
         *
         * @param query      查詢物件 {@link Query}
         * @param bucketName MongoDB桶的名字
         * @throws CustomSystemException 丟擲非受檢異常,外部注意捕獲
         * @author zpl
         * @date 2021/9/2 14:18
         */
        public void delete(Query query, String bucketName) {
            for (GridFSFile fsFile : find(query, bucketName)) {
                getGridFs(bucketName).delete(fsFile.getId());
            }
        }
    
    }
    
    
  3. 把自定義的類裝配到spring容器,方便管理以及使用,程式碼如下

    /**
     * {@link org.springframework.data.mongodb.gridfs.GridFsTemplate} 建立
     *
     * @author: pinlin
     * @date: 2021/8/26 16:41
     */
    @Configuration
    public class GridFsTemplateConfig {
    
        @Bean(name = "gridFsTemplate")
        public CustomGridFsTemplate gridFsTemplate(MongoDbFactory mongoDbFactory, MongoConverter converter) {
            return new CustomGridFsTemplate(mongoDbFactory, converter);
        }
    }
    
    

    說明

    這個裝配,如果是微服務工程,可以建立一個mongodb工程,把所有的封裝都抽象在這個工程裡面。然後寫一個註解,使用spring-boot@Import的註解匯入GridFsTemplateConfig就可以實現插拔裝配自定義類

  4. 使用自定義配置類下載,程式碼如下

    public class Test {
    
        @Autowired
        private CustomGridFsTemplate customGridFsTemplate;
    
        @Autowired
        private MongoDbFactory mongoDbFactory;
    
        public void download(@PathVariable("fileId") String fileId,
                             @PathVariable("bucketName") String bucketName,
                             HttpServletResponse response,
                             HttpServletRequest request) throws IOException {
            Query query = Query.query(Criteria.where("_id").is(fileId));
            // 查詢根據傳入的bucketName ,這個bucketName最後和後臺預設的對比以及校驗寫,不然別人亂傳,就會導致建立很多個bucket儲存桶,不安全
            GridFSFile gridFSFile = customGridFsTemplate.findOne(query, bucketName);
            if (gridFSFile != null) {
                GridFSBucket bucket = GridFSBuckets.create(mongoDbFactory.getDb(), bucketName);
                GridFSDownloadStream gridFSDownloadStream = bucket.openDownloadStream(gridFSFile.getObjectId());
                GridFsResource gridFsResource = new GridFsResource(gridFSFile, gridFSDownloadStream);
    
                // 獲取檔名
                String fileName = FileUtils.getFileName(request, gridFSFile);
    
                response.setHeader(HttpHeaders.CONTENT_DISPOSITION, String.format("attachment;filename=\"%s\"", fileName));
                response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(gridFSFile.getLength()));
                response.setHeader(HttpHeaders.CONTENT_TYPE, "application/octet-stream;charset=ISO8859-1");
                response.setHeader(HttpHeaders.CONTENT_TYPE, gridFsResource.getContentType());
                IOUtils.copy(gridFsResource.getInputStream(), response.getOutputStream());
            }
        }
    }
    

最後

此寫法是我在專案中分析得到的。目前已經應用到我的專案中,如果你們有更好的方案,麻煩留言互相學習。如果指導有誤的地方,還請指出。謝謝。