在神奇的说明——Java Annotation(注解)中介绍了如何在运行时透过反射的方式获取注解信息。
那么编译时如何获取注解信息呢?
其实,编译时 Annotation 指 @Retention
为 CLASS 的 Annotation,是由 APT(Annotation Processing Tool) 自动解析的。APT在编译时根据resources资源文件夹下的META-INF/services/javax.annotation.processing.Processor
自动查找所有继承自 AbstractProcessor 的类,然后调用他们的 process 方法去处理。
核心步骤如下:
- 新建两个注解
PrintMe
和PrintMe2
package panda.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.SOURCE)
public @interface PrintMe {
}
package panda.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.SOURCE)
public @interface PrintMe2 {
}
- 新建编译时处理类MyProcessor
package panda.annotation;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
@SupportedAnnotationTypes({"panda.annotation.PrintMe","panda.annotation.PrintMe2"})
public class MyProcessor extends AbstractProcessor {
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
Messager messager = processingEnv.getMessager();
for (TypeElement te : annotations) {
messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + te);
if (te instanceof PrintMe){
for (Element e : env.getElementsAnnotatedWith(te)) {
messager.printMessage(Diagnostic.Kind.NOTE, "\t**Printing: " + e.toString());
}
}
else{
for (Element e : env.getElementsAnnotatedWith(te)) {
messager.printMessage(Diagnostic.Kind.NOTE, "\t--Printing: " + e.toString());
}
}
}
return true;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
- 为了这个处理器在编译时被调用,需要在META-INF中显示标识。在resources资源文件夹下新建META-INF/services/
javax.annotation.processing.Processor,把MyProcessor放在里面
panda.annotation.MyProcessor
- 新建测试类,进行注解测试
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import panda.annotation.PrintMe;
import panda.annotation.PrintMe2;
public class MainActivity extends AppCompatActivity {
@PrintMe
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@PrintMe
@Override
protected void onStart() {
super.onStart();
}
@PrintMe
@Override
protected void onResume() {
super.onResume();
}
@PrintMe2
@Override
protected void onPause() {
super.onPause();
}
@PrintMe2
@Override
protected void onStop() {
super.onStop();
}
@PrintMe2
@Override
protected void onDestroy() {
super.onDestroy();
}
}
编译后,输入的编译日志为
注: Printing: panda.annotation.PrintMe
注: --Printing: onCreate(android.os.Bundle)
注: --Printing: onStart()
注: --Printing: onResume()
注: Printing: panda.annotation.PrintMe2
注: --Printing: onPause()
注: --Printing: onStop()
注: --Printing: onDestroy()
解析JsonAnnotation
JsonAnnotation是一个典型的编译时注解库。
package kale.net.json.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Jack Tony
* @date 2015/8/13
*/
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Json2Model {
String modelName();
String jsonStr();
// custom the model's package name
String packageName() default "";
}
其处理流程为:
- 根据注解,获得
包名
、类名
和json字符串
。 - 把
类名
和json字符串
送入 jsonformat 包,获得对应的Model类字符串
。 - 根据
包名
、类名
和Model类字符串
,写入java文件。
核心代码如下:
package kale.net.json.processor;
import com.jsonformat.JsonParserHelper;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import kale.net.json.annotation.Json2Model;
/**
* @author Jack Tony
* @date 2015/8/13
*/
@SupportedAnnotationTypes({"kale.net.json.annotation.Json2Model"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class Json2ModelProcessor extends AbstractProcessor {
private static final String TAG = "[ " + Json2Model.class.getSimpleName() + " ]:";
private Elements elementUtils;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
}
private String packageName;
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement te : annotations) {
for (Element e : roundEnv.getElementsAnnotatedWith(te)) {
log("Working on: " + e.toString());
VariableElement varE = (VariableElement) e;
Json2Model json2Model = e.getAnnotation(Json2Model.class);
if (json2Model.packageName().equals("")) {
// no custom package name
/**
* example:
* String GET_USER_INFO = "create/info/user/info";
* result:create/info/user/info
*/
if (varE.getConstantValue() == null) {
fatalError("jsonStr couldn't be final");
}
String url = varE.getConstantValue().toString();
packageName = url2packageName(url);
} else {
// has custom package name
packageName = json2Model.packageName();
}
if (json2Model.jsonStr() == null || json2Model.jsonStr().equals("")) {
fatalError("json string is null");
}
final String clsName = json2Model.modelName();
JsonParserHelper helper = new JsonParserHelper();
helper.parse(json2Model.jsonStr(), clsName, new JsonParserHelper.ParseListener() {
public void onParseComplete(String str) {
createModelClass(packageName, clsName, "package " + packageName + ";\n" + str);
}
public void onParseError(Exception e) {
e.printStackTrace();
fatalError(e.getMessage());
}
});
log("Complete on: " + e.toString());
}
}
return true;
}
private void createModelClass(String packageName, String clsName, String content) {
//PackageElement pkgElement = elementUtils.getPackageElement("");
TypeElement pkgElement = elementUtils.getTypeElement(packageName);
OutputStreamWriter osw = null;
try {
// create a model file
JavaFileObject fileObject = processingEnv.getFiler().createSourceFile(packageName + "." + clsName, pkgElement);
OutputStream os = fileObject.openOutputStream();
osw = new OutputStreamWriter(os, Charset.forName("UTF-8"));
osw.write(content, 0, content.length());
} catch (IOException e) {
e.printStackTrace();
fatalError(e.getMessage());
} finally {
try {
if (osw != null) {
osw.flush();
osw.close();
}
} catch (IOException e) {
e.printStackTrace();
fatalError(e.getMessage());
}
}
}
private void log(String msg) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, TAG + msg);
}
private void fatalError(String msg) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, TAG + " FATAL ERROR: " + msg);
}
/**
* /user/test/ - > user.test
*/
public static String url2packageName(String url) {
String packageName = url.replaceAll("/", ".");
if (packageName.startsWith(".")) {
packageName = packageName.substring(1);
}
if (packageName.substring(packageName.length() - 1).equals(".")) {
packageName = packageName.substring(0, packageName.length() - 1);
}
return packageName;
}
}
思考
编译时注解可以将一些固定的代码隐藏起来,只保留核心逻辑,最典型的应用是butterknife
透过注解BindView
把冗余常见的findViewById
替换掉了,最后的代码结构简单清晰,便于理解。
class ExampleActivity extends Activity {
@BindView(R.id.title) TextView title;
@BindView(R.id.subtitle) TextView subtitle;
@BindView(R.id.footer) TextView footer;
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}
而json字符串放在java里面,并不是一个可读性很强的结构,以后字段增减看起来也不直观。
String simpleStr = "{\n"
+ " \"id\": 100,\n"
+ " \"body\": \"It is my post\",\n"
+ " \"number\": 0.13,\n"
+ " \"created_at\": \"2014-05-22 19:12:38\"\n"
+ "}";
// 简单格式
@Json2Model(modelName = "Simple", jsonStr = simpleStr)
String TEST_SIMPLE = "test/simple"; // api url
所以这时候采用编译时注解并不太合适。还是提取一个jar包,批量将json转成model,以后的增减由GsonFormat单个操作吧。
参考
Panda
2016-07-13