MinIO实现分布式文件服务
文章目录
- 什么是MinIO?
- 应用场景
- 特点
- 安装MinIO
- 获取镜像
- 启动镜像
- 图形界面操作
- SpringBoot整合MinIO
- 获取accessKey和secretKey
- 添加依赖
- 添加配置
- 配置类
- Utils
- 文件类型
- 操作Minio
- Service
- Controller
- 测试
- 总结
什么是MinIO?
Minio
是个基于Golang
编写的开源对象存储套件,基于Apache License v2.0
开源协议,虽然轻量,却拥有着不错的性能。它兼容亚马逊S3云存储服务接口。可以很简单的和其他应用结合使用,例如NodeJS
、Redis
、MySQL
等。
应用场景
MinIO
的应用场景除了可以作为私有云的对象存储服务来使用,也可以作为云对象存储的网关层,无缝对接Amazon S3
或者MicroSoft Azure
。
特点
高性能
:作为一款高性能存储,在标准硬件条件下,其读写速率分别可以达到55Gb/s
和35Gb/s
。并且MinIO支持一个对象文件可以是任意大小,从几kb到最大5T不等。可扩展
:不同MinIO集群可以组成联邦,并形成一个全局的命名空间,并且支持跨越多个数据中心。云原生
:容器化、基于K8S的编排、多租户支持。Amazon S3兼容
:使用 Amazon S3 v2 / v4 API。可以使用Minio SDK,Minio Client,AWS SDK 和 AWS CLI 访问Minio服务器。SDK支持
:
- GO SDK:https://github.com/minio/minio-go
- JavaSDK:https://github.com/minio/minio-java
- PythonSDK:https://github.com/minio/minio-py
图形化界面
:有操作页面支持纠删码
:MinIO使用纠删码、Checksum来防止硬件错误和静默数据污染。在最高冗余度配置下,即使丢失1/2的磁盘也能恢复数据。
功能很强大,本文只是抛砖引玉,有兴趣的朋友自己去探索吧~
安装MinIO
这里使用docker安装,步骤如下:
获取镜像
执行命令如下:
docker pull minio/minio
启动镜像
执行命令如下:
docker run -p 9000:9000 -p 9001:9001 --name minio -d --restart=always -e "MINIO_ACCESS_KEY=admin" -e "MINIO_SECRET_KEY=admin" -v /home/data:/data -v /home/config:/root/.minio minio/minio server --console-address ":9000" --address ":9001" /data
命令解释如下:
-p
:9000是图形界面的端口,9001是API的端口,在使用SDK连接需要用到MINIO_ACCESS_KEY
:指定图形界面的用户名MINIO_SECRET_KEY
:指定图形界面的密码
按照上述两个步骤启动成功即可。
图形界面操作
安装成功后直接访问地址:http:/ip:9000/login
,如下:
输入用户名和密码登录成功后,如下:
菜单很多,这里就不再详细介绍了,笔者这里直接在Buckets菜单中创建一个桶为test,如下图:
并且设置这个桶的隐私规则为public,如下:
MinIO到此已经安装设置成功了
SpringBoot整合MinIO
虽然MinIO在图形界面提供了手动上传的操作,但是也可以通过SDK的方式去上传,下面介绍一下Spring Boot 整合MinIO上传文件。
获取accessKey和secretKey
这里的accessKey
和secretKey
并不是图形界面登录名和密码,获取很简单,直接在图形界面中操作,如下图:
添加依赖
添加MinIO的依赖,如下:
<dependency><groupId>io.minio</groupId><artifactId>minio</artifactId><version>8.2.1</version>
</dependency>
添加配置
在aplication.yml
配置中添加MInIO
相关的配置,如下:
minio:# 访问的urlendpoint: http://192.168.47.148# API的端口port: 9001# 秘钥accessKey: 账户secretKey: 密码secure: falsebucket-name: test # 桶名 我这是给出了一个默认桶名image-size: 10485760 # 我在这里设定了 图片文件的最大大小file-size: 1073741824 # 此处是设定了文件的最大大小
配置类
import io.minio.MinioClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Data
@Configuration
@ConfigurationProperties(prefix = "minio")
public class MinioProperties {/*** 是一个URL,域名,IPv4或者IPv6地址*/private String endpoint;/*** TCP/IP端口号*/private Integer port;/*** accessKey类似于用户ID,用于唯一标识你的账户*/private String accessKey;/*** secretKey是你账户的密码*/private String secretKey;/*** 如果是true,则用的是https而不是http,默认值是true*/private boolean secure;/*** 默认存储桶*/private String bucketName;/*** 图片的最大大小*/private long imageSize;/*** 其他文件的最大大小*/private long fileSize;/*** 官网给出的 构造方法* 此类是 客户端进行操作的类*/@Beanpublic MinioClient minioClient() {MinioClient minioClient =MinioClient.builder().credentials(accessKey, secretKey).endpoint(endpoint,port,secure).build();return minioClient;}
}
Utils
文件类型
import cn.hutool.core.io.FileTypeUtil;
import org.springframework.web.multipart.MultipartFile;import java.io.IOException;
import java.io.InputStream;/*** 文件类型工具类,定义了部分文件格式,有缺失的自行添加*/
public class FileTypeUtils {private final static String IMAGE_TYPE = "image/";private final static String AUDIO_TYPE = "audio/";private final static String VIDEO_TYPE = "video/";private final static String APPLICATION_TYPE = "application/";private final static String TXT_TYPE = "text/";public static String getFileType(MultipartFile multipartFile) {InputStream inputStream = null;String type = null;try {inputStream = multipartFile.getInputStream();type = FileTypeUtil.getType(inputStream);System.out.println(type);if (type.equalsIgnoreCase("JPG") || type.equalsIgnoreCase("JPEG")|| type.equalsIgnoreCase("GIF") || type.equalsIgnoreCase("PNG")|| type.equalsIgnoreCase("BMP") || type.equalsIgnoreCase("PCX")|| type.equalsIgnoreCase("TGA") || type.equalsIgnoreCase("PSD")|| type.equalsIgnoreCase("TIFF")) {return IMAGE_TYPE+type;}if (type.equalsIgnoreCase("mp3") || type.equalsIgnoreCase("OGG")|| type.equalsIgnoreCase("WAV") || type.equalsIgnoreCase("REAL")|| type.equalsIgnoreCase("APE") || type.equalsIgnoreCase("MODULE")|| type.equalsIgnoreCase("MIDI") || type.equalsIgnoreCase("VQF")|| type.equalsIgnoreCase("CD")) {return AUDIO_TYPE+type;}if (type.equalsIgnoreCase("mp4") || type.equalsIgnoreCase("avi")|| type.equalsIgnoreCase("MPEG-1") || type.equalsIgnoreCase("RM")|| type.equalsIgnoreCase("ASF") || type.equalsIgnoreCase("WMV")|| type.equalsIgnoreCase("qlv") || type.equalsIgnoreCase("MPEG-2")|| type.equalsIgnoreCase("MPEG4") || type.equalsIgnoreCase("mov")|| type.equalsIgnoreCase("3gp")) {return VIDEO_TYPE+type;}if (type.equalsIgnoreCase("doc") || type.equalsIgnoreCase("docx")|| type.equalsIgnoreCase("ppt") || type.equalsIgnoreCase("pptx")|| type.equalsIgnoreCase("xls") || type.equalsIgnoreCase("xlsx")|| type.equalsIgnoreCase("zip")||type.equalsIgnoreCase("jar")) {return APPLICATION_TYPE+type;}if (type.equalsIgnoreCase("txt")) {return TXT_TYPE+type;}} catch (IOException e) {e.printStackTrace();}return null;}
}
操作Minio
import com.java.family.minio.config.MinioProperties;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.SneakyThrows;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;@Component
public class MinioUtil {private final MinioClient minioClient;private final MinioProperties minioProperties;public MinioUtil(MinioClient minioClient, MinioProperties minioProperties) {this.minioClient = minioClient;this.minioProperties = minioProperties;}/*** 检查存储桶是否存在** @param bucketName 存储桶名称* @return*/@SneakyThrowspublic boolean bucketExists(String bucketName) {boolean found =minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());if (found) {System.out.println(bucketName + " exists");} else {System.out.println(bucketName + " does not exist");}return found;}/*** 创建存储桶** @param bucketName 存储桶名称*/@SneakyThrowspublic boolean makeBucket(String bucketName) {boolean flag = bucketExists(bucketName);if (!flag) {minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());return true;} else {return false;}}/*** 列出所有存储桶名称** @return*/@SneakyThrowspublic List<String> listBucketNames() {List<Bucket> bucketList = listBuckets();List<String> bucketListName = new ArrayList<>();for (Bucket bucket : bucketList) {bucketListName.add(bucket.name());}return bucketListName;}/*** 列出所有存储桶** @return*/@SneakyThrowspublic List<Bucket> listBuckets() {return minioClient.listBuckets();}/*** 删除存储桶** @param bucketName 存储桶名称* @return*/@SneakyThrowspublic boolean removeBucket(String bucketName) {boolean flag = bucketExists(bucketName);if (flag) {Iterable<Result<Item>> myObjects = listObjects(bucketName);for (Result<Item> result : myObjects) {Item item = result.get();// 有对象文件,则删除失败if (item.size() > 0) {return false;}}// 删除存储桶,注意,只有存储桶为空时才能删除成功。minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());flag = bucketExists(bucketName);if (!flag) {return true;}}return false;}/*** 列出存储桶中的所有对象名称** @param bucketName 存储桶名称* @return*/@SneakyThrowspublic List<String> listObjectNames(String bucketName) {List<String> listObjectNames = new ArrayList<>();boolean flag = bucketExists(bucketName);if (flag) {Iterable<Result<Item>> myObjects = listObjects(bucketName);for (Result<Item> result : myObjects) {Item item = result.get();listObjectNames.add(item.objectName());}}else{listObjectNames.add("存储桶不存在");}return listObjectNames;}/*** 列出存储桶中的所有对象** @param bucketName 存储桶名称* @return*/@SneakyThrowspublic Iterable<Result<Item>> listObjects(String bucketName) {boolean flag = bucketExists(bucketName);if (flag) {return minioClient.listObjects(ListObjectsArgs.builder().bucket(bucketName).build());}return null;}/*** 文件上传** @param bucketName* @param multipartFile*/@SneakyThrowspublic void putObject(String bucketName, MultipartFile multipartFile, String filename, String fileType) {InputStream inputStream = new ByteArrayInputStream(multipartFile.getBytes());minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(filename).stream(inputStream, -1, minioProperties.getFileSize()).contentType(fileType).build());}/*** 文件访问路径** @param bucketName 存储桶名称* @param objectName 存储桶里的对象名称* @return*/@SneakyThrowspublic String getObjectUrl(String bucketName, String objectName) {boolean flag = bucketExists(bucketName);String url = "";if (flag) {url = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().method(Method.GET).bucket(bucketName).object(objectName).expiry(2, TimeUnit.MINUTES).build());System.out.println(url);}return url;}/*** 删除一个对象** @param bucketName 存储桶名称* @param objectName 存储桶里的对象名称*/@SneakyThrowspublic boolean removeObject(String bucketName, String objectName) {boolean flag = bucketExists(bucketName);if (flag) {minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());return true;}return false;}/*** 以流的形式获取一个文件对象** @param bucketName 存储桶名称* @param objectName 存储桶里的对象名称* @return*/@SneakyThrowspublic InputStream getObject(String bucketName, String objectName) {boolean flag = bucketExists(bucketName);if (flag) {StatObjectResponse statObject = statObject(bucketName, objectName);if (statObject != null && statObject.size() > 0) {InputStream stream =minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());return stream;}}return null;}/*** 获取对象的元数据** @param bucketName 存储桶名称* @param objectName 存储桶里的对象名称* @return*/@SneakyThrowspublic StatObjectResponse statObject(String bucketName, String objectName) {boolean flag = bucketExists(bucketName);if (flag) {StatObjectResponse stat =minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());return stat;}return null;}/*** 删除指定桶的多个文件对象,返回删除错误的对象列表,全部删除成功,返回空列表** @param bucketName 存储桶名称* @param objectNames 含有要删除的多个object名称的迭代器对象* @return*/@SneakyThrowspublic boolean removeObject(String bucketName, List<String> objectNames) {boolean flag = bucketExists(bucketName);if (flag) {List<DeleteObject> objects = new LinkedList<>();for (int i = 0; i < objectNames.size(); i++) {objects.add(new DeleteObject(objectNames.get(i)));}Iterable<Result<DeleteError>> results =minioClient.removeObjects(RemoveObjectsArgs.builder().bucket(bucketName).objects(objects).build());for (Result<DeleteError> result : results) {DeleteError error = result.get();System.out.println("Error in deleting object " + error.objectName() + "; " + error.message());return false;}}return true;}/*** 以流的形式获取一个文件对象(断点下载)** @param bucketName 存储桶名称* @param objectName 存储桶里的对象名称* @param offset 起始字节的位置* @param length 要读取的长度 (可选,如果无值则代表读到文件结尾)* @return*/@SneakyThrowspublic InputStream getObject(String bucketName, String objectName, long offset, Long length) {boolean flag = bucketExists(bucketName);if (flag) {StatObjectResponse statObject = statObject(bucketName, objectName);if (statObject != null && statObject.size() > 0) {InputStream stream =minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).offset(offset).length(length).build());return stream;}}return null;}/*** 通过InputStream上传对象** @param bucketName 存储桶名称* @param objectName 存储桶里的对象名称* @param inputStream 要上传的流* @param contentType 要上传的文件类型 MimeTypeUtils.IMAGE_JPEG_VALUE* @return*/@SneakyThrowspublic boolean putObject(String bucketName, String objectName, InputStream inputStream,String contentType) {boolean flag = bucketExists(bucketName);if (flag) {minioClient.putObject(PutObjectArgs.builder().bucket(bucketName).object(objectName).stream(inputStream, -1, minioProperties.getFileSize()).contentType(contentType).build());StatObjectResponse statObject = statObject(bucketName, objectName);if (statObject != null && statObject.size() > 0) {return true;}}return false;}
}
Service
import com.java.family.minio.config.MinioProperties;
import com.java.family.minio.service.MinioService;
import com.java.family.minio.utils.MinioUtil;
import io.minio.MinioClient;
import io.minio.messages.Bucket;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;import java.io.InputStream;
import java.util.List;
import java.util.UUID;@Service
public class MinioServiceImpl implements MinioService {@Autowiredprivate MinioUtil minioUtil;@Autowiredprivate MinioProperties minioProperties;@Overridepublic boolean bucketExists(String bucketName) {return minioUtil.bucketExists(bucketName);}@Overridepublic void makeBucket(String bucketName) {minioUtil.makeBucket(bucketName);}@Overridepublic List<String> listBucketName() {return minioUtil.listBucketNames();}@Overridepublic List<Bucket> listBuckets() {return minioUtil.listBuckets();}@Overridepublic boolean removeBucket(String bucketName) {return minioUtil.removeBucket(bucketName);}@Overridepublic List<String> listObjectNames(String bucketName) {return minioUtil.listObjectNames(bucketName);}@Overridepublic String putObject(MultipartFile file, String bucketName,String fileType) {try {bucketName = StringUtils.isNotBlank(bucketName) ? bucketName : minioProperties.getBucketName();if (!this.bucketExists(bucketName)) {this.makeBucket(bucketName);}String fileName = file.getOriginalFilename();String objectName = UUID.randomUUID().toString().replaceAll("-", "")+ fileName.substring(fileName.lastIndexOf("."));minioUtil.putObject(bucketName, file, objectName,fileType);return minioProperties.getEndpoint() + ":" + minioProperties.getPort() + "/" + bucketName + "/" + objectName;} catch (Exception e) {e.printStackTrace();return "上传失败";}}@Overridepublic InputStream downloadObject(String bucketName, String objectName) {return minioUtil.getObject(bucketName,objectName);}@Overridepublic boolean removeObject(String bucketName, String objectName) {return minioUtil.removeObject(bucketName, objectName);}@Overridepublic boolean removeListObject(String bucketName, List<String> objectNameList) {return minioUtil.removeObject(bucketName,objectNameList);}@Overridepublic String getObjectUrl(String bucketName,String objectName) {return minioUtil.getObjectUrl(bucketName, objectName);}
}
Controller
import com.java.family.minio.service.MinioService;
import com.java.family.minio.utils.FileTypeUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;@RequestMapping("/minio")
@RestController
public class MinioController {@Autowiredprivate MinioService minioService;@PostMapping("/upload")public String uploadFile(MultipartFile file, String bucketName) {String fileType = FileTypeUtils.getFileType(file);if (fileType != null) {return minioService.putObject(file, bucketName, fileType);}return "不支持的文件格式。请确认格式,重新上传!!!";}@PostMapping("/addBucket/{bucketName}")public String addBucket(@PathVariable String bucketName) {minioService.makeBucket(bucketName);return "创建成功!!!";}@GetMapping("/show/{bucketName}")public List<String> show(@PathVariable String bucketName) {return minioService.listObjectNames(bucketName);}@GetMapping("/showBucketName")public List<String> showBucketName() {return minioService.listBucketName();}@GetMapping("/showListObjectNameAndDownloadUrl/{bucketName}")public Map<String, String> showListObjectNameAndDownloadUrl(@PathVariable String bucketName) {Map<String, String> map = new HashMap<>();List<String> listObjectNames = minioService.listObjectNames(bucketName);String url = "localhost:8085/minio/download/" + bucketName + "/";listObjectNames.forEach(System.out::println);for (int i = 0; i <listObjectNames.size() ; i++) {map.put(listObjectNames.get(i),url+listObjectNames.get(i));}return map;}@DeleteMapping("/removeBucket/{bucketName}")public String delBucketName(@PathVariable String bucketName) {return minioService.removeBucket(bucketName) == true ? "删除成功" : "删除失败";}@DeleteMapping("/removeObject/{bucketName}/{objectName}")public String delObject(@PathVariable("bucketName") String bucketName, @PathVariable("objectName") String objectName) {return minioService.removeObject(bucketName, objectName) == true ? "删除成功" : "删除失败";}@DeleteMapping("/removeListObject/{bucketName}")public String delListObject(@PathVariable("bucketName") String bucketName, @RequestBody List<String> objectNameList) {return minioService.removeListObject(bucketName, objectNameList) == true ? "删除成功" : "删除失败";}@RequestMapping("/download/{bucketName}/{objectName}")public void download(HttpServletResponse response, @PathVariable("bucketName") String bucketName, @PathVariable("objectName") String objectName) {InputStream in = null;try {in = minioService.downloadObject(bucketName, objectName);response.setHeader("Content-Disposition", "attachment;filename="+ URLEncoder.encode(objectName, "UTF-8"));response.setCharacterEncoding("UTF-8");//将字节从InputStream复制到OutputStream 。IOUtils.copy(in, response.getOutputStream());} catch (UnsupportedEncodingException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (in != null) {try {in.close();} catch (IOException e) {e.printStackTrace();}}}}
}
测试
直接调用接口上传一张图片试一下,如下:
接口返回的URL就是文件的访问地址,直接输入浏览器访问即可。
在MInIO中也可以看到存储的文件,如下图:
如果你需要分享给别人,也可以手动分享,有效期是7天,一旦过了这个有效期将会失效
,如下:
总结
MInIO虽然是个开源项目,但是功能非常强大,小型项目中完全可以用它实现对象存储,也可以使用MinIO搭建一个免费的图床。
MinIO实现分布式文件服务相关推荐
- 从windows server的文件服务到分布式文件服务(一)
一.序言 不知道大家是否考虑过服务器与终端机或者叫桌面机.工作站等的区别都有什么?也许你会说体积不同,也许你会说功能不同,甚至有些人会拉一个清单出来说说硬件上面的区别等.其实我最想说的是,两者最大的区 ...
- HarmonyOS之数据管理·分布式文件服务的应用
一.简介 ① 基本概念 分布式文件服务能够为用户设备中的应用程序提供多设备之间的文件共享能力,支持相同帐号下同一应用文件的跨设备访问,应用程序可以不感知文件所在的存储设备,能够在多个设备之间无缝获取文 ...
- 从windows server的文件服务到分布式文件服务(二)
四.默认共享描述 除非使用GHOST系统安装,当使用原版的系统光盘安装系统,默认安装会启动共享功能.说到共享,微软实际上提供了两种共享服务,一个就是打印机的共享服务,被称为"打印和文件服务& ...
- Atittit HDFS hadoop 大数据文件系统java使用总结 目录 1. 操作系统,进行操作 1 2. Hdfs 类似nfs ftp远程分布式文件服务 2 3. 启动hdfs服务start
Atittit HDFS hadoop 大数据文件系统java使用总结 目录 1. 操作系统,进行操作 1 2. Hdfs 类似nfs ftp远程分布式文件服务 2 3. 启动hdfs服务start- ...
- .net core 封装文件服务操作——分布式文件服务之OSS
这里分享本人所写的文件微服务系统,在每个SAAS系统内,都或多或少的涉及到文件服务,我们既可以写的非常复杂,又可以写的简洁干练,不管怎么编写文件服务,但始终需要记住的是,一定要支持分布式文件服务,否则 ...
- 从windows server的文件服务到分布式文件服务(九)
八.共享文件夹的磁盘配额管理 磁盘配额管理,是在WINDOWS 2000 SERVER就开始拥有的功能,用于本地的磁盘配额管理.在文件服务中,如果我们只是提供共享的文件给用户读取,仅有管理员有权限往共 ...
- 共享文件服务器设置配额,从windows server的文件服务到分布式文件服务(九):共享文件夹的磁盘配额管理...
八.共享文件夹的磁盘配额管理 磁盘配额管理,是在WINDOWS 2000 SERVER就开始拥有的功能,用于本地的磁盘配额管理.在文件服务中,如果我们只是提供共享的文件给用户读取,仅有管理员有权限往共 ...
- 分布式文件服务minio集群搭建
minio集群搭建过程记录 minio集群搭建 1.服务器:2台服务器(存储数据目录不能是/root目录,否则会报错) 2.从官网获取Minio二进制文件并上传至2台集群主机: 3.创建目录(2台集群 ...
- fastdfs 测试客户端_分布式文件服务 FastDFS (第一篇)
[ 基础环境准备 ] 1.yum install -y gcc gcc-c++ 2.yum -y install libevent & vsftpd安装 &selinux关闭 防火墙权 ...
最新文章
- stale element reference: element is not attached to the page document 异常
- UA OPTI501 电磁波 求解Maxwell方程组的波动方程方法
- const对象,NULL和nullptr,C++中创建对象数组
- WebDriver API 元素定位(三)
- DynamipsGUI下CISCO SDM的安装配置
- 《精通Python设计模式》学习之原型模式
- PostgreSQL数据目录深度揭秘
- 【JAVA】Maven profiles控制多环境数据源日志打包(转载)
- promise的理解和应用
- python安装在哪个盘比较好_python编写器用哪个比较好?
- IPv6隧道技术基础知识
- opencv 不同边缘检测算子效果比较
- 智能电视也需“杀毒”
- C#:合并EXCEL文件工作表列
- 第十三届蓝桥杯C B组 J:砍竹子
- kera中各种accuracy的介绍
- golang快速入门[6.2]-集成开发环境-emacs详解
- Android 版本API对应表
- 小女子菜鸟一枚,因本科毕业设计第一次接触安卓开发……图片上传问题求教
- 2021北航计算机考研人数,惊了!2021考研人数422万?!淘汰率或超过70%?8所高校报考人数汇总...
热门文章
- springboot+vue中小学生课外知识学习网站
- ArrayList()和Collections.emptyList()的区别emptyList()、emptySet()、emptyMap()的作用和好处以及要注意的地方
- [力扣题解] 174. 地下城游戏 DP
- 使用未初始化内存的指针
- 焦(jiao)作(yi)一中培训游记
- 攻陷leetcode,你我行!!!(不在话下),小意思666
- JS(七)内置对象-简单类型与复杂类型
- 在Win8下无法打开 hlp 帮助文件的问题
- bat脚本删除目标文件夹
- IT专业人士口碑差的十大原因