这是一个由 Kotlin&Java 混编开发的附件操作库 (SpringBootStarter)。
纯 Java 开发的 SpringBoot 工程可以直接引入一键使用。
经历过生产环境验证,基本功能方面没发现有什么问题。
核心用处请参考下方 README 或仓库。  
代码结构友好。适合学习 Kotlin 和参考编写属于你的 SpringBootStarter 。
如果你觉得多少有些用处的话,请不要吝啬你的 star 。QAQ
仓库地址: https://github.com/sorakylin/poet
下面是 README, 可以进 github 那边观看。
在系统开发中对于附件的操作多少得废些功夫, 尤其体现在个人开发者业余时间写项目时, 脱离了公司基础服务的情况下基本只能一个个去手写文件的增删改查。
使用云服务时如果你的附件不太正经,为了避免触发风控你还是得在本地保存一份。
此项目即是为了提升开发者的效率而诞生,基于本地文件系统。 一键引入即可实现对附件的各种操作。 由个人开发并在个人项目中使用过一段时间后开源。
使用 Kotlin & Java 混编开发,所有的逻辑都基于 Kotlin 实现。 为了方便没有使用过 Kotlin 的开发者也能够轻松的看源码+自定义组件的扩展, 所有接口抽象层使用了 Java 来定义
@Autowired 注入附件操作上下文即可使用众多附件操作方法MySQL、 PostgreSQL 两种支持上面说起来似乎东西不少, 但实际上用默认的组件+傻瓜式配置已经足以满足大部分需求
<dependency>
  <groupId>com.skypyb.poet</groupId>
  <artifactId>poet-spring-boot-starter</artifactId>
  <version>v1.1.0.RELEASE</version>
</dependency>
application.yml中进行最小配置poet:
  #存储路径 支持 windows 系统, 如 E:\tempfile\annex
  storage-location: /home/static/poet
使用上下文进行附件操作
例子(保存):
@RestController
@RequestMapping("/img")
public class TestResource {
    @Autowired
    private PoetAnnexContext poetAnnexContext;
    //文件流会安全的关闭
    @PostMapping("/upload-main")
    public ResponseEntity uploadMainImage(@RequestParam("file") MultipartFile file) throws IOException {
        //自定义模块(路径)
        String module = "main/customize1";
        PoetAnnex save = poetAnnexContext.save(file.getInputStream(), file.getOriginalFilename(), module);
        //上传到默认模块
//        PoetAnnex save = poetAnnexContext.save(file.getInputStream(), file.getOriginalFilename());
        save.getName();//生成的文件名 全局唯一
        save.getRealName();//文件原名
        save.getSuffix(); //后缀
        save.getKey(); //抽象路径
        save.getLength(); //文件长度
        return ResponseEntity.ok(save);
    }
}
通过 HTTP 端点进行附件操作
端点可以查看下面的 HTTP 端点说明, 或者进入 com.skypyb.poet.spring.boot.web.PoetResource 类查看实现
使用 HTTP 端点时必须开启 (默认开启)数据库储存模块
预览公开的图片这种需求, 建议上传到指定目录后使用反代来动静分离。 不是很推荐直接使用 HTTP 端点 , 毕竟要走一道 DB 查询操作,当然那几毫秒在普通项目里其实也没啥关系, 懒得配 Nginx 的直接用端点也可
文件的上传、删除建议使用上下文手动控制(避免被刷接口)  或实现内置的拦截器 PoetHandlerInterceptor 做鉴权操作
直接使用附件操作客户端对附件进行操作 (直接操作文件, 不会经过任何流程如拦截器、存储 等。 不推荐)
值得注意的是, 使用 context 操作附件只需要根据文件名 name 进行操作,  使用 client 则需要使用文件的 key (module+name)
例子
@RestController
@RequestMapping("/img")
public class TestResource {
    @Autowired
    private PoetAnnexClient poetAnnexClient;//基本操作客户端
    @Autowired
    private PoetAnnexClientHttpSupport poetAnnexClientHttpSupport; //HTTP 支持客户端
    @PostMapping("/upload-main")
    public ResponseEntity uploadMainImage(@RequestParam("file") MultipartFile file) throws IOException {
        PoetAnnex save = poetAnnexClient.save(file.getInputStream(), file.getOriginalFilename());
        //true
        Assert.isTrue(Objects.equals(save.getRealName(), save.getName()), "eq");
        return ResponseEntity.ok(save);
    }
}
| key | 默认值 | 说明 | 
|---|---|---|
| poet.storageLocation | 非空. 附件默认默认储存位置。 附件最终保存的路径为 storageLocation+module+name | |
| poet.enableDBStore | true | 是否使用 DB 储存附件信息, 关闭的话则无法使用 HTTP 相关操作 | 
| poet.enableWebResource | true | 是否启用 web 资源层. | 
| poet.webUrlPrefix | /poet | web 资源接口请求路径前缀 | 
| poet.defaultModule | 默认模块, 在文件保存时若不指定则将直接保存到此模块之中.  为空时直接保存在 storageLocation上 | |
| poet.tableName | poet_annex | 储存附件信息的表名 | 
| 端点 | 说明 | 入参 | 
|---|---|---|
| GET /poet/view/{name} | 预览文件, 解析后缀响应对应的 Content-Type 。 可直接显示图片 | |
| GET /poet/view-media/{name} | 预览媒体, 包括视频和音频 | |
| GET /poet/download/{name} | 下载附件 | |
| POST /up | 上传附件, 可指定模块(非必须) | file: MultipartFile, module: String? | 
| GET /bs/{name} | 获取 DB 中存储的附件业务信息 | |
| DELETE /bs/{name} | 删除一个附件 | 
图片处理的统一接口, 可自行拓展图片处理方式。
自带默认的处理实现DefaultImageHandler,可自定义宽、高、压缩比、输出格式(不支持 webp)
@FunctionalInterface
public interface PoetImageHandler {
    /**
     * 涉及到图片处理,  原名已经无意义了。
     * 这个图片的原名在流程中将会被抹去,  自己保存时定义新的名字
     *
     * @param bytes 图片字节数组
     * @return 新图片的字节数组
     */
    byte[] handle(@NotNull byte[] bytes);
}
//得到原图的 Bytes, 也可以是 InputStream 。InputStream 在传入 warp()后会被自动关闭
byte[] imgBytes = getBytes(originImage);
//创建一个将图片处理为宽度 980px + 压缩比 0.8 + 格式化为 jpg 的处理器
DefaultImageHandler handler = new DefaultImageHandler(980, Integer.MAX_VALUE, 0.8f, "jpg");
//得到的结果 Map 中,key 即为 use 时传入的 key, value 是处理完毕的结果。 
//可多次调用 use(), invoke()内的回调将会循环触发。Map 结果是所有 use 的 key + handler 处理结果
Map<String, PoetAnnex> invokeResult = PoetImageWrap.<String>warp(imgBytes)
    .use("w980", handler)
    .invoke((k, bytes) -> {//k = "w980"
        return poetAnnexContext.save(bytes, "name.jpg", "module");
    });
//通过指定的 key 得到 Map 中对应经过对应处理的图片
PoetAnnex thumbnailAnnex = invokeResult.get("w980");
实现 PoetHandlerInterceptor 接口+交给 Spring 控制 即可注册拦截器, 支持排序  
返回 true 表示通过, 也可以自定义异常抛出来中止操作
Mode 有三种, 分别为 ACCESS 、DELETE 、SAVE ,  更多细节注释可以查看源码
示例:
@Slf4j
@Component
public class LogInterceptor implements PoetHandlerInterceptor {
    @Override
    public boolean preHandle(Mode mode, String name, @Nullable String module) {
        log.info("Annex operation record. mode:{}, name:{}, module:{} ", mode, name, module);
        return true;
    }
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}
@Configuration
public class PoetAnnexConfig {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    //使用自带的 Pg 存储替换默认的 MySQL
    @Bean
    public PoetAnnexRepository poetAnnexRepository() {
        return new PostgresPoetAnnexRepository(jdbcTemplate);
    }
    //自定义附件的目录结构分割方式
    @Bean
    public PoetAnnexSlicer poetAnnexSlicer() {
        return new Hash2AnnexSlicer();
    }
    //自定义附件名字生成策略 (默认 UUID,  需保证全局唯一)
    @Bean
    public PoetAnnexNameGenerator poetAnnexNameGenerator() {
        return new RandomNameGenerator();
    }
}
看出来了吧? 只要你实现了对应的接口, 并且将其作为一个 Bean 交给 Spring 管理。 那么就会替换掉默认的实现
PoetAccessRouter 访问路由器,   没特殊需求的话默认即可PoetAnnexRepository DB 存储控件, 目前支持 MySQL 和 PostgreSQL,  需要支持其他 DB 比如 Oracle 、NoSQL 之类的可以自行实现PoetAnnexClient 附件操作客户端,  使用文件系统时直接默认即可PoetAnnexClientHttpSupport   附件操作 HTTP 支持,  使用文件系统时直接默认即可PoetAnnexSlicer  附件分割器,  可自定义路径分割。  默认不做任何操作直接组装 module&namePoetAnnexNameGenerator  名字生成器,  默认 UUID 。  需保证全局唯一附件操作上下文提供的所有操作附件方法
public interface PoetAnnexContext {
    PoetAnnex save(InputStream in, String name);
    PoetAnnex save(InputStream in, String name, String module);
    PoetAnnex save(byte[] data, String name);
    PoetAnnex save(byte[] data, String name, String module);
    boolean exist(String name);
    void delete(String name);
    byte[] getBytes(String name);
    void view(String name, HttpServletResponse response);
    void viewMedia(String name, HttpServletResponse response);
    void viewMedia(String name, HttpServletRequest request, HttpServletResponse response);
    void down(String name, HttpServletResponse response);
    void down(String name, String realName, HttpServletResponse response);
}
嘛... 总之无限欢迎 PR
如果帮到了你,请点个 star~