• 对象存储?CRUD Boy实现对文件的增删改查


    大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

    以下是正文!

    对象存储是什么?

    对象存储是一种数据存储方式,它将数据分割成不同的对象,并为每个对象分配一个唯一的标识符,用于访问和操作数据。这些对象被存储在多个服务器上,以确保数据的可靠性和可用性。对象存储适合存储大量数据,具有可扩展性、低成本和高安全性等特点。

    这段话太专业了,以至于有点让人看不懂。私以为对象存储就是『分布式文件存储系统』,可能是我们只会用它来存储图片、视频、文档等文件吧😅,然后数据库(比如MySQL)只存储文件的访问链接。目前接触的对象存储有
    阿里云对象存储OSS、天翼云对象存储融合版、自建对象存储MinIO。

    • 阿里云对象存储OSS

    帮助文档链接

    • 天翼云对象存储融合版

    帮助文档链接

    • MinIO对象存储

    帮助文档链接

    这三款对象存储产品对比如下

    产品 功能特点 是否收费 是否开源 对接难易度
    阿里云对象存储OSS 阿里云OSS提供了丰富的存储、数据处理和分发功能,可以满足各种场景的需求 收费 不开源 只需要ak/sk,然后看文档即可
    天翼云对象存储融合版 天翼云对象存储融合版主要面向移动互联网应用,提供了数据管理、在线处理等功能 收费 不开源 只需要ak/sk,然后看文档即可
    MinIO对象存储 MinIO专注于提供高性能、高可用的对象存储服务。 免费 开源 只需要ak/sk,然后看文档即可

    总之有钱的话就买服务,没钱就自己搭,总有合适自己的😉。

    对象存储和数据库的区别

    各维度对比

    存储 数据结构 数据处理 存储方式 可伸缩性
    数据库存储 数据库是基于表格的存储方式,每个表格有特定的列和行。 数据库主要用于存储结构化数据,如文本、数字和日期等。数据库可以进行更复杂的数据处理,如查询、过滤和排序等。 数据库通常使用关系型数据库或NoSQL数据库等 数据库在扩展性上需要更多的运维和管理
    对象存储 对象存储是基于对象的存储方式,每个对象可以是任何类型的文件 对象存储通常用于存储大量非结构化数据,如图片、视频和音频等 对象存储通常使用分布式存储技术将数据分散存储在不同的节点上 对象存储具有良好的扩展性,因此可以轻松地添加新的节点来处理更多的数据

    项目对接流程图对比

    CRUD之阿里云对象存储

    1. 安装Java SDK

    一般都是通过maven直接引入

    <dependency>
        <groupId>com.aliyun.ossgroupId>
        <artifactId>aliyun-sdk-ossartifactId>
        <version>3.15.1version>
    dependency>
    

    2. 获取Client

    官方获取Client代码示例

    // yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
    String endpoint = "yourEndpoint";
    // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
    String accessKeyId = "yourAccessKeyId";
    String accessKeySecret = "yourAccessKeySecret";
    
    // 创建OSSClient实例。
    OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    
    // 关闭OSSClient。
    ossClient.shutdown();                    
    

    我一般会将其变成一个Component

    @Component
    public class AliyunOssClient {
    
        @Value("${oss.endpoint}")
        private String endpoint;
        @Value("${aliyun.accessKeyId}")
        private String accessKeyId;
        @Value("${aliyun.accessKeySecret}")
        private String accessKeySecret;
    
        @Bean(name = "aliyunOssClient")
        public OSS aliyunOssClient() {
            // 构建并返回OSSClient
            return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        }
    }
    

    3. 增删改查操作

    (1)对象公共读&上传和访问

    官方文档

     /**
        * 简单上传-流式上传-公共读
        *
        * @param bucketName  bucket名称
        * @param key         文件名
        * @param inputStream 输入流
        * @return PutObjectResult 上传结果
        */
    public static PutObjectResult putObjectByInputStreamAndPublicRead(String bucketName, String key,
            InputStream inputStream) {
            ObjectMetadata metadata = new ObjectMetadata();
            //设置StorageClass为Standard即为
            metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
            //设置读写为公共读写
            metadata.setObjectAcl(CannedAccessControlList.PublicRead);
            // 准备OSS上传对象请求
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, inputStream);
            putObjectRequest.setMetadata(metadata);
            // 上传
            return ossClient.putObject(putObjectRequest);
    }
    

    当设置文件的访问权限为公共读时,直接拼接文件的访问链接就可以了,比如endpoint为oss-cn-hangzhou.aliyuncs.com,bucket为file-bucket,key为/test/file/1.png
    那么访问链接就为:https://file-bucket.oss-cn-hangzhou.aliyuncs.com/test/file/1.png

    (2)对象私有读&上传和访问

    官方文档

     /**
         * 简单上传-流式上传-私有读写
         *
         * @param bucketName  bucket名称
         * @param key         文件名
         * @param inputStream 输入流
         * @return PutObjectResult 上传结果
         */
        public static PutObjectResult putObjectByInputStreamAndPrivate(String bucketName, String key,
            InputStream inputStream) {
            ObjectMetadata metadata = new ObjectMetadata();
            //设置StorageClass为Standard即为
            metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
            //设置读写为公共读写
            metadata.setObjectAcl(CannedAccessControlList.Private);
            // 准备OSS上传对象请求
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, inputStream);
            putObjectRequest.setMetadata(metadata);
            // 上传
            return ossClient.putObject(putObjectRequest);
        }
    

    当设置文件的访问权限为私有读时,直接拼接访问链接是没法访问该文件的,访问会报如下错误:

    <Error>
    <Code>AccessDeniedCode>
    <Message>You do not have read permission on this object.Message>
    <RequestId>6488223453BCC63831BB3EC4RequestId>
    <HostId>file-bucket.oss-cn-hangzhou.aliyuncs.comHostId>
    <EC>0003-00000005EC>
    Error>
    

    这时就需要对key进行加签操作了,加签代码如下:

     /**
         * 通过bucketName、key、过期时间生成文件访问链接(时效性)
         *
         * @param bucketName bucket名称
         * @param key        文件名
         * @param expiration 过期时间
         * @return 文件访问链接
         */
        public static String getSignObjectUrl(String bucketName, String key, Date expiration) {
            return ossClient.generatePresignedUrl(bucketName, key, expiration).toString();
        }
    

    那么获取的链接格式就是这样了:https://file-bucket.oss-cn-hangzhou.aliyuncs.com/test/file/test.png?Expires=xxx&OSSAccessKeyId=xxxx&Signature=xxxx

    (3)对象下载

    官方文档

    下载到文件

       /**
         * 通过bucketName、key、文件路径 下载OSS文件到本地文件
         *
         * @param bucketName bucket名称
         * @param key        文件名
         * @param pathName   本地文件路径
         */
        public static void getObjectToFile(String bucketName, String key, String pathName) {
            // 下载OSS文件到本地
            ossClient.getObject(new GetObjectRequest(bucketName, key), new File(pathName));
        }
    

    流式下载

     /**
         * 通过bucketName、key 下载OSS变成字节流
         *
         * @param bucketName bucket名称
         * @param key        文件名
         */
        public static void getObjectToStream(String bucketName, String key) {
            OSSObject ossObject = null;
            BufferedReader reader = null;
            try {
                ossObject = ossClient.getObject(bucketName, key);
                reader = new BufferedReader(new InputStreamReader(ossObject.getObjectContent()));
                while (true) {
                    String line = null;
                    line = reader.readLine();
                    if (line == null) { break; }
    
                    System.out.println("\n" + line);
                }
            } catch (IOException e) {
                log.error("下载oss文件异常", e);
            } finally {
                try {
                    // 数据读取完成后,获取的流必须关闭,否则会造成连接泄漏,导致请求无连接可用,程序无法正常工作。
                    reader.close();
                    // ossObject对象使用完毕后必须关闭,否则会造成连接泄漏,导致请求无连接可用,程序无法正常工作。
                    ossObject.close();
                } catch (IOException e) {
                    log.error("关闭流发生异常", e);
                }
            }
        }
    

    (4)对象删除

    官方文档

       /**
         * 通过bucketName、key删除文件
         *
         * @param bucketName bucket名称
         * @param key        文件名
         */
        public static void delObject(String bucketName, String key) {
            // 删除文件或目录。如果要删除目录,目录必须为空。
            ossClient.deleteObject(bucketName, key);
        }
    

    (5)图片处理

    官方文档
    这个接口目前只有阿里云有写文档,其他云都是一笔带过没有详细说明,作用是将上传到OSS的原始图片进行缩放、旋转、加水印等操作,非常好用,在此强烈安利一波!!!

    • 将图片缩放为固定宽高100 px
    // 将图片缩放为固定宽高100 px。
    String style = "image/resize,m_fixed,w_100,h_100";
    GetObjectRequest request = new GetObjectRequest(bucketName, objectName);
    request.setProcess(style);
    // 将处理后的图片命名为example-resize.jpg并保存到本地。
    // 填写本地文件的完整路径,例如D:\\localpath\\example-resize.jpg。如果指定的本地文件存在会覆盖,不存在则新建。
    // 如果未指定本地路径只填写了本地文件名称(例如example-resize.jpg),则文件默认保存到示例程序所属项目对应本地路径中。
    ossClient.getObject(request, new File("D:\\localpath\\example-resize.jpg"));
    
    • 从坐标(100,100)开始,将图片裁剪为宽高100 px
    // 从坐标(100,100)开始,将图片裁剪为宽高100 px。
    style = "image/crop,w_100,h_100,x_100,y_100";
    request = new GetObjectRequest(bucketName, objectName);
    request.setProcess(style);
    // 将处理后的图片命名为example-crop.jpg并保存到本地。
    ossClient.getObject(request, new File("D:\\localpath\\example-crop.jpg"));
    
    • 将图片旋转90°
     // 将图片旋转90°。
    style = "image/rotate,90";
    request = new GetObjectRequest(bucketName, objectName);
    request.setProcess(style);
    // 将处理后的图片命名为example-rotate.jpg并保存到本地。
    ossClient.getObject(request, new File("D:\\localpath\\example-rotate.jpg"));
    
    • 在图片中添加文字水印。
    // 在图片中添加文字水印。
    // 文字水印的文字内容经过Base64编码后,再将编码结果中的加号(+)替换成短划线(-),正斜线(/)替换成下划线(_)并去掉尾部的等号(=),从而得到水印字符串。
    // 指定文字水印的文字内容为Hello World,文字内容进行编码处理后得到的水印字符串为SGVsbG8gV29ybGQ。
    style = "image/watermark,text_SGVsbG8gV29ybGQ";
    request = new GetObjectRequest(bucketName, objectName);
    request.setProcess(style);
    // 将处理后的图片命名为example-watermarktext.jpg并保存到本地。
    ossClient.getObject(request, new File("D:\\localpath\\example-watermarktext.jpg"));
    
    
    • 在图片中添加图片水印。请确保水印图片已保存在图片所在Bucket中
    // 在图片中添加图片水印。请确保水印图片已保存在图片所在Bucket中。
    // 水印图片的完整路径经过Base64编码后,再将编码结果中的加号(+)替换成短划线(-),正斜线(/)替换成下划线(_)并去掉尾部的等号(=),从而得到水印字符串。
    // 指定水印图片的完整路径为panda.jpg,完整路径进行编码处理后得到的水印字符串为cGFuZGEuanBn。
    style = "image/watermark,image_cGFuZGEuanBn";
    request = new GetObjectRequest(bucketName, objectName);
    request.setProcess(style);
    // 将处理后的图片命名为example-watermarkimage.jpg并保存到本地。
    ossClient.getObject(request, new File("D:\\localpath\\example-watermarkimage.jpg"));
          
    

    除了这些之外还有很多其他的功能,大家可以自己看文档~
    顺便说一个我们使用的案例

    我们之前在开发项目的时候,做了个文章发布的功能,简单来说就是PC后台管理端发布文章,然后在微信小程序、H5小程序点击查看。文章的内容是包括图文的,有些文章图片上传的是原图,一张好几兆大小,用户在手机端查看文章时加载慢不说,那流量更是跑得飞起。我们就是用这个功能对图片进行等比例缩放,控制图片大小来解决的。

    当时我们使用富文本框写的文章,这种文章会把图片、文字和dom元素混在一起,而且每张图片大小、分辨率都不同,这种情况下后端是没法对图片进行处理的,能处理图片的只有前端。为了解决这个问题,阿里云OSS提供了一个参数:x-oss-process。具体效果,我们直接看对比图:

    从上图看,加了参数后,图片直接变小,但也模糊了。也就是说,图片根本不需要后端处理,前端自己拼接参数就可以处理图片了,非常的方便,而且原图链接也在前端,还可以做长按查看原图功能。

    CRUD之天翼云对象存储

    1. 安装Java SDK

    同样都是通过maven直接引入,但是多了好几个依赖,毕竟不是自研的。

    
    <dependency>
      <groupId>cn.chinatelecomgroupId>
      <artifactId>oss-java-sdkartifactId>
    <version>2.0.3version>
    dependency>
    <dependency>
      <groupId>com.amazonawsgroupId>
      <artifactId>aws-java-sdk-s3artifactId>
    <version>1.11.336version>
    dependency>
    
    <dependency>
      <groupId>com.amazonawsgroupId>
      <artifactId>aws-java-sdk-stsartifactId>
      <version>1.11.336version>
    dependency>
    <dependency>
      <groupId>joda-timegroupId>
      <artifactId>joda-timeartifactId>
      <version>2.10.3version>
    dependency>
    

    2. 获取Client

    我还是将其变成一个Component

    @Component
    public class TianyiyunOssClient {
    
        @Value("${oss.endpoint}")
        private String endpoint;
        @Value("${tianyiyun.accessKeyId}")
        private String accessKeyId;
        @Value("${tianyiyun.accessKeySecret}")
        private String accessKeySecret;
    
        @Bean
        public AmazonS3 getOssClient() {
            BasicAWSCredentials credentials = new BasicAWSCredentials(accessKeyId, accessKeySecret);
            ClientConfiguration clientConfiguration = new ClientConfiguration();
            EndpointConfiguration endpointConfiguration = new EndpointConfiguration(
                endpoint, Regions.DEFAULT_REGION.getName());
            return AmazonS3ClientBuilder.standard()
                //客户端设置
                .withClientConfiguration(clientConfiguration)
                //凭证设置
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                //endpoint设置
                .withEndpointConfiguration(endpointConfiguration)
                .build();
        }
    }
    

    3. 增删改查操作

    (1)上传对象

    官方文档

        /**
         * 简单上传-流式上传-公共读
         *
         * @param bucketName  bucket名称
         * @param key         文件名
         * @param inputStream 输入流
         * @return PutObjectResult 上传结果
         */
        public static PutObjectResult putObjectByInputStream(String bucketName, String key, InputStream inputStream) {
            ObjectMetadata metadata = new ObjectMetadata();
            metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
            // 准备OSS上传对象请求
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, inputStream, metadata);
            putObjectRequest.setMetadata(metadata);
            //设置上传对象的Acl为公共读,私有写
            putObjectRequest.setCannedAcl(CannedAccessControlList.PublicRead);
            // 上传
            return tianyiyunOssClient.putObject(putObjectRequest);
        }
    

    基本和阿里云一模一样,这里就不赘述怎么实现公共读和私有读了,核心就是设置setCannedAcl。

    (2)获取对象访问链接

    官方文档

    
        /**
         * 生成预签名下载链接
         *
         * @param bucketName bucket名称
         * @param key        文件名
         * @param expiration 过期时间
         * @return 文件访问链接
         */
        public static String getObjectUrl(String bucketName, String key, Date expiration) {
            GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, key)
                .withMethod(HttpMethod.GET)
                .withExpiration(expiration);
            return tianyiyunOssClient.generatePresignedUrl(request).toString();
        }
    

    这里稍微和阿里云不太一样,虽然GeneratePresignedUrlRequest也有setExpiration方法,但好像不生效,必须要使用上面这种形式才可以加签,奇怪🤔。

    (3)删除对象

    官方文档

    /**
         * 通过bucketName、key删除文件
         *
         * @param bucketName bucket名称
         * @param key        文件名
         */
        public static void delObject(String bucketName, String key) {
            // 删除文件或目录。如果要删除目录,目录必须为空。
            tianyiyunOssClient.deleteObject(bucketName, key);
        }
    

    (4)图片处理

    官方文档
    这里天翼云的文档没有详细说明,但我还是找到了天翼云对象存储是怎么处理图片的。同阿里云的x-oss-process,天翼云的处理参数为x-amz-process。用法也和阿里云的一样,直接把参数拼接在url后面即可。

    try {
        GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, objectKey)
                .withMethod(HttpMethod.GET)
                .withExpiration(expiration);
        // 在URL中添加额外参数
        request.addRequestParameter("x-amz-limit", String.format("rate=%d", 100));
        request.addRequestParameter("x-amz-process", "image/watermark,text_12345678");
        URL url = s3.generatePresignedUrl(request);
    } catch (AmazonServiceException e) {
        System.err.println(e.getErrorMessage());
    }
    

    CRUD之MinIO对象存储

    1. 安装Java SDK

    同上

         <dependency>
                <groupId>io.miniogroupId>
                <artifactId>minioartifactId>
                <version>8.4.5version>
            dependency>
    

    2. 获取Client

    同上,注册为Component

    @Component
    public class MinioOssClient {
    
        @Value("${minio.endpoint}")
        private String endpoint;
        @Value("${minio.accessKey}")
        private String accessKey;
        @Value("${minio.secretKey}")
        private String secretKey;
    
        /**
         * 注入minio 客户端
         *
         * @return 客户端
         */
        @Bean
        public MinioClient minioClient() {
            return MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .httpClient(getUnsafeOkHttpClient())
                .build();
        }
    
        private OkHttpClient getUnsafeOkHttpClient() {
            try {
                final TrustManager[] trustAllCerts = new TrustManager[] {
                    new X509TrustManager() {
                        @Override
                        public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws
                            CertificateException {
                        }
    
                        @Override
                        public void checkServerTrusted(X509Certificate[] x509Certificates, String s)
                            throws CertificateException {
                        }
    
                        @Override
                        public X509Certificate[] getAcceptedIssuers() {
                            return new X509Certificate[] {};
                        }
                    }
                };
    
                X509TrustManager x509TrustManager = (X509TrustManager)trustAllCerts[0];
                final SSLContext sslContext = SSLContext.getInstance("SSL");
                sslContext.init(null, trustAllCerts, new SecureRandom());
                final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
                OkHttpClient.Builder builder = new OkHttpClient.Builder();
                builder.sslSocketFactory(sslSocketFactory, x509TrustManager);
    
                builder.hostnameVerifier(new HostnameVerifier() {
                    @Override
                    public boolean verify(String s, SSLSession sslSession) {
                        return true;
                    }
                });
                return builder.build();
    
            } catch (NoSuchAlgorithmException | KeyManagementException e) {
                throw new RuntimeException(e);
            }
        }
    
    }
    

    这里多了一个方法getUnsafeOkHttpClient方法,原因是MinIO是我们自建的,使用的https://ip:port的方式去调用,而不是域名方式。对接过这种地址的小伙伴都知道,如果不处理一下https的安全证书,调用的时候就会报错。该方法是为了解决SSL证书验证异常,即当请求的URL使用的是HTTPS协议时,如果证书无效或不被信任,会抛出SSLHandshakeException异常,通过自定义TrustManager和SSLSocketFactory来实现忽略证书的验证,从而避免SSL异常。

    3. 增删改查操作

    (1)上传对象

     /**
         * 简单上传-流式上传
         *
         * @param bucketName  bucket名称
         * @param key         文件名
         * @param inputStream 输入流
         * @return ObjectWriteResponse 上传结果
         */
        public static ObjectWriteResponse uploadInputStream(String bucketName, String key, InputStream inputStream)
            throws Exception {
            //设置权限
            Map userMetadata = new HashMap<>();
            //设置为公有读
            userMetadata.put("access-control", "public-read");
            //设置为私有读
            // userMetadata.put("access-control", "private");
            // 准备OSS上传对象请求
            PutObjectArgs putObjectArgs = PutObjectArgs.builder()
                // bucketName
                .bucket(bucketName)
                // 文件名称
                .object(key)
                //设置文件权限
                .userMetadata(userMetadata)
                .stream(inputStream, inputStream.available(), -1)
                .build();
            // 上传文件
            return minioClient.putObject(putObjectArgs);
        }
    

    (2)获取对象访问链接

      /**
         * 获取文件地址
         *
         * @param bucketName bucketName
         * @param key        文件名称
         * @param duration   过期时长
         * @param unit       过期时长单位
         * @return 文件地址
         */
        public static String getObjectUrl(String bucketName, String fileName, int duration, TimeUnit unit)
            throws Exception {
            // 查看文件地址
            return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
                // bucketName
                .bucket(bucketName)
                // 文件名称
                .object(fileName)
                // 过期参数
                .expiry(duration, unit)
                // 请求方式
                .method(Method.GET)
                // 构建参数
                .build()
            );
        }
    

    (3)删除对象

        /**
         * 通过bucketName、key删除文件
         *
         * @param bucketName bucket名称
         * @param key        文件名
         */
        public static void delObject(String bucketName, String key) throws Exception {
            minioClient.removeObject(
                RemoveObjectArgs.builder()
                    .bucket(bucketName)
                    .object(key)
                    .build());
        }
    

    (4)图片处理

    很可惜,MinIO没有图片处理的功能。

    总结一下

    这三种对象存储工具都是我在真实项目中使用过的,它们的使用流程相似,接口名称也差不多,但是在一些细节上还是有些不同的地方。为了让大家更好地了解它们,我在文章中都有标明区别。阿里云和天翼云使用起来比较方便,只需要购买服务就可以了。而MinIO就比较麻烦,需要自己搭建一个服务器环境,并在生产环境中满足主备、证书等方面的要求,这也让我在使用过程中踩了不少坑。不过,我会在新的文章中详细介绍如何搭建MinIO服务器以及如何解决它的问题。

    作者:不若为止
    欢迎任何形式的转载,但请务必注明出处。
    限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。

  • 相关阅读:
    【R语言】plot输出窗口大小的控制
    vue手写卡片切换,并且点击获取到卡片信息
    DxO PureRAW:赋予RAW图像生命,打造非凡视觉体验 mac/win版
    JupyterLab使用指南(二):JupyterLab基础
    【扩散模型】4、Improved DDPM | 引入可学习方差和余弦加噪机制来提升 DDPM
    第三篇【传奇开心果系列】Python的自动化办公库技术点案例示例:深度解读Pandas股票市场数据分析
    基于知名微服务框架go-micro开发gRPC应用程序
    C++知识精讲12——取整方式及实战讲解【全网最详细取整“集合”】
    opencv--使用直方图找谷底进行确定分割阈值
    前端成神之路-HTML
  • 原文地址:https://www.cnblogs.com/wlovet/p/17477069.html