`spring-boot-data-mongodb` 动态创建 `bucketName`
当项目使用MongoDB存储文件的时候,小于16M是可以直接存在集合里面的,如果大于就需要用到GridFs来存储了。
小于16M存储的集合是可以运行时动态创建的,但是大于不可以。GridFs默认的存储桶的名是fs来定义的。具体可参考GridFSBucketImpl实现类。这里对这个类不做过多介绍。这里主要介绍如何在程序运行时动态创建 bucketName
-
先看下
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了 -
我们自定义一个类继承
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()); } } } -
把自定义的类装配到
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就可以实现插拔装配自定义类 -
使用自定义配置类下载,代码如下
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()); } } }
最后
此写法是我在项目中分析得到的。目前已经应用到我的项目中,如果你们有更好的方案,麻烦留言互相学习。如果指导有误的地方,还请指出。谢谢。

浙公网安备 33010602011771号