目录

1. MinIO概述

2. MinIO下载安装

2.1 下载地址

2.2. 安装使用

3. 快速入门 

3.1 创建工程

3.2 导入依赖 

3.3 添加配置文件

3.4 添加Swagger2配置类

3.5 统一返回数据格式

3.6 统一异常处理

3.7 添加MinIO配置类

3.8 添加MinIO工具类

3.9 添加MinIO控制类

3.10 测试

4. Demo下载地址


1. MinIO概述

        MinIO 是一个基于 Apache License v2.0 开源协议的对象存储服务。它兼容亚马逊 S3 云存储服务接口,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等,而一个对象文件可以是任意大小,从几 kb 到最大 5T 不等。

        官网:https://www.minio.org.cn/

2. MinIO下载安装

2.1 下载地址

        https://dl.min.io/server/minio/release/windows-amd64/minio.exe

2.2. 安装使用

    1. 进入到minio.exe所在的目录,使用 minio.exe server C:\MinioDB 命令启动minio服务。(备注:可以将C:\MinioDB 替换为希望 MinIO 存储数据的驱动器或目录的路径)


    2. 在浏览器输入:http://localhost:9000/ ,进入minIO登录界面


    3. 使用默认的RootUser和RootPass,都为minioadmin,进入MinIO控制台


    4. 创建bucket。

        点击Buckets---->Create Bucke 

5. 创建访问用户

        点击Identity--->Create user

3. 快速入门 

3.1 创建工程

        设置Maven


        设置自动导入包 Auto Import


        设置启动注解 Annotation Processors

3.2 导入依赖 

<!--引入springboot依赖-->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.4.RELEASE</version>
</parent>

<dependencies>
    <!--引入spring-boot启动器依赖, 添加启动器后web工程常用的依赖会自动帮你引入-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
    <!--test-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <!--lombok-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    <!--Swagger2-->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.7.0</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.7.0</version>
    </dependency>
    <!-- minio依赖 -->
    <dependency>
        <groupId>io.minio</groupId>
        <artifactId>minio</artifactId>
        <version>8.2.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.11</version>
    </dependency>
    <!-- 日期工具栏依赖 -->
    <dependency>
        <groupId>joda-time</groupId>
        <artifactId>joda-time</artifactId>
        <version>2.10.10</version>
    </dependency>
</dependencies>

<!--打包-->
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

3.3 添加配置文件

resources\application.yml 当中 添加数据库配置信息

# 配置服务端口
server:
  port: 8002

# 配置日志输出格式
logging:
  pattern:
    console: "%clr(%5p) %clr(-){faint} %clr(%-80.80logger{79}){cyan} %clr(:) %m%n"

#定义minio相关属性
minio:
  endpoint: http://127.0.0.1:9000 #Minio服务所在地址
  bucketName: miaxis-data #存储桶名称
  accessKey: miaxis #访问的key
  secretKey: miaxis-data #访问的秘钥

3.4 添加Swagger2配置类

package com.miaxis.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.miaxis"))// 指定扫描包下面的注解
                .paths(PathSelectors.any())
                .build();
    }
    // 创建api的基本信息
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("集成Swagger2构建RESTful APIs")
                .description("集成Swagger2构建RESTful APIs")
                .termsOfServiceUrl("https://www.miaxis.com")
                .contact("Mickey")
                .version("1.0.0")
                .build();
    }
}

3.5 统一返回数据格式

  • 创建状态码接口类

package com.miaxis.util;

public interface ResultCode {
    /*成功状态码*/
    Integer SUCCESS = 20000;
    /*失败的状态码*/
    Integer ERROR = 20001;
}
  • 创建统一结果类

package com.miaxis.util;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.HashMap;
import java.util.Map;
@Data
public class ResponseResult{
    private ResponseResult(){}
    @ApiModelProperty(value = "是否成功")
    private Boolean success;
    @ApiModelProperty(value = "状态码")
    private Integer code;
    @ApiModelProperty(value = "返回消息")
    private String message;
    @ApiModelProperty(value = "返回的数据")
    private Map<String,Object> data = new HashMap<>();

    /*提供工具方法*/
    public static ResponseResult ok(){
        ResponseResult responseResult = new ResponseResult();
        responseResult.setSuccess(true);
        responseResult.setCode(ResultCode.SUCCESS);
        responseResult.setMessage("成功");
        return responseResult;
    }
    public static ResponseResult error(){
        ResponseResult responseResult = new ResponseResult();
        responseResult.setSuccess(false);
        responseResult.setCode(ResultCode.ERROR);
        responseResult.setMessage("失败");
        return responseResult;
    }
    public ResponseResult success(Boolean success){
        this.setSuccess(success);
        return this;
    }
    public ResponseResult message(String message){
        this.setMessage(message);
        return this;
    }
    public ResponseResult code(Integer code){
        this.setCode(code);
        return  this;
    }
    public ResponseResult data(String key,Object value){
        this.data.put(key,value);
        return this;
    }
    public ResponseResult data(Map<String,Object> map){
        this.setData(map);
        return this;
    }
}

3.6 统一异常处理

  • 创建自定义异常处理类

package com.miaxis.exception;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor  //生成带参数的构造器
public class CustomException extends RuntimeException{
    private Integer code;
    private String msg;
}
  • 创建全局异常处理类

package com.miaxis.exception;

import com.miaxis.utils.ResponseResult;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/*全局异常处理器, 只要发生了异常,如果在自己控制当中 没有去捕获 , 就会到此控制器*/
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseResult error(Exception e){
        e.printStackTrace();
        return ResponseResult.error().message(e.getMessage());
    }

    //自定义异常
    @ExceptionHandler(CustomException.class)
    @ResponseBody //返回json数据
    public ResponseResult error(CustomException e){
        e.printStackTrace();
        return ResponseResult.error().code(e.getCode()).message(e.getMsg());
    }
}

3.7 添加MinIO配置类

package com.miaxis.config;

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 MinioConfig {
    private String endpoint;
    private String accessKey;
    private String secretKey;
    private String bucketName;

    @Bean
    public MinioClient minioClient() {
        return MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .build();
    }
}

3.8 添加MinIO工具类

package com.miaxis.util;

import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.Bucket;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import sun.misc.BASE64Decoder;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.*;

@Slf4j
@Component
@RequiredArgsConstructor
public class MinioUtil {
    private final MinioClient minioClient;

    /******************************  Operate Bucket Start  ******************************/

    /**
     *  init Bucket  when start SpringBoot container
     * create bucket if the bucket is not exists
     *
     * @param bucketName
     */
    @SneakyThrows(Exception.class)
    private void createBucket(String bucketName) {
        if (!bucketExists(bucketName)) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }

    /**
     * verify Bucket is exist?,true:false
     *
     * @param bucketName
     * @return
     */
    @SneakyThrows(Exception.class)
    public boolean bucketExists(String bucketName) {
        return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }

    /**
     * get Bucket strategy
     *
     * @param bucketName
     * @return
     */
    @SneakyThrows(Exception.class)
    public String getBucketPolicy(String bucketName) {
        return minioClient.getBucketPolicy(GetBucketPolicyArgs
                .builder()
                .bucket(bucketName)
                .build());
    }

    /**
     * get all Bucket list
     *
     * @return
     */
    @SneakyThrows(Exception.class)
    public List<Bucket> getAllBuckets() {
        return minioClient.listBuckets();
    }

    /**
     * Get related information based on bucketName
     *
     * @param bucketName
     * @return
     */
    @SneakyThrows(Exception.class)
    public Optional<Bucket> getBucket(String bucketName) {
        return getAllBuckets().stream().filter(b -> b.name().equals(bucketName)).findFirst();
    }

    /**
     * Delete Bucket based on bucketName, true: deletion successful; false: deletion failed, file may no longer exist
     *
     * @param bucketName
     * @throws Exception
     */
    @SneakyThrows(Exception.class)
    public void removeBucket(String bucketName) {
        minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucketName).build());
    }

    /******************************  Operate Bucket End  ******************************/


    /******************************  Operate Files Start  ******************************/

    /**
     * check file is exist
     *
     * @param bucketName
     * @param objectName
     * @return
     */
    public boolean isObjectExist(String bucketName, String objectName) {
        boolean exist = true;
        try {
            minioClient.statObject(StatObjectArgs.builder().bucket(bucketName).object(objectName).build());
        } catch (Exception e) {
            log.error("[MinioUtils]>>>>check file exist, Exception:", e);
            exist = false;
        }
        return exist;
    }

    /**
     * check directory exist?
     *
     * @param bucketName
     * @param objectName
     * @return
     */
    public boolean isFolderExist(String bucketName, String objectName) {
        boolean exist = false;
        try {
            Iterable<Result<Item>> results = minioClient.listObjects(
                    ListObjectsArgs.builder().bucket(bucketName).prefix(objectName).recursive(false).build());
            for (Result<Item> result : results) {
                Item item = result.get();
                if (item.isDir() && objectName.equals(item.objectName())) {
                    exist = true;
                }
            }
        } catch (Exception e) {
            log.error("[MinioUtils]>>>>check file exist, Exception:", e);
            exist = false;
        }
        return exist;
    }

    /**
     * Query files based on file prefix
     *
     * @param bucketName
     * @param prefix
     * @param recursive
     * @return MinioItem
     */
    @SneakyThrows(Exception.class)
    public List<Item> getAllObjectsByPrefix(String bucketName,
                                            String prefix,
                                            boolean recursive) {
        List<Item> list = new ArrayList<>();
        Iterable<Result<Item>> objectsIterator = minioClient.listObjects(
                ListObjectsArgs.builder().bucket(bucketName).prefix(prefix).recursive(recursive).build());
        if (objectsIterator != null) {
            for (Result<Item> o : objectsIterator) {
                Item item = o.get();
                list.add(item);
            }
        }
        return list;
    }

    /**
     * get file InputStream
     *
     * @param bucketName
     * @param objectName
     * @return
     */
    @SneakyThrows(Exception.class)
    public InputStream getObject(String bucketName, String objectName) {
        return minioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build());
    }

    /**
     * Breakpoint download
     *
     * @param bucketName
     * @param objectName
     * @param offset
     * @param length
     * @return
     */
    @SneakyThrows(Exception.class)
    public InputStream getObject(String bucketName, String objectName, long offset, long length) {
        return minioClient.getObject(
                GetObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .offset(offset)
                        .length(length)
                        .build());
    }

    /**
     * Get the list of files under the path
     *
     * @param bucketName
     * @param prefix
     * @param recursive
     * @return
     */
    public Iterable<Result<Item>> listObjects(String bucketName, String prefix, boolean recursive) {
        return minioClient.listObjects(
                ListObjectsArgs.builder()
                        .bucket(bucketName)
                        .prefix(prefix)
                        .recursive(recursive)
                        .build());
    }

    /**
     * use MultipartFile to upload files
     *
     * @param bucketName
     * @param file
     * @param objectName
     * @param contentType
     * @return
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse uploadFile(String bucketName, MultipartFile file, String objectName, String contentType) {
        InputStream inputStream = file.getInputStream();
        return minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .contentType(contentType)
                        .stream(inputStream, inputStream.available(), -1)
                        .build());
    }

    /**
     * picture upload
     * @param bucketName
     * @param imageBase64
     * @param imageName
     * @return
     */
    public ObjectWriteResponse uploadImage(String bucketName, String imageBase64, String imageName) {
        if (!StringUtils.isEmpty(imageBase64)) {
            InputStream in = base64ToInputStream(imageBase64);
            String newName = System.currentTimeMillis() + "_" + imageName + ".jpg";
            String year = String.valueOf(new Date().getYear());
            String month = String.valueOf(new Date().getMonth());
            return uploadFile(bucketName, year + "/" + month + "/" + newName, in);

        }
        return null;
    }

    public static InputStream base64ToInputStream(String base64) {
        ByteArrayInputStream stream = null;
        try {
            byte[] bytes = new BASE64Decoder().decodeBuffer(base64.trim());
            stream = new ByteArrayInputStream(bytes);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return stream;
    }


    /**
     * upload local files
     *
     * @param bucketName
     * @param objectName
     * @param fileName
     * @return
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse uploadFile(String bucketName, String objectName, String fileName) {
        return minioClient.uploadObject(
                UploadObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .filename(fileName)
                        .build());
    }

    /**
     * upload files based on stream
     *
     * @param bucketName
     * @param objectName
     * @param inputStream
     * @return
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse uploadFile(String bucketName, String objectName, InputStream inputStream) {
        return minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .stream(inputStream, inputStream.available(), -1)
                        .build());
    }

    /**
     * create file or direatory
     *
     * @param bucketName
     * @param objectName
     * @return
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse createDir(String bucketName, String objectName) {
        return minioClient.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .stream(new ByteArrayInputStream(new byte[]{}), 0, -1)
                        .build());
    }

    /**
     * get file info
     *
     * @param bucketName
     * @param objectName
     * @return
     */
    @SneakyThrows(Exception.class)
    public String getFileStatusInfo(String bucketName, String objectName) {
        return minioClient.statObject(
                StatObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build()).toString();
    }

    /**
     * copy file
     *
     * @param bucketName
     * @param objectName
     * @param srcBucketName
     * @param srcObjectName
     */
    @SneakyThrows(Exception.class)
    public ObjectWriteResponse copyFile(String bucketName, String objectName, String srcBucketName, String srcObjectName) {
        return minioClient.copyObject(
                CopyObjectArgs.builder()
                        .source(CopySource.builder().bucket(bucketName).object(objectName).build())
                        .bucket(srcBucketName)
                        .object(srcObjectName)
                        .build());
    }

    /**
     * delete file
     *
     * @param bucketName
     * @param objectName
     */
    @SneakyThrows(Exception.class)
    public void removeFile(String bucketName, String objectName) {
        minioClient.removeObject(
                RemoveObjectArgs.builder()
                        .bucket(bucketName)
                        .object(objectName)
                        .build());
    }

    /**
     * batch delete file
     *
     * @param bucketName
     * @param keys
     * @return
     */
    public void removeFiles(String bucketName, List<String> keys) {
        List<DeleteObject> objects = new LinkedList<>();
        keys.forEach(s -> {
            objects.add(new DeleteObject(s));
            try {
                removeFile(bucketName, s);
            } catch (Exception e) {
                log.error("[MinioUtil]>>>>batch delete file,Exception:", e);
            }
        });
    }

    /**
     * get file url
     *
     * @param bucketName
     * @param objectName
     * @param expires
     * @return url
     */
    @SneakyThrows(Exception.class)
    public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) {
        GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder().expiry(expires).bucket(bucketName).object(objectName).build();
        return minioClient.getPresignedObjectUrl(args);
    }

    /**
     * get file url
     *
     * @param bucketName
     * @param objectName
     * @return url
     */
    @SneakyThrows(Exception.class)
    public String getPresignedObjectUrl(String bucketName, String objectName) {
        GetPresignedObjectUrlArgs args = GetPresignedObjectUrlArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .method(Method.GET).build();
        return minioClient.getPresignedObjectUrl(args);
    }

    /**
     * change URLDecoder to UTF8
     *
     * @param str
     * @return
     * @throws UnsupportedEncodingException
     */
    public String getUtf8ByURLDecoder(String str) throws UnsupportedEncodingException {
        String url = str.replaceAll("%(?![0-9a-fA-F]{2})", "%25");
        return URLDecoder.decode(url, "UTF-8");
    }
}

3.9 添加MinIO控制类

package com.miaxis.controller;

import com.miaxis.config.MinioConfig;
import com.miaxis.exception.CustomException;
import com.miaxis.util.MinioUtil;
import com.miaxis.util.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.InputStream;
import java.util.UUID;
import org.joda.time.DateTime;

@Slf4j
@RestController
@RequestMapping("/oss")
public class MinioController {
    @Autowired
    private MinioUtil minioUtil;

    @Autowired
    private MinioConfig minioConfig;

    /**
     * file upload
     *
     * @param file
     */
    @PostMapping("/upload")
    public ResponseResult upload(@RequestParam("file") MultipartFile file) {
        try {
            //file name
            String srcFileName = file.getOriginalFilename();
            //处理文件名称
            //String uuid = UUID.randomUUID().toString().replaceAll("-", "");
            //String newFileName = uuid+"_"+file.getOriginalFilename();
            String timestamp = new DateTime().toString("HHmmssSSS");
            String newFileName = timestamp+"_"+file.getOriginalFilename();
            /*把同一天上传的文件 放到同一个文件夹当中  20201001/fileName*/
            String date = new DateTime().toString("yyyyMMdd");
            newFileName = date+"/"+newFileName;

            //type
            String contentType = file.getContentType();
            minioUtil.uploadFile(minioConfig.getBucketName(), file, newFileName, contentType);
            return ResponseResult.ok().data("filename",newFileName);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("upload fail");
            //return ResponseResult.error();
            throw new CustomException(20002,"上传文件失败");
        }
    }

    /**
     * delete
     *
     * @param fileName
     */
    @DeleteMapping("/")
    public ResponseResult delete(@RequestParam("fileName") String fileName) {
        if(minioUtil.isObjectExist(minioConfig.getBucketName(), fileName)) {
            minioUtil.removeFile(minioConfig.getBucketName(), fileName);
            return ResponseResult.ok();
        }else{
            return ResponseResult.error().data("msg","文件不存在");
        }
    }

    /**
     * get file info
     *
     * @param fileName
     * @return
     */
    @GetMapping("/info")
    public ResponseResult getFileStatusInfo(@RequestParam("fileName") String fileName) {
        if(minioUtil.isObjectExist(minioConfig.getBucketName(), fileName)) {
            String strInfo = minioUtil.getFileStatusInfo(minioConfig.getBucketName(), fileName);
            return ResponseResult.ok().data("Info",strInfo);
        }else{
            return ResponseResult.error().data("msg","文件不存在");
        }
    }

    /**
     * get file url
     *
     * @param fileName
     * @return
     */
    @GetMapping("/url")
    public ResponseResult getPresignedObjectUrl(@RequestParam("fileName") String fileName) {
        if(minioUtil.isObjectExist(minioConfig.getBucketName(), fileName)) {
            String strUrl = minioUtil.getPresignedObjectUrl(minioConfig.getBucketName(), fileName);
            return ResponseResult.ok().data("url",strUrl);
        }else{
            return ResponseResult.error().data("msg","文件不存在");
        }
    }

    /**
     * file download
     *
     * @param fileName
     * @param response
     */
    @GetMapping("/download")
    public void download(@RequestParam("fileName") String fileName, HttpServletResponse response) {
        try {
            InputStream fileInputStream = minioUtil.getObject(minioConfig.getBucketName(), fileName);
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
            response.setContentType("application/force-download");
            response.setCharacterEncoding("UTF-8");
            IOUtils.copy(fileInputStream, response.getOutputStream());
        } catch (Exception e) {
            log.error("download fail");
        }
    }
}

3.10 测试

  • 通过Swagger测试  ​http://localhost:8002/swagger-ui.html​    

4. Demo下载地址

编译器版本:IntelliJ IDEA 2020.3.2 x64

JDK版本:java 1.8.0_111

下载地址:https://download.csdn.net/download/mickey2007/89085102

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐