基于 AOP 的 M 层抽象

Frank Xu
微信读书
Published in
9 min readOct 17, 2017

在日常的应用开发中,把代码放到哪总是可以纠结很久,而且这种纠结消耗的时间丝毫不弱于给变量起名字。

MVC/MVP/MVVM.. 等等一系列架构里,关注点在 C 层,M 和 V 其实比较少提到。可能是由于这两层的定位过于纯粹,而 C 层跟业务有更加密切的关系,对其抽象的需求更强一些。

而日常开发中,M 层提供数据的角色很清晰,却不能够含糊对待,从后台获得数据,本地加工后落地DB,根据 VC 层需要提供,每一步都不能够含糊,在一个快速迭代的版本开发里,如何确保每一个步骤的准确性;在每一次发布之前,如何回归没有变动的接口。

我们采取了服务化的架构方式,通过 AOP 把 M 层分割成为网络,存储两层,这两者又根据自身需要继续分层,对每一层做一个有效的“切面”,每一个业务单元都是一个完整的服务,可以有效的针对每一层数据进行调度和测试。

服务化的M层抽象

从C层调用的角度看

Observable<User> user = 
WRService.of(UserService.class).getUserById(userId);

UserService 被定义为抽象类

public abstract class UserService implements BaseUserService {
public Observable<User> getUserById(final int userId) {
return getUserFromDB(userId)
.flatMap(new Func1<User, Observable<User>>() {
@Override
public Observable<User> call(User user) {
if (user == null) {
return getUserFromNetwork(userId);
}
return Observable.just(user);
}
});
}
}

同时,定义 BaseUserService

interface BaseUserService {
@GET("/user")
Observable<User> getUserFromNetwork(@Query("id") int userId);
}

这里从上到下定义了一个 UserService 的服务, BaseUserService 的定义是满足 retrofit 接口规范的定义,而 UserService 实现了具体的落地逻辑和查询逻辑,并且调用了尚未被实现的接口方法 getUserFromNetwork 。(这个方法的实现会通过 RestAdapter来生成)

抽象类的方法调用

到了最有意思的部分,抽象类方法如何能够被调用?最开始我们用的是 dexmaker ,同类的框架还有 javassistcglibASM,思路都是在运行时生成 JVM 的字节码,构造一个代理方法,来响应一些切面的需求,各个库适应不同的 Java 环境。

dexmaker 提供了一个 ProxyBuilder 的实现,通过他,可以实现抽象类方法的实现代理,比如以上 UserService#getUserFromNetwork 方法,可以通过检查其定义,转发给 retrofit 的 RestAdapter

final HashSet<Class<?>> interfaces = Reflections.getAllInterfaces(clazz);Reflections.proxy(clazz, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method,
Object[] args) throws Throwable {
// 如果是继承的接口 Service 定义的方法,调用对应的 service 执行
for (Class<?> interfaze : interfaces) {
Method m;
try {
m = interfaze.getDeclaredMethod(
method.getName(), method.getParameterTypes());
} catch (NoSuchMethodException e) {
m = null;
}
if (m == null) continue;
if (interfaze.getInterfaces().length != 0) {
// 对于实现多个接口的情况,仅允许没有继承的接口调用这个方法
// 参考 {@link retrofit.Utils#validateServiceClass(Class)}
return ClassProxyBuilder.callSuper(proxy, method, args);
}
try {
return method.invoke(getService(interfaze), args);
} catch (InvocationTargetException ex) {
throw ex.getCause();
}
}
return ClassProxyBuilder.callSuper(proxy, method, args);
}
});

如上,如果方法在接口(而不是抽象类)定义,则转发给 retrofit,如果方法在抽象类里定义,则假定一定有实现代码(而不是一个抽象方法),可以直接调用,否则抛出异常。

同时通过工具方法 getAllInterfaces 方法查找出所有实现的接口,扩展 retrofit 对接口的代理,允许多层接口抽象,子类按需组合特定的接口。

执行开销

运行时生成字节码来构造方法的做法,比较常用于单元测试和集成测试,mock 某个方法的行为,这类库一般不会用到生产环境,所以性能不是考量的重点。

从 dexmaker 的实现里,初始化的开销相当大,每一个类都需要遍历所有方法,构造一个内存中的 class 对象,然后将其落地,这里可能会占用启动时间长达几百毫秒(主要是加载类的开销)。同时由于每个方法都是独立文件,多进程的时候竞争写可能导致文件损坏,造成类加载失败,抛出 ClassNotFoundException

事实上,这些代理类是完全可以在编译期确定下来的,最终我们通过 Processor 重新实现了这一过程。对于每一个抽象的 XXXService 类,生成一个子类 XXXService_proxy ,该子类实现了所有需要实现的方法,但是都委托给了一个工具方法,然后把方法正确的委托给目标 InvocationHandler

@Override
public Observable<User> getUser(String userVid) {
return (Observable<User>) Utils.invoke(5, new Object[]{userVid}, this, $__methodArray, $__handler);
}

$__methodArray$__handler 对应 dexmaker 的 FIELD_NAME_HANDLERFIELD_NAME_METHODS ,还有一些方法排序的 trick,最终生成出的类直接打到 classes.dex 里,消除了额外的类加载开销和竞争错误。

切面的应用场景

比如:在业务发展到如日中天的时候,突然来一个所有请求都要处理的黑名单需求(对被拉入黑名单的所有用户请求都返回失败),我们就不再需要每个地方都检查输入参数是否命中黑名单,通过抽象一层网络层 Interceptor ,在转发给 retrofit 之前,预先检查输入参数:

InterceptBy interceptBy = m.getAnnotation(InterceptBy.class);
if (interceptBy != null) {
Interceptor interceptor;
if ((interceptor = mInterceptBy.get(interceptBy.value())) == null) {
interceptor = interceptBy.value().newInstance();
mInterceptBy.put(interceptBy.value(), interceptor);
}

if (interceptor.isNeedIntercept()) {
Annotation[][] parameter = m.getParameterAnnotations();
HashMap<String, Object> pars = new HashMap<>();
for (int i = 0; i < parameter.length; i++) {
for (Annotation a : parameter[i]) {
if (a.annotationType().equals(InterceptField.class)) {
InterceptField field = (InterceptField) a;
pars.put(field.value(), args[i]);
}
}
}

InterceptResult result = interceptor.check(pars);
if (result.isResult()) {
return interceptor.intercept(result);
}
}
}

如此,在定义接口的地方配置:

@GET("/user/profile")
@InterceptBy(BlockInterceptor.class)
Observable<UserInfo> getUserInfo(@InterceptField("vid") @Query("vid") String vid)

Service 在调用方法的时候,找出来 @InterceptField及该参数值,构造一个 Map 传入 BlockInterceptor,即可在所有请求之前,检查是否命中黑名单,抛出相应异常,而不在需要每个具体的请求发送之前都做同样的逻辑。

切入请求之前

写在最后

由于服务框架拦截了所有方法调用,正如 retrofit 通过 Proxy 实现 Mock 调用一样的原理,我们可以非常轻松地实现方法级别的调试,监控和测试。

M 层的抽象非常依赖具体业务,配合这个轻量的ORM,进一步探讨如何深度结合 VM 模型,演进为一个全面的开发框架。

--

--

Frank Xu
微信读书

WeRead at WeChat. Growth Analyst, Data & Infra Engineer.