阿里云OSS client实现单例模式流水账

·8237·17 分钟·
AI摘要: 本文主要介绍了阿里云OSS客户端实现单例模式流水账。在原有的代码中,每次用户对bucket内部文件的请求,都会在后端create和destory一遍,造成了极大的性能浪费。下载文件采用的方式是:oss下载文件到后端,后端通过通过输入输出流将文件内容传输到前端,这也对后端造成了极大的压力。原来代码的流程图如下:

今天维护组里的后端系统,发现了不少让人脑溢血的代码,主要是对阿里云oss的CRUD部分。

首先来看原始代码:

Controller层:


@ApiOperation("下载指定文件")

@GetMapping("/download")

Blog void download(@RequestParam String url, HttpSession session, HttpServletResponse response)

        throws IOException {

    try {

        InputStream inputStream = new ByteArrayInputStream(

            ossService.newfileDownload(String.valueOf(session.getAttribute("accessKeyId")),

                    String.valueOf(session.getAttribute("accessKeySecret")),

                    String.valueOf(session.getAttribute("endpoint")),

                    url));

        

        OutputStream outputStream = response.getOutputStream();

        String[] parts = url.split("/");

        String filename = parts[parts.length - 1];

        response.setContentType("application/x-download");

        String saveName = new String(filename.getBytes("GBK"), "iso8859-1");

        response.addHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + saveName);

        IoUtil.copy(inputStream, outputStream);

        

    } catch (IORuntimeException | IOException e) {

        // 解决每次下载后报错问题,不影响实际应用

    }

    // return new ResultWrapper(res);

}



@ApiOperation("删除指定文件")

@PostMapping("/delete")

Blog ResultWrapper delete(@RequestParam String url, HttpSession session) {

    boolean result = ossService.deleteFile(String.valueOf(session.getAttribute("accessKeyId")),

            String.valueOf(session.getAttribute("accessKeySecret")),

            String.valueOf(session.getAttribute("endpoint")),

            url);

    return new ResultWrapper(result);

}



@ApiOperation("获取所有bucket")

@PostMapping("/buckets")

Blog ResultWrapper getAllBuckets(HttpSession session) {

    List<Bucket> result = ossService.listBuckets(String.valueOf(session.getAttribute("accessKeyId")),

            String.valueOf(session.getAttribute("accessKeySecret")));

    return new ResultWrapper(result);

}

Service层:


@Override

Blog String fileUpload(byte[] bytes, String dir, String name) {

    OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

    try {

        // 上传代码的逻辑

    } catch (OSSException oe) {

        // 异常处理的逻辑

    } catch (ClientException ce) {

        // 异常处理的逻辑

    } finally {

        if (ossClient != null) {

            ossClient.shutdown();

        }

    }

    return name;

}



@Override

Blog byte[] fileDownload(String dir, String fileName) throws IOException {

    File template = null;

    String[] split = fileName.split("\\.");

    template = File.createTempFile(split[0], "." + split[1]);

    // 创建OSSClient实例。

    OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

    // 下载Object到本地文件,并保存到指定的本地路径中。如果指定的本地文件存在会覆盖,不存在则新建。

    ossClient.getObject(new GetObjectRequest(bucketName, dir + "/" + fileName), template);

    // 关闭OSSClient。

    ossClient.shutdown();

    return IoUtil.readBytes(IoUtil.toStream(template));

}



@Override

Blog List<Bucket> listBuckets(String accessKeyId, String accessKeySecret) {

    // 创建OSSClient实例

    OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

    List<Bucket> bucketList = new ArrayList<>();

    try {

        // 获取Bucket列表的逻辑

    } catch (Exception e) {

        e.printStackTrace();

    } finally {

        // 关闭OSSClient

        ossClient.shutdown();

    }

    return bucketList;

}



总结如下:

  1. 每一次用户对bucket内部文件的请求,都会在后端create和destory一遍,造成了极大的性能浪费。

  2. 下载文件采用的方式是:oss下载文件到后端,后端通过通过输入输出流将文件内容传输到前端,这也对后端造成了极大的压力

原来代码的流程图如下:

image-20240714215817762

对于同一个bucket的请求,无论是增删改查,其实都可以复用一个实例,避免频繁创建销毁对象,所以改用单例模式:

image-20240714215935881

但是,再考虑深入一些,如果是多用户对多个bucket进行增删改查,这个时候就不能只复用一个oss client实例,因为一个oss client实例只和一个bucket绑定。

所以,需要改成如下的模式:

image-20240714220148137

对于上传和下载,可以不必采用后端中转的方式,但是也不能直接把keysecret存放在前端,这样有安全隐患。因此,采用后端签发临时URL供前端进行上传/下载

最终修改后的代码如下:

工厂类(用来实现单例模式)


Blog class OssClientFactory {

    // volatile是Java提供的一种轻量级的同步机制,在并发编程中,也扮演着比较重要的角色.

    // 同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级,相比使用

    // synchronized所带来的庞大开销,倘若能恰当的合理的使用volatile,则wonderful



    private volatile static OSSClientBuilder ossClientBuilder;

    // ConcurrentHashMap是线程安全的HashMap,它的线程安全是通过分段锁实现的,它的效率比Hashtable高

    private static ConcurrentHashMap <String, OSSClient> clientMap = new ConcurrentHashMap();



    Blog OssClientFactory() {

    }



    @Bean

    @Scope("prototype")

    Blog static OSS getOSSClient(String endpoint, String accessKeyId, String accessKeySecret) {



        if (clientMap.containsKey(endpoint)) {

            OSSClient client = clientMap.get(endpoint);

            return client;

        }



        synchronized (OssClientFactory.class) {

            ClientBuilderConfiguration conf = new ClientBuilderConfiguration();

            // 设置OSSClient允许打开的最大HTTP连接数,默认为1024个。

            conf.setMaxConnections(200);

            // 设置Socket层传输数据的超时时间,默认为50000毫秒。

            conf.setSocketTimeout(10000);

            // 设置建立连接的超时时间,默认为50000毫秒。

            conf.setConnectionTimeout(10000);

            // 设置从连接池中获取连接的超时时间(单位:毫秒),默认不超时。

            conf.setConnectionRequestTimeout(1000);

            // 设置连接空闲超时时间。超时则关闭连接,默认为60000毫秒。

            conf.setIdleConnectionTime(10000);

            // 设置失败请求重试次数,默认为3次。

            conf.setMaxErrorRetry(5);



            OSSClient client = (OSSClient) getOSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret, conf);

            clientMap.put(endpoint, client);

        }



        return clientMap.get(endpoint);

    }



    Blog static void closeOSSClient(String endpoint) {

        if (clientMap.containsKey(endpoint)) {

            clientMap.get(endpoint).shutdown();

            clientMap.remove(endpoint);

        }

    }



    Blog static OSSClientBuilder getOSSClientBuilder() {

        System.out.println("获取OSSClientBuilder");

        if (ossClientBuilder == null) {

            System.out.println("OSSClientBuilder为空,创建中");

            synchronized (OssClientFactory.class) {

                if (ossClientBuilder == null) {

                    System.out.println("进入同步实例化OSSClientBuilder");

                    ossClientBuilder = new OSSClientBuilder();

                }

            }

        }

        return ossClientBuilder;

    }



}


@Override

Blog String fileUpload(byte[] bytes, String dir, String name) {

    // 创建实例

    OSS ossClient = OssClientFactory.getOSSClient(endpoint, accessKeyId, accessKeySecret);

    try {

        ossClient.putObject(bucketName, dir + "/" + name, new ByteArrayInputStream(bytes));

    } catch (OSSException oe) {

	// do something

    }  finally {

        if (ossClient != null) {

            // 关闭实例

            OssClientFactory.closeOSSClient(endpoint);

        }

    }

    return name;

}

// 生成临时签名的url,这个url可供前端直接上传/下载

@Override

Blog String generatePresignedUrl(String bucketName, String objectName, String endpoint, Long expireTime,

        HttpMethod method) {

    GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucketName, objectName, method);

    Date expiration = new Date(System.currentTimeMillis() + expireTime);

    request.setExpiration(expiration);

    // 创建OSSClient实例。

    OSS ossClient = OssClientFactory.getOSSClient(endpoint, accessKeyId, accessKeySecret);

    try {

        // 生成签名URL。

        return ossClient.generatePresignedUrl(request).toString();

    } finally {

        // 关闭OSSClient。

        OssClientFactory.closeOSSClient(endpoint);

    }

}

Kaggle学习赛初探