一种组件化框架的探究之旅
概述
本文主要就组件化中服务实现类的实例化方法做简要探究,希望可以探索出一种简洁易用的组件化框架,本文到的主要技术有:
- 编译时注解
javapoet
的使用- 反射的使用
问题的引入
在软件开发中,当一款软件的规模和功能不断增多、丰富,原先的“一勺烩”架构往往显得捉襟见肘,为了便于团队协作、便于维护、便于升级,我们往往需要将一个软件划分为若干个模块(即我们所说的模块化),而这若干个模块又是依赖于很多个组件的(即我们所说的组件化),这里涉及了两个概念,即模块化和组件化,经常有读者反映搞不清楚这二者的区别,这里我帮大家总结了一下模块化与组件化的区别与联系:
以抖音APP
为例,其可以这样进行模块和组件的划分:
如上图所示,按照业务,可以将抖音分为首页模块、直播模块、视频模块、消息模块4个模块(实际肯定比这个分的多,这里只是为了说明问题),每个模块完成了一定的业务逻辑,而这4个模块又是基于下面的视频播放组件、IO
组件、网络组件等若干个组件来实现自己的业务逻辑的,这些组件反映在Android
项目中就是若干个 'com.android.library'
,组件为模块提供服务(Service
),模块并不关心组件是如何实现服务的,只关心组件提供了什么服务,反映在代码上就是,使用组件的模块只知道组件暴露出来的接口,不清楚对应接口在组件中是由谁实现的(实现类是什么),这样就有一个问题,组件的使用者在使用组件的时候应该如何实例化对象?因为接口是不能被实例化的,比如:
组件A提供服务IServiceA
,将接口IServiceA
暴露给外界,IServiceA
的实现类为ServiceAImpl
,组件的使用者模块B要使用组件A提供的服务IServiceA
。按照常理来说,最直接的方法就是模块B使用new
关键字实例化一个ServiceAImpl
类的对象,然后基于这个对象调用IServiceA
提供的各个功能(方法),但是问题是模块B只知道自己所需要的功能是由组件A的IServiceA
接口定义的,它根本不知道IServiceA
具体的实现类是什么,而IServiceA
是不能使用new
关键字进行对象的实例化的的的(因为不能实例化接口),那么怎么解决这个问题呢?
问题的解决思路
我们可以使用编译时注解,为服务的实现类添加我们自定义的注解,然后在编译时对所有添加我们自定义注解的类进行遍历,将遍历的结果写入文件,然后在运行时将文件读出,这样模块就可以知晓服务的具体实现类,自然可以成功实例化,整体思路如下图所示:
具体到代码层面,思路如下:
- 定义注解
Component
,用来修饰服务(接口)的实现类,其接收一个Class
类型的参数,这个参数的意义是该类实现的服务(接口)的Class
类 - 自定义
AbstractProcessor
类,在该类的process()
方法中遍历被@Component
修饰的类(称作Service
类),并拿到注解对应的参数即Service
类对应的服务接口(称作IService
),然后将IService-Service
作为键值对添加到Map
中,最终解析完所有被@Component
注解修饰的类后将Map
的内容转换为json
字符串(记为jsonString
) 使用代码生成一个
ComponentResource
类:- 将上一步生成的
jsonString
作为ComponentResource
的成员变量 - 在
ComponentResource
类的构造方法中将jsonString
解析为map
,并且将map
作为ComponentResource
的成员变量 - 为
ComponentResource
类生成String getServiceImplUrl(String iServiceClassName)
方法,即根据IService
的类名查找到其实现类的URL
并返回,方法的内容自然是返回map.get(iServiceClassName)
- 将上一步生成的
定义
ServiceManager
类,即我们框架的管理类:- 定义
register(Application application)
方法,利用反射实例化编译期间生成的ComponentResource
类(生成的实例作为ServiceManager
的成员变量) - 定义
getService(Class<T> clazz)
方法,在该方法中首先调用ComponentResource.getServiceImplUrl(clazz.getCanonicalName()
)方法获取当前IService
对应实现类的URL
,然后利用反射实例化该URL
对应的类,然后将实例返回
- 定义
- 在应用启动的时候(比如
Application
的onCreate
方法中),手动调用ServiceManager
的register(Application application)
方法 - 当某模块需要
IService
实现类的实例时,调用ServiceManager
的getService(Class<T> clazz)
方法,获得接口类对应的实现类的实例
将以上步骤形象化可以表示为:
代码实现
定义注解
注解的定义很简单,需要注意的是,注解的@Target
要设置为TYPE
,因为我们定义的这个注解是要应用到类上的,另外一点,我们定义的这个注解接收一个Class
类型的参数,我们希望将接口类的class
传递进来,以便下一步生成键值对:
@Retention(RUNTIME)
@Target(TYPE)
public @interface Component {
Class value();
}
定义AbstractProcessor类
要想在编译时解析被特定注解修饰的类,我们就需要使用AbstractProcessor
,该类在编译时会被自动执行,java api
会调用AbstractProcessor
的process()
方法并传入相关参数,我们只需要在process
方法中找到被@Component
注解修饰的类,并且拿到注解中的参数即可:
public class UgComponentProcessor extends AbstractProcessor {
private Filer filer;
private HashMap<String, Set<String>> map = new HashMap<>();
...
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
//获取当前env,用于后面代码的写入
filer = env.getFiler();
...
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//寻找并解析被@Component注解修饰的类,并将类和接口以键值对的形式塞进map中
findAndParseTargets(roundEnvironment);
//将map转换为json字符串
String jsonString = generateJsonString(map);
//生成ComponentResources类的代码文件,将jsonString作为ComponentResources的成员变量
JavaFile javaFile = brewJava(jsonString);
try {
//将生成的代码文件进行写入
javaFile.writeTo(filer);
} catch (IOException e) {
System.out.println("warning:多次写入filter");
}
return false;
}
}
我们来一步一步进行代码的实现,首先是寻找并解析被@Component注解修饰的类,并将类和接口以键值对的形式塞进map中,我们定义一个findAndParseTargets()方法进行实现:
public class UgComponentProcessor extends AbstractProcessor {
...
/**
* 寻找被 @Component注解修饰的类
*
* @param env
* @return
*/
private void findAndParseTargets(RoundEnvironment env) {
// 遍历被@Component修饰的元素
for (Element element : env.getElementsAnnotatedWith(Component.class)) {
try {
//解析被@Component修饰的元素
parseComponentAnimation(element);
} catch (Exception e) {
}
}
}
/**
* 解析被 @Component修饰的元素
*
* @param element
*/
private void parseComponentAnimation(Element element) {
//实现类的类名
String name = element.getSimpleName().toString();
//实现类的包名
PackageElement e = (PackageElement) element.getEnclosingElement();
String implPackageName = e.getQualifiedName().toString();
//接口的包名+类名
String interfacePackageWithClassName = getUgValueTypeMirror(element.getAnnotation(Component.class));
//实现类对应的URL
String implUrl = implPackageName + "." + name;
Set<String> impls = new HashSet<>(map.get(interfacePackageWithClassName));
if (impls.size() == 0) {
impls = new LinkedHashSet<>();
impls.add(implUrl);
} else if (impls.contains(implUrl)) {
return;
} else {
impls.add(implUrl);
}
//put 进map
map.put(interfacePackageWithClassName, impls);
}
}
generateJsonString()
方法的主要作用是将map
转为json
字符串,是借助gson
实现的,代码比较简单,这里不再赘述,然后我们看看brawJava()
方法的实现:
public class UgComponentProcessor extends AbstractProcessor {
//生成java代码
private JavaFile brewJava(String jsonStringValue) {
jsonStringValue = jsonStringValue.replaceAll("\"", "\\\\\"");
jsonStringValue = "\"" + jsonStringValue + "\"";
ClassName gson = ClassName.get("com.google.gson", "Gson");
ClassName arrayList = ClassName.get("java.util", "ArrayList");
MethodSpec cons = MethodSpec.constructorBuilder()
.beginControlFlow("if (\"\".equals(jsonString) || jsonString == null)")
.addStatement("return")
.endControlFlow()
.addStatement("$T gson = new $T()", gson, gson)
.addStatement("maps = gson.fromJson(jsonString, HashMap.class)")
.addModifiers(Modifier.PUBLIC)
.build();
MethodSpec getInstanceOfService = MethodSpec.methodBuilder("getServiceImplUrl")
.addModifiers(Modifier.PUBLIC)
.returns(String.class)
.addParameter(String.class, "iServiceClassName")
.addStatement("$T<String> list = ($T<String>) maps.get(iServiceClassName)", arrayList, arrayList)
.beginControlFlow("if(list==null)")
.addStatement("return null")
.endControlFlow()
.addStatement("return list.get(0)")
.addAnnotation(Override.class)
.build();
FieldSpec jsonString = FieldSpec.builder(String.class, "jsonString", Modifier.PRIVATE)
.initializer(jsonStringValue).build();
FieldSpec hashMap = FieldSpec.builder(HashMap.class, "maps", Modifier.PRIVATE)
.initializer("new HashMap<>()").build();
ClassName serviceCacheInterface = ClassName.get("com.bytedance.annotation", "IComponentResource");
TypeSpec My_Component = TypeSpec.classBuilder("ComponentResource")
.addSuperinterface(serviceCacheInterface)
.addModifiers(Modifier.PUBLIC)
.addField(jsonString)
.addField(hashMap)
.addMethod(cons)
.addMethod(getInstanceOfService)
.build();
return JavaFile.builder("com.component", My_Component)
.build();
}
...
}
这样就可以生成一个包含IService
与ServiceImpl
键值对的ComponentResource
类文件,就像这样:
public class ComponentResource implements IComponentResource {
private String jsonString = "{\"com.modelb.IServiceB\":[\"com.modelb.ServiceBimpl2\",\"com.modelb.ServiceBimpl1\"],\"com.componentframe.IServiceA\":[\"com.componentframe.ServiceAImpl2\",\"com.componentframe.ServiceAImpl1\"]}";
private HashMap maps = new HashMap<>();
public ComponentResourceBeta() {
if ("".equals(jsonString) || jsonString == null) {
return;
}
Gson gson = new Gson();
maps = gson.fromJson(jsonString, HashMap.class);
}
@Override
public String getServiceImplUrl(String iServiceClassName) {
ArrayList<String> list = (ArrayList<String>) maps.get(iServiceClassName);
if(list==null) {
return null;
}
return list.get(0);
}
}
然后我们需要定义一个ServiceManager
来将用户和ComponentResource
连接起来:
public class ServiceManager {
private static boolean inited = false;
private static IComponentResource iComponentResource = null;
public static boolean register(Application application) {
if (inited) {
return true;
} else {
try {
Class<?> serviceCacheClass = application.getClass().getClassLoader().loadClass("com.component.ComponentResource");
Constructor constructor = serviceCacheClass.getConstructor();
iComponentResource = (IComponentResource) constructor.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
inited = true;
return true;
}
}
public static <T> T getService(Class<T> clazz) {
if (!inited) {
return null;
}
String targetUrl = iComponentResource.getServiceImplUrl(clazz.getCanonicalName());
Class<?> serviceClazz = null;
T service = null;
try {
serviceClazz = Class.forName(targetUrl);
Constructor constructor = serviceClazz.getConstructor();
service = (T) constructor.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return service;
}
}
在使用的时候我们只需要:
ServiceManager.register(this);
IServiceB serviceB = ServiceManager.getService(IServiceB.class);
即可获得IServiceB
的实现类的实例。
改进
上面的实现思路是先将Map
转化为json String
,然后写入ComponentResource
类文件,当实例化ComponentResource
的时候再将json String
解析为Map
,这样由于json
的解析比较耗时,势必导致编译速度过慢,改进方法是略去map
与String
互转的步骤,直接将map
的内容写在ComponentResource
的构造方法中,即将生成ComponentResource
的brewJava()
方法改进为:
private JavaFile brewJava(HashMap<String, Set<String>> hashMap) {
ClassName arrayList = ClassName.get("java.util", "ArrayList");
ClassName linckedHashSet = ClassName.get("java.util", "LinkedHashSet");
ClassName collection = ClassName.get("java.util", "Collection");
ClassName hashSet = ClassName.get("java.util", "HashSet");
StringBuilder mapStr = new StringBuilder();
for (Map.Entry entry : hashMap.entrySet()) {
String key = (String) entry.getKey();
Set<String> value = new HashSet<String>((Collection<? extends String>) entry.getValue());
StringBuilder implSetStr = new StringBuilder();
mapStr.append("implSet.clear();\n");
for (String str : value) {
implSetStr.append("implSet.add(\"").append(str).append("\");\n");
}
mapStr.append(implSetStr);
mapStr.append("interfaceToImplUrlMap.put(").append("\"").append(key).append("\"").append(",new HashSet<>(implSet));\n");
}
MethodSpec cons = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addStatement("$T<String> implSet=new $T<>()", hashSet, linckedHashSet)
.addCode(mapStr.toString())
.build();
MethodSpec getInstanceOfService = MethodSpec.methodBuilder("getServiceImplUrl")
.addModifiers(Modifier.PUBLIC)
.returns(String.class)
.addParameter(String.class, "iServiceClassName")
.addStatement("$T<String> list = new ArrayList<String>(($T<? extends String>) interfaceToImplUrlMap.get(iServiceClassName))", arrayList, collection)
.beginControlFlow("if(list==null)")
.addStatement("return null")
.endControlFlow()
.addStatement("return list.get(0)")
.addAnnotation(Override.class)
.build();
FieldSpec interfaceToImplUrlMap = FieldSpec.builder(HashMap.class, "interfaceToImplUrlMap", Modifier.PRIVATE)
.initializer("new HashMap<>()").build();
ClassName ugInterface = ClassName.get("com.annotation", "IComponentResource");
TypeSpec Ug_Component = TypeSpec.classBuilder("ComponentResource")
.addSuperinterface(ugInterface)
.addModifiers(Modifier.PUBLIC)
.addField(interfaceToImplUrlMap)
.addMethod(cons)
.addMethod(getInstanceOfService)
.build();
return JavaFile.builder("com.component", Ug_Component)
.build();
}
最后生成的代码就像这样:
public class ComponentResource implements IComponentResource {
private HashMap interfaceToImplUrlMap = new HashMap<>();
public ComponentResource() {
HashSet<String> implSet=new LinkedHashSet<>();
implSet.clear();
implSet.add("com.modelb.ServiceBimpl2");
implSet.add("com.modelb.ServiceBimpl1");
interfaceToImplUrlMap.put("com.modelb.IServiceB",new HashSet<>(implSet));
implSet.clear();
implSet.add("com.componentframe.ServiceAImpl1");
implSet.add("com.componentframe.ServiceAImpl2");
interfaceToImplUrlMap.put("com.componentframe.IServiceA",new HashSet<>(implSet));
}
@Override
public String getServiceImplUrl(String iServiceClassName) {
ArrayList<String> list = new ArrayList<String>((Collection<? extends String>) interfaceToImplUrlMap.get(iServiceClassName));
if(list==null) {
return null;
}
return list.get(0);
}
}
我们来测试一下改进前后的区别:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
long start = System.currentTimeMillis();
ServiceManagerBefore.register(this);
IServiceB serviceBfromBefore = ServiceManagerBefore.getService(IServiceB.class);
String value = serviceBfromBefore.getValue();
Log.e(MyApplication.class.getSimpleName(), "改进前测试:" + value);
long end = System.currentTimeMillis();
Log.e(MyApplication.class.getSimpleName(), "改进前总耗时:" + (end - start));
start = System.currentTimeMillis();
ServiceManager.register(this);
IServiceB serviceBfromAfter = ServiceManager.getService(IServiceB.class);
value = serviceBfromAfter.getValue();
Log.e(MyApplication.class.getSimpleName(), "改进后测试:" + value);
end = System.currentTimeMillis();
Log.e(MyApplication.class.getSimpleName(), "改进后总耗时:" + (end - start));
}
}
运行效果如下:
可见改进后的速度比改进前快了不止一点点。
待完成
- 为
@Component
增加参数,使其适应一个借口多个实现类的场景下,按照用户传入参数的不同实例化不同的实现类的返回给用户
还没有评论,来说两句吧...