7

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

 1 year ago
source link: https://www.cnblogs.com/wlovet/p/17477069.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

以下是正文!

对象存储是什么?

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

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

  • 阿里云对象存储OSS

帮助文档链接

1127399-20230613104049807-851980789.png
  • 天翼云对象存储融合版

帮助文档链接

1127399-20230613103853020-685228216.png
  • MinIO对象存储

帮助文档链接

1127399-20230613104200008-77256438.png

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

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

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

对象存储和数据库的区别

各维度对比

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

项目对接流程图对比

1127399-20230613143206044-1144048735.png

CRUD之阿里云对象存储

1. 安装Java SDK

一般都是通过maven直接引入

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.15.1</version>
</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>AccessDenied</Code>
<Message>You do not have read permission on this object.</Message>
<RequestId>6488223453BCC63831BB3EC4</RequestId>
<HostId>file-bucket.oss-cn-hangzhou.aliyuncs.com</HostId>
<EC>0003-00000005</EC>
</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。具体效果,我们直接看对比图:

1127399-20230615151123496-274433456.png

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

CRUD之天翼云对象存储

1. 安装Java SDK

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

<!-- 天翼云 -->
<dependency>
  <groupId>cn.chinatelecom</groupId>
  <artifactId>oss-java-sdk</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
  <groupId>com.amazonaws</groupId>
  <artifactId>aws-java-sdk-s3</artifactId>
<version>1.11.336</version>
</dependency>
<!-- 使用sts服务需要添加以下依赖 -->
<dependency>
  <groupId>com.amazonaws</groupId>
  <artifactId>aws-java-sdk-sts</artifactId>
  <version>1.11.336</version>
</dependency>
<dependency>
  <groupId>joda-time</groupId>
  <artifactId>joda-time</artifactId>
  <version>2.10.3</version>
</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.minio</groupId>
            <artifactId>minio</artifactId>
            <version>8.4.5</version>
        </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<String, String> 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服务器以及如何解决它的问题。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK