利用Annotation Processing生成Hibernate工具

Jun 26, 2016


开发原因

在使用Hibernate时候,经常会写这样的代码:

@Override
public List getTemplateList(long id) {
    DetachedCriteria criteria = DetachedCriteria.forClass(Template.class);
    criteria.add(Restrictions.eq("id", id));
    criteria.addOrder(Order.desc("create_time"));
    return findByDetachedCriteria(criteria);
}

在设置条件的时候,不得不使用hibernate 实体类中对应的字段,这个时侯要么是跳到对应类或xml文件里复制粘贴,要么是自己手打,还容易出错。这个时侯就会想,如果能为实体类生成一些方法,以获取对应的实体类字段名,那就方便多了,达到如下这种效果:

@Override
public List getTemplateList(long id) {
    DetachedCriteria criteria = DetachedCriteria.forClass(Template.class);
    criteria.add(Restrictions.eq(TemplateTool.getId(), id));
    criteria.addOrder(Order.desc(TemplateTool.getCreate_Time());
    return findByDetachedCriteria(criteria);
}

这样是不是方便了很多,在IDE的帮助下,能有效提高写代码的效率。

开发组件

Intellij

开发思路

根据开发原因,很容联想到一个开源工具LomBok,它能够自动生成Getter/SettertoStringhashCode等方法,在intellij上配合lombok的插件,能够实时调出这些方法。于是我尝试在Lombok上去拓展,但发现Lombok并不能直接拓展,需要修改其源码并重新编译,这就没必要了。然后了解了一下Lombok的原理,是基于Annotation Processing来实现的,简单来说就是为Annotation注册一个Processor,在编译时动态生成代码,这算是实现工具的神器了。(这里当时认识有错误,Lombok实际是通过修改AST来实现的,并非Annotation Processing 2016-07-01)

于是我学习了一下这片文章 Annotation Processing,照着上面所诉的模式写了一个Hibernate工具。

实现细节

源码参见:Huang Huan Github Annotation Processing

注解

首先需要定义一个作用于source阶段的注解:

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface ORMTool {
}

注解Processor的实现

定义一个ORMToolProcessor来对应该注解:

@AutoService(Processor.class)
@SupportedAnnotationTypes("bj.huanghuan.annotation.ORMTool")
public class ORMToolProcessor extends AbstractProcessor {

    /**
     * 打印信息
     **/
    private Messager messager;

    /**
     * 创建文件工具
     **/
    private Filer filer;

    /**
     * element工具
     **/
    private Elements elementUtils;


    private static final String SUFFIX = "Tool";

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        filer = processingEnv.getFiler();
        elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, 
    RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(ORMTool.class)) {
            if (element.getKind() != ElementKind.CLASS) {
                printError(element, 
                "Only Classes can be annotated with @%s",
                 ORMTool.class.getSimpleName());
                return true;
            }
            TypeElement typeElement = (TypeElement) element;
            String className = typeElement.getSimpleName() + SUFFIX;
            try {
                JavaFileObject classFile = filer.createSourceFile(className);
                Writer writer = classFile.openWriter();
                JavaWriter javaWriter = new JavaWriter(writer);
                /*************** package Name *************/
                PackageElement packageElement = elementUtils.getPackageOf(typeElement);
                if (!packageElement.isUnnamed()) {
                    javaWriter.emitPackage(packageElement.getQualifiedName().toString());
                    javaWriter.emitEmptyLine();
                } else {
                    javaWriter.emitPackage("");
                }

                javaWriter.beginType(className, "class", EnumSet.of(Modifier.PUBLIC));
                javaWriter.emitEmptyLine();

                /************ generate getter for fields ************/
                List<VariableElement> insideElements =
                 ElementFilter.fieldsIn(typeElement.getEnclosedElements());
                for (VariableElement variableElement : insideElements) {
                    javaWriter.beginMethod("String",
                     variableElement.getSimpleName().toString(),
                     EnumSet.of(Modifier.PUBLIC, Modifier.STATIC),
                     null, null);
                    javaWriter.emitStatement("return \"" +
                     variableElement.getSimpleName().toString() + "\"");
                    javaWriter.endMethod();
                    javaWriter.emitEmptyLine();
                }

                javaWriter.endType();
                javaWriter.close();
            } catch (IOException e) {
                messager.printMessage(Diagnostic.Kind.ERROR, 
                "Gennerate Source Code Failed!");
            }
        }
        return false;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 错误日志打印
     *
     * @param element
     * @param msg
     * @param args
     */
    private void printError(Element element, String msg, Object... args) {
        messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), element);
    }
}

这部门代码里有一些需要解释的地方:

  • @AutoService是Google提供的注册一个工具,其实作用应该是省去后面我将要讲的配置文件,不过影响不大。
  • process(Set<? extends TypeElement> annotations, ...)是处理的入口,首先判断被ORMTool注解的元素是不是Class类型,然后利用JavaWriter这个代码生成工具来构建新的Java文件,为注解的类生成一个对应的tool类。该类包含了被注解类中所有Field的参数名Getter
  • 这里面的TypeElementVariableElement分别指的类类型和域类型。

配置文件

这里需要一个配置文件,文件全路径是/META-INF/services/javax.annotation.processing.Processor,配置文件中写入ORMToolProcessor中的路径,如bj.huanghuan.processor.ORMToolProcessor。实际上又了上面的@AutoService其实应该不需要手写这个文件了,不过我没试过,读者有兴趣可以试试。

截图:

文件结构

生成Maven包

最后需要生成一个Maven包,加入到其它项目中供调用。

测试

测试用的Test类:

@ORMTool
public class Test {

	private int id;

	private String name;

	private Boolean isNew;
}

生成的TestTool类:

public class TestTool {
	public TestTool() {
	}

	public static String id() {
    	return "id";
	}

	public static String name() {
    	return "name";
	}

	public static String isNew() {
    	return "isNew";
	}
}

总结

这种方式的工具需要在使用前编译一下代码,而且不能在原始代码上进行修改,也算是点小遗憾了。 PS: intellij上有个叫GeneratePropertyNameConstants的插件,用这个也可以,只不过麻烦些


上一篇博客:什么是数据库事务?
下一篇博客:Redux开发初探