Java中的屠龙之术(二):如何方便快捷地生成.class文件
在之前的“Java中的屠龙之术:如何修改语法树”中,我们详细介绍了如何使用Javac源码提供的工具类来修改语法树。
而在此基础上,有一款开源工具javapoet可以更加快捷地生成字节码,实现原理其实也就是对JavaAPT的封装,然而Javapoet有一个局限性,就是只能生成新的.class文件,却无法修改原有的类,这也是它的一大局限性所在。接下来就让我们看看它的使用方法把。
0x00 概述
注解系列
- 注解基础
- JavaPoet
- 编译期注解处理之APT
上一篇限于篇幅只介绍了APT,这篇来继续介绍javapoet,是square公司的开源库。正如其名,java诗人,通过注解来生成java源文件,通常要使用javapoet这个库与Filer配合使用。主要和注解配合用来干掉那些重复的模板代码(如butterknife 和databinding所做的事情),当然你也可以使用这个技术让你的代码更加的炫酷。
0x01 简单使用
使用之前要先引入这个库
compile 'com.squareup:javapoet:1.7.0'
复制代码
javapoet是用来生成代码的,需要借助
常用类
使用javapoet前需要了解8个常用类
类名 | 作用 |
---|---|
MethodSpec | 代表一个构造函数或方法声明 |
TypeSpec | 代表一个类,接口,或者枚举声明 |
FieldSpec | 代表一个成员变量,一个字段声明 |
JavaFile | 包含一个顶级类的Java文件 |
ParameterSpec | 用来创建参数 |
AnnotationSpec | 用来创建注解 |
ClassName | 用来包装一个类 |
TypeName | 类型,如在添加返回值类型是使用 TypeName.VOID |
除此之外 JavaPoet提供了一套自定义的字符串格式化规则,常用的有
格式化规则 | 表示含义 |
---|---|
$L | 字面量 |
$S | 字符串 |
$T | 类、接口 |
$N | 变量 |
0x02 使用进阶
下面由浅入深,循序渐进的说明用法
方法&控制流:
- 添加方法
addcode
和addstatement
对与无需类引入的极简代码可以直接使用addCode
MethodSpec main = MethodSpec.methodBuilder("main").addCode(""+ "int total = 0;\n"+ "for (int i = 0; i < 10; i++) {\n"+ " total += i;\n"+ "}\n").build();
复制代码
生成的是
void main() {int total = 0;for (int i = 0; i < 10; i++) {total += i;}
}
复制代码
要是需要import的方法,如上面的.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
就需要使用.addStatement
来声明
- 更优雅的流控制
beginControlFlow
流开启 addStatement
处理语句 endControlFlow()
流结束
如上面的用流改写就是
MethodSpec main = MethodSpec.methodBuilder("main").addStatement("int total = 0").beginControlFlow("for (int i = 0; i < 10; i++)").addStatement("total += i").endControlFlow().build();
复制代码
占位符
javapoet里面提供了占位符来帮助我们更好地生成代码
- $L 字面常量(Literals)
private MethodSpec computeRange(String name, int from, int to, String op) {return MethodSpec.methodBuilder(name).returns(int.class).addStatement("int result = 0").beginControlFlow("for (int i = $L; i < $L; i++)", from, to).addStatement("result = result $L i", op).endControlFlow().addStatement("return result").build();
}
复制代码
这个就是一个for循环,op负责加减乘除等符号
- $S 字符串常量(String)
- $T 类型(Types)
最大的特点是自动导入包
MethodSpec today = MethodSpec.methodBuilder("today").returns(Date.class).addStatement("return new $T()", Date.class).build();TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld").addModifiers(Modifier.PUBLIC, Modifier.FINAL).addMethod(today).build();JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld).build();javaFile.writeTo(System.out);复制代码
生成的代码如下,而且会自动导包
package com.example.helloworld;import java.util.Date;public final class HelloWorld {Date today() {return new Date();}
}
复制代码
- $N 命名(Names),通常指我们自己生成的方法名或者变量名等等
比如这样的代码块
public String byteToHex(int b) {char[] result = new char[2];result[0] = hexDigit((b >>> 4) & 0xf);result[1] = hexDigit(b & 0xf);return new String(result);
}public char hexDigit(int i) {return (char) (i < 10 ? i + '0' : i - 10 + 'a');
}
复制代码
我们可以传递hexDigit()
来代替。
MethodSpec hexDigit = MethodSpec.methodBuilder("hexDigit").addParameter(int.class, "i").returns(char.class).addStatement("return (char) (i < 10 ? i + '0' : i - 10 + 'a')").build();MethodSpec byteToHex = MethodSpec.methodBuilder("byteToHex").addParameter(int.class, "b").returns(String.class).addStatement("char[] result = new char[2]").addStatement("result[0] = $N((b >>> 4) & 0xf)", hexDigit).addStatement("result[1] = $N(b & 0xf)", hexDigit).addStatement("return new String(result)").build();
复制代码
获取对应类
有两种方式:
ClassName.bestGuess(“类全名称”) 返回ClassName对象,这里的类全名称表示的类必须要存在,会自动导入相应的包
ClassName.get(“包名”,”类名”) 返回ClassName对象,不检查该类是否存在
因此如果使用JavaPoet的话后续代码重构改变类名往往需要格外注意一点
ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");
ClassName list = ClassName.get("java.util", "List");
ClassName arrayList = ClassName.get("java.util", "ArrayList");
TypeName listOfHoverboards = ParameterizedTypeName.get(list, hoverboard);MethodSpec beyond = MethodSpec.methodBuilder("beyond").returns(listOfHoverboards).addStatement("$T result = new $T<>()", listOfHoverboards, arrayList).addStatement("result.add(new $T())", hoverboard).addStatement("result.add(new $T())", hoverboard).addStatement("result.add(new $T())", hoverboard).addStatement("return result").build();
复制代码
然后生成
package com.example.helloworld;import com.mattel.Hoverboard;
import java.util.ArrayList;
import java.util.List;public final class HelloWorld {List<Hoverboard> beyond() {List<Hoverboard> result = new ArrayList<>();result.add(new Hoverboard());result.add(new Hoverboard());result.add(new Hoverboard());return result;}
}
复制代码
构建类的元素
- 方法
方法的修饰,如Modifiers.ABSTRACT
MethodSpec flux = MethodSpec.methodBuilder("flux").addModifiers(Modifier.ABSTRACT, Modifier.PROTECTED).build();TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld").addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT).addMethod(flux).build();
复制代码
这将会生成如下代码
public abstract class HelloWorld {protected abstract void flux();
}复制代码
当然Methods需要和MethodSpec.Builder
配置来增加方法参数、异常、javadoc、注解等。
- 构造器
这个其实也是个函数方法而已,因此可以使用MethodSpec来生成构造器方法。比如:
MethodSpec flux = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).addParameter(String.class, "greeting").addStatement("this.$N = $N", "greeting", "greeting").build();TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld").addModifiers(Modifier.PUBLIC).addField(String.class, "greeting", Modifier.PRIVATE, Modifier.FINAL).addMethod(flux).build();
复制代码
将会生成
public class HelloWorld {private final String greeting;public HelloWorld(String greeting) {this.greeting = greeting;}
}
复制代码
- 参数(重要)
之前我们是通过addstatement
直接设置参数,其实参数也有自己的一个专用类ParameterSpec
,我们可以使用ParameterSpec.builder()
来生成参数,然后MethodSpec的addParameter去使用,这样更加优雅。
ParameterSpec android = ParameterSpec.builder(String.class, "android").addModifiers(Modifier.FINAL).build();MethodSpec welcomeOverlords = MethodSpec.methodBuilder("welcomeOverlords").addParameter(android).addParameter(String.class, "robot", Modifier.FINAL).build();
复制代码
生成的代码
void welcomeOverlords(final String android, final String robot) {
}
复制代码
稍微复杂点的类型 比如泛型 、Map之类的,需要了解下JavaPoet定义的几种专门描述类型的类
常见的有
分类 | 生成的类型 | JavaPoet 写法 | 也可以这么写 (等效的 Java 写法) |
---|---|---|---|
内置类型 | int | TypeName.INT | int.class |
数组类型 | int[] | ArrayTypeName.of(int.class) | int[].class |
需要引入包名的类型 | java.io.File | ClassName.get(“java.io”, “File”) | java.io.File.class |
参数化类型 (ParameterizedType | List | ParameterizedTypeName.get(List.class, String.class) | - |
类型变量 (WildcardType) 用于声明泛型 | T | TypeVariableName.get(“T”) | - |
通配符类型 | ? extends String | WildcardTypeName.subtypeOf(String.class) | - |
/**Build input type, format as :*Map<String, Class<? extends IRouteGroup>>*/ParameterizedTypeName inputMapTypeOfRoot = ParameterizedTypeName.get(ClassName.get(Map.class),ClassName.get(String.class),ParameterizedTypeName.get(ClassName.get(Class.class),WildcardTypeName.subtypeOf(ClassName.get(type_IRouteGroup))));/**Map<String, RouteMeta>*/ParameterizedTypeName inputMapTypeOfGroup = ParameterizedTypeName.get(ClassName.get(Map.class),ClassName.get(String.class),ClassName.get(RouteMeta.class)); /**Build input param name.*/ParameterSpec rootParamSpec = ParameterSpec.builder(inputMapTypeOfRoot, "routes").build();ParameterSpec groupParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "atlas").build();
复制代码
生成参数类型
public class ARouter$$Root$$app implements IRouteRoot {@Overridepublic void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {routes.put("service", ARouter$$Group$$service.class);routes.put("test", ARouter$$Group$$test.class);}
}public class ARouter$$Group$$service implements IRouteGroup {@Overridepublic void loadInto(Map<String, RouteMeta> atlas) {atlas.put("/service/hello", RouteMeta.build(RouteType.PROVIDER, HelloServiceImpl.class, "/service/hello", "service", null, -1, -2147483648));atlas.put("/service/json", RouteMeta.build(RouteType.PROVIDER, JsonServiceImpl.class, "/service/json", "service", null, -1, -2147483648));atlas.put("/service/single", RouteMeta.build(RouteType.PROVIDER, SingleService.class, "/service/single", "service", null, -1, -2147483648));}
}复制代码
- 字段
可以使用FieldSpec去声明字段,然后加到Method中处理
FieldSpec android = FieldSpec.builder(String.class, "android").addModifiers(Modifier.PRIVATE, Modifier.FINAL).build();TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld").addModifiers(Modifier.PUBLIC).addField(android).addField(String.class, "robot", Modifier.PRIVATE, Modifier.FINAL).build();
复制代码
然后生成代码
public class HelloWorld {private final String android;private final String robot;
}
复制代码
通常Builder可以更加详细的创建字段的内容,比如javadoc、annotations或者初始化字段参数等,如:
FieldSpec android = FieldSpec.builder(String.class, "android").addModifiers(Modifier.PRIVATE, Modifier.FINAL).initializer("$S + $L", "Lollipop v.", 5.0d).build();
复制代码
对应生成的代码
private final String android = "Lollipop v." + 5.0;
复制代码
- 接口
接口方法必须是PUBLIC ABSTRACT并且接口字段必须是PUBLIC STATIC FINAL ,使用TypeSpec.interfaceBuilder
如下
TypeSpec helloWorld = TypeSpec.interfaceBuilder("HelloWorld").addModifiers(Modifier.PUBLIC).addField(FieldSpec.builder(String.class, "ONLY_THING_THAT_IS_CONSTANT").addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL).initializer("$S", "change").build()).addMethod(MethodSpec.methodBuilder("beep").addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT).build()).build();
复制代码
生成的代码如下
public interface HelloWorld {String ONLY_THING_THAT_IS_CONSTANT = "change";void beep();
}
复制代码
- 继承父类 实现接口
接口代码
package com.test.javapoet;
public interface TestInterface<T> {void test(T testPara);
}
复制代码
父类代码
public class TestExtendesClass {}
复制代码
使用javapoet实现接口并且继承父类
final ClassName InterfaceName = ClassName.get("com.test.javapoet","TestInterface");ClassName superinterface = ClassName.bestGuess("com.test.javapoet.TestClass");//ClassName superinterface = ClassName.get("com.test.javapoet","aa");TypeSpec.Builder spec = TypeSpec.classBuilder("TestImpl").addModifiers(Modifier.PUBLIC)// 添加接口,ParameterizedTypeName的参数1是接口,参数2是接口的泛型.addSuperinterface(ParameterizedTypeName.get(InterfaceName, superinterface)) //使用ClassName.bestGuess会自动导入包.superclass(ClassName.bestGuess("com.zs.javapoet.test.TestExtendesClass"));MethodSpec.Builder methodSpec = MethodSpec.methodBuilder("test").addAnnotation(Override.class).returns(TypeName.VOID).addParameter(superinterface, "testPara").addStatement("System.out.println(hello)" );TypeSpec typeSpec = spec.addMethod(methodSpec.build()).build();JavaFile file = JavaFile.builder("com.zs.javapoet", typeSpec).build();file.writeTo(System.out);复制代码
生成代码
package com.test.javapoet;import com.zs.javapoet.test.TestExtendesClass;import java.lang.Override;public class TestImpl extends TestExtendesClass implements TestInterface<TestClass> {@Overridevoid test(TestClass testPara) {System.out.println(hello);}}
复制代码
- 枚举类型
使用TypeSpec.enumBuilder
来创建,使用addEnumConstant
来添加
TypeSpec helloWorld = TypeSpec.enumBuilder("Roshambo").addModifiers(Modifier.PUBLIC).addEnumConstant("ROCK").addEnumConstant("SCISSORS").addEnumConstant("PAPER").build();
复制代码
生成的代码
public enum Roshambo {ROCK,SCISSORS,PAPER
}
复制代码
更复杂的类型也可以支持,如重写、注解等
TypeSpec helloWorld = TypeSpec.enumBuilder("Roshambo").addModifiers(Modifier.PUBLIC).addEnumConstant("ROCK", TypeSpec.anonymousClassBuilder("$S", "fist").addMethod(MethodSpec.methodBuilder("toString").addAnnotation(Override.class).addModifiers(Modifier.PUBLIC).addStatement("return $S", "avalanche!").build()).build()).addEnumConstant("SCISSORS", TypeSpec.anonymousClassBuilder("$S", "peace").build()).addEnumConstant("PAPER", TypeSpec.anonymousClassBuilder("$S", "flat").build()).addField(String.class, "handsign", Modifier.PRIVATE, Modifier.FINAL).addMethod(MethodSpec.constructorBuilder().addParameter(String.class, "handsign").addStatement("this.$N = $N", "handsign", "handsign").build()).build();
复制代码
生成代码
public enum Roshambo {ROCK("fist") {@Overridepublic void toString() {return "avalanche!";}},SCISSORS("peace"),PAPER("flat");private final String handsign;Roshambo(String handsign) {this.handsign = handsign;}
}
复制代码
- 匿名内部类
需要使用Type.anonymousInnerClass("")
,通常可以使用$L占位符来指代
TypeSpec comparator = TypeSpec.anonymousClassBuilder("").addSuperinterface(ParameterizedTypeName.get(Comparator.class, String.class)).addMethod(MethodSpec.methodBuilder("compare").addAnnotation(Override.class).addModifiers(Modifier.PUBLIC).addParameter(String.class, "a").addParameter(String.class, "b").returns(int.class).addStatement("return $N.length() - $N.length()", "a", "b").build()).build();TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld").addMethod(MethodSpec.methodBuilder("sortByLength").addParameter(ParameterizedTypeName.get(List.class, String.class), "strings").addStatement("$T.sort($N, $L)", Collections.class, "strings", comparator).build()).build();
复制代码
生成代码
void sortByLength(List<String> strings) {Collections.sort(strings, new Comparator<String>() {@Overridepublic int compare(String a, String b) {return a.length() - b.length();}});
}
复制代码
定义匿名内部类的一个特别棘手的问题是参数的构造。在上面的代码中我们传递了不带参数的空字符串。TypeSpec.anonymousClassBuilder("")。
- 注解
注解使用起来比较简单
MethodSpec toString = MethodSpec.methodBuilder("toString").addAnnotation(Override.class).returns(String.class).addModifiers(Modifier.PUBLIC).addStatement("return $S", "Hoverboard").build();
复制代码
生成代码
@Overridepublic String toString() {return "Hoverboard";}
复制代码
通过AnnotationSpec.builder()
可以对注解设置属性:
MethodSpec logRecord = MethodSpec.methodBuilder("recordEvent").addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT).addAnnotation(AnnotationSpec.builder(Headers.class).addMember("accept", "$S", "application/json; charset=utf-8").addMember("userAgent", "$S", "Square Cash").build()).addParameter(LogRecord.class, "logRecord").returns(LogReceipt.class).build();
复制代码
代码生成如下
@Headers(accept = "application/json; charset=utf-8",userAgent = "Square Cash"
)
LogReceipt recordEvent(LogRecord logRecord);
复制代码
注解同样可以注解其他注解,通过$L引用如
MethodSpec logRecord = MethodSpec.methodBuilder("recordEvent").addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT).addAnnotation(AnnotationSpec.builder(HeaderList.class).addMember("value", "$L", AnnotationSpec.builder(Header.class).addMember("name", "$S", "Accept").addMember("value", "$S", "application/json; charset=utf-8").build()).addMember("value", "$L", AnnotationSpec.builder(Header.class).addMember("name", "$S", "User-Agent").addMember("value", "$S", "Square Cash").build()).build()).addParameter(LogRecord.class, "logRecord").returns(LogReceipt.class).build();
复制代码
生成代码
@HeaderList({@Header(name = "Accept", value = "application/json; charset=utf-8"),@Header(name = "User-Agent", value = "Square Cash")
})
LogReceipt recordEvent(LogRecord logRecord);
复制代码
注释
- javadoc
0x03 后续
在javapoet之前有javawriter,但javapoet有着更强大的代码模型,并且对类的理解更加到位,因此推荐使用javapoet
参考文章
juejin.cn/post/684490…
cloud.tencent.com/developer/a…
blog.csdn.net/qq_26376637…
作者:渡口一艘船
链接:https://juejin.cn/post/6844903456629587976
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Java中的屠龙之术(二):如何方便快捷地生成.class文件相关推荐
- Java中如何实现QR(二维码)的生成
二维码在我们的日常生活中的使用现在可谓是如火如荼.二维码,即一种新生代的信息存储介质.最早被使用在汽车生产厂,用于跟踪售后的汽车. 同样,在互联网时代,二维码也被广泛使用,随着二维码的迅速发展,一些手 ...
- Java 中的屠龙之术:如何修改语法树?
作者:不学无数的程序员 来源:https://my.oschina.net/u/4030990/blog/3211858 在网上关于如何修改Java的抽象语法树的相关API文档并不多,于是本篇记录一下 ...
- 在 Java 中,如何批量读取本项目资源目录下的所有文件
在 Java 中,如何批量读取本项目资源目录下的所有文件 读取资源目录下的指定文件 方法 1:使用 JDK 中原始 API 方法 2:借助 Spring 附录 将 InputStream 转化为 by ...
- si446使用记录(二):使用WDS3生成头文件
目录 一.概述 二.使用默认配置生成头文件 三.逐项修改配置 3.1 根据注释修改 3.1.1 速率.带宽.频偏 3.1.2 基频 -- 设置868M 3.1.3 使能中断 3.2 具体指令修改 3. ...
- c++中string插入一个字符_Java内存管理-探索Java中字符串String(十二)
做一个积极的人 编码.改bug.提升自己 我有一个乐园,面向编程,春暖花开! 一.初识String类 首先JDK API的介绍: public final class String extends O ...
- Java中的一维数组和二维数组
Java中的数组 常用的就是一维数组.二维数组. 一维数组 数组定义:数据就是相同数据元素的集合. 数组是一种引用数据类型.不属于基本数据类型. (1) 如何定义(声明)一维数组? 声明格式:数据类型 ...
- Java 中的一维数组以及二维数组(8千字零基础超全详解)
文章目录 前言 一.一维数组 1. 数组的初始化 1.1 动态初始化一 1.2 动态初始化二 1.3 静态初始化 2. 数组使用的注意事项和细节 3. 经典例题 4. 数组赋值机制(重难点!) 5. ...
- Java中的数组方法and二维数组
今日份鼓励:如果你想中一颗树,那么它最好的栽种时间,一个是十年前,一个是现在!! 在这里因为博主的知识能力有限,我将会使用自己能够使用并且解释清楚的方法来完成这篇博客 目录 一.数组的拷贝 二.数组的 ...
- Java中如何读写cookie (二)
Java中删除cookie Cookie[] cookies=request.getCookies(); //cookies不为空,则清除 if(cookies!=nu ...
最新文章
- 讨论下 Java 流文件读写缓存大小设置的问题
- 改变Jupyter的默认项目路径
- 配置tomcat6.0的HTTPS(单向)
- linux的nfs端口号,#Linux NFS服务 固定端口及防火墙配置#
- LeetCode Week 4:第 31 ~ 40 题
- 蓝桥杯——黑色星期五
- Java基础知识拾遗--IO篇
- CUBRID学习笔记 21 查看主键外键索引
- 生产环境 xfs filesystem 上安装Mariadb
- 一步步教你写一份优秀的软件测试简历(带样例)
- 解耦电容选型定性与定量分析
- IIS 发布网站无法显示CSS、背景及图片文件---另一个思路--终极方案
- Android安卓-UI自动打卡Appium+PO+Pytest(1)
- 转: 学习开源项目的若干建议(infoq)
- FT5316调试记录
- spark+mongodb大数据框架搭建
- 计算机主机显卡安装,安装后如何在计算机中启用独立显卡
- 竞赛排名_纪中1743_模拟
- Gradle排除依赖模块的某个类
- 三个月考研英语复习法
热门文章
- XSKY全新一代SDS一体机五大场景之存储+灾备
- TinyWS —— 一个C++写的简易WEB服务器(三)
- 应届生,找java后端开发要什么水平才算合格?
- BZOJ 1875 [SDOI2009]HH去散步 矩阵乘法
- 区位码,国标码以及汉字内码的关系以及为什么是这样的关系
- Mozilla 正在火狐中集成谷歌翻译;内存产能增长减缓,基本告别降价
- 聚类算法实践(2)——谱聚类、Chameleon聚类
- springboot打包成jar包部署,无法访问到fastDFS配置文件【已解决】
- 关于移动端插件扩展:lsuperslide和l iscroll以及zy.media.js视频插件
- OpenCV——Canny直线检测