模块管理最佳实践-ModuleManager

·  阅读 1220
模块管理最佳实践-ModuleManager

前言

在模块化和组件化横行的今天,module的数量越来越多,module数量增加的同时也给项目编译带来了极大的负担,相信大家都经历过一次冷编译耗时五六分钟,甚至七八分钟的时候,编译优化显然是势在必行,一种常见的思路是将module打包成aar本地引入,这样在编译速度上能有一个明显的提升,一些跨部门通用的module组件我们更是会发布到远程仓库来使用,而绝大多数情况下我们使用本地仓库就够了,虽然编译速度提升了,但发布配置和依赖切换依旧让人觉得麻烦,因此,一套简单高效的模块管理方案显得尤为重要。

module -> aar

方案实践前,我们先来看一些aar常规发布的问题,有的人会说,既然绝大多数情况本地引入就够了,那为什么还需要发布到仓库呢,踩过坑的同学都知道直接引用本地aar,内部的第三方依赖关系是不能传递出来的,但很明显,我们在使用远程仓库的时候不会出现这个问题,这是因为我们从远程仓库拉取第三方库时拉取的不仅仅是aar文件,还有一个很重要的文件,pom文件。

pom文件

pom文件是什么?在官方的介绍里,pom文件就是一个maven项目的所有。简单一点说,pom是一个xml文件,定义了一系列的元素和依赖关系,这里我就不吹了,再吹也吹不过官方文档,大家有兴趣的可以去看看官方文档maven.apache.org/pom.html

继续回到我们的module来,将module发布到本地仓库就一定能生成完整的pom文件吗?当然不一定,如果你是从网上随便抄代码发布的话,你或许会发现根本无法生成pom文件,或者生成的pom文件不包含第三方依赖,从稳定性考虑,我们应当了解pom文件是如何生成的,在gradle源码里面,官方为我们提供了大量常用的插件,其中就包括了我们用来发布maven产物的插件maven-publish

apply plugin: 'maven-publish'

maven-publish插件为我们提供了以maven产物形式发布到maven仓库的能力。当我们使用maven-publish发布maven物件到仓库时,maven-publish会自动为我们生成pom文件,maven-publish插件的实现类是 MavenPublishPlugin,让我们来跟下源码看下MavenPublishPlugin是如何生成pom文件的,我们尽量不去陷入到繁琐的源码探索里面。

MavenPublishPlugin

@Override
public void apply(final Project project) {
    project.getPluginManager().apply(PublishingPlugin.class);
    ...
    project.getExtensions().configure(PublishingExtension.class, extension -> {
        ...
        realizePublishingTasksLater(project, extension);
    });
}

可以看到在加载maven-publish插件的同时立马加载了PublishingPlugin插件,这个插件是用来构建Publication的,我们暂时不需要管它,往下走,来到realizePublishingTasksLater(project, extension);

MavenPublishPlugin#realizePublishingTasksLater
private void realizePublishingTasksLater(final Project project, final PublishingExtension extension) {
    final NamedDomainObjectSet<MavenPublicationInternal> mavenPublications = extension.getPublications().withType(MavenPublicationInternal.class);
    ...
    mavenPublications.all(publication -> {
        ...
        this.createGeneratePomTask(tasks, publication, buildDirectory, project);
        createLocalInstallTask(tasks, publishLocalLifecycleTask, publication);
        ...
    });
}

为了不陷入到源码里面,尽量只展示相关的部分,这里做的事也很简单,从project的PublishingExtension里面取出所有类型为MavenPublicationInternal的Publication,这话有点绕,理解为拿到当前project下所有的MavenPublication就可以了,MavenPublication是gradle用来表示Maven格式的发布件,拿到MavenPublication之后可以看到插件为每个MavenPublication都创建了构建Pom的任务,继续看createGeneratePomTask方法。

MavenPublishPlugin#createGeneratePomTask
private void createGeneratePomTask(TaskContainer tasks, final MavenPublicationInternal publication, final DirectoryProperty buildDir, final Project project) {
    final String publicationName = publication.getName();
    String descriptorTaskName = "generatePomFileFor" + capitalize(publicationName) + "Publication";
    TaskProvider<GenerateMavenPom> generatorTask = tasks.register(descriptorTaskName, GenerateMavenPom.class, generatePomTask -> {
        ...
        generatePomTask.setPom(publication.getPom());
        if (generatePomTask.getDestination() == null) {
            generatePomTask.setDestination(buildDir.file("publications/" + publication.getName() + "/pom-default.xml"));
        }
    ...
    publication.setPomGenerator(generatorTask);
}

这里创建了生成pom文件的task并设置了pom文件的默认存放路径,我们重点关注生成pom文件的task,该task的实现类是GenerateMavenPom,看下它的执行方法

GenerateMavenPom#doGenerate

@TaskAction
public void doGenerate() {
    MavenPomInternal pomInternal = (MavenPomInternal) getPom();
    MavenPomFileGenerator pomGenerator = new MavenPomFileGenerator(
           ...
           );
    pomGenerator.configureFrom(pomInternal);
    for (MavenDependency mavenDependency : pomInternal.getApiDependencyManagement()) {
        pomGenerator.addApiDependencyManagement(mavenDependency);
    }
    for (MavenDependency mavenDependency : pomInternal.getRuntimeDependencyManagement()) {
        pomGenerator.addRuntimeDependencyManagement(mavenDependency);
    }
    ...
    pomGenerator.withXml(pomInternal.getXmlAction());

    pomGenerator.writeTo(getDestination());
}

@TaskAction注解是标识task被执行时调用的方法,这个方法内容很直白,通过MavenPomFileGenerator从Pom接口读取数据然后生成pom的xml文件。分析到这里,pom文件怎么生成的就不需要往下看了,我们需要关注的是Pom数据从哪里来,还记得我们上面分析的createGeneratePomTask方法吗,大家回头看一看,我就不回头了。

generatePomTask.setPom(publication.getPom());

可以看到,Pom数据是从publication拿的,也就是我们上面说的MavenPublication,MavenPublication的默认实现类是DefaultMavenPublication,我们再看看DefaultMavenPublication的Pom数据是哪里来的

DefaultMavenPublication

pom = instantiator.newInstance(DefaultMavenPom.class, this, instantiator, objectFactory);

在GenerateMavenPom Task里面拿到的Pom数据其实就是DefaultMavenPom,而DefaultMavenPom的入参是DefaultMavenPublication本身,这里使用了代理模式,外部只需要从MavenPomInternal接口(上面的getPom())获取数据即可,而真正的数据来源则是DefaultMavenPublication本身,我们随便找一个数据获取流程跟踪一下。

//获取所有的api依赖关系(这个api不是我们常用的api依赖,而是包括多个)
DefaultMavenPom#getApiDependencies ->

//实际获取数据是DefaultMavenPublication类
DefaultMavenPublication#getApiDependencies
@Override
public Set<MavenDependencyInternal> getApiDependencies() {
    populateFromComponent();
    return apiDependencies;
}

populateFromComponent方法的逻辑就是解析数据,这个方法有点长我就不贴代码了,大家有兴趣的可以自己去看,大致逻辑就是从DefaultMavenPublication.component属性中解析出各种依赖关系以及其他的一些信息,那component是哪儿来的呢?我们很快能查到是通过DefaultMavenPublication.from方法传入的,经常写插件的同学一定不会陌生,因为我们在构建插件Publication的时候经常会配置这样一段代码

* publishing {
*   publications {
*     maven(MavenPublication) {
*       from components.java
*     }
*   }
* }

这里的components.java就是数据来源了,到此pom文件生成和数据来源的分析就基本结束了。

选择合适的component

上面我们分析了pom文件生成的数据来源,但遗憾的是gradle官方目前只提供三种类型的component,分别是components.java、components.web、components.javaPlatform,对应的插件分别是javaPlugin、WarPlugin、JavaPlatFormPlugin。显然这些都不是我们想要的,难道我们自己再写一个插件来提供android的component吗?Google表示这种小事交给我来就行了,Android Gradle 插件在3.6.0 及更高版本以上开始支持maven-publish插件,根据你依赖插件的类型来生成对应的components,来看下插件类型和components的对应关系。

image.png 对应关系相当清晰了,当你使用module插件的时候会为你自动构建components.variant和aar,当你使用app插件的时候会为你生成apk文件及components.variant_apk,根据这些信息我们很容易就能写出一个标准的module Publication配置脚本

publishing {
    publications {
        libraryA(MavenPublication) {
            from components.release
            groupId = 'com.xxx'
            artifactId = 'xxx'
            version = '1.1.1'
        }
    }
}

配置完publication再依赖maven-publish插件我们就可以通过publishToMavenLocal愉快的发布aar到本地仓库了,但是一两个module还好,module数量一旦多起来,难道我要一个个去配置吗?这也太难为老夫了吧~

构建蓝图

一个个去配置是不可能的,这辈子都不可能,我们希望能够通过一种极其简洁明了的方式来配置所有的module,并且代码不侵入到module的build script里面去(先来做个梦,画出我们想要的蓝图),比如像下面这样:

moduleSettings {
    libraryA(
            groupId: 'com.default',
            artifactId: 'libraryA',
            version: '1.3',
    )
    libraryB(
            groupId: 'com.default',
            artifactId: 'libraryB',
            version: '1.2',
    )
    ...
}

libraryA、libraryB是module的名称,groupId、artifactId、version不用说了,maven发布三剑客,除了这些必要的参数外,其他的我们统统不想管,我们想只在工程目录下配置这个脚本就能完成所有module的发布配置。

上帝:“嗯,问题不大”

:“那配置完之后我不可能一个个module去执行任务发布吧,这也太累了,能不能一键发布所有module啊”

上帝:“good idea~”

:“那。。。发布完之后我怎么依赖aar呢?这么多module我每次切换aar和project依赖那得多累啊,能不能完成自动切换,不侵入到module的build script呢”

上帝:“That's a great idea~”

:“哈哈,那还不错,满足的从睡梦中笑醒,揭开被子才发现上帝竟是我自己。”

完成蓝图

梦是做完了,但实现还是得努把力,下面我们就来圆梦

module发布件统一配置

毫无疑问,实现这个功能需要通过插件来处理,先来看看一个module配置publication的必要步骤,说是必要步骤,其实所有步骤也就两步。

  • 依赖maven-publish
  • 配置publication

话不多说,先来定义一个插件

public class ModuleManagePlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        for (Project subProject : subProjects) {
            project.afterEvaluate(p -> {
                if (p.getPluginManager().hasPlugin("com.android.library")){
                p.getPluginManager().apply("maven-publish");
            }
        });
    }
}

在项目工程下build.gradle apply该插件我们就能获取到所有settings脚本里面配置的module project,接着为每一个project都增加对maven-publish插件的引用,第一步就完成了,再来看第二步,前面我们给出了module publication配置标准模板,除了maven三件套需要用户自己配置外(groupId、artifactId、version),其他的我们都可以通过插件来完成配置,我们可以定义一个root project的extension(ModuleConfig.class)来接收三件套的信息,然后在插件里面完成自动配置,还是和上面的方式一样,extension以subProject名字来命名

        for (Project subProject : subProjects) {
            subProject.getExtensions().create(subProject.getName(), ModuleConfig.class);
        });

然后在每个subProject里面配置publication,代码也很简单

ModuleConfig modulePublish = project.getRootProject().getExtensions().getByName(project.getName());

PublishingExtension publishingExt = project.getExtensions().getByType(PublishingExtension.class);

PublicationContainer publications = publishingExt.getPublications();
if (publications.findByName(PUBLISH_NAME) != null) {
    return;
}
publications.create(PUBLISH_NAME, MavenPublication.class, publication -> {
    SoftwareComponent release = project.getComponents().findByName(DEFAULT_COMPONENT);
    if (release == null) {
        System.out.println("can't find default component");
        return;
    }
    publication.from(release);
    publication.setGroupId(modulePublish.getGroupId());
    publication.setArtifactId(modulePublish.getArtifactId());
    publication.setVersion(modulePublish.getVersion());
});

做完这些我们基本上就完成了module发布的统一配置,看起来没啥问题,但是有一个体验很不好的地方,那就是extension的命名取的是subProject的名称,如果settings文件project的module配置被注释掉了,此时将无法获取到正确的subProject名称,配置脚本自然也就会报错,我总不可能再把配置文件对应的module配置也注释掉吧,本来是为了减轻工作量,这下反而又增加了,那有没有办法根据settings配置的module动态激活配置而不需要更改配置脚本呢?

MethodMissing机制

要实现根据settings配置的module动态激活配置,extension肯定是行不通了,因为extension需要提前创建,在groovy语言有一个很好玩的特性,那就是methodMissing,methodMissing允许你调用一个未定义过的方法并通过MethodMixIn接口转发,利用这一特性,我们完全不需要事先创建extension,我们只需要将配置文件转换成实体,然后再根据settings脚本配置的module来决定是否激活module配置,直接上代码

public class DynamicPublishMethods implements MethodAccess {
    private Map<String, ModuleConfig> moduleConfigHashMap;


    public DynamicPublishMethods(Map<String, ModuleConfig> moduleConfigHashMap) {
        this.moduleConfigHashMap = moduleConfigHashMap;
    }

    @Override
    public boolean hasMethod(String name, Object... arguments) {
        return true;
    }

    @Override
    public DynamicInvokeResult tryInvokeMethod(String name, Object... arguments) {
        for (Object object : arguments) {
            if (object instanceof Map) {
                @SuppressWarnings("unchecked")
                Map<String, Object> map = (Map<String, Object>) object;
                ModuleConfig moduleConfig = new ModuleConfig();
                attemptPackageModuleConfig(moduleConfig, name, map);
            }
        }
        return DynamicInvokeResult.found(moduleConfigHashMap);
    }

    private void attemptPackageModuleConfig(@NotNull ModuleConfig moduleConfig,
            @NotNull String name, @NotNull Map<String, Object> paramsMap) {
        if (paramsMap.isEmpty()) {
            return;
        }
        try {
            Class<? extends ModuleConfig> moduleConfigClass = moduleConfig.getClass();
            Set<Map.Entry<String, Object>> entries = paramsMap.entrySet();
            for (Map.Entry<String, Object> entry : entries) {
                Field field = moduleConfigClass.getField(entry.getKey());
                field.setAccessible(true);
                field.set(moduleConfig, entry.getValue());
                field.setAccessible(false);
            }
            moduleConfigHashMap.put(name, moduleConfig);
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println(e.getMessage());
        }
    }
}

在上面的代码里面我们定义一个MethodAccess的类来接收未定义方法的跳转,获取所有配置信息然后通过反射将这些信息转换成ModuleConfig实体,然后再在subProject里面完成配置。

for (Project subProject : subProjects) { 
    ...
    ModuleConfig moduleConfig = moduleSettings.getModuleConfigHashMap().get(roject.getName())
    });
    ...

取到配置实体之后的步骤就和上面定义extension配置publication一样了,这里就不贴代码了,到这里我们就完成了module发布件统一配置。

module依赖方式自动切换

在我们项目中module大都是以这种方式来引用。

implementation project(':path')

如果想要改成aar引用, 不可避免的会改动build script,我们希望能在插件内部解决这件事,第一反应当然是手动删除替换依赖规则,但遗憾的是,直接对依赖进行删除的话则会直接抛出异常,官方不允许我们对已经添加的依赖直接做删除操作

@Override
public boolean remove(Object o) {
    throw new UnsupportedOperationException();
}

当然这样做本身其实是有风险的,容易出现其他不可预期的问题,庆幸的是,善解人意的gradle为我们提供了官方解决方案,那就是ResolutionStrategy,通过配置ResolutionStrategy,我们可以实现根据不同的策略在执行阶段来调整依赖。我们在之前配置maven三件套的实体(ModuleConfig)里再定义一个字段 useByAar,通过这个字段来控制是否切换成aar依赖,完整的配置如下所示:

moduleSettings {
    libraryA(
            useByAar: true,
            groupId: 'com.default',
            artifactId: 'libraryA',
            version: '1.3',
    )
    libraryB(
            useByAar: true,
            groupId: 'com.default',
            artifactId: 'libraryB',
            version: '1.2',
    )
    ...
    
}

然后在插件内部读取该字段来实现依赖的替换

private void configResolutionStrategy(Project project, ModuleSettings moduleSettings) {
    System.out.println("configResolutionStrategy");
    Map<String, String> resolutions = getResolutions(project, moduleSettings);
    if (resolutions.isEmpty()) {
        return;
    }
    project.getConfigurations().all(configuration -> {
        Set<Map.Entry<String, String>> entries = resolutions.entrySet();
        for (Map.Entry<String, String> entry : entries) {
            configuration.resolutionStrategy(
                    resolutionStrategy -> resolutionStrategy.dependencySubstitution(
                            dependencySubstitutions -> {
                                DependencySubstitutions.Substitution substitute =
                                        dependencySubstitutions.substitute(
                                                dependencySubstitutions.project(entry.getKey()));
                                substitute.with(
                                        dependencySubstitutions.module(entry.getValue()));
                            }));
        }
    });
}

到这里我们已经实现了module aar和project依赖的动态切换,只需要在module配置文件里将对应module的useByAar设置为true,项目中所有以project方式引用该module的依赖全部会自动切换成aar依赖引用,而不需要改动引用方build script的任何代码。

一键发布所有module至本地仓库

这个就很简单了,我们只需要定义一个oneKeyPulish的task,然后重新定义该task和当前所有已配置module的publishToMavenLocal的依赖关系即可,需要注意的是要提前依赖assembleRelease来构建components,由于篇幅原因这里就不展开说了,大家有兴趣的可以直接看下方链接源码,嗯、、、、烂尾王。

总结

通过一番探索,我们解决了module管理中几个比较核心的痛点

  • 自动构建发布脚本(除了用户必填的maven三件套)
  • 动态切换依赖
  • 一键发布所有module

以下是插件完整代码,可以说基本实现了一个非常实用的module管理插件,并在该基础插件上扩展了一些功能,但需求是因人而异的,如果大家有其他想法和需求,也可以提issue给我
ModuleManager

分类:
Android
标签:
分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改