彻底理解maven

前言

最近解决jar包冲突问题时,很头疼,发现自己对maven的理解太肤浅了,很多细节都一知半解,于是最近又学习了一把maven,总结如下:

基本概念

maven有两个最基本的概念: pom和lifecycle, 这里的pom不是maven构建过程中使用的pom文件,但他们之间有联系。 pom全称为Project Object Model, 简单说就是要对构建的项目进行建模,将要构建的项目看成是一个对象Object,既然是一个对象,这个对象有哪些属性呢? 在maven中一个项目使用唯一的坐标来表示,它包括groupId, artifactId, version, classifier, type(也叫packaging)这五部分,另外一个方面,一个项目肯定不是孤立存在的,可能会依赖其他项目,也就是说这个对象应该还有dependencies属性,用PO表示构建对象,使用java代码描述这个对象的话:

class PO{
    private String groupId;
    private String artifactId;
    private String version;
    private String classifier;
    private String type;
    private Set dependencies;
}

xml具有很强的表达能力,一个java对象可以用xml来描述,用xml表达上面这个java对象可以为:


    
    
    
    
    
    
        
        
        ...
    

这个是不是和pom.xml很类似? 其实pom.xml就是PO对象的xml描述,上面这个PO定义还不完整,我们知道在java中类是可以继承的,PO也有继承关系,PO对象存在父类父类对象,用parent表示,它会继承父类对象的所有属性。 另一方面,一个项目可能根据不同职责分为多个模块(module),所有模块其实也就是一个单独的项目,只不过这些项目会使用其父对象的一些属性来进行构建。我们将这些新的属性加到PO的定义中去:

class PO{
    private String groupId;
    private String artifactId;
    private String version;
    private String classifier;
    private String type;
    private Set dependencies;
    private PO parent;
    private Set modules;
}

再将这个定义用XML语言表示一下:


    
    
    
    
    
    
    
        
        
        ...
    
    
        ...
    

是不是更像pom.xml了? pom.xml其实就是对PO对象的xml描述!!

构建

项目的构建过程对应的是PO对象的build属性,对应pom.xml中也就是元素中的内容,这里就有引入maven中第二个核心概念:Lifecycle。Lifecycle直译过来就是生命周期。我们平常会接触到哪些周期呢?一年中春夏秋冬就是一个周期。一个周期中可能分为多个阶段,比如这里的春夏秋冬。在maven中一个构建过程就对应一个Lifecycle,这个Lifecycle也分为多个阶段,每个阶段叫做phase。你可能会问,那这个Lifecycle中包含多少个phase呢?一个标准的构建Lifecycle包含了如下的phase:

validate: 用于验证项目的有效性和其项目所需要的内容是否具备
initialize:初始化操作,比如创建一些构建所需要的目录等。
generate-sources:用于生成一些源代码,这些源代码在compile phase中需要使用到
process-sources:对源代码进行一些操作,例如过滤一些源代码
generate-resources:生成资源文件(这些文件将被包含在最后的输入文件中)
process-resources:对资源文件进行处理
compile:对源代码进行编译
process-classes:对编译生成的文件进行处理
generate-test-sources:生成测试用的源代码
process-test-sources:对生成的测试源代码进行处理
generate-test-resources:生成测试用的资源文件
process-test-resources:对测试用的资源文件进行处理
test-compile:对测试用的源代码进行编译
process-test-classes:对测试源代码编译后的文件进行处理
test:进行单元测试
prepare-package:打包前置操作
package:打包
pre-integration-test:集成测试前置操作   
integration-test:集成测试
post-integration-test:集成测试后置操作
install:将打包产物安装到本地maven仓库
deploy:将打包产物安装到远程仓库

在maven中,你执行任何一个phase时,maven会将其之前的phase都执行。例如 mvn install,那么maven会将deploy之外的所有phase按照他们出现的顺序一次执行。

Lifecycle还牵涉到另外一个非常重要的概念:goal。注意上面Lifecycle的定义,也就是说maven为程序的构建定义了一套规范流程:第一步需要validate,第二步需要initialize... ... compile,test,package,... ... install,deploy,但是并没有定义每一个phase具体应该如何操作。这里的phase的作用有点类似于Java语言中的接口,只协商了一个契约,但并没有定义具体的动作。比如说compile这个phase定义了在构建流程中需要经过编译这个阶段,但没有定义应该怎么编译(编译的输入是什么?用什么编译javac/gcc?)。这里具体的动作就是由goal来定义,一个goal在maven中就是一个Mojo(Maven old java object)。Mojo抽象类中定义了一个execute()方法,一个goal的具体动作就是在execute()方法中实现。实现的Mojo类应该放在哪里呢?答案是maven plugin里,所谓的plugin其实也就是一个maven项目,只不过这个项目会引用maven的一些API,plugin项目也具备maven坐标。

在执行具体的构建时,我们需要为lifecycle的每个phase都绑定一个goal,这样才能够在每个步骤执行一些具体的动作。比如在lifecycle中有个compile phase规定了构建的流程需要经过编译这个步骤,而maven-compile-plugin这个plugin有个compile goal就是用javac来将源文件编译为class文件的,我们需要做的就是将compile这个phase和maven-compile-plugin中的compile这个goal进行绑定,这样就可以实现Java源代码的编译了。那么有人就会问,在哪里绑定呢?答案是在pom.xml元素中配置即可。例如:



  
    maven-myquery-plugin
    1.0
    
      
        execution1
        test
        
          http://www.foo.com/query
          10
          
            
            
            
          
        
        
          query
        
      
    
  


就将maven-myquery-plugin中的query这个goal绑定到了test这个phase,后续在maven执行到test phase时就会执行query goal。还有有人可能会问,我都没有指定Java源文件的位置,编译啥?这就引出了maven的design principle。在maven中,有一个非常著名的principle就是convention over configuration(约定优于配置)。这一点和ant有非常大的区别,例如使用ant来进行编译时,我们需要指定源文件的位置,输出文件的位置,javac的位置,classpath... ...在maven中这些都是不需要,若没有手动配置,maven默认从<项目根目录>/src/main/java这个目录去查找Java源文件,编译后的class文件会保存在<项目根目录>/target/classes目录。在maven中,所有的PO都有一个根对象,就是Super POM。Super POM中定义了所有的默认的配置项,Super POM对应的pom.xml文件可以在maven安装目录下lib/maven-model-builder-3.0.3.jar:org/apache/maven/model/pom-4.0.0.xml中找到。用一张图来表示maven Lifecycle,phase,goal之间的关系:

彻底理解maven_第1张图片
image.png

插件

上面我们提到,Maven 将所有项目的构建过程统一抽象成一套生命周期: 项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成 … 几乎所有项目的构建,都能映射到这一组生命周期上. 但生命周期是抽象的(Maven的生命周期本身是不做任何实际工作), 任务执行(如编译源代码)均交由插件完成. 其中每个构建步骤都可以绑定一个或多个插件的目标,而且Maven为大多数构建步骤都编写并绑定了默认插件.当用户有特殊需要的时候, 也可以配置插件定制构建行为, 甚至自己编写插件.

再说生命周期

Maven 拥有三套相互独立的生命周期: clean、default 和 site, 而每个生命周期包含一些phase阶段, 阶段是有顺序的, 并且后面的阶段依赖于前面的阶段. 而三套生命周期相互之间却并没有前后依赖关系, 即调用site周期内的某个phase阶段并不会对clean产生任何影响.

clean
clean生命周期的目的是清理项目:

执行如$ mvn clean;
default
default生命周期定义了真正构建时所需要执行的所有步骤:

执行如$ mvn clean install;

site
site生命周期的目的是建立和发布项目站点: Maven能够基于POM所包含的信息,自动生成一个友好的站点,方便团队交流和发布项目信息

执行命令如$ mvn clean deploy site-deploy;

这三个lifecycle定义了其包含的phase。maven会在这三个lifecycle中匹配对应的phase。当执行某个phase时,maven会依次执行在这个phase之前的phase

插件
生命周期的阶段phase与插件的目标goal相互绑定, 用以完成实际的构建任务. 而对于插件本身, 为了能够复用代码,它往往能够完成多个任务, 这些功能聚集在一个插件里,每个功能就是一个目标.
如:$ mvn compiler:compile: 冒号前是插件前缀, 后面是该插件目标(即: maven-compiler-plugin的compile目标).
而该目标绑定了default生命周期的compile阶段: 他们的绑定能够实现项目编译的目的.

内置绑定

为了能让用户几乎不用任何配置就能使用Maven构建项目, Maven 默认为一些核心的生命周期绑定了插件目标, 当用户通过命令调用生命周期阶段时, 对应的插件目标就会执行相应的逻辑.

彻底理解maven_第2张图片
image.png
彻底理解maven_第3张图片
image.png

上图只列出了打包方式为jar且拥有插件绑定关系的阶段(packaging 定义了Maven项目打包方式, 通常打包方式与所生成构件扩展名对应,有jar(默认)、war、pom、maven-plugin等., 其他打包类型生命周期的默认绑定关系可参考: Built-in Lifecycle Bindings、Plugin Bindings for default Lifecycle Reference.

彻底理解maven_第4张图片
image.png

自定义绑定

除了内置绑定以外, 用户还能够自定义将某个插件目标绑定到生命周期的某个阶段上. 如创建项目的源码包, maven-source-plugin插件的jar-no-fork目标能够将项目的主代码打包成jar文件, 可以将其绑定到verify阶段上:


    
        
            org.apache.maven.plugins
            maven-source-plugin
            3.0.0
            
                
                    attach-sources
                    verify
                    
                        jar-no-fork
                    
                
            
        
    

executions下每个execution子元素可以用来配置执行一个任务.

聚合与继承

Maven的聚合特性(aggregation)能够使项目的多个模块聚合在一起构建, 而继承特性(inheritance)能够帮助抽取各模块相同的依赖、插件等配置,在简化模块配置的同时, 保持各模块一致.

模块聚合
随着项目越来越复杂(需要解决的问题越来越多、功能越来越重), 我们更倾向于将一个项目划分几个模块并行开发, 如: 将feedcenter-push项目划分为client、core和web三个模块, 而我们又想一次构建所有模块, 而不是针对各模块分别执行$ mvn命令. 于是就有了Maven的模块聚合 -> 将feedcenter-push作为聚合模块将其他模块聚集到一起构建:

聚合POM
聚合模块POM仅仅是帮助聚合其他模块构建的工具, 本身并无实质内容:



    4.0.0

    com.vdian.feedcenter
    feedcenter-push
    pom
    1.0.0.SNAPSHOT

    
        feedcenter-push-client
        feedcenter-push-core
        feedcenter-push-web
    


通过在一个打包方式为pom的Maven项目中声明任意数量的module以实现模块聚合:

  • packaging: pom, 否则无法聚合构建.
  • modules: 实现聚合的核心,module值为被聚合模块相对于聚合POM的相对路径, 每个被聚合模块下还各自包含有pom.xml、src/main/java、src/test/java等内容, 离开聚合POM也能够独立构建

元素的内容是jar,那么我们很好理解,也就是说这个项目最终会被打包成一个jar包,那元素为pom又是什么意思呢?从字面上的意思来看,这个项目将打包成一个pom。我们不妨去maven仓库里去瞧瞧(前提是已经在项目下运行了mvn install命令)。可以发现这个文件其实和项目中的pom.xml是同一个文件,这样做的目的是什么呢?上面我们说过PO对象也是有继承关系的,pom的作用就在这里,这就是maven中project inheritance的概念。当实际执行maven命令的时候,会根据project inheritance关系对项目的pom.xml进行转化,得到真正执行时所用到的pom.xml,即所谓的effective pom,因此可以得到一个结论:所有元素为pom的项目其实并不会输出一个可供外部使用,类似于jar包的东西。这类项目的作用有两个:

管理子项目

  • 例如这里的api和biz就是echo项目的两个module。若没有echo这个父项目,我们需要到api和biz两个项目下分别执行mvn install命令才能完成整个构建过程,而有了echo这个父项目之后,我们只需在echo项目中执行mvn install即可,maven会解析pom.xml,发现该项目有api和biz两个module,它会分别到这两个项目下去执行mvn install命令。当module数量比较多的时候,能大大提高构建的效率。
    管理继承属性
  • 比如A和B都需要某个依赖,那么在父类项目的pom.xml中声明即可,因为根据PO对象的继承关系,A和B项目会继承父类项目的依赖,这样就可以减少一些重复的输入。

effective pom包含了当前项目的PO对象,直到Super POM对应的PO对象中的信息。要看一个项目的effective pom,只需在项目中执行

mvn help:effective-pom

命令即可查看。这里顺带说一句,有的同学可能不理解上面这个命令是什么意思。maven命令的语法为

mvn [options] [goal(s)] [phase(s)]

goal和phase。maven允许你执行一个或者多个goals/phases。很明显这面的命令help:effective-pom并不是一个phase,那么也就是说它是一个goal。对这个goal只不过是采用了缩写的形式,其全称是这样的:

org.apache.maven.plugins:maven-help-plugin:2.2:effective-pom

以分号为分隔符,包含了groupId,artifactId,version,goal四部分。若groupId为org.apache.maven.plugins则可以使用上述的简写形式。也就是说

mvn help:effective-pom
mvn org.apache.maven.plugins:maven-help-plugin:2.2:effective-pom

是等价的,都是执行了maven-help-plugin这个plugin中的effective-pom这个goal。

我们知道一个plugin中可以包含多个goal,goal可以绑定到lifecycle中的某一个phase,这样在执行这个phase的时候就会调用该goal。那那些没有绑定到phase上的goal应该如何执行呢?这就是 mvn [goal(s)]
这里的goal也就是官方文档中所说的standalone goal,也就是说若一个plugin中的某个goal没有和一个phase进行绑定,可以通过这种方式来执行。可能有的读者使用过

mvn dependency:tree

这条命令,这里其实就是单独执行一个goal,这个goal的作用是分析该工程的依赖并使用树状的形式打印出来。这里的dependency:tree其实是一个简写的形式,其完整形式是:

mvn org.apache.maven.plugins:maven-dependency-plugin:<版本号信息>:tree

也就是说单独执行一个goal的方式是:
mvn :::

每次都要敲这么长一串命令是很繁琐的,因此才有了上述的简写的形式。maven规定了对于plugin的artifactId是如下两种形式:
maven-{prefix}-maven-plugin

的可以使用简写的方式${prefix}来表示一个plugin.

模块继承
在面向对象中, 可以通过类继承实现复用. 在Maven中同样也可以创建POM的父子结构, 通过在父POM中声明一些配置供子POM继承来实现复用与消除重复

父pom
与聚合类似, 父POM的打包方式也是pom, 因此可以继续复用聚合模块的POM(这也是在开发中常用的方式):



    4.0.0

    com.vdian.feedcenter
    feedcenter-push
    pom
    1.0.0.SNAPSHOT

    
        feedcenter-push-client
        feedcenter-push-core
        feedcenter-push-web
    

    
        feedcenter-push
        ${finalName}.war
        4.0.6.RELEASE
        4.12
        UTF-8
        exploded/${warName}
    

    
        
            
                org.springframework
                spring-core
                ${spring.version}
            
            
                org.springframework
                spring-beans
                ${spring.version}
            
            
                org.springframework
                spring-context
                ${spring.version}
            

           
                junit
                junit
                ${junit.version}
                test
            
        
    

    
        
            
                
                    org.apache.maven.plugins
                    maven-source-plugin
                    3.0.0
                    
                        
                            attach-sources
                            verify
                            
                                jar-no-fork
                            
                        
                    
                
            
        
    

  • dependencyManagement: 能让子POM继承父POM的配置的同时, 又能够保证子模块的灵活性: 在父POMdependencyManagement元素配置的依赖声明不会实际引入子模块中, 但能够约束子模块dependencies下的依赖的使用(子模块只需配置groupId与artifactId, 见下).
  • pluginManagement: 与dependencyManagement类似, 配置的插件不会造成实际插件的调用行为, 只有当子POM中配置了相关plugin元素, 才会影响实际的插件行为.



    
        com.vdian.feedcenter
        feedcenter-push
        1.0.0.SNAPSHOT
    
    4.0.0

    feedcenter-push-client

    
        
            org.springframework
            spring-core
        
        
            org.springframework
            spring-context
        
        
            org.springframework
            spring-beans
        

        
            junit
            junit
        
    

    
        
            
                org.apache.maven.plugins
                maven-source-plugin
            
            
                org.apache.maven.plugins
                maven-compiler-plugin
            
        
    


**元素继承 **
可以看到, 子POM中并未定义模块groupId与version, 这是因为子POM默认会从父POM继承了如下元素:

groupId、version
dependencies
developers and contributors
plugin lists (including reports)
plugin executions with matching ids
plugin configuration
resources 

因此所有的springframework都省去了version、junit还省去了scope, 而且插件还省去了executions与configuration配置, 因为完整的声明已经包含在父POM中.

优势: 当依赖、插件的版本、配置等信息在父POM中声明之后, 子模块在使用时就无须声明这些信息, 也就不会出现多个子模块使用的依赖版本不一致的情况, 也就降低了依赖冲突的几率. 另外如果子模块不显式声明依赖与插件的使用, 即使已经在父POM的dependencyManagement、pluginManagement中配置了, 也不会产生实际的效果.

推荐: 模块继承与模块聚合同时进行,这意味着, 你可以为你的所有模块指定一个父工程, 同时父工程中可以指定其余的Maven模块作为它的聚合模块. 但需要遵循以下三条规则:

  • 在所有子POM中指定它们的父POM;
  • 将父POM的packaging值设为pom;
  • 在父POM中指定子模块/子POM的目录.

parent元素内还包含一个relativePath元素, 用于指定父POM的相对路径, 默认../pom.xml

超级pom-约定优先于配置

任何一个Maven项目都隐式地继承自超级POM, 因此超级POM的大量配置都会被所有的Maven项目继承, 这些配置也成为了Maven所提倡的约定.



  4.0.0

  
  
    
      central
      Central Repository
      https://repo.maven.apache.org/maven2
      default
      
        false
      
    
  

  
    
      central
      Central Repository
      https://repo.maven.apache.org/maven2
      default
      
        false
      
      
        never
      
    
  

  
  
    ${project.basedir}/target
    ${project.build.directory}/classes
    ${project.artifactId}-${project.version}
    ${project.build.directory}/test-classes
    ${project.basedir}/src/main/java
    ${project.basedir}/src/main/scripts
    ${project.basedir}/src/test/java
    
      
        ${project.basedir}/src/main/resources
      
    
    
      
        ${project.basedir}/src/test/resources
      
    

    
    
      
      
      
        
          maven-antrun-plugin
          1.3
        
        
          maven-assembly-plugin
          2.2-beta-5
        
        
          maven-dependency-plugin
          2.8
        
        
          maven-release-plugin
          2.3.2
        
      
    
  

  
  
    ${project.build.directory}/site
  

  
  
    
    
      release-profile

      
        
          performRelease
          true
        
      

      
        
          
            true
            maven-source-plugin
            
              
                attach-sources
                
                  jar
                
              
            
          
          
            true
            maven-javadoc-plugin
            
              
                attach-javadocs
                
                  jar
                
              
            
          
          
            true
            maven-deploy-plugin
            
              true
            
          
        
      
    
  



Maven Plugin 开发

详细代码在maven plugin demo

  1. 创建plugin项目
mvn archetype:generate 
    -DgroupId=com.fq.plugins -DartifactId=lc-maven-plugin -Dversion=0.0.1-SNAPSHOT
    -DarchetypeArtifactId=maven-archetype-plugin 
    -DinteractiveMode=false 
    -DarchetypeCatalog=internal

使用maven-archetype-plugin Archetype可以快速创建一个Maven插件项目。
** pom.xml **
插件本身也是Maven项目, 特殊之处在于packaging方式为maven-plugin:



    4.0.0

    com.fq.plugins
    lc-maven-plugins
    maven-plugin
    0.0.1-SNAPSHOT

    
        
            com.google.guava
            guava
            19.0
        

        
            org.apache.maven
            maven-plugin-api
            3.3.3
        
        
            org.apache.maven.plugin-tools
            maven-plugin-annotations
            3.3
        
    


maven-plugin 打包方式能控制Maven为其在生命周期阶段绑定插件处理的相关目标.

  1. 编写目标Mojo
@Mojo(name = "lc", defaultPhase = LifecyclePhase.VERIFY)
public class LCMavenMojo extends AbstractMojo {

    private static final List DEFAULT_FILES = Arrays.asList("java", "xml", "properties");

    @Parameter(defaultValue = "${project.basedir}", readonly = true)
    private File baseDir;

    @Parameter(defaultValue = "${project.build.sourceDirectory}", readonly = true)
    private File srcDir;

    @Parameter(defaultValue = "${project.build.testSourceDirectory}", readonly = true)
    private File testSrcDir;

    @Parameter(defaultValue = "${project.build.resources}", readonly = true)
    private List resources;

    @Parameter(defaultValue = "${project.build.testResources}", readonly = true)
    private List testResources;

    @Parameter(property = "lc.file.includes")
    private Set includes = new HashSet<>();

    private Log logger = getLog();

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        if (includes.isEmpty()) {
            logger.debug("includes/lc.file.includes is empty!");
            includes.addAll(DEFAULT_FILES);
        }
        logger.info("includes: " + includes);

        try {
            long lines = 0;
            lines += countDir(srcDir);
            lines += countDir(testSrcDir);

            for (Resource resource : resources) {
                lines += countDir(new File(resource.getDirectory()));
            }
            for (Resource resource : testResources) {
                lines += countDir(new File(resource.getDirectory()));
            }

            logger.info("total lines: " + lines);
        } catch (IOException e) {
            logger.error("error: ", e);
            throw new MojoFailureException("execute failure: ", e);
        }
    }

    private LineProcessor lp = new LineProcessor() {

        private long line = 0;

        @Override
        public boolean processLine(String fileLine) throws IOException {
            if (!Strings.isNullOrEmpty(fileLine)) {
                ++this.line;
            }
            return true;
        }

        @Override
        public Long getResult() {
            long result = line;
            this.line = 0;
            return result;
        }
    };


    private long countDir(File directory) throws IOException {
        long lines = 0;
        if (directory.exists()) {
            Set files = new HashSet<>();
            collectFiles(files, directory);

            for (File file : files) {
                lines += CharStreams.readLines(new FileReader(file), lp);
            }

            String path = directory.getAbsolutePath().substring(baseDir.getAbsolutePath().length());
            logger.info("path: " + path + ", file count: " + files.size() + ", total line: " + lines);
            logger.info("\t-> files: " + files.toString());
        }

        return lines;
    }

    private void collectFiles(Set files, File file) {
        if (file.isFile()) {
            String fileName = file.getName();
            int index = fileName.lastIndexOf(".");
            if (index != -1 && includes.contains(fileName.substring(index + 1))) {
                files.add(file);
            }
        } else {
            File[] subFiles = file.listFiles();
            for (int i = 0; subFiles != null && i < subFiles.length; ++i) {
                collectFiles(files, subFiles[i]);
            }
        }
    }
}

@Parameter: 配置点, 提供Mojo的可配置参数. 大部分Maven插件及其目标都是可配置的, 通过配置点, 用户可以自定义插件行为


    com.fq.plugins
    lc-maven-plugins
    0.0.1-SNAPSHOT
    
        
            lc
            verify
            
                lc
            
            
                
                    java
                    lua
                    json
                    xml
                    properties
                
            
        
    

execute(): 实际插件功能;
异常: execute()方法可以抛出以下两种异常:
MojoExecutionException: Maven执行目标遇到该异常会显示 BUILD FAILURE 错误信息, 表示在运行期间发生了预期的错误;
MojoFailureException: 表示运行期间遇到了未预期的错误, 显示 BUILD ERROR 信息

  1. 测试&执行

通过mvn clean install将插件安装到仓库后, 就可将其配置到实际Maven项目中, 用于统计项目代码了:

$ mvn com.fq.plugins:lc-maven-plugins:0.0.1-SNAPSHOT:lc 

你可能注意到为了调用该插件的goal,我们需要给出该插件的所有坐标信息,包裹groupId, artifactId,version号,你可能之前已经执行过"mvn eclipase:eclipase"或"mvn idea:idea"这样简洁的命令,让我们也来将自己的插件调用变简单一点。要通过简单别名的方式调用Maven插件,我们需要做到以下两点:

  1. 插件的artifactId应该遵循-maven-plugin或maven--plugin命名规则,对于本文中的插件,我们已经遵循了。
  2. 需要将插件的groupId放在Maven默认的插件搜寻范围之内,默认情况下Maven只会在org.apache.maven.plugins和org.codehaus.mojo两个groupId下搜索插件,要让Maven同时搜索我们自己的groupId,我们需要在~/.m2/settings.xml中加入:
  
       com.fq.plugins  

在达到以上两点之后,我们便可以通过以下命令来调用自己的插件了:
mvn lc:lc

要在别的项目中应用插件也是简单的,我们只需要在该项目的pom.xml文件中使用上面标签声明该插件即可。

你可能感兴趣的:(彻底理解maven)