如何用apt(annotationprocessingtool)自动生成代码(代码片段)

流子 流子     2022-12-01     621

关键词:

我们很多人都写过代码自动生成的工具,比如用python结合moko模板引擎,或者java 结合freemarker模板引擎,protoc 等,实现解析策划数据类,proto协议类,或者数据库层的实体类。大大节省了我们开发的时间,让我们可以懒得光明正大。
那么,你有没有办法当你写好几个协议后,只要一保存编译,相关的协议接收和发送类的接口就自动实现了呢?答案是有的。

组件化开发
注解处理器是(Annotation Processor)是javac的一个工具,用来在编译时扫描和编译和处理注解(Annotation)。你可以自己定义注解和注解处理器去搞一些事情。一个注解处理器它以Java代码或者(编译过的字节码)作为输入,生成文件(通常是java文件)。这些生成的java文件不能修改,并且会同其手动编写的java代码一样会被javac编译。看到这里加上之前理解,应该明白大概的过程了,就是把标记了注解的类,变量等作为输入内容,经过注解处理器处理,生成想要生成的java代码。Annotation Process的实质用处就是在编译时通过注解获取相关数据

什么是APT?
APT(Annotation Processing Tool)是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件,将它们一起生成class文件。

理解了以上的理论,我们来实践一下:

需求:
通过实现协议类,让客户端请求工具和服务端接收处Controller自动添加代码。
1.首先添加依赖包

    // annotation
    implementation "com.google.auto.service:auto-service:1.0.1"

2.添加annotation类:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;


/**
 * http proto注解,会被@link ProtoServiceProcessor处理
 *
 * @author Allen Jiang
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)

public @interface HttpProto 


添加Processor处理类:


/**
 * 用于自动生成admin和业务进程通讯的底层代码
 *
 * @author Allen Jiang
 */
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.gamioo.http.annotation.HttpProto")
public class ProtoServiceProcessor extends AbstractProcessor 
    public static final String C_2_S_MSG = "_C2S_Msg";
    public static final String S_2_C_MSG = "_S2C_Msg";
    public static final String HTTP_PROCESSOR = "IHttpProcessor";
    public static final String GAME_CLIENT = "GameClient";


    private Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) 
        super.init(processingEnvironment);
        messager = processingEnvironment.getMessager();
    

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 
        if (roundEnv.processingOver()) 
            return false;
        

        for (TypeElement annotation : annotations) 
            messager.printMessage(Diagnostic.Kind.NOTE, String.format("process annotation start: %s", annotation));
            try 
                List<HttpProtoDTO> list = this.getMethodList(annotation, roundEnv);
                this.buildClass(list, HTTP_PROCESSOR);
                this.buildClass(list, GAME_CLIENT);
             catch (Exception e) 
                messager.printMessage(Diagnostic.Kind.ERROR, ExceptionUtils.getStackTrace(e));
            
            messager.printMessage(Diagnostic.Kind.NOTE, String.format("process annotation end: %s", annotation));
            return false;
        

        return false;
    


    /**
     * 构建模板
     */
    private void buildClass(List<HttpProtoDTO> list, String className) throws IOException, TemplateException 
        messager.printMessage(Diagnostic.Kind.NOTE, String.format("build class start: %s", className));
        StatDTO<HttpProtoDTO> dto = new StatDTO<>();
        dto.setArray(list);
        String content = ViewTemplateUtils.getContentFromJar(className + ".ftl", dto);
        //  messager.printMessage(Diagnostic.Kind.NOTE, String.format("content: %s", content));
        FileObject fileObject = processingEnv.getFiler().createSourceFile(className);
        try (PrintWriter writer = new PrintWriter(fileObject.openWriter())) 
            writer.write(content);
            writer.flush();
            messager.printMessage(Diagnostic.Kind.NOTE, String.format("build class end: %s", className));
         catch (IOException e) 
            throw e;
        
    

    /**
     * 获取协议列表
     */
    private List<HttpProtoDTO> getMethodList(TypeElement annotation, RoundEnvironment roundEnv) throws IOException 
        List<HttpProtoDTO> ret = new ArrayList<>();
        FileObject fileObject = processingEnv.getFiler().getResource(StandardLocation.SOURCE_OUTPUT, "", HTTP_PROCESSOR + ".java");
        Map<String, Integer> store = new HashMap<>();
        try 
            CharSequence content = fileObject.getCharContent(false);
            String[] list = StringUtils.substringsBetween(content.toString(), "(", C_2_S_MSG);

            for (String e : list) 
                store.put(StringUtils.uncapitalize(e), 2);
            
         catch (Exception e) 
            messager.printMessage(Diagnostic.Kind.NOTE, e.getMessage() + ",ignore it");
        

        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(annotation);
        for (Element element : elements) 
            //   messager.printMessage(Diagnostic.Kind.NOTE, String.format("element: %s", element.getSimpleName().toString()));
            String method = StringUtils.uncapitalize(StringUtils.substringBefore(element.getSimpleName().toString(), "_"));
            Integer num = store.get(method);
            if (num == null) 
                num = 1;
             else 
                num += 1;
            
            store.put(method, num);
        
        messager.printMessage(Diagnostic.Kind.NOTE, String.format("element: %s", JSONUtils.toJSONString(store)));
        store.forEach((method, value) -> 
            String prefix = StringUtils.capitalize(method);
            String c2s = prefix + C_2_S_MSG;
            String s2c = prefix + S_2_C_MSG;
            HttpProtoDTO protoDTO = new HttpProtoDTO(c2s, s2c, method);
            ret.add(protoDTO);
        );
        messager.printMessage(Diagnostic.Kind.NOTE, String.format("message number: %s", ret.size()));
        // fileObject.delete();
        return ret;
    


辅助类:

public class HttpProtoDTO 
    private String c2s;
    private String s2c;
    private String method;

    public HttpProtoDTO() 

    

    public HttpProtoDTO(String c2s, String s2c, String method) 
        this.c2s = c2s;
        this.s2c = s2c;
        this.method = method;
    

    public String getC2s() 
        return c2s;
    

    public void setC2s(String c2s) 
        this.c2s = c2s;
    

    public String getS2c() 
        return s2c;
    

    public void setS2c(String s2c) 
        this.s2c = s2c;
    

    public String getMethod() 
        return method;
    

    public void setMethod(String method) 
        this.method = method;
    

    @Override
    public String toString() 
        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    

以上类所在的模块我们要编译出一个jar.

依赖的项目dependencies中添加

  annotationProcessor project(':gamioo-core')

接下去我们说如何使用。
首先,定义协议:

import com.gamioo.http.annotation.HttpProto;

/**
 * 执行脚本命令.
 */
@HttpProto
public class ExecScript_C2S_Msg 

    private String script;

    public String getScript() 
        return script;
    

    public void setScript(String script) 
        this.script = script;
    

/**
 * @author Allen Jiang
 */
@HttpProto
public class ExecScript_S2C_Msg 
    private String result;
    private long interval;

    public long getInterval() 
        return interval;
    

    public void setInterval(long interval) 
        this.interval = interval;
    

    public String getResult() 
        return result;
    

    public void setResult(String result) 
        this.result = result;
    


然后在com.gamioo.common.http.proto包执行一下编译,就自动生成了com.gamioo.admin.network.GameClient和com.gamioo.common.http.IHttpProcessor的代码
client 端:
com.gamioo.admin.network.GameClient
admin:

public ExecScript_S2C_Msg execScript(ExecScript_C2S_Msg msg) 
    ExecScript_S2C_Msg ret = null;
    String content = this.sendWithReturn(msg);
    if (content != null) 
        ret = JSONUtils.parseObject(content, ExecScript_S2C_Msg.class);
    
    return ret;

server端:

com.yorha.common.http.IHttpProcessor
ExecScript_S2C_Msg execScript(ExecScript_C2S_Msg msg);

以上两个文件都会生成在generated目录下,并且不支持文件修改,这样就完成的代码自动生成,后续就可以进行协议的具体逻辑实现和调用测试。

Q1: 自定义jsr-269注解处理器 Error:服务配置文件不正确,或构造处理程序对象javax.annotation.processing.Processor: Provider not found
出现的原因
自定义处理器还没有被编译就被调用,所以报 not found
在根据配置寻找自定义的注解处理器时,自定义处理器还未被编译
解决方式
maven项目可以配置编译插件,在编译项目之前先编译处理器,或者编译项目时跳过执行处理器
参考:https://stackoverflow.com/questions/38926255/maven-annotation-processing-processor-not-found

Q2: gradle项目可以将自定义处理器分离出去,单独作为一个项目,并且打包成jar包,将这个项目build后作为依赖使用
例如:

dependencies 
 	compile project(':anno-project-core')
 	annotationProcessor project(':anno-project-core')

Q3: 如何进到Processor调试?
需要用gradle的assemble功能,然后选择调试。

参考资料如下:
注解入坑笔记:关于注解使用必须了解的——Annotation、AbstraceProcessor、APT
IDEA+Gradle使用Annotation Processor
Annotation Tool(注解工具-IntelliJ IDEA 插件)

如何用ubuntu创建数据库

参考技术A打开命令终端输入apt-getinstallmysql回车安装完成后在界面输入mysql-uroot-p回车然后在提示下输入密码出现wekcome。。。即为登录成功.showdatabases;查看数据库列表use数据库名使用该数据库以便以后对其中的表进行操作也可以... 查看详情

如何用python写一个ubuntu的换源程序(用到os.system)

参考技术A换镜像源?参考这个:  sed -i "s!http://archive.ubuntu.com!https://mirrors.163.com!g" /etc/apt/sources.list  sed -i "s!http://security.ubuntu.com!https://mirrors.163.com!g" /etc/apt/sources.list和这个:py... 查看详情

googleplay支付如何用php验证订单完成的合法性

参考技术A进入php源程序目录中的ext目录中,这里存放着各个扩展模块的源代码,选择你需要的模块,比如curl模块:cdcurl执行phpize生成编译文件,phpize在PHP安装目录的bin目录下/usr/local/php5/bin/phpize运行时,可能会报错:Cannotfindauto... 查看详情

如何用阿里云ecs服务器搭建自己的个人网站

参考技术A1.购买云服务器ecs购买地址请点击云翼计划一定要是学生哦,学信网有注册的才有效。购买流程在此去就不做介绍了,选择9.9元的就行,有钱就任性点买贵点的。选择任何操作系统都行,本人用习惯ubuntu就选了ubuntu。我... 查看详情

java示例代码_APT如何处理嵌套注释类的注释

java示例代码_APT如何处理嵌套注释类的注释 查看详情

keil中如何用keil中如何用汇编调用c函数?

如图所示,我在汇编中用IMPORT声明,调用C函数,但是编译提示错误,请问是什么原因?检查C文件是否加入项目。检查C文件中是否有这两个函数的原型声明。参考技术A关注这个问题 查看详情

如何用excel做散点图(如何用excel做柱状图)

参考技术A您好,现在我来为大家解答以上的问题。如何用excel做散点图,如何用excel做柱状图相信很多小伙伴还不知道,现在让我们一起来看看吧!1、exc...您好,现在我来为大家解答以上的问题。如何用excel做散点图,如何用excel做... 查看详情

如何用手机查看ip地址

...A  如何查看手机IP地址和MAC地址呢?下面是我整理的如何用手机查看ip地址的两个方法,欢迎大家阅读!  如何用手机查看ip地址  方法1:进入【设置】—【WLAN】—【高级设置】查看。  方法2:进入【设置】—【关于... 查看详情

如何用端口访问网站

1、准备一台windows2008R2操作系统的虚拟机环境2、3、 查看详情

如何用黑色填充灰色空间? SwiftUI

】如何用黑色填充灰色空间?SwiftUI【英文标题】:Howtofillgrayspacewithblack?SwiftUI【发布时间】:2021-10-2814:08:42【问题描述】:如何用黑色绘制一个完全灰色的空间?看图片这是我的代码:.onTapGestureself.showModal=true.sheet(isPresented:self.$... 查看详情

脑筋急转弯:如何用两个栈实现一个队列&&如何用两个队列实现一个栈(代码片段)

文章目录前言我的想法如何用两个栈实现一个队列如何用两个队列实现一个栈别人的想法两个栈实现一个队列如何用两个队列实现一个栈前言其实我也不知道这两个问题又什么意义,就像不知道我们平时刷的算法题除了练脑... 查看详情

如何用电脑发布视频号?

用电脑发布视频号的方法有以下几步: 查看详情

如何用动态值编写场景大纲?

】如何用动态值编写场景大纲?【英文标题】:HowtowriteScenarioOutlineswithdinamicvalues​?【发布时间】:2021-09-0115:45:49【问题描述】:enterimagedescriptionhere例如:ScenarioOutline:GiventheuserisloggedintoHomeOperationsWhensearchingfor"<status>"Thenthe" 查看详情

如何用零填充数组?

】如何用零填充数组?【英文标题】:Howtopadanarraywithzeros?【发布时间】:2021-11-2509:42:57【问题描述】:fnmain()letarr:[u8;8]=[97,112,112,108,101];println!("Lenis",arr.len());println!("Elementsare:?",arr);error[E0308]:mismatchedtypes-->src/main.rs 查看详情

如何用护照开会?

】如何用护照开会?【英文标题】:howtoholdasessionwithpassport?【发布时间】:2018-08-2721:22:12【问题描述】:所以,我希望我的用户会话在登录/注册时持续存在,但事实并非如此。官方文档说要添加这个开头:app.use(express.session(secre... 查看详情

如何用java写一个小数

】如何用java写一个小数【英文标题】:Howtowriteasmallnumberusingjava【发布时间】:2015-11-1501:12:49【问题描述】:如何写一个非常小的数,接近极限0?像这样:doubleeps=0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000... 查看详情

你如何用 Javascript 编写一个类? [复制]

】你如何用Javascript编写一个类?[复制]【英文标题】:HowdoyouwriteaclassinJavascript?[duplicate]【发布时间】:2010-10-2923:37:08【问题描述】:可能重复:What\'sthebestwaytodefineaclassinjavascript你如何用Javascript编写一个类?有没有可能?【问题... 查看详情

如何用androidstudio查看jar包源码

参考技术A您好,请问您是想知道如何用androidstudio查看jar包源码吗? 查看详情