相关文章:《Aether使用指南(主体功能概述)

本文章为针对一年前技术探索的回想与记录,因时间跨度过大,有部分实现细节已经遗忘,望谅解。

因为篇幅原因,本文只做Aether的简单介绍以及在Android下的兼容开发过程,Aether的使用以及针对Gradle Dependency Conflict Resolution的适配见后续文章。

Aether is a library for working with artifact repositories. Aether deals with the specification of local repository, remote repository, developer workspaces, artifact transports, and artifact resolution.
(Aether是一个用于处理Artifact仓库的库。 Aether能够处理本地Maven仓库、远程Maven仓库、开发工作区、Artifact的传输和Artifact的解析。)

什么是Aether?我们为什么要使用Aether?

Aether是Eclipse Foundation下的一个用于Maven拉取与本地仓库管理的项目。不同于Maven,Aether是Maven包装后的可插件版。

通过Aether,开发者可以把控Artifact拉取以及存放过程中的各种细节,并自定义依赖冲突解决策略版本比较方案依赖树遍历策略等功能。

当然,因为我的目的是在Android上运行一个Maven Resolver,经过和朋友们的筛选,发现Aether较符合我们的要求(兼容度与功能丰富度),所以使用了他。

Aether现已废弃,更名为maven-resolver并持续更新中。

相关链接:
Aether Project Website(已废弃)
maven-resolver
Aether Wiki

Aether的依赖结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<properties>
<aetherVersion>1.0.0.v20140518</aetherVersion>
<mavenVersion>3.1.0</mavenVersion>
<wagonVersion>1.0</wagonVersion>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-api</artifactId>
<version>${aetherVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-util</artifactId>
<version>${aetherVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-impl</artifactId>
<version>${aetherVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-connector-basic</artifactId>
<version>${aetherVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-transport-file</artifactId>
<version>${aetherVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-transport-http</artifactId>
<version>${aetherVersion}</version>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-transport-wagon</artifactId>
<version>${aetherVersion}</version>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-aether-provider</artifactId>
<version>${mavenVersion}</version>
</dependency>
<dependency>
<groupId>org.apache.maven.wagon</groupId>
<artifactId>wagon-ssh</artifactId>
<version>${wagonVersion}</version>
</dependency>

</dependencies>

上述依赖用途说明 *(摘自Aether Wiki)*:

  • aether-api
    此Jar依赖包含了Aether interfaces,Aether系统的入口类是org.eclipse.aether.RepositorySystem
  • aether-util
    如名,此依赖包含了许多工具类,并提供了常用系统组件。
  • aether-impl
    此依赖包含了仓库系统接口的实例化类。除非在特殊情况下,需要自定义Aether系统的内部处理逻辑,或者需要手动协同一些功能,在程序的开发过程中不建议直接访问/操作此依赖中的任何类。
  • aether-connector-basic
    Artifact到远程Repository的上传和下载是通过Repository connector实现的。 这一connector一部分通用,并将部分工作委托给可插拔的传输协议和repository layouts。 因此,需要明确的是,该connector本身无法访问任何repository,必需包含一个或多个传输模块才能组合为正常运行的系统。
  • ether-transport-file
    此依赖提供了通过file或者URL访问repository的支持库。
  • aether-transport-http
    此依赖提供了访问基于http或者https协议的repository的支持库。
  • aether-transport-wagon
    此依赖基于Maven Wagon,通过已有Wagon providers来访问repository。
  • wagon-ssh
    此依赖项补充了前面提到的 aether-transport-wagon库,并添加了对使用 scp: 和 sftp: 方案进行传输的支持。 它包含在上面的 POM 片段中只是一个示例,可以使用任何符合需求的 Wagon provider;也可以根本不使用,此时,可以从依赖中删除 aether-transport-wagon。
  • maven-aether-provider
    此依赖提供了使用 Maven POM 作为Artifact descriptors并从中提取依赖关系信息。 此外,它还提供了对Maven Repository中使用的其他元数据(metadata)文件的处理。

注意: Aether需要1.5及以上的JDK来编译及允许。

针对Android的适配

适配问题其实很简单明了,只需要抓住主要矛盾:需要什么?有什么不同导致了需要的东西缺失?以什么方式来补全缺失?

从Jre上来看(暂且不论版本问题,毕竟Aether是个老项目,Java 1.5+即可),Android上的Jre与PC端的不同点主要在于Android上的Jre是针对Android的阉割优化版,大体内容不变,但是不包含许多javax包下的类,以及所有针对PC平台的类。

剖析:需要什么?有什么不同导致了需要的东西缺失?

(当然,此文编造的是不断的尝试后的事后诸葛亮行为,解决问题的最好方式还是实践求真理,做个简单的demo并且不断地尝试了。)

我们仍从上述依赖配置中探索,首先针对Aether API、util、impl以及provider部分,因为只使用了基础的类,显然兼容度很高;然后,分析剩下的transport协议库,对于file部分没啥争议,毕竟Java 1.8之后才会考虑Path这个影响兼容性的东东,那么就是网络协议部分可能有问题了。

为了不纸上谈兵,我们先做一个简单的项目,并写一行,debug打包允许:

1
DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();

果不其然,我们可以得到一个报错:

1
2
3
4
5
6
7
java.lang.NoSuchMethodError:No direct method <init>(Ljavax/net/ssl/SSLSocketFactory;Lorg/apache/http/conn/ssl/X509HostnameVerifier;)V in class Lorg/apache/http/conn/ssl/SSLSocketFactory; or its super classes(declaration of 'org.apache.http.conn.ssl.SSLSocketFactory' appears in /system/framework/framework.jar!classes3.dex)

framework/framework.jar!classes3.dex)

at org.eclipse.aether.transport.http.SslSocketFactory.<init(SslSocketFactory.java:57)

...

原来是org.eclipse.aether.transport.http.SslSocketFactory的构造方法里面出现问题了,我们顺藤摸瓜看看为啥:

(在Android Studio内,切换到Project工程结构展示,展开External Libraries,进入org.eclipse.aether.transport-http包内查找)

org.eclipse.aether.transport.http.SslSocketFactory文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final class SslSocketFactory
extends org.apache.http.conn.ssl.SSLSocketFactory
{
...
private SslSocketFactory( SSLSocketFactory socketfactory, X509HostnameVerifier hostnameVerifier,
String[] cipherSuites, String[] protocols )
{
super( socketfactory, hostnameVerifier );

this.cipherSuites = cipherSuites;
this.protocols = protocols;
}
...
}

其实在AS内可以看到这里已经显示报错了:
AS code error

显然是父类根本没这个构造方法。那么问题来了,为什么在PC上的JRE可以用,Android上的却不行了?查资料可知:
apache_http_client_removal

原来是,Android这小子不识抬举,大大阉割了Apache Http提供的Android特别版,转向使用OKhttp了,导致使用不了大部分Apache Http API。

知道问题了那么解决方案是什么?

以什么方式来补全缺失?

Try 1: 导入 Apache Http Legacy 包 (失败)

导入org.apache.http.legacy包,但是这么简单的办法当然是不行的,在许多设备上无法成功,很小一部分设备可以。(点名批评Homo OS based AOSP,就是它兼容性最差。)

Try 2: 使用JarFilter替换不兼容类,改用兼容代码 (失败)

JarFilter是一个用于编译时替换所依赖Jar中指定类的Gradle Plugin。

于是有了以下操作:

1.使用JarFilter移除org.eclipse.aether.transport.http.SslSocketFactory

2.改用新自定义类,来适配安卓版的Apache Http,完成相关功能

实践

首先,在app模块下的build.gradle文件内添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
apply plugin: "jar-filter"

jarFilters {
//兼容安卓版本的apache SSLSocketFactory
"org.eclipse.aether:aether-transport-http:(.*)" {
excludes = [
'org/eclipse/aether/transport/http/SslSocketFactory.class',
'org/eclipse/aether/transport/http/SslSocketFactory\\$(.*).class'
]
}
}

再创建一个同名的替换类org.eclipse.aether.transport.http.SslSocketFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182

package org.eclipse.aether.transport.http;

import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.HttpInetSocketAddress;
import org.apache.http.conn.scheme.HostNameResolver;
import org.apache.http.conn.scheme.LayeredSchemeSocketFactory;
import org.apache.http.conn.scheme.LayeredSocketFactory;
import org.apache.http.conn.scheme.SchemeLayeredSocketFactory;
import org.apache.http.conn.ssl.X509HostnameVerifier;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;

final class SslSocketFactory
extends org.apache.http.conn.ssl.SSLSocketFactory implements SchemeLayeredSocketFactory,
LayeredSchemeSocketFactory, LayeredSocketFactory {

private final SSLContext sslcontext;
private final javax.net.ssl.SSLSocketFactory socketfactory;
private final HostNameResolver nameResolver;
private X509HostnameVerifier hostnameVerifier = BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;

private final String[] cipherSuites;

private final String[] protocols;

public SslSocketFactory(SslConfig config) throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, KeyManagementException, CertificateException, IOException {
this(getSocketFactory(config.context), getHostnameVerifier(config.verifier), config.cipherSuites,
config.protocols);
}

private static SSLSocketFactory getSocketFactory(SSLContext context) {
return (context != null) ? context.getSocketFactory() : (SSLSocketFactory) SSLSocketFactory.getDefault();
}

private static X509HostnameVerifier getHostnameVerifier(HostnameVerifier verifier) {
return (verifier != null) ? X509HostnameVerifierAdapter.adapt(verifier)
: org.apache.http.conn.ssl.SSLSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;
}

private SslSocketFactory(SSLSocketFactory socketfactory, X509HostnameVerifier hostnameVerifier,
String[] cipherSuites, String[] protocols) throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException, KeyManagementException, CertificateException, IOException {
super(defaultKeyStore());
this.sslcontext = null;
this.socketfactory = socketfactory;
this.nameResolver = null;
this.setHostnameVerifier(hostnameVerifier);
this.cipherSuites = cipherSuites;
this.protocols = protocols;
}

private static KeyStore defaultKeyStore() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
return trustStore;
}

protected void prepareSocket(SSLSocket socket)
throws IOException {
if (cipherSuites != null) {
socket.setEnabledCipherSuites(cipherSuites);
}
if (protocols != null) {
socket.setEnabledProtocols(protocols);
}
}

@Override
public Socket createLayeredSocket(Socket socket, String target, int port, boolean autoClose) throws IOException, UnknownHostException {
SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(
socket,
target,
port,
autoClose
);
prepareSocket(sslSocket);
sslSocket.startHandshake();
if (this.hostnameVerifier != null) {
this.hostnameVerifier.verify(target, sslSocket);
}
// verifyHostName() didn't blowup - good!
return sslSocket;
}

@Override
public Socket createLayeredSocket(Socket socket, String target, int port, HttpParams params) throws IOException, UnknownHostException {
SSLSocket sslSocket = (SSLSocket) this.socketfactory.createSocket(
socket,
target,
port,
true);
prepareSocket(sslSocket);
sslSocket.startHandshake();
if (this.hostnameVerifier != null) {
this.hostnameVerifier.verify(target, sslSocket);
}
// verifyHostName() didn't blowup - good!
return sslSocket;
}

@Override
public Socket createSocket(HttpParams params) throws IOException {
SSLSocket sock = (SSLSocket) this.socketfactory.createSocket();
prepareSocket(sock);
return sock;
}

@Override
public Socket connectSocket(Socket socket, InetSocketAddress remoteAddress, InetSocketAddress localAddress, HttpParams params) throws IOException, UnknownHostException, ConnectTimeoutException {
if (remoteAddress == null) {
throw new IllegalArgumentException("Remote address may not be null");
}
if (params == null) {
throw new IllegalArgumentException("HTTP parameters may not be null");
}
Socket sock = socket != null ? socket : this.socketfactory.createSocket();
if (localAddress != null) {
sock.setReuseAddress(params.getBooleanParameter("http.socket.reuseaddr", false));
sock.bind(localAddress);
}

int connTimeout = HttpConnectionParams.getConnectionTimeout(params);
int soTimeout = HttpConnectionParams.getSoTimeout(params);

try {
sock.setSoTimeout(soTimeout);
sock.connect(remoteAddress, connTimeout);
} catch (SocketTimeoutException ex) {
throw new ConnectTimeoutException(
"Connect to " + remoteAddress + " timed out :" + ex.getMessage());
“连接到” + 远程地址 + “ 超时 :” + ex.getMessage());
}

String hostname;
if (remoteAddress instanceof HttpInetSocketAddress) {
hostname = ((HttpInetSocketAddress) remoteAddress).getHttpHost().getHostName();
} else {
hostname = remoteAddress.getHostName();
}

SSLSocket sslsock;
// Setup SSL layering if necessary
if (sock instanceof SSLSocket) {
sslsock = (SSLSocket) sock;
} else {
int port = remoteAddress.getPort();
sslsock = (SSLSocket) this.socketfactory.createSocket(sock, hostname, port, true);
prepareSocket(sslsock);
}
sslsock.startHandshake();
if (this.hostnameVerifier != null) {
try {
this.hostnameVerifier.verify(hostname, sslsock);
// verifyHostName() didn't blowup - good!
} catch (IOException iox) {
// close the socket before re-throwing the exception
try {
sslsock.close();
} catch (Exception x) { /*ignore*/ }
throw iox;
}
}
return sslsock;
}
}

这里我们直接使用Android所兼容的Apache Http API即可。

结论

经实验,部分手机可以允许,Homo OS依旧无法运行。最大的问题在于,这个JarFilter支持的AGP版本太低,在AGP版本较高时便不能在build release时正常运行,在部分版本下只能在build debug正常运行。

寄。

Try3: 强行塞入PC端Jre的一些类+Apache Http包 (成功)

既然,Aether是transport缺少Apache Http API而无法运行,那么是不是可以直接把Apache Http给强行迁移过来呢?是,但是需要一定的魔改。

众所周知,JVM运行中,因为双亲委托机制的存在,用户能操作的ClassLoader是不能覆盖系统提供的类文件的,否则会有各种奇妙的异常(因为不同设备对Apache Http的阉割情况可能不同),所以我们不能简单地直接依赖原版Apache Http包。

那么,较优的方法是通过JarJar修改所有包下关于Apache Http的包名信息为自定义的,然后强行塞入App内,让它成为你想要的形状

实践

(因为当时没完整记录这个流程,导致没有截图啥的信息,只在此说明大概思路了。)

首先,下载所需版本的aether-transport-http.jarApache Http Core.jarApache Http Client.jar

然后,利用JarJar手写规则,将org.apache.http包名更改为你自定义的包名;

最后,去掉build.gradle中对于aether-transport-http、Apache Http Core以及Apache Http Client的依赖,改为上述已修改的Jar。

如果没有意外的话,就有意外了,发现又有新的报错,是缺少javax.*包下的类,这个解决也很简单暴力,直接复制一份Jre 1.8下lib文件夹内的rt.jar,删除一些无关类即可。(删除什么我已经忘记了,可以自行尝试,即使删的少了也并不会影响运行。)

结论

经实验,本解决方案完美适配已知所有设备(Homo也难不倒它),可以放心地使用此方案。