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

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

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

首先来看原始代码:

Controller层:



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


@GetMapping("/download")


public 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")


public 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")


public 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


public 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


public 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


public 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供前端进行上传/下载

最终修改后的代码如下:

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



public 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();





    public OssClientFactory() {


    }





    @Bean


    @Scope("prototype")


    public 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);


    }





    public static void closeOSSClient(String endpoint) {


        if (clientMap.containsKey(endpoint)) {


            clientMap.get(endpoint).shutdown();


            clientMap.remove(endpoint);


        }


    }





    public 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


public 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


public 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);


    }


}