一、开头
某一天一个同事跟我讨论有没有做过这样的事情:在写Android应用时,
只需写实现类,而不在实现类以外的任何地方(包括配置文件)写任何代码,就能在程序执行时,调用到这个实现类。
这样的应用场景主要是在要写很多个具有相同父类或者接口类的算法类,写完这些算法类又需要在特定的地方做这些算法类的初始化,才能在程序中使用。而做这样的事情的时候,我们可能是很多人在同时写,或者隔了很长时间又新加了几个算法类,很有可能在写完算法类之后忘记在特定的地方做初始化。虽然,我们在后来的测试中会发现这个问题,但有时候花费时间来调试这种问题会觉得非常的“不爽”。
我们知道,Java的可执行Jar包在运行时通过遍历Jar中的class文件来获取整个Jar中的所有类(在不考虑混淆的情况下)。比如:
try {
File directory = new File("");//设定为当前文件夹
String jarDir = directory.getAbsolutePath();//获取绝对路径
String jarName = System.getProperty("java.class.path");
String jarPath = jarDir + File.separator + jarName;
JarFile jarFile = new JarFile(jarPath);
Enumeration<JarEntry> entrys = jarFile.entries();
while (entrys.hasMoreElements()) {
JarEntry jarEntry = entrys.nextElement();
System.out.println(jarEntry.getName());
}
} catch (Exception e) {
e.printStackTrace();
}
二、问题
但是Android应用apk“好像不能”,Android apk虽然是个类似zip一样的文件,但是所有的class文件是被集中在了dex文件中,所以无法像遍历Jar包里的class文件那样获取所有的类。我们写Android应用时
虽然不能通过这种方式,但是我们可以通过其他方式类解决。
先看一下,一般情况下我们是怎么写的代码:
有一个算法的父类(SupperFun.java),两个算法的具体实现类(AddFun.java,SubFun.java),一个管理算法类(FunManager.java),以及一个Activity。
SupperFun.java
public abstract class SupperFun{
// 能够处理的功能
public abstract String canHandleMethod();
// 功能
public abstract int fun( int a, int b );
}
AddFun.java
public class AddFun extends SupperFun {
@Override
public String canHandleMethod() {
return "add";
}
@Override
public int fun(int a, int b) {
return a + b;
}
}
SubFun.java
public class SubFun extends SupperFun {
@Override
public String canHandleMethod() {
return "sub";
}
@Override
public int fun(int a, int b) {
return a - b;
}
}
FunManager.java
public class FunManager {
private static HashMap<String,SupperFun> funMap = new HashMap<>();
static {
AddFun addFun = new AddFun();
funMap.put(addFun.canHandleMethod(),addFun);
SubFun subFun = new SubFun();
funMap.put(subFun.canHandleMethod(),subFun);
}
public int fun1(String method, int first, int second) throws NoSuchMethodException {
if (!funMap.containsKey(method)) {
throw new NoSuchMethodException("Can not find method[" + method + "]");
}
return funMap.get(method).fun(first, second);
}
}
界面的代码很简单就不贴了,如下所示,主要是点击按钮,根据第一行写的方法来对两个数据进行运算:
如果当我们增加了一个MulFun.java类时,还得需要在FunManager中的static中增加相应的代码。
MulFun mulFun = new MulFun ();
funMap.put(mulFun.canHandleMethod(),mulFun);
三、一种解决思路
针对前面提到的问题,会有几种解决方法,其中一种就是“使用Java编译时注解”。
什么是Java编译时注解?就是在代码编译时对注解进行处理,通过注解获取必要的信息,在项目中生成代码,并运行时调用,又称为APT(Annotation Processing Tool)。
当然,
Java编译时注解并不仅仅是用来解决上面的问题的。如果运用的得当,可以提高开发效率,避免写重复、易错的代码。比如我们在做Android应用开发时,都知道ButterKnife,它就用到了编译时注解。
关于Java注解的基本概念,这里不再着重介绍,网上很多,可以自行搜索。
那怎么才能做到“在代码编译是对注解进行处理”呢?
需要写一个继承自AbstractProcessor的类(注解处理器),
需要注意的是,这个类所在的工程不能是Android工程,而是Java工程。下
面我们开始编写例子,而这些代码都在Android Studio下进行编写的。
3.1 先在Android Studio中新建一个Java Library的Module。
3.2 定义一个注解类,此注解类在后面会用到,用于注解到算法实现类上:
// 表示此注解用于源码、类文件阶段。
@Retention(RetentionPolicy.CLASS)
// 表示此注解作用于 类、接口(包括注释类型)或枚举声明
@Target(ElementType.TYPE)
public @interface NeedAdd {
}
3.3
写一个注解处理器:
/**
* AutoService注解处理器是Google开发的,
* 用来生成 META-INF/services/javax.annotation.processing.Processor 文件的,
* 只需要在自定义的注解处理器上添加 @AutoService(Processor.class) 就可以了,非常方便。
* 在build.gradle中的dependencies中添加依赖:
* compile 'com.google.auto.service:auto-service:1.0-rc3'
*/
@AutoService(Processor.class)
public class SelfProcessor extends AbstractProcessor {
/**
* 用于指定,本注解处理器所支持的注解类型集合
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(NeedAdd.class.getCanonicalName());
return types;
}
/**
* 这相当于每个注解处理器的主函数main(),在这里写扫描和处理注解,以及生成Java文件 的代码。
*
* @param set 请求处理的注解类型
* @param roundEnv 有关当前和以前的信息环境
* @return 如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;
* 如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
ArrayList<String> list = new ArrayList<>();
// 遍历找出所有带有@NeedAdd的类
for (Element element : roundEnv.getElementsAnnotatedWith(NeedAdd.class)) {
TypeElement classElement = (TypeElement) element;
String fullClassName = classElement.getQualifiedName().toString();
list.add(fullClassName);
}
writeCode(list);
return true;
}
/**
* 生成Java code
*/
private void writeCode(ArrayList<String> list) {
String packageName = "com.zxl.forannotation.fun";
String className = "GenClass";
String classFullName = packageName + "." + className;
try {
JavaFileObject jfo = processingEnv.getFiler().createSourceFile(classFullName);
StringBuilder builder = new StringBuilder();
builder.append("package ").append(packageName).append(";\n\n");
builder.append("import java.util.ArrayList;\n");
builder.append('\n');
builder.append("public class ").append(className);
builder.append(" {\n");
builder.append("public static ArrayList<String> getClassList(){\n");
builder.append("ArrayList<String> result = new ArrayList<>();\n");
for (String str : list) {
builder.append("result.add(\"");
builder.append(str);
builder.append("\");\n");
}
builder.append('\n');
builder.append("return result;\n");
builder.append("}\n");
builder.append("}\n");
Writer writer = jfo.openWriter();
writer.write(builder.toString());
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
这样,一个注解处理器就写好了。可以看到在WriteCode中是写我们生成java类的具体实现。
3.4
运行Gradle中此module的build这个Task之后,在build/libs目录下会有一个jar。之后要写的Android应用,会引用到这个jar。到此,我们已经准备好了应用要使用的注解处理器jar包了。
3.5 我们新建一个Android应用module。
而AddFun.java和SubFun.java分别只增加一行:
@NeedAdd // 仅增加这一行即可
public class AddFun extends SupperFun {
@Override
public String canHandleMethod() {
return "add";
}
@Override
public int fun(int a, int b) {
return a + b;
}
}
@NeedAdd
// 仅增加这一行即可public class SubFun extends SupperFun {
@Override
public String canHandleMethod() {
return "sub";
}
@Override
public int fun(int a, int b) {
return a - b;
}
}
3.7 FunManager类改为:
public class FunManager {
private HashMap<String, Object> objMap = new HashMap<>();
/**
* 功能
* @throws NoSuchMethodException 当不支持方法时,会抛出异常
*/
public int fun(String method, int first, int second) throws NoSuchMethodException {
if (!objMap.containsKey(method)) {
throw new NoSuchMethodException("Can not find method[" + method + "]");
}
return ((SupperFun) objMap.get(method)).fun(first, second);
}
/**
* 初始化
*/
public void init(Context context) {
ArrayList<Object> objList = getLoadObjectList(context, getGenClassList(context));
for (Object obj : objList) {
String method = ((SupperFun) obj).canHandleMethod();
objMap.put(method, obj);
}
}
/**
* 将类实例化成对象
*/
private ArrayList<Object> getLoadObjectList(Context context, ArrayList<String> loadList) {
ArrayList<Object> objList = new ArrayList<>();
for (String className : loadList) {
try {
Class mClass = context.getClassLoader().loadClass(className);
Object mObject = mClass.newInstance();
objList.add(mObject);
} catch (Exception e) {
e.printStackTrace();
}
}
return objList;
}
/**
* 得到我们想要的类的类名
* @return
*/
private ArrayList<String> getGenClassList(Context context){
ArrayList<String> strList = new ArrayList<>();
String className = "com.zxl.useapt.fun.GenClass";
try {
Class GenClass = context.getClassLoader().loadClass(className);
Method getClassList = GenClass.getDeclaredMethod("getClassList");
strList = (ArrayList<String>) getClassList.invoke(null);
} catch (Exception e) {
e.printStackTrace();
}
return strList;
}
}
在FunManager.java中,有一个"com.zxl.useapt.fun.GenClass",这个类在SelfProcessor.java中也出现过。没错,这个类就是我们使用注解器生成的代码。我们在编译apk之后,可以在build/generated/source/apt/debug/com/zxl/useapt/fun/ 目录下看到GenClass这个生成的类:
package com.zxl.useapt.fun;
import java.util.ArrayList;
public class GenClass {
public static ArrayList<String> getClassList(){
ArrayList<String> result = new ArrayList<>();
result.add("com.zxl.useapt.fun.AddFun");
result.add("com.zxl.useapt.fun.SubFun");
return result;
}
}
FunManager的主要逻辑是,找到GenClass这个类,并调用这个类的getClassList方法,得到所有的算法类的类名,并存放到一个HashMap中,当在其他地方调用FunManager中的fun方法时,从这个HashMap中获取具体的算法实例来进行运算。
3.8 当我们再新写一个MulFun.java
@NeedAdd
public class MulFun extends SupperFun {
@Override
public String canHandleMethod() {
return "mul";
}
@Override
public int fun(int a, int b){
return a*b;
}
}
现在,不用在其他地方添加任何代码,在某些条件达到时,就可以调用到MulFun这个类了。因为再编译apk后,GenClass变成了如下所示,多了一行代码,就是我们新写的类名:
package com.zxl.useapt.fun;
import java.util.ArrayList;
public class GenClass {
public static ArrayList<String> getClassList(){
ArrayList<String> result = new ArrayList<>();
result.add("com.zxl.useapt.fun.AddFun");
result.add("com.zxl.useapt.fun.MulFun");
result.add("com.zxl.useapt.fun.SubFun");
return result;
}
}
四、最后
有人会说,这样写有代码有什么实用价值?而且代码没有写的简单,反而有了更多的代码。
本文要解决的不是代码增多的问题,而是使用Java编译时注解解决问题的一种方式。还是之前说的,这种方式不只是用于解决这个问题,我们可以通过学习这种方式,多掌握一些技术点,当实际的项目中遇到问题时,多一种思路,想想ButterKnife和EventBus的由来以及要解决的问题。
那么除了使用这种方式,还有没有其他方式解决开头提到的问题:”
只需写实现类,而不在实现类以外的任何地方(包括配置文件)写任何代码,就能在程序执行时,调用到这个实现类
“。
有,还有不止一种,以后我们再讨论。