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

概述

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

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

问题的引入

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

image-20190718111759360

以抖音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关键字进行对象的实例化的的的(因为不能实例化接口),那么怎么解决这个问题呢?

问题的解决思路

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

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传递进来,以便下一步生成键值对:

@Retention(RUNTIME)
@Target(TYPE)
public @interface Component {
    Class value();
}
定义AbstractProcessor类

要想在编译时解析被特定注解修饰的类,我们就需要使用AbstractProcessor,该类在编译时会被自动执行,java api会调用AbstractProcessorprocess()方法并传入相关参数,我们只需要在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();

    }

	...

}

这样就可以生成一个包含IServiceServiceImpl键值对的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的解析比较耗时,势必导致编译速度过慢,改进方法是略去mapString互转的步骤,直接将map的内容写在ComponentResource的构造方法中,即将生成ComponentResourcebrewJava()方法改进为:

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));


    }
}

运行效果如下:

image-20190729172322004

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

待完成

  • @Component增加参数,使其适应一个借口多个实现类的场景下,按照用户传入参数的不同实例化不同的实现类的返回给用户
  • 7
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值