java每日精进 8.04【文件管理细致分析】
Controller
FileConfigController
@Tag(name = "管理后台 - 文件配置")
@RestController
@RequestMapping("/infra/file-config")
@Validated
public class FileConfigController {@Resourceprivate FileConfigService fileConfigService;@PostMapping("/create")@Operation(summary = "创建文件配置")@PreAuthorize("@ss.hasPermission('infra:file-config:create')")public CommonResult<Long> createFileConfig(@Valid @RequestBody FileConfigSaveReqVO createReqVO) {return success(fileConfigService.createFileConfig(createReqVO));}@PutMapping("/update")@Operation(summary = "更新文件配置")@PreAuthorize("@ss.hasPermission('infra:file-config:update')")public CommonResult<Boolean> updateFileConfig(@Valid @RequestBody FileConfigSaveReqVO updateReqVO) {fileConfigService.updateFileConfig(updateReqVO);return success(true);}@PutMapping("/update-master")@Operation(summary = "更新文件配置为 Master")@PreAuthorize("@ss.hasPermission('infra:file-config:update')")public CommonResult<Boolean> updateFileConfigMaster(@RequestParam("id") Long id) {fileConfigService.updateFileConfigMaster(id);return success(true);}@DeleteMapping("/delete")@Operation(summary = "删除文件配置")@Parameter(name = "id", description = "编号", required = true)@PreAuthorize("@ss.hasPermission('infra:file-config:delete')")public CommonResult<Boolean> deleteFileConfig(@RequestParam("id") Long id) {fileConfigService.deleteFileConfig(id);return success(true);}@GetMapping("/get")@Operation(summary = "获得文件配置")@Parameter(name = "id", description = "编号", required = true, example = "1024")@PreAuthorize("@ss.hasPermission('infra:file-config:query')")public CommonResult<FileConfigRespVO> getFileConfig(@RequestParam("id") Long id) {FileConfigDO config = fileConfigService.getFileConfig(id);return success(BeanUtils.toBean(config, FileConfigRespVO.class));}@GetMapping("/page")@Operation(summary = "获得文件配置分页")@PreAuthorize("@ss.hasPermission('infra:file-config:query')")public CommonResult<PageResult<FileConfigRespVO>> getFileConfigPage(@Valid FileConfigPageReqVO pageVO) {PageResult<FileConfigDO> pageResult = fileConfigService.getFileConfigPage(pageVO);return success(BeanUtils.toBean(pageResult, FileConfigRespVO.class));}@GetMapping("/test")@Operation(summary = "测试文件配置是否正确")@PreAuthorize("@ss.hasPermission('infra:file-config:query')")public CommonResult<String> testFileConfig(@RequestParam("id") Long id) throws Exception {String url = fileConfigService.testFileConfig(id);return success(url);}
}
- 作用:这是 yudao-module-infra 模块中的文件配置管理控制器,位于管理后台,提供 RESTful API 用于管理文件存储配置(如 S3、磁盘、数据库存储)。
- 包路径:cn.iocoder.moyun.module.infra.controller.admin.file 表示这是基础设施模块(infra)的管理员相关控制器,专门处理文件配置操作。
- 依赖:
- 使用 BeanUtils 进行 DO 和 VO 之间的转换。
- 使用 FileConfigService 处理文件配置的业务逻辑。
- 使用 Spring Security 的 @PreAuthorize 进行权限控制。
- 使用 Swagger 注解 (@Tag, @Operation, @Parameter) 生成 API 文档。
- 使用 Spring MVC 注解 (@RestController, @RequestMapping, @Validated) 处理 HTTP 请求和参数校验。
方法 1: createFileConfig
@PostMapping("/create")@Operation(summary = "创建文件配置")@PreAuthorize("@ss.hasPermission('infra:file-config:create')")public CommonResult<Long> createFileConfig(@Valid @RequestBody FileConfigSaveReqVO createReqVO) {return success(fileConfigService.createFileConfig(createReqVO));}
- 功能:创建新的文件配置(如 S3、磁盘、数据库存储的配置)。
- 注解:
- @PostMapping("/create"):处理 POST 请求,路径为 /infra/file-config/create。
- @Operation(summary = "创建文件配置"):Swagger 文档,描述接口用途。
- @PreAuthorize("@ss.hasPermission('infra:file-config:create')"):权限校验,要求用户具有 infra:file-config:create 权限(@ss 是一个自定义的 Spring Security 表达式)。
- @Valid:对请求体进行校验,确保 createReqVO 符合定义的约束。
- @RequestBody:表示参数从请求体中获取,格式为 JSON。
- 参数:
- FileConfigSaveReqVO createReqVO:请求体对象,包含文件配置信息,结构如下(来自 FileConfigSaveReqVO 类):
@Schema(description = "管理后台 - 文件配置创建/修改 Request VO")@Datapublic class FileConfigSaveReqVO {@Schema(description = "编号", example = "1")private Long id; // 可为空,创建时不需要@Schema(description = "配置名", requiredMode = Schema.RequiredMode.REQUIRED, example = "S3 - 阿里云")@NotNull(message = "配置名不能为空")private String name; // 配置名称,必填@Schema(description = "存储器,参见 FileStorageEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")@NotNull(message = "存储器不能为空")private Integer storage; // 存储器类型,必填,参考 FileStorageEnum(如 S3=20)@Schema(description = "存储配置,配置是动态参数,所以使用 Map 接收", requiredMode = Schema.RequiredMode.REQUIRED)@NotNull(message = "存储配置不能为空")private Map<String, Object> config; // 存储配置,动态参数,必填@Schema(description = "备注", example = "我是备注")private String remark; // 备注,可选}
- 参数说明:
- id:创建时为空,更新时需要提供。
- name:配置名称,如“S3 - 阿里云”,用于标识配置。
- storage:存储器类型,参考 FileStorageEnum(如 S3=20, LOCAL=10, DB=1)。
- config:存储配置,格式为键值对,根据 storage 类型动态变化。例如:
- S3 存储:需要 endpoint(节点地址,如 s3.cn-south-1.qiniucs.com)、bucket(存储桶,如 ruoyi-vue-pro)、accessKey、accessSecret、domain(自定义域名,如 http://test.yudao.iocoder.cn)、pathStyle(是否启用 Path Style)。
- 本地磁盘存储:需要 basePath(存储路径)、domain(访问域名)。
- 数据库存储:通常无需额外配置。
- remark:可选备注,用于记录配置的额外信息。
- 参数说明:
- FileConfigSaveReqVO createReqVO:请求体对象,包含文件配置信息,结构如下(来自 FileConfigSaveReqVO 类):
- 返回值:CommonResult<Long>,包装了创建的文件配置 ID,success 方法封装结果为标准响应格式。
- 实现:
- 调用 fileConfigService.createFileConfig(createReqVO),将请求 VO 转换为 DO 并保存到数据库,返回新配置的 ID。
- FileConfigServiceImpl 中会校验 config 参数并根据 storage 类型转换为对应的配置类(如 S3FileClientConfig)。
- 不同配置的影响:
- S3 存储:需要提供完整的 S3 配置(如 endpoint, bucket, accessKey, accessSecret, domain)。文档中提到,S3 存储支持 HTTP 直接访问,返回的 URL 是 S3 的访问路径(如 http://test.yudao.iocoder.cn/xxx.jpg)。
- 磁盘存储:需要配置存储路径(如 basePath),文件存储在本地或 FTP/SFTP 服务器,返回的 URL 需要通过后端的 /infra/file/{configId}/get/{path} API 访问。
- 数据库存储:文件内容存储在数据库中,访问也需要通过后端 API。
- 前端直传 S3:创建的 S3 配置需要设置跨域(CORS)规则以支持前端直传,config 中需要 domain 字段,且 Bucket 需设置为公共读。
方法 2: updateFileConfig
@PutMapping("/update")@Operation(summary = "更新文件配置")@PreAuthorize("@ss.hasPermission('infra:file-config:update')")public CommonResult<Boolean> updateFileConfig(@Valid @RequestBody FileConfigSaveReqVO updateReqVO) {fileConfigService.updateFileConfig(updateReqVO);return success(true);}
- 功能:更新现有的文件配置。
- 注解:
- @PutMapping("/update"):处理 PUT 请求,路径为 /infra/file-config/update。
- @Operation(summary = "更新文件配置"):Swagger 文档描述。
- @PreAuthorize("@ss.hasPermission('infra:file-config:update')"):权限校验,要求 infra:file-config:update 权限。
- @Valid @RequestBody:校验请求体,确保 updateReqVO 合法。
- 参数:
- FileConfigSaveReqVO updateReqVO:与创建时的 VO 相同,但 id 字段必须提供,用于标识要更新的配置。
- 参数说明:同 createFileConfig,但 id 必填,config 根据 storage 类型动态更新。
- FileConfigSaveReqVO updateReqVO:与创建时的 VO 相同,但 id 字段必须提供,用于标识要更新的配置。
- 返回值:CommonResult<Boolean>,返回 true 表示更新成功。
- 实现:
- 调用 fileConfigService.updateFileConfig(updateReqVO),校验配置存在后更新数据库中的记录,并清空相关缓存。
- FileConfigServiceImpl 会验证 id 对应的配置是否存在,并将 config 转换为对应的配置类。
- 不同配置的影响:
- S3 存储:更新 config 中的 S3 参数(如 accessKey 或 domain),需要确保新配置符合 S3 协议要求(如公共读 Bucket)。
- 磁盘存储:更新存储路径或域名,可能影响文件访问 URL。
- 数据库存储:通常只需更新 name 或 remark,config 较少变化。
- 前端直传 S3:若更新 domain 或 bucket,需确保前端的跨域配置同步更新,否则可能导致直传失败。
方法 3: updateFileConfigMaster
@PutMapping("/update-master")@Operation(summary = "更新文件配置为 Master")@PreAuthorize("@ss.hasPermission('infra:file-config:update')")public CommonResult<Boolean> updateFileConfigMaster(@RequestParam("id") Long id) {fileConfigService.updateFileConfigMaster(id);return success(true);}
- 功能:将指定 ID 的文件配置设为主配置(Master),用于默认文件上传。
- 注解:
- @PutMapping("/update-master"):处理 PUT 请求,路径为 /infra/file-config/update-master。
- @Operation(summary = "更新文件配置为 Master"):Swagger 文档描述。
- @PreAuthorize("@ss.hasPermission('infra:file-config:update')"):权限校验,要求 infra:file-config:update 权限。
- @RequestParam("id"):从查询参数获取配置 ID。
- 参数:
- Long id:要设为主配置的文件配置 ID。
- 返回值:CommonResult<Boolean>,返回 true 表示设置成功。
- 实现:
- 调用 fileConfigService.updateFileConfigMaster(id),先校验配置存在,然后将所有配置的 master 字段设为 false,再将指定 ID 的配置设为 true,并清空 Master 缓存。
- 主配置用于默认的文件上传操作(如前端直传或后端上传)。
- 不同配置的影响:
- S3 存储:设为 Master 后,前端直传或后端上传默认使用该 S3 配置,需确保跨域和公共读设置正确。
- 磁盘存储:设为 Master 后,文件存储在指定路径,访问需通过后端 API。
- 数据库存储:设为 Master 后,文件存储在数据库,适合小文件场景,访问也需通过后端 API。
- 前端直传 S3:主配置通常用于前端直传,需确保 domain 和跨域配置正确,否则上传会失败。
方法 4: deleteFileConfig
@DeleteMapping("/delete")@Operation(summary = "删除文件配置")@Parameter(name = "id", description = "编号", required = true)@PreAuthorize("@ss.hasPermission('infra:file-config:delete')")public CommonResult<Boolean> deleteFileConfig(@RequestParam("id") Long id) {fileConfigService.deleteFileConfig(id);return success(true);}
- 功能:删除指定 ID 的文件配置。
- 注解:
- @DeleteMapping("/delete"):处理 DELETE 请求,路径为 /infra/file-config/delete。
- @Operation(summary = "删除文件配置"):Swagger 文档描述。
- @Parameter(name = "id", description = "编号", required = true):描述查询参数 id。
- @PreAuthorize("@ss.hasPermission('infra:file-config:delete')"):权限校验,要求 infra:file-config:delete 权限。
- @RequestParam("id"):从查询参数获取配置 ID。
- 参数:
- Long id:要删除的文件配置 ID。
- 返回值:CommonResult<Boolean>,返回 true 表示删除成功。
- 实现:
- 调用 fileConfigService.deleteFileConfig(id),校验配置存在且不是主配置(Master 不可删除),然后删除记录并清空缓存。
- 如果尝试删除主配置,会抛出 FILE_CONFIG_DELETE_FAIL_MASTER 异常。
- 不同配置的影响:
- S3 存储:删除后,相关 S3 配置不可用,已上传的文件可能仍可通过 S3 URL 访问(需手动清理 Bucket)。
- 磁盘存储:删除后,相关路径的文件仍存在,需手动清理。
- 数据库存储:删除后,数据库中的文件记录不受影响,需手动清理。
- 前端直传 S3:若删除的是主配置,会导致前端直传失败,需重新设置主配置。
方法 5: getFileConfig
@GetMapping("/get")@Operation(summary = "获得文件配置")@Parameter(name = "id", description = "编号", required = true, example = "1024")@PreAuthorize("@ss.hasPermission('infra:file-config:query')")public CommonResult<FileConfigRespVO> getFileConfig(@RequestParam("id") Long id) {FileConfigDO config = fileConfigService.getFileConfig(id);return success(BeanUtils.toBean(config, FileConfigRespVO.class));}
- 功能:查询指定 ID 的文件配置详情。
- 注解:
- @GetMapping("/get"):处理 GET 请求,路径为 /infra/file-config/get。
- @Operation(summary = "获得文件配置"):Swagger 文档描述。
- @Parameter(name = "id", description = "编号", required = true, example = "1024"):描述查询参数 id。
- @PreAuthorize("@ss.hasPermission('infra:file-config:query')"):权限校验,要求 infra:file-config:query 权限。
- @RequestParam("id"):从查询参数获取配置 ID。
- 参数:
- Long id:要查询的文件配置 ID。
- 返回值:CommonResult<FileConfigRespVO>,返回文件配置的 VO 对象,包含配置详情。
- 实现:
- 调用 fileConfigService.getFileConfig(id) 获取 FileConfigDO。
- 使用 BeanUtils.toBean 将 DO 转换为 FileConfigRespVO(VO 结构未提供,但通常包含 id, name, storage, config, master, remark 等字段)。
- 不同配置的影响:
- S3 存储:返回的 config 包含 S3 特定的配置(如 endpoint, bucket)。
- 磁盘存储:返回的 config 包含路径和域名信息。
- 数据库存储:返回的 config 可能为空或仅包含基本信息。
- 前端直传 S3:查询主配置时,返回的 config 用于前端直传的初始化(如获取 domain)。
方法 6: getFileConfigPage
@
GetMapping("/page")@Operation(summary = "获得文件配置分页")@PreAuthorize("@ss.hasPermission('infra:file-config:query')")public CommonResult<PageResult<FileConfigRespVO>> getFileConfigPage(@Valid FileConfigPageReqVO pageVO) {PageResult<FileConfigDO> pageResult = fileConfigService.getFileConfigPage(pageVO);return success(BeanUtils.toBean(pageResult, FileConfigRespVO.class));}
- 功能:分页查询文件配置列表。
- 注解:
- @GetMapping("/page"):处理 GET 请求,路径为 /infra/file-config/page。
- @Operation(summary = "获得文件配置分页"):Swagger 文档描述。
- @PreAuthorize("@ss.hasPermission('infra:file-config:query')"):权限校验,要求 infra:file-config:query 权限。
- @Valid:校验分页请求参数。
- 参数:
- FileConfigPageReqVO pageVO:分页查询参数,结构未提供,但通常包含:
- name:配置名称(模糊查询)。
- storage:存储器类型(如 S3=20)。
- createTime:创建时间范围。
- pageNo:页码。
- pageSize:每页大小。
- FileConfigPageReqVO pageVO:分页查询参数,结构未提供,但通常包含:
- 返回值:CommonResult<PageResult<FileConfigRespVO>>,返回分页结果,包含配置列表和总数。
- 实现:
- 调用 fileConfigService.getFileConfigPage(pageVO) 获取分页的 FileConfigDO 列表。
- 使用 BeanUtils.toBean 将 PageResult<FileConfigDO> 转换为 PageResult<FileConfigRespVO>。
- 不同配置的影响:
- S3 存储:分页结果包含 S3 配置,config 字段包含 S3 特定的参数。
- 磁盘存储:分页结果包含磁盘路径配置。
- 数据库存储:分页结果包含数据库配置(通常较简单)。
- 前端直传 S3:分页结果可用于前端展示配置列表,主配置用于直传初始化。
方法 7: testFileConfig
@GetMapping("/test")@Operation(summary = "测试文件配置是否正确")@PreAuthorize("@ss.hasPermission('infra:file-config:query')")public CommonResult<String> testFileConfig(@RequestParam("id") Long id) throws Exception {String url = fileConfigService.testFileConfig(id);return success(url);}
- 功能:测试指定 ID 的文件配置是否有效,通过上传一个测试文件并返回访问 URL。
- 注解:
- @GetMapping("/test"):处理 GET 请求,路径为 /infra/file-config/test。
- @Operation(summary = "测试文件配置是否正确"):Swagger 文档描述。
- @PreAuthorize("@ss.hasPermission('infra:file-config:query')"):权限校验,要求 infra:file-config:query 权限。
- @RequestParam("id"):从查询参数获取配置 ID。
- 参数:
- Long id:要测试的文件配置 ID。
- 返回值:CommonResult<String>,返回测试文件上传后的访问 URL。
- 实现:
- 调用 fileConfigService.testFileConfig(id),读取内置的测试文件(erweima.jpg),通过 FileClient 上传并返回 URL。
- 不同配置的影响:
- S3 存储:测试上传到 S3 Bucket,返回 HTTP 访问 URL(如 http://test.yudao.iocoder.cn/xxx.jpg)。需确保 Bucket 公共读和跨域配置正确。
- 磁盘存储:测试上传到指定路径,返回的 URL 需通过后端 API 访问(如 /infra/file/{configId}/get/{path})。
- 数据库存储:测试文件存储到数据库,返回的 URL 也需通过后端 API 访问。
- 前端直传 S3:测试 URL 可用于验证前端直传的配置是否正确(如跨域和域名设置)。
FileConfigServiceImpl
/*** 文件配置 Service 实现类*/
@Service
@Validated
@Slf4j
public class FileConfigServiceImpl implements FileConfigService {private static final Long CACHE_MASTER_ID = 0L;/*** {@link FileClient} 缓存,通过它异步刷新 fileClientFactory* 这个缓存是连接文件配置(FileConfigDO)和文件操作客户端(FileClient)的中间层*/@Getterprivate final LoadingCache<Long, FileClient> clientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),new CacheLoader<Long, FileClient>() {@Overridepublic FileClient load(Long id) {FileConfigDO config = Objects.equals(CACHE_MASTER_ID, id) ?fileConfigMapper.selectByMaster() : fileConfigMapper.selectById(id);if (config != null) {fileClientFactory.createOrUpdateFileClient(config.getId(), config.getStorage(), config.getConfig());}return fileClientFactory.getFileClient(null == config ? id : config.getId());}});@Resourceprivate FileClientFactory fileClientFactory;@Resourceprivate FileConfigMapper fileConfigMapper;@Resourceprivate Validator validator;@Overridepublic Long createFileConfig(FileConfigSaveReqVO createReqVO) {FileConfigDO fileConfig = FileConfigConvert.INSTANCE.convert(createReqVO).setConfig(parseClientConfig(createReqVO.getStorage(), createReqVO.getConfig())).setMaster(false); // 默认非 masterfileConfigMapper.insert(fileConfig);return fileConfig.getId();}@Overridepublic void updateFileConfig(FileConfigSaveReqVO updateReqVO) {// 校验存在FileConfigDO config = validateFileConfigExists(updateReqVO.getId());// 更新FileConfigDO updateObj = FileConfigConvert.INSTANCE.convert(updateReqVO).setConfig(parseClientConfig(config.getStorage(), updateReqVO.getConfig()));fileConfigMapper.updateById(updateObj);// 清空缓存clearCache(config.getId(), null);}@Override@Transactional(rollbackFor = Exception.class)public void updateFileConfigMaster(Long id) {// 校验存在validateFileConfigExists(id);// 更新其它为非 masterfileConfigMapper.updateBatch(new FileConfigDO().setMaster(false));// 更新fileConfigMapper.updateById(new FileConfigDO().setId(id).setMaster(true));// 清空缓存clearCache(null, true);}private FileClientConfig parseClientConfig(Integer storage, Map<String, Object> config) {// 获取配置类Class<? extends FileClientConfig> configClass = FileStorageEnum.getByStorage(storage).getConfigClass();FileClientConfig clientConfig = JsonUtils.parseObject2(JsonUtils.toJsonString(config), configClass);// 参数校验ValidationUtils.validate(validator, clientConfig);// 设置参数return clientConfig;}@Overridepublic void deleteFileConfig(Long id) {// 校验存在FileConfigDO config = validateFileConfigExists(id);if (Boolean.TRUE.equals(config.getMaster())) {throw exception(FILE_CONFIG_DELETE_FAIL_MASTER);}// 删除fileConfigMapper.deleteById(id);// 清空缓存clearCache(id, null);}/*** 清空指定文件配置** @param id 配置编号* @param master 是否主配置*/private void clearCache(Long id, Boolean master) {if (id != null) {clientCache.invalidate(id);}if (Boolean.TRUE.equals(master)) {clientCache.invalidate(CACHE_MASTER_ID);}}private FileConfigDO validateFileConfigExists(Long id) {FileConfigDO config = fileConfigMapper.selectById(id);if (config == null) {throw exception(FILE_CONFIG_NOT_EXISTS);}return config;}@Overridepublic FileConfigDO getFileConfig(Long id) {return fileConfigMapper.selectById(id);}@Overridepublic PageResult<FileConfigDO> getFileConfigPage(FileConfigPageReqVO pageReqVO) {return fileConfigMapper.selectPage(pageReqVO);}@Overridepublic String testFileConfig(Long id) throws Exception {// 校验存在validateFileConfigExists(id);// 上传文件byte[] content = ResourceUtil.readBytes("file/erweima.jpg");return getFileClient(id).upload(content, IdUtil.fastSimpleUUID() + ".jpg", "image/jpeg");}@Overridepublic FileClient getFileClient(Long id) {return clientCache.getUnchecked(id);}@Overridepublic FileClient getMasterFileClient() {return clientCache.getUnchecked(CACHE_MASTER_ID);}
}
FileConfigServiceImpl 提供了 FileConfigController 的核心逻辑,以下是一些关键点,与不同配置方式相关:
- 缓存机制:
- 使用 LoadingCache<Long, FileClient> 缓存 FileClient 实例,CACHE_MASTER_ID=0L 用于主配置。
- 每次创建、更新、删除或设置主配置时,会清空相关缓存以确保数据一致性。
- 配置解析:
- parseClientConfig 方法根据 storage 类型将 config Map 转换为特定配置类(如 S3FileClientConfig),并进行参数校验。
- S3 配置需要更多字段(如 endpoint, bucket),而磁盘存储需要 basePath,数据库存储配置较简单。
- 文件客户端:
- FileClientFactoryImpl 根据 storage 类型创建对应的 FileClient(如 S3FileClient, LocalFileClient, DBFileClient)。
- S3 存储支持直接 HTTP 访问,磁盘和数据库存储需通过后端 API。
不同配置方式的比较
根据文档和代码,以下是 S3 对象存储(包括前端直传)、磁盘存储、数据库存储的对比,以及对 FileConfigController 方法的影响:
特性 | S3 对象存储 | 磁盘存储(本地/FTP/SFTP) | 数据库存储 |
---|---|---|---|
存储方式 | 文件存储在云服务(如 MinIO、七牛云),支持 HTTP 访问 | 文件存储在本地磁盘或远程 FTP/SFTP 服务器 | 文件内容存储在数据库(如 MySQL) |
配置参数 | endpoint, bucket, accessKey, accessSecret, domain, pathStyle | basePath, domain | 无需复杂配置 |
访问方式 | 直接通过 S3 返回的 URL 访问(如 http://test.yudao.iocoder.cn/xxx.jpg) | 通过后端 API /infra/file/{configId}/get/{path} 访问 | 通过后端 API /infra/file/{configId}/get/{path} 访问 |
前端直传支持 | 支持,需配置跨域(CORS)和公共读 Bucket,VITE_UPLOAD_TYPE=client | 不支持,需经过后端上传 | 不支持,需经过后端上传 |
性能 | 高性能,适合大文件和并发上传,流量不经过后端 | 性能受磁盘 I/O 或网络限制,流量经过后端 | 适合小文件,性能受数据库限制 |
高可用性 | 高,依赖云服务提供商的 HA 机制 | 较低,需自行实现高可用(如 RAID 或主从) | 高,依赖数据库主从机制 |
推荐场景 | 大规模文件存储、前端直传、分布式系统 | 小规模本地存储或已有 FTP/SFTP 环境 | 小文件存储、备份方便 |
Controller 方法影响 | - createFileConfig/updateFileConfig:需提供完整 S3 配置,校验严格 - testFileConfig:返回 S3 URL - updateFileConfigMaster:设为主配置后用于直传 | - createFileConfig/updateFileConfig:配置简单,仅需路径 - testFileConfig:返回需通过后端 API 的 URL | - createFileConfig/updateFileConfig:配置简单 - testFileConfig:返回需通过后端 API 的 URL |
前端直传配置 | 需设置跨域和 VITE_UPLOAD_TYPE=client,主配置用于直传初始化 | 不支持直传 | 不支持直传 |
FileController
@Tag(name = "管理后台 - 文件存储")
@RestController
@RequestMapping("/infra/file")
@Validated
@Slf4j
public class FileController {@Resourceprivate FileService fileService;@PostMapping("/upload")@Operation(summary = "上传文件", description = "模式一:后端上传文件")public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception {MultipartFile file = uploadReqVO.getFile();String path = uploadReqVO.getPath();return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream())));}@GetMapping("/presigned-url")@Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(@RequestParam("path") String path) throws Exception {return success(fileService.getFilePresignedUrl(path));}@PostMapping("/create")@Operation(summary = "创建文件", description = "模式二:前端上传文件:配合 presigned-url 接口,记录上传的文件")public CommonResult<Long> createFile(@Valid @RequestBody FileCreateReqVO createReqVO) {return success(fileService.createFile(createReqVO));}@DeleteMapping("/delete")@Operation(summary = "删除文件")@Parameter(name = "id", description = "编号", required = true)@PreAuthorize("@ss.hasPermission('infra:file:delete')")public CommonResult<Boolean> deleteFile(@RequestParam("id") Long id) throws Exception {fileService.deleteFile(id);return success(true);}@GetMapping("/{configId}/get/**")@PermitAll@Operation(summary = "下载文件")@Parameter(name = "configId", description = "配置编号", required = true)public void getFileContent(HttpServletRequest request,HttpServletResponse response,@PathVariable("configId") Long configId) throws Exception {// 获取请求的路径String path = StrUtil.subAfter(request.getRequestURI(), "/get/", false);if (StrUtil.isEmpty(path)) {throw new IllegalArgumentException("结尾的 path 路径必须传递");}// 解码,解决中文路径的问题 https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/807/path = URLUtil.decode(path);// 读取内容byte[] content = fileService.getFileContent(configId, path);if (content == null) {log.warn("[getFileContent][configId({}) path({}) 文件不存在]", configId, path);response.setStatus(HttpStatus.NOT_FOUND.value());return;}FileTypeUtils.writeAttachment(response, path, content);}@GetMapping("/page")@Operation(summary = "获得文件分页")@PreAuthorize("@ss.hasPermission('infra:file:query')")public CommonResult<PageResult<FileRespVO>> getFilePage(@Valid FilePageReqVO pageVO) {PageResult<FileDO> pageResult = fileService.getFilePage(pageVO);return success(BeanUtils.toBean(pageResult, FileRespVO.class));}}
createFile(String name, String path, byte[] content)
其中下面一句最为重要:
FileClient client = fileConfigService.getMasterFileClient();
调用 FileConfigServiceImpl.getMasterFileClient:
@Overridepublic FileClient getMasterFileClient() {return clientCache.getUnchecked(CACHE_MASTER_ID);}
- 动态生成 FileClient:
- clientCache 是 LoadingCache<Long, FileClient>,键为 CACHE_MASTER_ID=0L。
- 调用 getUnchecked(0L),触发 CacheLoader.load:
/*** {@link FileClient} 缓存,通过它异步刷新 fileClientFactory* 这个缓存是连接文件配置(FileConfigDO)和文件操作客户端(FileClient)的中间层*/@Getterprivate final LoadingCache<Long, FileClient> clientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),new CacheLoader<Long, FileClient>() {@Overridepublic FileClient load(Long id) {FileConfigDO config = Objects.equals(CACHE_MASTER_ID, id) ?fileConfigMapper.selectByMaster() : fileConfigMapper.selectById(id);if (config != null) {fileClientFactory.createOrUpdateFileClient(config.getId(), config.getStorage(), config.getConfig());}return fileClientFactory.getFileClient(null == config ? id : config.getId());}});
- 查询主配置:
- fileConfigMapper.selectByMaster:
SELECT * FROM file_config WHERE master = 1;
- 返回 FileConfigDO(id=1, storage=1, master=true)。
- fileConfigMapper.selectByMaster:
- 创建或更新 FileClient:
- 调用 fileClientFactory.createOrUpdateFileClient:
public <Config extends FileClientConfig> void createOrUpdateFileClient(Long configId, Integer storage, Config config) {AbstractFileClient<?> client = clients.get(configId);if (client == null) {client = this.createFileClient(configId, storage, config);client.init();clients.put(client.getId(), client);} else {client.refresh(config);}}
- configId=1, storage=1(FileStorageEnum.DB)。
- 如果 clients 中无 configId=1,调用 createFileClient:
- 根据 storage=1,创建 DBFileClient 实例。
- 调用 client.init() 初始化。
- 存入 clients(Map<Long, AbstractFileClient<?>>)。
- 如果存在,调用 client.refresh(config) 更新配置。
- 存入 clientCache(键为 0L)。
- 调用 fileClientFactory.createOrUpdateFileClient:
- 返回 FileClient:DBFileClient(configId=1)。
假如我们创建的是一个S3FileClient:
/*** 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务* <p>* S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库** @author 芋道源码*/
public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {private AmazonS3Client client;public S3FileClient(Long id, S3FileClientConfig config) {super(id, config);}@Overrideprotected void doInit() {// 补全 domainif (StrUtil.isEmpty(config.getDomain())) {config.setDomain(buildDomain());}// 初始化客户端client = (AmazonS3Client)AmazonS3ClientBuilder.standard().withCredentials(buildCredentials()).withEndpointConfiguration(buildEndpointConfiguration()).build();}/*** 基于 config 秘钥,构建 S3 客户端的认证信息* 通过配置中的accessKey(访问密钥 ID)和accessSecret(访问密钥密钥),构建BasicAWSCredentials对象,用于 S3 服务的身份验证(确保上传操作有权限)* @return S3 客户端的认证信息*/private AWSStaticCredentialsProvider buildCredentials() {return new AWSStaticCredentialsProvider(new BasicAWSCredentials(config.getAccessKey(), config.getAccessSecret()));}/*** 构建 S3 客户端的 Endpoint 配置,包括 region、endpoint* 构建服务端点配置(buildEndpointConfiguration)* @return S3 客户端的 EndpointConfiguration 配置*/private AwsClientBuilder.EndpointConfiguration buildEndpointConfiguration() {return new AwsClientBuilder.EndpointConfiguration(config.getEndpoint(),null); // 无需设置 region}/*** 基于 bucket + endpoint 构建访问的 Domain 地址* 若endpoint是 HTTP/HTTPS 地址(如 MinIO 的http://127.0.0.1:9000),* 则域名格式为endpoint/bucket(如http://127.0.0.1:9000/my-bucket)* 若endpoint是普通域名(如阿里云 OSS 的oss-cn-beijing.aliyuncs.com),* 则域名格式为https://bucket.endpoint(如https://my-bucket.oss-cn-beijing.aliyuncs.com)* @return Domain 地址*/private String buildDomain() {// 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIOif (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket());}// 阿里云、腾讯云、华为云都适合。七牛云比较特殊,必须有自定义域名return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());}@Overridepublic String upload(byte[] content, String path, String type) throws Exception {// 元数据,主要用于设置文件类型ObjectMetadata objectMetadata = new ObjectMetadata();objectMetadata.setContentType(type);objectMetadata.setContentLength(content.length); // 如果不设置,会有 “ No content length specified for stream data” 警告日志// 执行上传client.putObject(config.getBucket(),path, // 相对路径new ByteArrayInputStream(content), // 文件内容objectMetadata);// 拼接返回路径return config.getDomain() + "/" + path;}@Overridepublic void delete(String path) throws Exception {client.deleteObject(config.getBucket(), path);}@Overridepublic byte[] getContent(String path) throws Exception {S3Object tempS3Object = client.getObject(config.getBucket(), path);return IoUtil.readBytes(tempS3Object.getObjectContent());}@Overridepublic FilePresignedUrlRespDTO getPresignedObjectUrl(String path) throws Exception {// 设定过期时间为 10 分钟。取值范围:1 秒 ~ 7 天Date expiration = new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(10));// 生成上传 URLString uploadUrl = String.valueOf(client.generatePresignedUrl(config.getBucket(), path, expiration , HttpMethod.PUT));return new FilePresignedUrlRespDTO(uploadUrl, config.getDomain() + "/" + path);}}
开始时会调用
public S3FileClient(Long id, S3FileClientConfig config) {super(id, config);}@Overrideprotected void doInit() {// 补全 domainif (StrUtil.isEmpty(config.getDomain())) {config.setDomain(buildDomain());}// 初始化客户端client = (AmazonS3Client)AmazonS3ClientBuilder.standard().withCredentials(buildCredentials()).withEndpointConfiguration(buildEndpointConfiguration()).build();}
调用父类的构造方法,来实现初始化
/*** 基于 bucket + endpoint 构建访问的 Domain 地址* 若endpoint是 HTTP/HTTPS 地址(如 MinIO 的http://127.0.0.1:9000),* 则域名格式为endpoint/bucket(如http://127.0.0.1:9000/my-bucket)* 若endpoint是普通域名(如阿里云 OSS 的oss-cn-beijing.aliyuncs.com),* 则域名格式为https://bucket.endpoint(如https://my-bucket.oss-cn-beijing.aliyuncs.com)* @return Domain 地址*/private String buildDomain() {// 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIOif (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) {return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket());}// 阿里云、腾讯云、华为云都适合。七牛云比较特殊,必须有自定义域名return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint());}
/*** 基于 config 秘钥,构建 S3 客户端的认证信息* 通过配置中的accessKey(访问密钥 ID)和accessSecret(访问密钥密钥),构建BasicAWSCredentials对象,用于 S3 服务的身份验证(确保上传操作有权限)* @return S3 客户端的认证信息*/private AWSStaticCredentialsProvider buildCredentials() {return new AWSStaticCredentialsProvider(new BasicAWSCredentials(config.getAccessKey(), config.getAccessSecret()));}
/*** 构建 S3 客户端的 Endpoint 配置,包括 region、endpoint* 构建服务端点配置(buildEndpointConfiguration)* @return S3 客户端的 EndpointConfiguration 配置*/private AwsClientBuilder.EndpointConfiguration buildEndpointConfiguration() {return new AwsClientBuilder.EndpointConfiguration(config.getEndpoint(),null); // 无需设置 region}
然后逐步初始化即可;
主配置父类为:FileClientConfig,每个不同的文件上传方式对应不同的配置类
主客户端父类为:AbstractFileClient IM FileClient
createFile(FileCreateReqVO createReqVO)
直接创建文件元信息记录(不上传文件内容),用于前端直传模式(如 S3)后记录元信息
@Override
public Long createFile(FileCreateReqVO createReqVO) {FileDO file = BeanUtils.toBean(createReqVO, FileDO.class);fileMapper.insert(file);return file.getId();
}
参数:
- FileCreateReqVO createReqVO:包含:
- configId:配置 ID。
- name:文件名。
- path:文件路径。
- url:访问 URL。
- type:文件类型。
- size:文件大小。
deleteFile
删除指定 ID 的文件,包括存储器中的内容和 file 表中的元信息
//删除指定 ID 的文件,包括存储器中的内容和 file 表中的元信息@Overridepublic void deleteFile(Long id) throws Exception {// 校验存在FileDO file = validateFileExists(id);// 从文件存储器中删除FileClient client = fileConfigService.getFileClient(file.getConfigId());Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId());client.delete(file.getPath());// 删除记录fileMapper.deleteById(id);}
getFileContent
获取指定配置和路径的文件内容。
- 功能:获取指定配置和路径的文件内容。
- 参数:
- configId:配置 ID(如 1)。
- path:文件路径(如 /root/home/pic/test.jpg)。
- 返回值:byte[],文件二进制内容。
- 实现:
- 获取 FileClient:
FileClient client = fileConfigService.getFileClient(configId);
- 调用 FileConfigServiceImpl.getFileClient(1),返回 其子类,可能是任意一种子类(从 clientCache 获取)。
- 调用 (以DBF举例)DBFileClient.getContent:
public byte[] getContent(String path) {FileContentDO fileContent = fileContentMapper.selectOne(FileContentDO::getConfigId, getId(),FileContentDO::getPath, path);return fileContent != null ? fileContent.getContent() : null;}
- 查询 file_content 表:
SELECT content FROM file_content WHERE config_id = 1 AND path = '/root/home/pic/test.jpg';
- 查询 file_content 表:
- 获取 FileClient:
- 数据库存储场景:
- 返回 test.jpg 的二进制内容,用于 /infra/file/1/get/root/home/pic/test.jpg API
getFilePresignedUrl
@Overridepublic FilePresignedUrlRespVO getFilePresignedUrl(String path) throws Exception {FileClient fileClient = fileConfigService.getMasterFileClient();FilePresignedUrlRespDTO presignedObjectUrl = fileClient.getPresignedObjectUrl(path);return BeanUtils.toBean(presignedObjectUrl, FilePresignedUrlRespVO.class,object -> object.setConfigId(fileClient.getId()));}
- 功能:获取文件的预签名 URL,用于前端直传(如 S3)。
- 参数:
- path:文件路径(如 /root/home/pic/test.jpg)。
- 返回值:FilePresignedUrlRespVO,包含预签名 URL 和配置 ID。
- 实现:
- 获取主配置的 FileClient(DBFileClient)。
- 调用 getPresignedObjectUrl:
- 数据库存储不支持预签名,抛出异常或返回空。
- 转换 FilePresignedUrlRespDTO 为 FilePresignedUrlRespVO。
- 数据库存储场景:
- 不支持前端直传,调用无效。
- 不适用于本次模拟。
动态生成 FileClient 的逻辑
FileClient 的动态生成是文件上传逻辑的核心,依赖 FileConfigServiceImpl 的 clientCache 和 FileClientFactory。以下详细分析:
1. clientCache 和 FileConfigServiceImpl.getMasterFileClient
private static final Long CACHE_MASTER_ID = 0L;@Getterprivate final LoadingCache<Long, FileClient> clientCache = buildAsyncReloadingCache(Duration.ofSeconds(10L),new CacheLoader<Long, FileClient>() {@Overridepublic FileClient load(Long id) {FileConfigDO config = Objects.equals(CACHE_MASTER_ID, id) ?fileConfigMapper.selectByMaster() : fileConfigMapper.selectById(id);if (config != null) {fileClientFactory.createOrUpdateFileClient(config.getId(), config.getStorage(), config.getConfig());}return fileClientFactory.getFileClient(null == config ? id : config.getId());}});@Overridepublic FileClient getMasterFileClient() {return clientCache.getUnchecked(CACHE_MASTER_ID);}
- 作用:
- clientCache 是一个 Guava LoadingCache,缓存 FileClient 实例,键为配置 ID(0L 表示主配置)。
- getMasterFileClient 获取主配置的 FileClient(DBFileClient)。
- 动态生成逻辑:
- 调用 getUnchecked(CACHE_MASTER_ID):
- 检查 clientCache 是否有键 0L 的缓存。
- 若缓存命中,直接返回 DBFileClient。
- 若未命中,触发 CacheLoader.load(0L)。
- 查询主配置:
- fileConfigMapper.selectByMaster:
sql
SELECT * FROM file_config WHERE master = 1;
- 返回 FileConfigDO(id=1, storage=1, config={}, master=true)。
- fileConfigMapper.selectByMaster:
- 创建或更新 FileClient:
- 调用 fileClientFactory.createOrUpdateFileClient(1, 1, config):
public <Config extends FileClientConfig> void createOrUpdateFileClient(Long configId, Integer storage, Config config) {AbstractFileClient<?> client = clients.get(configId);if (client == null) {client = this.createFileClient(configId, storage, config);client.init();clients.put(client.getId(), client);} else {client.refresh(config);}}
- 检查 clients(Map<Long, AbstractFileClient<?>>)是否已有 configId=1。
- 创建新客户端:
- storage=1(FileStorageEnum.DB)。
- createFileClient 创建 DBFileClient:
- 设置 configId=1。
- config 解析为 DBFileClientConfig(可能为空对象 {})。
- 调用 client.init() 初始化。
- 存入 clients(键为 1)。
- 更新现有客户端:
- 调用 client.refresh(config),更新配置。
- 存入 clientCache(键为 0L)。
- 调用 fileClientFactory.createOrUpdateFileClient(1, 1, config):
- 返回 FileClient:
- 返回 DBFileClient(configId=1)。
- 调用 getUnchecked(CACHE_MASTER_ID):
- 存储时机:
- 初次生成:第一次调用 getMasterFileClient 时,CacheLoader.load 创建 DBFileClient 并存入 clientCache。
- 配置变更:
- 创建配置(/infra/file-config/create):插入 file_config,默认 master=false。
- 设为主配置(/infra/file-config/update-master):
@Override@Transactional(rollbackFor = Exception.class)public void updateFileConfigMaster(Long id) {validateFileConfigExists(id);fileConfigMapper.updateBatch(new FileConfigDO().setMaster(false));fileConfigMapper.updateById(new FileConfigDO().setId(id).setMaster(true));clearCache(null, true);}
- 更新 file_config 表:
sql
UPDATE file_config SET master = 0;
UPDATE file_config SET master = 1 WHERE id = 1;
- 清除缓存:
private void clearCache(Long id, Boolean master) {if (id != null) {clientCache.invalidate(id);}if (Boolean.TRUE.equals(master)) {clientCache.invalidate(CACHE_MASTER_ID);}}
- 清除 CACHE_MASTER_ID=0L 缓存。
- 下次调用 getMasterFileClient 时,重新加载 DBFileClient。
- 更新 file_config 表:
- 更新或删除配置:清除对应 clientCache 缓存。
- 缓存刷新:每 10 秒(Duration.ofSeconds(10L))异步刷新 clientCache。
文件上传整体流程(数据库存储)
结合 FileController.uploadFile 和 FileServiceImpl.createFile,以下是上传 test.jpg 的完整流程:
- 前端请求:
POST /infra/file/upload
Content-Type: multipart/form-data
Authorization: Bearer <token>
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="test.jpg"
Content-Type: image/jpeg
<binary_data>
------WebKitFormBoundary
Content-Disposition: form-data; name="path"
/root/home/pic/test.jpg
------WebKitFormBoundary--
- 控制器处理(FileController.uploadFile):
@PostMapping("/upload")
@Operation(summary = "上传文件", description = "模式一:后端上传文件")
public CommonResult<String> uploadFile(FileUploadReqVO uploadReqVO) throws Exception {
MultipartFile file = uploadReqVO.getFile();
String path = uploadReqVO.getPath();
return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream())));
}
- 解析 MultipartFile,提取 name="test.jpg", path=/root/home/pic/test.jpg, content=<binary_data>。
- 调用 fileService.createFile("test.jpg", "/root/home/pic/test.jpg", <binary_data>).
- 服务层处理(FileServiceImpl.createFile):
- 文件类型:FileTypeUtils.getMineType 返回 image/jpeg。
- 路径和文件名:使用提供的 path 和 name。
- 获取 FileClient:
- fileConfigService.getMasterFileClient() 返回 DBFileClient(动态生成,configId=1)。
- 上传:
- DBFileClient.upload 插入 file_content 表:
INSERT INTO file_content (config_id, path, content, type, create_time)
VALUES (1, '/root/home/pic/test.jpg', <binary_data>, 'image/jpeg', '2025-08-04 18:47:00');
- 返回 URL:/infra/file/1/get/root/home/pic/test.jpg.
- DBFileClient.upload 插入 file_content 表:
- 保存元信息:
- 插入 file 表:
INSERT INTO file (config_id, name, path, url, type, size, create_time)
VALUES (1, 'test.jpg', '/root/home/pic/test.jpg', '/infra/file/1/get/root/home/pic/test.jpg', 'image/jpeg', <size>, '2025-08-04 18:47:00');
- 插入 file 表:
- 返回 URL。
- 响应:
{
"code": 200,
"data": "/infra/file/1/get/root/home/pic/test.jpg",
"msg": "success"
}
关键点和注意事项
- 动态生成 FileClient:
- clientCache 确保高效获取 FileClient,延迟加载减少开销。
- CacheLoader.load 根据 storage(如 1=DB)创建具体客户端(DBFileClient)。
- 配置变更(create, update, delete, update-master)触发 clearCache,保证客户端与最新配置同步。
- 数据库存储特点:
- 文件内容存储在 file_content.content(LONGBLOB)。
- 访问需通过 /infra/file/{configId}/get/{path},流量经过后端。
- 适合小文件(<1MB),大文件影响性能。
- 错误处理:
- 配置不存在:抛出 FILE_CONFIG_NOT_EXISTS。
- 客户端为空:抛出 Assert.notNull 异常。
- 数据库插入失败:需事务回滚。
总结
FileServiceImpl 方法作用:
- getFilePage:分页查询文件元信息。
- createFile(String, String, byte[]):上传文件到数据库,保存元信息,返回逻辑 URL。
- createFile(FileCreateReqVO):记录前端直传文件的元信息(不适用于数据库存储)。
- deleteFile:删除文件内容和元信息。
- getFileContent:获取文件内容,支持访问。
- getFilePresignedUrl:获取预签名 URL(数据库存储无效)。
动态生成 FileClient:
- 通过 clientCache.getUnchecked(CACHE_MASTER_ID) 获取 DBFileClient。
- 初次调用时,CacheLoader.load 查询主配置,创建 DBFileClient,存入缓存。
- 配置变更(如设为主配置)清除缓存,确保动态加载最新配置。
文件上传逻辑:
- 前端上传 test.jpg 到 /infra/file/upload。
- FileController.uploadFile 解析请求,调用 FileServiceImpl.createFile。
- FileServiceImpl 使用 DBFileClient 存储文件内容到 file_content,元信息到 file,返回逻辑 URL。