文章目录
前言
在模块化和组件化横行的今天,module的数量越来越多,module数量增加的同时也给项目编译带来了极大的负担,相信大家都经历过一次冷编译耗时五六分钟,甚至七八分钟的时候,编译优化显然是势在必行,一种常见的思路是将module打包成aar本地引入,这样在编译速度上能有一个明显的提升,一些跨部门通用的module组件我们更是会发布到远程仓库来使用,而绝大多数情况下我们使用本地仓库就够了,虽然编译速度提升了,但发布配置和依赖切换依旧让人觉得麻烦,因此,一套简单高效的模块管理方案显得尤为重要。
module -> aar
方案实践前,我们先来看一些aar常规发布的问题,有的人会说,既然绝大多数情况本地引入就够了,那为什么还需要发布到仓库呢,踩过坑的同学都知道直接引用本地aar,内部的第三方依赖关系是不能传递出来的,但很明显,我们在使用远程仓库的时候不会出现这个问题,这是因为我们从远程仓库拉取第三方库时拉取的不仅仅是aar文件,还有一个很重要的文件,pom文件。
pom文件
pom文件是什么?在官方的介绍里,pom文件就是一个maven项目的所有。简单一点说,pom是一个xml文件,定义了一系列的元素和依赖关系,这里我就不吹了,再吹也吹不过官方文档,大家有兴趣的可以去看看官方文档https://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的对应关系。
对应关系相当清晰了,当你使用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