引言
有许多开源框架在编译时通过注解信息生成新的源文件,已到达简化样板代码的书写,比如说典型的Builder模式,或者实现框架的功能的桥接代码。因为我使用编译时注解只是想要简化样板代码,下面我就以Builder模式作为示例。
Builder模式
Builder模式主要是解决创建对象过程中的必须参数和非必须参数的的传入,以及以及传入过程中导致对象的中间状态暴露在外面。下面是简单Builder模式的实现:
#User.java
package Model.builder;
public class User{
//定义为final 的属性必需在构造函数中初始化
private final String id; //必需
private final String sex; //必需
private final String name; //必需
private String location; //非必需
private String email; //非必需
private User(String id,String sex,String name){
this.id= id ;
this.name =name ;
this.sex = sex;
}
private User(User origin){
this.id = origin.id;
this.sex = origin.sex;
this.name = origin .name;
this.location = origin.location;
this.email = origin.email;
}
public String getId() {
return id;
}
public String getSex() {
return sex;
}
public String getName() {
return name;
}
public String getLocation() {
return location;
}
public String getEmail() {
return email;
}
static class UserBuilder{
private User target;
public UserBuilder(String id ,String sex,String name){
target = new User(id, sex, name);
}
public UserBuilder location(String location){
target.location = location;
return this;
}
public UserBuilder email(String email){
target.email = email;
return this;
}
public User build(){
return new User(target);
}
}
}
如果我们要使用上面的builder创建行的User对象的话,像下面这样调用
User user = new User.UserBuilder(“123”,”man”,”sosky”).email(“1xxxxx@qq.com”).location(“xxxxxx”).build()
像这样链式的调用写起来思路很清晰(虽然这里并没有很复杂的逻辑,但是如果这里是多线程下网络通信和UI操作,这样写的话逻辑就非常容易理清了,可以参考一下Rxjava的链式API),并且避免了User对象中间状态暴露。
代码虽然好用,但是除了规定那些是必需的成员变量,那些是非必需的成员变量,其他的代码完全就是重复的样板代码。因此我们就可以采用编译时注解帮我们处理这些样板代码,只需要我们规定那些是必需的成员变量就行了;
注解的声明如下:
package Annotation;
//标记builder属性
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target({ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.CLASS)
public @interface toBuilder {
boolean essential();
}
package Annotation;
//标记builder类
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface BuilderClass {
}
我们像这样定义了一个注解,其中essential表示是否是必需的成员变量,如果是必需的话我们就需要在新的源文件中定义为final的变量,并在构造函数中初始化它;
注解解释器声明:
package Annotation;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.List;
import java.util.Set;
/**
* toBuilder注解解析器
*/
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes({"Annotation.toBuilder","Annotation.BuilderClass"})
public class toBuilderProcess extends AbstractProcessor{
private Filer filer;
private String className;
//essential为真的属性名
private List<String> fields_t;
//essential为假的属性名
private List<String> fields_f;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
filer = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//标记的类
Set<? extends Element> classAnnotated = roundEnv.getElementsAnnotatedWith(BuilderClass.class);
for (Element classelement :classAnnotated){
className = classelement.getSimpleName().toString();
}
//标记的成员变量
Set<? extends Element> fieldAnnotated = roundEnv.getElementsAnnotatedWith(toBuilder.class);
for (Element field: fieldAnnotated){
if(field.getAnnotation(toBuilder.class).essential()) {
fields_t.add(field.getSimpleName().toString());
}else{
fields_f.add(field.getSimpleName().toString());
}
}
//创建源文件
createFile();
return true;
}
private void createFile() {
StringBuilder cls = new StringBuilder();
cls.append("package Annotation;\n\npublic class ")
.append(className)
。。。。。。。
源文件内容,根据之前注解信息拼接字符串;
try {
JavaFileObject sourceFile = filer.createSourceFile("Annotation." + className);
Writer writer = sourceFile.openWriter();
writer.write(cls.toString());
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
只有我们就可以通过javac使用编译后的注解解析器了:
javac -encoding UTF-8 -cp {注解器和注解的包路径,或者class路径} -processor {包名.注解器名} -d out\production -s src\ src\Annotation*.java
在这里注意一下 javac -s标志的含义,是指定源文件位置,而我们在注解器中JavaFileObject sourceFile = filer.createSourceFile(“Annotation.” + className); sourceFile对象的Writer的输出位置就是其指定的路径,如果没有- s标志的话,默认会生成的源文件会生成到-d 指定的路径下。我写的时候遇到了源文件生成位置不对的坑,就是这个原因。
结语
当然现在有很多IDE插件能帮我们自动生成Builder代码,这只是一个例子,有很多其他样板代码,或者因为小小的不同,需要重复写的业务逻辑代码,都可以用这个方法简化。更重要的假如我们的框架能够通过不同注解配置其功能,这样的高度自定义的框架使用编译时注解是最为合适的了。