java註解之編譯時註解RetentionPolicy.CLASS 基本用法
1 前言
我們知道,在日常開發中我們常用的兩種註解是執行時註解和編譯時註解,執行時註解是通過反射來實現註解處理器的,對效能稍微有一點損耗,而編譯時註解是在程式編譯期間生成相應的代理類,替我們完成某些功能。今天我們來講解一下編譯時註解以及寫一個小例子,以便加深對編譯時註解的理解。
2 編譯時註解
編譯時註解(RetentionPolicy.CLASS),指@Retention(RetentionPolicy.CLASS)作用域class位元組碼上,生命週期只有在編譯器間有效。編譯時註解註解處理器的實現主要依賴於AbstractProcessor來實現,這個類是在javax.annotation.processing包中,同時為了我們自己生成java原始檔方便,我們還需要引入一些第三方庫,主要包括
javapoet 用於生成java原始檔,可參考
auto-service 主要用於生成一些輔助資訊,例如META-INF/services 一些資訊等
編譯時註解的核心就是實現AbstractProcessor的process()方法,一般來說主要有以下兩個步驟
1 蒐集資訊,包括被註解的類的類資訊,方法,欄位等資訊,還有註解的值
2 生成對應的java原始碼,主要根據上一步的資訊,生成響應的程式碼
下面我們來實際的寫一個編譯時註解的例子
3 編譯時註解例子
假設我們寫這樣一個類註解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Bind {
int value();
}
這個Bind註解只能作用與類上面,一般我們作用於某個類上面了,我們做什麼工作呢,我們新建一個類,這個類的內容大體如下:
public class XXX$$value {
public int sayHello(int n) {
return value;
}
}
其中,XXX是被註解的類名,value是註解的值。
看起來很簡單,對吧?下面我們就開始吧。
1 新建兩個java model,注意不是Android library model
model的gradle配置如下
apply plugin: 'java'
為什麼要新建兩個module呢,
(1) 是因為後面再引用的時候,兩者有些不一樣,可以看一下
api project(':ioc-annotation')
annotationProcessor project(':ioc-compiler')
(2) 因為兩者的引用方式不同,導致最好把註解處理器和註解分開,這樣我們也能更好的解耦
注意,ioc-compiler需要引用以下兩個開源庫
apply plugin: 'java'
//解決編譯中文亂碼問題
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.google.auto.service:auto-service:1.0-rc4'
compile 'com.squareup:javapoet:1.7.0'
compile project(':ioc-annotation')
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
2 新建Bind註解,指定註解型別為class
/**
* @author Created by qiyei2015 on 2018/4/14.
* @version: 1.0
* @email: [email protected]
* @description: Bind註解 指明註解作用域為類
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Bind {
int value();
}
這一步挺好理解的。
3 新建BindProcessor繼承與AbstractProcessor並實現其process方法
/**
* @author Created by qiyei2015 on 2018/4/15.
* @version: 1.0
* @email: [email protected]
* @description: Bind註解處理器
*/
@AutoService(Processor.class)
public class BindProcessor extends AbstractProcessor{
/**
* java原始檔操作相關類,主要用於生成java原始檔
*/
private Filer mFiler;
/**
* 註解型別工具類,主要用於後續生成java原始檔使用
* 類為TypeElement,變數為VariableElement,方法為ExecuteableElement
*/
private Elements mElementsUtils;
/**
* 日誌列印,類似於log,可用於輸出錯誤資訊
*/
private Messager mMessager;
private static final ClassName sClassName = ClassName.get("com.qiyei.ioc.api", "Test");
/**
* 初始化,主要用於初始化各個變數
* @param processingEnv
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mElementsUtils = processingEnv.getElementUtils();
mMessager = processingEnv.getMessager();
}
/**
* 支援的註解型別
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> typeSet = new LinkedHashSet<>();
typeSet.add(Bind.class.getCanonicalName());
return typeSet;
}
/**
* 支援的版本
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
*
* 1.蒐集資訊
* 2.生成java原始檔
* @param annotations
* @param roundEnv
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!annotations.isEmpty()){
//獲取Bind註解型別的元素,這裡是類型別TypeElement
Set<? extends Element> bindElement = roundEnv.getElementsAnnotatedWith(Bind.class);
try {
generateCode(bindElement);
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
return false;
}
/**
*
* @param elements
*/
private void generateCode(Set<? extends Element> elements) throws IOException{
for (Element element : elements){
//由於是在類上註解,那麼獲取TypeElement
TypeElement typeElement = (TypeElement) element;
//獲取全限定類名
String className = typeElement.getQualifiedName().toString();
mMessager.printMessage(Diagnostic.Kind.WARNING,"className:" + className);
//獲取包路徑
PackageElement packageElement = mElementsUtils.getPackageOf(typeElement);
String packageName = packageElement.getQualifiedName().toString();
mMessager.printMessage(Diagnostic.Kind.WARNING,"packageName:" + packageName);
//獲取用於生成的類名
className = getClassName(typeElement,packageName);
//獲取註解值
Bind bindAnnotation = typeElement.getAnnotation(Bind.class);
int value = bindAnnotation.value();
System.out.println("value:" + value);
//生成方法
MethodSpec.Builder methodBuilder = MethodSpec
.methodBuilder("sayHello")
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeName.INT,"n")
.returns(TypeName.INT);
//$L表示字面量 $T表示型別
methodBuilder.addStatement("return $L",value);
//生成的類
TypeSpec type = TypeSpec
.classBuilder(className + "$$" + value)
.addModifiers(Modifier.PUBLIC)
.addMethod(methodBuilder.build())
.build();
//建立javaFile檔案物件
JavaFile javaFile = JavaFile.builder(packageName,type).build();
//寫入原始檔
javaFile.writeTo(mFiler);
}
}
/**
* 根據type和package獲取類名
* @param type
* @param packageName
* @return
*/
private static String getClassName(TypeElement type, String packageName) {
int packageLen = packageName.length() + 1;
return type.getQualifiedName().toString().substring(packageLen)
.replace('.', '$');
}
}
其他方法暫時不做講解,邏輯都很簡單,主要講解generateCode(Set
//獲取全限定類名
String className = typeElement.getQualifiedName().toString();
//獲取包路徑
PackageElement packageElement = mElementsUtils.getPackageOf(typeElement);
String packageName = packageElement.getQualifiedName().toString();
(2) 構造如上所示的java原始檔
//生成方法
MethodSpec.Builder methodBuilder = MethodSpec
.methodBuilder("sayHello")
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeName.INT,"n")
.returns(TypeName.INT);
//$L表示字面量 $T表示型別
methodBuilder.addStatement("return $L",value);
//生成的類
TypeSpec type = TypeSpec
.classBuilder(className + "$$" + value)
.addModifiers(Modifier.PUBLIC)
.addMethod(methodBuilder.build())
.build();
這裡主要是用到javapoet,關於為什麼用這個開源庫,首先是用法比較簡單,其次很多開源框架都在用,包括ButterKnife等。關於生成java程式碼請一定參考https://github.com/square/javapoet 裡面有很詳細的說明。
(3) 將java原始碼寫入檔案
//建立javaFile檔案物件
JavaFile javaFile = JavaFile.builder(packageName,type).build();
//寫入原始檔
javaFile.writeTo(mFiler);
(4) 使用Bind註解,然後編譯生成程式碼
我們在我們的一個Activity上使用Bind註解,如下:
/**
* @author Created by qiyei2015 on 2017/8/28.
* @version: 1.0
* @email: [email protected]
* @description:
*/
@Bind(10)
public class MainActivity extends BaseSkinActivity {
private RecyclerView mRecyclerView;
private static final int MY_PERMISSIONS_REQUEST_WRITE_STORE = 1;
/**
* ViewModel
*/
private MainMenuViewModel mMenuViewModel;
private MainMenuAdapter mMenuAdapter;
/**
* 標題欄
*/
private CommonTitleBar mTitleBar = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// PermissionManager.requestAllDangerousPermission(this);
initData();
initView();
LogManager.i(TAG,"onCreate");
}
.......
}
然後編譯,生成的程式碼如下:
程式碼路徑:\build\generated\source\apt\baidu\debug\com\qiyei\appdemo\ui\activity
package com.qiyei.appdemo.ui.activity;
public class MainActivity$$10 {
public int sayHello(int n) {
return 10;
}
}
可以看到,符合我們的預期,這樣我們一個簡單的編譯時註解就已經完成了
注:雖然我們的程式碼如約生成了,但是我們並沒有用生成的程式碼,所以我們還應該寫一個api來使用生成的java原始碼檔案,不過這都是後話了,下次再介紹。