一种组件化框架的探究之旅

我会带着你远行 2021-11-17 02:28 268阅读 0赞

概述

本文主要就组件化中服务实现类的实例化方法做简要探究,希望可以探索出一种简洁易用的组件化框架,本文到的主要技术有:

  • 编译时注解
  • javapoet的使用
  • 反射的使用

问题的引入

在软件开发中,当一款软件的规模和功能不断增多、丰富,原先的“一勺烩”架构往往显得捉襟见肘,为了便于团队协作、便于维护、便于升级,我们往往需要将一个软件划分为若干个模块(即我们所说的模块化),而这若干个模块又是依赖于很多个组件的(即我们所说的组件化),这里涉及了两个概念,即模块化和组件化,经常有读者反映搞不清楚这二者的区别,这里我帮大家总结了一下模块化与组件化的区别与联系:

image-20190718111759360

以抖音APP为例,其可以这样进行模块和组件的划分:

aHR0cDovL3BpY3R1cmUtcG9vbC5pbWctY24tYmVpamluZy5hbGl5dW5jcy5jb20vMjAxOS0wNy0yNi0wNjM4NDYuZ2lm

如上图所示,按照业务,可以将抖音分为首页模块、直播模块、视频模块、消息模块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关键字进行对象的实例化的的的(因为不能实例化接口),那么怎么解决这个问题呢?

问题的解决思路

我们可以使用编译时注解,为服务的实现类添加我们自定义的注解,然后在编译时对所有添加我们自定义注解的类进行遍历,将遍历的结果写入文件,然后在运行时将文件读出,这样模块就可以知晓服务的具体实现类,自然可以成功实例化,整体思路如下图所示:

gifhome\_480x277\_11s

具体到代码层面,思路如下:

  • 定义注解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对应的类,然后将实例返回
  • 在应用启动的时候(比如ApplicationonCreate方法中),手动调用ServiceManagerregister(Application application)方法
  • 当某模块需要IService实现类的实例时,调用ServiceManagergetService(Class<T> clazz)方法,获得接口类对应的实现类的实例

将以上步骤形象化可以表示为:

gifhome\_1900x1064\_20s

代码实现

定义注解

注解的定义很简单,需要注意的是,注解的@Target要设置为TYPE,因为我们定义的这个注解是要应用到类上的,另外一点,我们定义的这个注解接收一个Class类型的参数,我们希望将接口类的class传递进来,以便下一步生成键值对:

  1. @Retention(RUNTIME)
  2. @Target(TYPE)
  3. public @interface Component {
  4. Class value();
  5. }
定义AbstractProcessor类

要想在编译时解析被特定注解修饰的类,我们就需要使用AbstractProcessor,该类在编译时会被自动执行,java api会调用AbstractProcessorprocess()方法并传入相关参数,我们只需要在process方法中找到被@Component注解修饰的类,并且拿到注解中的参数即可:

  1. public class UgComponentProcessor extends AbstractProcessor {
  2. private Filer filer;
  3. private HashMap<String, Set<String>> map = new HashMap<>();
  4. ...
  5. @Override
  6. public synchronized void init(ProcessingEnvironment env) {
  7. super.init(env);
  8. //获取当前env,用于后面代码的写入
  9. filer = env.getFiler();
  10. ...
  11. }
  12. @Override
  13. public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
  14. //寻找并解析被@Component注解修饰的类,并将类和接口以键值对的形式塞进map中
  15. findAndParseTargets(roundEnvironment);
  16. //将map转换为json字符串
  17. String jsonString = generateJsonString(map);
  18. //生成ComponentResources类的代码文件,将jsonString作为ComponentResources的成员变量
  19. JavaFile javaFile = brewJava(jsonString);
  20. try {
  21. //将生成的代码文件进行写入
  22. javaFile.writeTo(filer);
  23. } catch (IOException e) {
  24. System.out.println("warning:多次写入filter");
  25. }
  26. return false;
  27. }
  28. }

我们来一步一步进行代码的实现,首先是寻找并解析被@Component注解修饰的类,并将类和接口以键值对的形式塞进map中,我们定义一个findAndParseTargets()方法进行实现:

  1. public class UgComponentProcessor extends AbstractProcessor {
  2. ...
  3. /**
  4. * 寻找被 @Component注解修饰的类
  5. *
  6. * @param env
  7. * @return
  8. */
  9. private void findAndParseTargets(RoundEnvironment env) {
  10. // 遍历被@Component修饰的元素
  11. for (Element element : env.getElementsAnnotatedWith(Component.class)) {
  12. try {
  13. //解析被@Component修饰的元素
  14. parseComponentAnimation(element);
  15. } catch (Exception e) {
  16. }
  17. }
  18. }
  19. /**
  20. * 解析被 @Component修饰的元素
  21. *
  22. * @param element
  23. */
  24. private void parseComponentAnimation(Element element) {
  25. //实现类的类名
  26. String name = element.getSimpleName().toString();
  27. //实现类的包名
  28. PackageElement e = (PackageElement) element.getEnclosingElement();
  29. String implPackageName = e.getQualifiedName().toString();
  30. //接口的包名+类名
  31. String interfacePackageWithClassName = getUgValueTypeMirror(element.getAnnotation(Component.class));
  32. //实现类对应的URL
  33. String implUrl = implPackageName + "." + name;
  34. Set<String> impls = new HashSet<>(map.get(interfacePackageWithClassName));
  35. if (impls.size() == 0) {
  36. impls = new LinkedHashSet<>();
  37. impls.add(implUrl);
  38. } else if (impls.contains(implUrl)) {
  39. return;
  40. } else {
  41. impls.add(implUrl);
  42. }
  43. //put 进map
  44. map.put(interfacePackageWithClassName, impls);
  45. }
  46. }

generateJsonString()方法的主要作用是将map转为json字符串,是借助gson实现的,代码比较简单,这里不再赘述,然后我们看看brawJava()方法的实现:

  1. public class UgComponentProcessor extends AbstractProcessor {
  2. //生成java代码
  3. private JavaFile brewJava(String jsonStringValue) {
  4. jsonStringValue = jsonStringValue.replaceAll("\"", "\\\\\"");
  5. jsonStringValue = "\"" + jsonStringValue + "\"";
  6. ClassName gson = ClassName.get("com.google.gson", "Gson");
  7. ClassName arrayList = ClassName.get("java.util", "ArrayList");
  8. MethodSpec cons = MethodSpec.constructorBuilder()
  9. .beginControlFlow("if (\"\".equals(jsonString) || jsonString == null)")
  10. .addStatement("return")
  11. .endControlFlow()
  12. .addStatement("$T gson = new $T()", gson, gson)
  13. .addStatement("maps = gson.fromJson(jsonString, HashMap.class)")
  14. .addModifiers(Modifier.PUBLIC)
  15. .build();
  16. MethodSpec getInstanceOfService = MethodSpec.methodBuilder("getServiceImplUrl")
  17. .addModifiers(Modifier.PUBLIC)
  18. .returns(String.class)
  19. .addParameter(String.class, "iServiceClassName")
  20. .addStatement("$T<String> list = ($T<String>) maps.get(iServiceClassName)", arrayList, arrayList)
  21. .beginControlFlow("if(list==null)")
  22. .addStatement("return null")
  23. .endControlFlow()
  24. .addStatement("return list.get(0)")
  25. .addAnnotation(Override.class)
  26. .build();
  27. FieldSpec jsonString = FieldSpec.builder(String.class, "jsonString", Modifier.PRIVATE)
  28. .initializer(jsonStringValue).build();
  29. FieldSpec hashMap = FieldSpec.builder(HashMap.class, "maps", Modifier.PRIVATE)
  30. .initializer("new HashMap<>()").build();
  31. ClassName serviceCacheInterface = ClassName.get("com.bytedance.annotation", "IComponentResource");
  32. TypeSpec My_Component = TypeSpec.classBuilder("ComponentResource")
  33. .addSuperinterface(serviceCacheInterface)
  34. .addModifiers(Modifier.PUBLIC)
  35. .addField(jsonString)
  36. .addField(hashMap)
  37. .addMethod(cons)
  38. .addMethod(getInstanceOfService)
  39. .build();
  40. return JavaFile.builder("com.component", My_Component)
  41. .build();
  42. }
  43. ...
  44. }

这样就可以生成一个包含IServiceServiceImpl键值对的ComponentResource类文件,就像这样:

  1. public class ComponentResource implements IComponentResource {
  2. private String jsonString = "{\"com.modelb.IServiceB\":[\"com.modelb.ServiceBimpl2\",\"com.modelb.ServiceBimpl1\"],\"com.componentframe.IServiceA\":[\"com.componentframe.ServiceAImpl2\",\"com.componentframe.ServiceAImpl1\"]}";
  3. private HashMap maps = new HashMap<>();
  4. public ComponentResourceBeta() {
  5. if ("".equals(jsonString) || jsonString == null) {
  6. return;
  7. }
  8. Gson gson = new Gson();
  9. maps = gson.fromJson(jsonString, HashMap.class);
  10. }
  11. @Override
  12. public String getServiceImplUrl(String iServiceClassName) {
  13. ArrayList<String> list = (ArrayList<String>) maps.get(iServiceClassName);
  14. if(list==null) {
  15. return null;
  16. }
  17. return list.get(0);
  18. }
  19. }

然后我们需要定义一个ServiceManager来将用户和ComponentResource连接起来:

  1. public class ServiceManager {
  2. private static boolean inited = false;
  3. private static IComponentResource iComponentResource = null;
  4. public static boolean register(Application application) {
  5. if (inited) {
  6. return true;
  7. } else {
  8. try {
  9. Class<?> serviceCacheClass = application.getClass().getClassLoader().loadClass("com.component.ComponentResource");
  10. Constructor constructor = serviceCacheClass.getConstructor();
  11. iComponentResource = (IComponentResource) constructor.newInstance();
  12. } catch (ClassNotFoundException e) {
  13. e.printStackTrace();
  14. } catch (NoSuchMethodException e) {
  15. e.printStackTrace();
  16. } catch (IllegalAccessException e) {
  17. e.printStackTrace();
  18. } catch (InstantiationException e) {
  19. e.printStackTrace();
  20. } catch (InvocationTargetException e) {
  21. e.printStackTrace();
  22. }
  23. inited = true;
  24. return true;
  25. }
  26. }
  27. public static <T> T getService(Class<T> clazz) {
  28. if (!inited) {
  29. return null;
  30. }
  31. String targetUrl = iComponentResource.getServiceImplUrl(clazz.getCanonicalName());
  32. Class<?> serviceClazz = null;
  33. T service = null;
  34. try {
  35. serviceClazz = Class.forName(targetUrl);
  36. Constructor constructor = serviceClazz.getConstructor();
  37. service = (T) constructor.newInstance();
  38. } catch (ClassNotFoundException e) {
  39. e.printStackTrace();
  40. } catch (NoSuchMethodException e) {
  41. e.printStackTrace();
  42. } catch (InstantiationException e) {
  43. e.printStackTrace();
  44. } catch (IllegalAccessException e) {
  45. e.printStackTrace();
  46. } catch (InvocationTargetException e) {
  47. e.printStackTrace();
  48. }
  49. return service;
  50. }
  51. }

在使用的时候我们只需要:

  1. ServiceManager.register(this);
  2. IServiceB serviceB = ServiceManager.getService(IServiceB.class);

即可获得IServiceB的实现类的实例。

改进

上面的实现思路是先将Map转化为json String,然后写入ComponentResource类文件,当实例化ComponentResource的时候再将json String解析为Map,这样由于json的解析比较耗时,势必导致编译速度过慢,改进方法是略去mapString互转的步骤,直接将map的内容写在ComponentResource的构造方法中,即将生成ComponentResourcebrewJava()方法改进为:

  1. private JavaFile brewJava(HashMap<String, Set<String>> hashMap) {
  2. ClassName arrayList = ClassName.get("java.util", "ArrayList");
  3. ClassName linckedHashSet = ClassName.get("java.util", "LinkedHashSet");
  4. ClassName collection = ClassName.get("java.util", "Collection");
  5. ClassName hashSet = ClassName.get("java.util", "HashSet");
  6. StringBuilder mapStr = new StringBuilder();
  7. for (Map.Entry entry : hashMap.entrySet()) {
  8. String key = (String) entry.getKey();
  9. Set<String> value = new HashSet<String>((Collection<? extends String>) entry.getValue());
  10. StringBuilder implSetStr = new StringBuilder();
  11. mapStr.append("implSet.clear();\n");
  12. for (String str : value) {
  13. implSetStr.append("implSet.add(\"").append(str).append("\");\n");
  14. }
  15. mapStr.append(implSetStr);
  16. mapStr.append("interfaceToImplUrlMap.put(").append("\"").append(key).append("\"").append(",new HashSet<>(implSet));\n");
  17. }
  18. MethodSpec cons = MethodSpec.constructorBuilder()
  19. .addModifiers(Modifier.PUBLIC)
  20. .addStatement("$T<String> implSet=new $T<>()", hashSet, linckedHashSet)
  21. .addCode(mapStr.toString())
  22. .build();
  23. MethodSpec getInstanceOfService = MethodSpec.methodBuilder("getServiceImplUrl")
  24. .addModifiers(Modifier.PUBLIC)
  25. .returns(String.class)
  26. .addParameter(String.class, "iServiceClassName")
  27. .addStatement("$T<String> list = new ArrayList<String>(($T<? extends String>) interfaceToImplUrlMap.get(iServiceClassName))", arrayList, collection)
  28. .beginControlFlow("if(list==null)")
  29. .addStatement("return null")
  30. .endControlFlow()
  31. .addStatement("return list.get(0)")
  32. .addAnnotation(Override.class)
  33. .build();
  34. FieldSpec interfaceToImplUrlMap = FieldSpec.builder(HashMap.class, "interfaceToImplUrlMap", Modifier.PRIVATE)
  35. .initializer("new HashMap<>()").build();
  36. ClassName ugInterface = ClassName.get("com.annotation", "IComponentResource");
  37. TypeSpec Ug_Component = TypeSpec.classBuilder("ComponentResource")
  38. .addSuperinterface(ugInterface)
  39. .addModifiers(Modifier.PUBLIC)
  40. .addField(interfaceToImplUrlMap)
  41. .addMethod(cons)
  42. .addMethod(getInstanceOfService)
  43. .build();
  44. return JavaFile.builder("com.component", Ug_Component)
  45. .build();
  46. }

最后生成的代码就像这样:

  1. public class ComponentResource implements IComponentResource {
  2. private HashMap interfaceToImplUrlMap = new HashMap<>();
  3. public ComponentResource() {
  4. HashSet<String> implSet=new LinkedHashSet<>();
  5. implSet.clear();
  6. implSet.add("com.modelb.ServiceBimpl2");
  7. implSet.add("com.modelb.ServiceBimpl1");
  8. interfaceToImplUrlMap.put("com.modelb.IServiceB",new HashSet<>(implSet));
  9. implSet.clear();
  10. implSet.add("com.componentframe.ServiceAImpl1");
  11. implSet.add("com.componentframe.ServiceAImpl2");
  12. interfaceToImplUrlMap.put("com.componentframe.IServiceA",new HashSet<>(implSet));
  13. }
  14. @Override
  15. public String getServiceImplUrl(String iServiceClassName) {
  16. ArrayList<String> list = new ArrayList<String>((Collection<? extends String>) interfaceToImplUrlMap.get(iServiceClassName));
  17. if(list==null) {
  18. return null;
  19. }
  20. return list.get(0);
  21. }
  22. }

我们来测试一下改进前后的区别:

  1. public class MyApplication extends Application {
  2. @Override
  3. public void onCreate() {
  4. super.onCreate();
  5. long start = System.currentTimeMillis();
  6. ServiceManagerBefore.register(this);
  7. IServiceB serviceBfromBefore = ServiceManagerBefore.getService(IServiceB.class);
  8. String value = serviceBfromBefore.getValue();
  9. Log.e(MyApplication.class.getSimpleName(), "改进前测试:" + value);
  10. long end = System.currentTimeMillis();
  11. Log.e(MyApplication.class.getSimpleName(), "改进前总耗时:" + (end - start));
  12. start = System.currentTimeMillis();
  13. ServiceManager.register(this);
  14. IServiceB serviceBfromAfter = ServiceManager.getService(IServiceB.class);
  15. value = serviceBfromAfter.getValue();
  16. Log.e(MyApplication.class.getSimpleName(), "改进后测试:" + value);
  17. end = System.currentTimeMillis();
  18. Log.e(MyApplication.class.getSimpleName(), "改进后总耗时:" + (end - start));
  19. }
  20. }

运行效果如下:

image-20190729172322004

可见改进后的速度比改进前快了不止一点点。

待完成

  • @Component增加参数,使其适应一个借口多个实现类的场景下,按照用户传入参数的不同实例化不同的实现类的返回给用户

发表评论

表情:
评论列表 (有 0 条评论,268人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Scrum探究()

    [原文链接][Link 1] 作者:Mark Levison 机械的Scrum对比真正的Scrum,差别在哪里? 最近,我和一个朋友聊到了他们公司实施Scrum的情况。