新一代构建工具Gradle

版权声明:作者已开启版权声明,如转载请注明转载地址。 https://blog.csdn.net/qq_34829447/article/details/82795700

一.自动化构建工具的作用

1.没有自动化构建工具的时代

  • 依赖管理:将所有依赖的jar包放到lib目录下,一不留神就会出现版本冲突
  • 测试:能不写就不写,写了也是一个一个运行main方法简单测试
  • 打包:通过eclipse的导出war包
  • 上传:通过ftp上传到服务器上

2.自动化构建工具的作用

  • 依赖管理
  • 测试、打包、发布
  • 机器能干的活,绝不自己动手

3.主流构建工具

  • Ant:编译、测试、打包
  • Maven:在Ant基础上添加了依赖管理和发布
  • Gradle:在Maven的基础上添加了使用Groovy进行管理构建脚本,而不再使用xml

二.关于Gradle

Gradle是什么

  • 一个开源的项目自动化构建工具,建立在Apache Ant和Apache Maven概念的基础上,并引入了基于Groovy的特定领域语言(DSL),而不再使用XML形式管理构建脚本

三.准备使用Gradle

1.Gradle安装

  • Gradle是基于JVM的,故本地必须要有JDK

  • 从Gradle官网下载Gradle,https://gradle.org

  • 配置环境变量GRADLE_HOME

  • 添加到PATH

    export GRADLE_HOME=/Users/wangzhe/Documents/class/gradle-4.10.2
    export PATH=$PATH:$GRADLE_HOME/bin
    
  • 验证是否安装成功gradle -v

2.Groovy是什么

  • Groovy是用于Java虚拟机的一种敏捷的动态语言,它是一种成熟的面向对象编程语言,既可以用于面向对象编程,又可以用于纯粹的脚本语言。使用该种语言不必编写过多的代码,同时又具有闭包和动态语言中的其他特性。
  • Groovy完全兼容Java的语法,即在Groovy中可以编写Java代码
  • 分号是可选的
  • 类和方法默认是public
  • 编译器给属性自动添加getter/setter方法
  • 属性可以直接用点号获取
  • 在方法中最后一个表达式的值会被作为返回值(即可以不写return)
  • 在Groovy中,==等同于equals(),不会有NullPointerExceptions
  • 高效的Groovy特性
    • 自带assert语句,可以在任何地方执行断言操作
    • 类型是可选的,即弱类型,def 变量名即可
    • 括号是可选的,调用方法时如果有参数,其参数是可以不写的
    • Groovy字符串有三种表达性
      • 'xxxx'
      • "xxxx"
      • '''xxxx'''
    • 集合API,Groovy中List和Map有更简单的写法
    • 具有闭包的特性

3.groovy基础知识-与java比较

//定义工程版本对象——点击运行按钮进行运行,结果会输出到console控制台
public class ProjectVersion{
    private int major;//大版本
    private int minor;//小版本
    ProjectVersion(int major, int minor) {
        this.major = major
        this.minor = minor
    }

    int getMajor() {
        major
    }

    void setMajor(int major) {
        this.major = major
    }
//
//    int getMinor() {
//        return minor
//    }
//
//    void setMinor(int minor) {
//        this.minor = minor
//    }
}

ProjectVersion v1 = new ProjectVersion(1,2);
println(v1.minor)//2 自动创建getter和setter
println(v1.major)//1 将最后一个表达式看做返回值

ProjectVersion v2 = null
println(v2 == v1)//false 使用==判断时相当于equals()方法,不会抛出空指针异常

4.groovy基础知识-高效特性

//Groovy 高效特性
//1. 可选的类型定义
def version = 1

//2. assert
//assert version==2//失败的断言,在任何地方都可以执行

//3. 括号是可选的
println(version)//1
println version //1

//4. 字符串
def s1 = 'Jack'//仅仅是一个字符串
def s2 = "Gradle version is ${version}"//可以使用${}插入变量
def s3 = '''Jack is so smart.'''//可以换行,输出的结果也是换行的
println s1//Jack
println s2//Gradle version is 1
println s3//Jack is so smart

//5. 集合API
// list
def buildTools = ['ant','maven']
buildTools << 'gradle' //表示向buildTools中追加元素
assert buildTools.getClass() == ArrayList//不会出现异常,表示是ArrayList类型
assert buildTools.size() == 3//不会出现异常,表示当前集合中有三个元素
//map——对应java中的LinkedHashMap
def buildYears = ['ant':2000,'maven':2004]
buildYears.gradle = 2009//表示向buildYears的map中追加元素
println buildYears.ant//2000
println buildYears['gradle']//2009
println buildYears.getClass()//class java.util.LinkedHashMap

//6.闭包——通常当做方法参数来使用
//c1的参数为v,方法体打印v;c2不包含参数
def c1 = {
    v ->
        println v
}
def c2 = {
    print 'hello'
}
def method1(Closure closure){
    closure('param')
}
def method2(Closure closure){
    closure()
}
method1(c1)//param
method2(c2)//hello

5.groovy基础知识-重点

//Gradle的构建脚本
//每个Gradle的构建脚本都有个Project实例,且每个Gradle构建脚本中所有代码默认的作用域都是Project
//plugin:'java'表示plugin这个参数的值为java;apply是一个方法
apply plugin:'java'
//version表示Project上有一个属性叫version值等于0.1
version = '0.1'
//repositories是一个方法,mavenCentral()作为参数调用repositories方法
repositories{
    mavenCentral()
}
//dependencies是一个方法,compile 'commons-codec:commons-codec:1.6'闭包作为参数调用dependencies方法
dependencies{
    compile 'commons-codec:commons-codec:1.6'
}

四.第一个Gradle项目

1.创建Gradle项目(TODO-应用程序版本)

  • 创建Gradle项目

File -> new -> Project -> Gradle(Java 1.8) -> 输入GroupId和ArtifactId -> use local gradle distribution并选择对应的本地gradle路径 -> 填入项目名 -> Finish

  • Gradle项目目录[固定的目录结构]

    • .gradle.idea是编译器自动生成的文件,可忽略

    • src表示代码目录main源代码目录;test测试代码目录

    • build.gradle文件

      //表示使用java的插件
      plugins {
          id 'java'
      }
      group 'com.gradletest'
      version '1.0-SNAPSHOT'
      //源文件适应的版本
      sourceCompatibility = 1.8
      //仓库
      repositories {
          mavenCentral()
      }
      //依赖管理
      dependencies {
          testCompile group: 'junit', name: 'junit', version: '4.12'
      }
      
    • setting.gradle文件

  • 在main/java下创建com.gradletest.todo.TodoItem.java用于定义Todo类

    package com.gradletest.todo;
    
    public class TodoItem {
        //待办事项的名称
        private String name;
        //已完成
        private boolean hasDone;
    
        public TodoItem(String name) {
            this.name = name;
            hasDone = false;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public boolean isHasDone() {
            return hasDone;
        }
    
        public void setHasDone(boolean hasDone) {
            this.hasDone = hasDone;
        }
    
        @Override
        public String toString() {
            return "TodoItem{" +
                    "name='" + name + '\'' +
                    ", hasDone=" + hasDone +
                    '}';
        }
    }
    
  • 在main/java下创建com.gradletest.todo.App.java用于执行对应的todo操作

    package com.gradletest.todo;
    import java.util.Scanner;
    public class App {
        public static void main(String [] args){
            int i = 0;
            Scanner scanner = new Scanner(System.in);
            while(++i>0){
                System.out.println(i+".please input todo item name:");
                TodoItem item = new TodoItem(scanner.nextLine());
                System.out.println(item);
            }
        }
    }
    
  • 使用自动化构建工具Gradle来构建jar包

    • 点击右侧Gradle->项目名->Tasks->build

      • jar 可以将当前文件打成jar包,打包在build/libs/todo-1.0-SNAPSHOT.jar
      • build 也可以,因为相当于执行build.gradle构建脚本,因为脚本中是以java的形式,所以构建完之后也是jar包
      • classes 将源代码编译成class类
      • clean 清除之前的构建

      在这里插入图片描述

    • 点击jar会创建build包并可以在libs路径下看到指定打包好的jar包

    • 在命令行执行jar包的对应类java -classpath build/libs/todo-1.0-SNAPSHOT.jar com.gradletest.todo.App,结果就是直接运行App.java的结果

2.创建Gradle项目(TODO-WEB版本)

  • 添加webapp包及包下的WEB-INF与其中的web.xml

    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                          http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
             version="3.1"
             metadata-complete="true">
        <!--metadata-complete为true时,该web不会加载注解配置的web组件(如Servlet、Filter、Listener等)-->
        <!--配置Web应用的首页列表-->
        <welcome-file-list>
            <welcome-file>index.html</welcome-file>
            <welcome-file>index.jsp</welcome-file>
            <welcome-file>index.htm</welcome-file>
        </welcome-file-list>
    </web-app>
    
  • 在webapp下编写一个简单的index.html用于测试

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        这里是首页
    </body>
    </html>
    
  • 在build.gradle中添加apply plugin:'war'的war插件

    plugins {
        id 'java'
    }
    
    group 'com.gradletest'
    version '1.0-SNAPSHOT'
    
    apply plugin:'war'//添加war插件
    
    //源文件的版本
    sourceCompatibility = 1.8
    
    repositories {
        mavenCentral()
    }
    
    dependencies {
        testCompile group: 'junit', name: 'junit', version: '4.12'
    }
    
  • 可以发现在右侧Gradle的build中会多一个war选项,点击即会将项目打成war包,war包放到Tomcat下即可运行

  • war包放到webapp中后会自动解压缩

    • 第一级目录为:META-INFWEB-INFindex.html,第一个目录为打包成war包时自动生成的,后两个文件为webapp下的内容
    • WEB-INF中存在classes文件和web.xml文件
    • classes中存在项目编写代码的包的字节码文件和resources文件夹下的文件

五.高级应用

1.构建脚本概要

  • 构建块

    • Gradle构建的两个基本概念是项目(project)和任务(task),每个构建至少包含一个项目,项目中包含一个或多个任务。在多项目构建中,一个项目可以依赖于其他项目;类似的,任务可以形成一个依赖关系图来确保他们的执行顺序。

    在这里插入图片描述

  • 项目(project)

    一个项目代表一个正在构建的组件(比如一个jar文件),当构建启动后,Gradle会基于build.gradle实例化一个org.gradle.api.Project类,并且能够通过project变量使其隐式可用。

    【即在build.gradle中所有的变量方法的调用都是在Project类中定义】

    //例如:
    group 'com.gradletest' //等同于project.group = 'com.gradletest'
    
    • 主要属性
      • group组
      • name名称(一个组中不能有重名的)
      • version(版本号)
    • 主要方法
      • apply应用插件
      • dependencies声明项目依赖于哪些jar包或其他项目
      • repositories表示去哪个仓库找依赖的jar包
      • task声明项目里有什么任务
      • 先找到仓库,根据group、name和version唯一确定一个组件
    • 属性的其他配置方式
      • ext定义的属性可以直接引用
      • gradle.properties用键值对的方式声明属性
  • 任务(task)

    任务对应org.gradle.api.Task,主要包括任务动作和任务依赖。任务动作定义了一个最小的工作单元。可以定义依赖其他任务、动作序列和执行条件。

    • 主要方法
      • dependsOn声明任务依赖
      • doFirst在任务列表的最前面添加动作
      • doLast(简写方式:<<)在任务列表的最末尾添加动作
  • Tasks中的war是war插件的任务,其余的是java插件带的任务

    在这里插入图片描述

  • 当我们执行jar的task任务时,只执行了jar任务,但是前面执行了,说明jar任务依赖其余三个任务:compileJava、processResources、classes。下图中的UP-TO-DATE表示从上次执行jar后信息没有改变,所以跳过了相关任务的执行。

    在这里插入图片描述

  • 在一个项目里面任务不是必须的,大多数情况下我们都使用插件提供的功能,如果插件完成不了的任务我们会自定义任务

  • settings.gradle文件用来管理多项目构建

2.自定义任务

  • 自定义创建包任务

    • 定义创建包的任务,并声明文件名称,在任务类中添加创建文件夹闭包的动作

      //定义闭包执行根据文件夹名称创建文件夹操作
      def createDir= {
          path ->
              File dir = new File(path);
              if(!dir.exists()){
                  dir.mkdirs();
              }
      }
      
      //创建一个新建文件夹的任务
      task makeJavaDir() {
          def paths = ["selfdefine/java","selfdefine/test"]
          //在动作列表的最前面插入一个动作
          doFirst{
              paths.forEach(createDir)
          }
      }
      
    • 自定义任务在右侧Gradle->Tasks->other下可以查看,并点击运行

    • 则会在项目根路径下添加对应的文件夹

  • 自定义创建web包任务(使用任务的依赖)

    • 定义创建web包任务,利用dependsOn设定任务依赖makeJavaDir任务,并添加创建文件夹闭包的动作

      task makeWebDir(){
          dependsOn 'makeJavaDir'
          def paths = ["src/main/webapp","src/test/webapp"]
          doLast{
              paths.forEach(createDir)
          }
      }
      
    • 同样在Gradle->Tasks->other查看并执行任务

    • 则会首先执行makeJavaDir的任务,再执行当前任务中定义的动作

    在这里插入图片描述

3.构建生命周期

  • 第一个阶段:初始化阶段

    • 初始化阶段主要是初始化项目,决定哪些项目要参与到项目构建当中
    • Gradle会根据脚本创建一个Project类对象并在构建脚本中隐式可用
    • 再多项目构建中,在初始化阶段会初始化所有将要参与构建中的项目
  • 第二个阶段:配置阶段

    • 用来生成Task的依赖顺序和执行顺序(根据配置代码,简单可以理解为除了动作代码之外全是动作代码)

    • 初始化任务

    • 举例:

      task loadVersion{
          project.version = '1.0'
      }
      
  • 第三个阶段:执行阶段

    • 执行task的动作代码

    • 举例:

      task loadVersion << {
          print 'success'
      }
      
  • 钩子方法(平常使用时,不常用到)

    在这里插入图片描述

4.依赖管理

  • 概述:几乎所有的基于JVM的软件项目都需要依赖外部类库来重用现有的功能。自动化的依赖管理可以明确依赖的版本,可以解决因传递性依赖带来的版本冲突。

  • 工件坐标:工件简单的可以理解为一个jar包,坐标为三个属性group、name、version。通过三个属性确定唯一的jar包。

  • 常用仓库:仓库就是存放jar包的地方。

    • 常用的仓库有mavenLocal、mavenCentral、jcenter。mavenCentral和jcenter为公网上的公共仓库,我们可以向上面上传jar包或依赖里面的jar包。mavenLocal是在本地通过maven使用过的jar包都会存在本地仓库中。
    • 自定义maven仓库(常用):maven私服,公司搭建内部仓库管理公司内部的jar包
    • 文件仓库(不常用):本地机器上面的文件路径也可以作为仓库,不建议使用,因为我们使用构建工具的目的是到各个机器上都可以使用,如果使用文件仓库则会违反这个原则。
  • 依赖的传递性

    • B依赖A,如果C依赖B,那么C依赖A

    • 正是由于依赖的传递性,会出现版本冲突

    • 自动化依赖管理

      在这里插入图片描述

    • 依赖管理的阶段

      • 编译阶段compile、运行阶段runtime
      • 测试编译阶段testCompile、测试运行阶段testRuntime
      • 运行时阶段都是扩展于编译阶段的,即在编译阶段依赖的jar包,在运行时都会依赖;在运行时依赖的jar包,在编译时不一定会依赖
      • 如果是源代码依赖的,测试代码都会依赖;测试代码依赖的,源代码不一定会依赖

在这里插入图片描述

  • 实例使用依赖

    • 将原dependencies中的依赖junit方式改成compile,则可以通过Gradle->Source Sets->main->Dependencies查看到对应依赖的jar包(即从测试编译阶段变成了编译阶段的依赖)

    • Gradle中的项目名右击,选择auto-import,则当修改对应依赖阶段即相关操作时自动刷新

    • 什么时候使用编译依赖,什么时候使用运行依赖?

      大部分都是使用编译依赖,因为我们编译时需要使用,运行时也需要使用。

    • 添加依赖

      • 在https://search.maven.org/搜索对应需要的依赖,本例添加的是日志记录依赖

      • 从网站上获得对应的依赖工件坐标,并引入编译阶段依赖

        compile 'ch.qos.logback:logback-classic:1.3.0-alpha4'
        
      • 依赖的写法 'ch.qos.logback:logback-classic:1.3.0-alpha4'等同于group:'ch.qos.logback',name:'logback-classic',version:'1.3.0-alpha4'

      • 由于logback依赖又依赖别的jar包,故可以通过Dependencies中看出其依赖的其他jar

      在这里插入图片描述

      • 使用添加的依赖

        package com.gradletest.todo;
        import org.slf4j.Logger;
        import org.slf4j.LoggerFactory;
        
        import java.util.Scanner;
        
        public class App {
            static Logger log = LoggerFactory.getLogger(App.class);
            public static void main(String [] args){
                int i = 0;
                Scanner scanner = new Scanner(System.in);
                while(++i>0){
                    log.info("{}.please input todo item name:",i);//以logback的日志格式打印
                    TodoItem item = new TodoItem(scanner.nextLine());
                    System.out.println(item);
                }
            }
        }
        

        则会以日志的形式进行输出18:38:19.727 [main] INFO com.gradletest.todo.App - 1.please input todo item name:

  • 添加仓库

    • 仓库会按顺序查找jar包,第一个查找到了就不往下查找,没查找到就继续往下查找

    • 使用maven私服——通常有私服都会将私服放在第一位,先找私服

      maven{
          url ''//私服的地址
      }
      
    • 可以配合多个仓库使用

      repositories {
          //按顺序查找jar包,第一个查找到了就不往下查找,没查找到就继续往下查找
          //使用maven私服——通常有私服都会将私服放在第一位,先找私服
          maven{
              url ''//私服的地址
          }
          //可以配多个仓库
          mavenLocal()
          //表示从mavenCentral仓库获取jar
          mavenCentral()
      }
      

5.解决版本冲突

  • 解决版本冲突的具体步骤

    • 查看依赖报告,确定哪些版本进行冲突
    • 排除传递性依赖
    • 强制指定一个版本
  • Gradle会默认使用冲突版本中最高版本的jar包

  • 修改Gradle默认解决策略(当版本冲突时,设置为构建失败)

    configurations.all{
        resolutionStrategy{
            failOnVersionConflict()
        }
    }
    
  • 自行解决冲突

    • 排除传递性依赖,如下:

      compile('org.hibernate:hibernate-core:3.6.3.Final'){
          exclude group:'org.slf4j',module:'slf4j-api'//排除具体jar包的依赖,module即是坐标中的name属性
          //transitive=false 排除所有的传递依赖(少见)
      }
      
    • 强制指定一个版本,如下:

      configurations.all{
          resolutionStrategy{
              force 'org.slf4j:slf4j-api:1.7.24'//强制指定版本
          }
      }
      
  • 可以使用Gradle->项目->help->dependencies查看所有jar包的依赖

6.多项目构建

  • 项目模块化:在企业项目中,包层次和类关系比较复杂,把代码拆分成模块通常是最佳实践,这需要你清晰的划分功能的边界,比如把业务逻辑和数据持久化拆分开来。项目符合高内聚低耦合时,模块就变得很容易,这是一条非常好的软件开发实践。

  • TODO模块依赖关系

    • Web(页面)依赖Model(源数据)和Repository(存储数据)
    • Repository依赖Model
    • 则根据依赖的传递性,Web只需要依赖Repository则完成对Repository和Model的依赖
  • 配置要求

    • 所有项目应用Java插件
    • web子项目打包成WAR
    • 所有项目添加logback日志功能
    • 统一配置公共属性,如version、group等
  • 构建项目

    • 创建子模块项目右击->new Module->填写模块名->Finish

      • 创建model模块:用于存储源数据
      • 创建repository模块:用于存储真实数据
      • 创建web模块:用于编写页面
    • 创建子模块的同时会自动将子模块信息include到settings.gradle中。多项目构建时,settings.gradle用来管理当前项目有哪些子项目,root为根项目。并根据右侧Gradle可以查看对应项目的相关信息。

      rootProject.name = 'todo'//根项目名
      include 'model'
      include 'repository'
      include 'web'
      
    • 由于repository中需要操作model内容,故repository模块需要依赖model模块

      //对子项目的依赖——特殊的写法project(":模块名")
      compile project(":model");
      
    • 由于web需要依赖repository和model,但是repository依赖model,故web只需要依赖repository即可传递依赖获得model

      compile project(":repository")
      
    • 注意:子模块依赖方式采用compile project(':子模块名')

  • 完成配置要求

    • 所有项目应用java插件

      • 将所有子项目的应用java插件的语句删除,统一在根项目的gradle中配置

        //删除内容
        plugins {
            id 'java'
        }
        sourceCompatibility = 1.8
        
      • 在根项目的build.gradle中添加

        allprojects{
            apply plugin:'java'
            sourceCompatibility = 1.8
        }
        
    • web子项目打包成WAR

      • 将根项目的war插件删除,添加到web子项目的build.gradle中

        apply plugin:'war'//添加war插件
        
      • 查看右侧Gradle,只有web子项目有war的任务

    • 所有子项目添加logback日志功能

      • 在根项目的build.gradle中添加subprojects{}并引入对应依赖【allprojects和subprojects中的配置跟build.gradle一样】

        allprojects{
            apply plugin:'java'
            sourceCompatibility = 1.8
        }
        
        subprojects {
            repositories {
                //表示从mavenCentral仓库获取jar
                mavenCentral()
            }
            dependencies {
                //在https://search.maven.org/搜索对应需要的依赖,本例添加的是日志记录依赖
                compile 'ch.qos.logback:logback-classic:1.3.0-alpha4'
                //表示在测试阶段依赖的jar包
                compile group: 'junit', name: 'junit', version: '4.12'
            }
        }
        group 'com.gradletest' //等同于project.group = 'com.gradletest'
        version '1.0-SNAPSHOT'
        repositories {
            //表示从mavenCentral仓库获取jar
            mavenCentral()
        }
        
      • 在子项目中删除引入的junit依赖,因为已经通过根项目配过了

        //删除的语句
        compile group: 'junit', name: 'junit', version: '4.12'
        
    • 统一配置公共属性,如version、group等

      • 在根项目下创建gradle.properties【注意不能写错】,并配置如下

        group=com.gradletest
        version='1.0-SNAPSHOT'
        
      • 删除所有子项目中的group和version配置

    • 在根项目执行clean则会clean根项目及所有子项目的构建文件。如果相对指定子项目进行clean,则在指定子项目中执行clean。

    • 在model模块执行jar,只会执行model模块中的内容。在web模块执行jar,会先编译model,之后编译repository,最后编译web模块内容。因为web依赖repository,repository依赖model。

    • 总结:如果想构建所有的项目,则在根项目下执行对应任务;如果想构建单项目,则在单项目下执行对应的任务。

7.Gradle测试

  • 自动化测试

    • 一些开源的测试框架比如JUnit,TestNG能够帮助你编写可复用的结构化的测试,为了运行这些测试,你要先编译它们,就像编译源代码一样。测试代码的作用仅仅用于测试的情况,不应该被发布到生产环境中,需要把源代码和测试代码分开来。
  • 测试项目布局

    在这里插入图片描述

  • 测试配置

    dependencies{
        testCompile 'junit:junit:4.11'
    }
    
  • 测试类被Gradle发现

    • 任何继承自junit.framework.TestCase或groovy.util.GroovyTestCase的类
    • 任何被@RunWith注解的类
    • 任何至少包含一个被@Test注解的类

8.发布

  • 发布到本地和远程仓库

    在这里插入图片描述

  • 项目发布的流程

    • 添加插件

      apply plugin:'maven-publish'
      
    • 配置发布信息(要发布什么和仓库地址)

      publishing{
          publications{
              //自定义发布方法名
              myPublish(MavenPublication){
                  from components.java
              }
          }
          //配置远程仓库的信息
          repositories {
              maven{
                  name "myPro"//仓库名称
                  url ""//仓库地址——通常用于私服(点击publishing中的publish发布到私服地址)
              }
          }
      }
      
    • 执行发布任务Gradle->publishing->publishToMavenLocal将项目发布到本地仓库中。若想发到公司私服,则可以配置repositories的url信息并点击publish选项。

    • Mac用户可以在~/.m2/repository/groupId路径查看对应发布的项目

猜你喜欢

转载自blog.csdn.net/qq_34829447/article/details/82795700