在我们系统中有这么一个需求,业务方会通过mq将一些用户信息传给我们,我们的服务处理完后,再将信息转发给子系统。mq的内容如下:
@Data
public class Person {
//第一部分
private Integer countryId;
private Integer companyId;
private String uid;//第二部分
private User userBaseInfo;
private List<UserContact> contactList;
private List<UserAddress> addressList;
private UserEducation educationInfo;
private UserProfession professionInfo;
private List<Order> orderList;
private List<Bill> billList;
private List<UserMerchant> merchantList;
private List<UserOperate> operateList;
private BeneficialOwner beneficialOwnerInfo;
}
主要分为两部分,第一部分是用户id,这部分用于唯一标识一个用户,不会改变。第二部分是一些基础信息,账单、订单、联系方式、地址等等,这部分信息内容经常增加。
后面业务新增了一个逻辑,会对第二部某些信息进行剔除,最后这部分信息如果还有,才转发到子系统。所以开发同学新增这么一个很长的条件判断:
public static boolean isNull(BizData bizData) {
return CollectionUtils.isEmpty(bizData.getBillList()) && CollectionUtils.isEmpty(bizData.getOrderList()) && CollectionUtils.isEmpty(bizData.getAddressList()) && CollectionUtils.isEmpty(bizData.getContactList()) && bizData.getEducationInfo() == null && bizData.getProfessionInfo() == null && bizData.getUserBaseInfo() == null && CollectionUtils.isEmpty(bizData.getMerchantList()) && CollectionUtils.isEmpty(bizData.getOperateList()) && bizData.getBeneficialOwnerInfo() == null;
}
在review代码的时候,发现这里是一个“坑”,是一个会变化的点,以后新增信息,很可能会漏了来改这里,在我的开发过程中,最担心的就是遇到这些会变化点又写死逻辑的,过段时间我就会忘记,如果换个人接手,那更难以发现,容易出现bug。因为这个条件判断并不会自动随着我们新增字段而自动修改,完全靠人记忆,容易遗漏。
思考
那有没有办法做到新增信息不需要修改这里吗,也就是isNull方法可以自动动态判断属性是否为空呢?
首先我们都会想到反射,通过反射可以读取class所有字段,每次处理都反射判断一下字段值是否为空即可做到动态判断。但反射的性能太低了,对于我们来说这是个调用量非常大的方法,尽量做到不损失性能,所以反射不在本次考虑范围内。
既然有不变和变化的两部分,那么我们可以先将其分离,将不变的抽取到一个基类去。为了简化代码,第二部分我们只保留两个属性。
@Data
public class PersonBase {
//第一部分
private Integer countryId;
private Integer companyId;
private String uid;
}
@Data
public class Person extend PersonBase {
//第二部分...
private User userBaseInfo;
private List<UserContact> contactList;
}
要动态生成isNull方法,可以先从结果反推是怎么样的。可以有如下两种方式:
1、在原Person类新增一个isNull方法,这种方式的特点是我们可以直接通过对象直接调用方法,如:
@Data
public class Person extend PersonBase {
private User userBaseInfo;
private List<UserContact> contactList;public boolean isNull() {return this.userBaseInfo != null && this.contactList != null;
}
}
2、动态新增一个类,动态新增一个isNull方法,参数是BizData。这种方式无法通过Preson对象调用方法,甚至无法直接通过生成类调用方法,因为动态类的名称我们都无法预知。如:
public class Person$Generated {
public boolean isNull(BizData bizData) {return bizData.getUserBaseInfo() != null && bizData.getContactList() != null;
}
}
这就是我们本篇要解决的问题,通过静态/动态编译技术生成代码。这里静态是指“编译期”,也就是类和方法在编译期间就存在了,动态是指“运行时”,意思编译期间类还不存在,等程序运行时才被加载,链接,初始化。
这两种方式大家实际都经常接触到,lombok可以帮我们生成getter/setter,本质就是在编译期为类新增方法,spring无处不在的动态代理就是运行时生成的类。
动态编译
我们先来看动态编译,因为动态编译我们都比较熟,也比较简单,在spring中随处可见,例如我们熟悉的动态代理类就是动态生成的。
我们编写的java代码会先经过编译称为字节码,字节码再由jvm加载运行,所以动态生成类就是要编写相应的字节码。
但由于java字节码太复杂了,需要熟悉各种字节码指令,一般我们不会直接编写字节码代码,会借助字节码框架或工具来生成。例如查看简单的hello world类的字节码,idea -> view -> show bytecode。
public class HelloWorld {
public static void main(String[] args) {System.out.println("hello world");
}
}
ASM 介绍
ASM是一个通用的 Java 字节码操作和分析框架。它可用于直接以二进制形式修改现有类或动态生成类。ASM 提供了一些常见的字节码转换和分析算法,可以从中构建自定义的复杂转换和代码分析工具。ASM 提供与其他 Java 字节码框架类似的功能,但重点关注性能。因为它的设计和实现尽可能小且尽可能快,所以它非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)。
接下来我们用asm来生成hello world,如下:
public class HelloWorldGenerator {
public static void main(String[] args) throws Exception {
// 创建一个ClassWriter,用于生成字节码
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
// 定义类的头部信息cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "HelloWorld", null, "java/lang/Object", null);// 生成默认构造函数MethodVisitor constructor = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);constructor.visitCode();constructor.visitVarInsn(Opcodes.ALOAD, 0);constructor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);constructor.visitInsn(Opcodes.RETURN);constructor.visitMaxs(1, 1);constructor.visitEnd();// 生成main方法MethodVisitor mainMethod = cw.visitMethod(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);mainMethod.visitCode();// 打印"Hello, World!"到控制台mainMethod.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mainMethod.visitLdcInsn("Hello, World!");mainMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);mainMethod.visitInsn(Opcodes.RETURN);mainMethod.visitMaxs(2, 2);mainMethod.visitEnd();// 完成类的生成cw.visitEnd();// 将生成的字节码写入一个类文件byte[] code = cw.toByteArray();MyClassLoader classLoader = new MyClassLoader();Class<?> helloWorldClass = classLoader.defineClass("HelloWorld", code);// 创建一个实例并运行main方法helloWorldClass.getDeclaredMethod("main", String[].class).invoke(null, (Object) new String[0]);
}// 自定义ClassLoader用于加载生成的类
private static class MyClassLoader extends ClassLoader {public Class<?> defineClass(String name, byte[] b) {return defineClass(name, b, 0, b.length);}
}
}
上面的代码我是用chatgpt生成的,只需要输入:“帮我用java asm字节码框架生成一个hello world,并注释每行代码写明它的作用。”
相比直接编写字节码指令,asm将其封装成各种类和方法,方便我们理解和编写,实际上asm还是比较底层的框架,所以许多框架会再它的基础上继续封装,如cglib,byte buddy等。
可以看到生成的结果和我们自己编写的是一样的。
实现
接下来我们就用asm来动态生成上面的isNull方法,由于目标类是动态生成的,类名我们都不知道,但我们最终是要调用它的isNull方法,这怎么办呢?
我们可以定义一个接口,然后动态生成的类实现它,最终通过接口来调用它,这就是接口的好处之一,我们可以不关注具体类是谁,内部怎么实现。
如定义接口如下:
public interface NullChecker {
/*** 参数固定为origin** @param origin 名称必须为origin*/
Boolean isNull(T origin);
}
这是个泛型接口,也就是所有类型都可以这么用。isNull方法参数名称必须为origin,因为在生成字节码时写死了这个名称。
接下来编写核心的生成方法,如下:
public class ClassByteGenerator implements Opcodes {
public static byte[] generate(Class originClass) {ClassWriter classWriter = new ClassWriter(0);MethodVisitor methodVisitor;//将.路径替换为/String originClassPath = originClass.getPackage().getName().replace(".", "/") + "/" + originClass.getSimpleName();//动态生成类的名称:原类$ASMGeneratedString generateClassName = originClass.getSimpleName() + "$ASMGenerated";String generateClassPatch = ClassByteGenerator.class.getPackage().getName().replace(".", "/") + "/" + generateClassName;String nullCheckerClassPath = NullChecker.class.getPackage().getName().replace(".", "/") + "/" + NullChecker.class.getSimpleName();classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, generateClassPatch, null, "java/lang/Object", new String[]{nullCheckerClassPath});classWriter.visitSource(generateClassName + ".java", null);{methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);methodVisitor.visitCode();Label label0 = new Label();methodVisitor.visitLabel(label0);methodVisitor.visitLineNumber(7, label0);methodVisitor.visitVarInsn(ALOAD, 0);methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);methodVisitor.visitInsn(RETURN);Label label1 = new Label();methodVisitor.visitLabel(label1);methodVisitor.visitLocalVariable("this", "L" + generateClassPatch + ";", null, label0, label1, 0);methodVisitor.visitMaxs(1, 1);methodVisitor.visitEnd();}{methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "isNull", "(L" + originClassPath + ";)Ljava/lang/Boolean;", null, null);methodVisitor.visitParameter("origin", 0);methodVisitor.visitCode();Label label0 = new Label();methodVisitor.visitLabel(label0);methodVisitor.visitVarInsn(ALOAD, 1);Label label1 = new Label();int index = 0;//过滤掉基类的PropertyDescriptor[] propertyDescriptors = Arrays.stream(BeanUtils.getPropertyDescriptors(originClass)).filter(p -> p.getReadMethod().getDeclaringClass() == originClass).toArray(PropertyDescriptor[]::new);for (PropertyDescriptor pd : propertyDescriptors) {String descriptor = "()" + Type.getDescriptor(pd.getPropertyType());if (index == 0) {methodVisitor.visitMethodInsn(INVOKEVIRTUAL, originClassPath, pd.getReadMethod().getName(), descriptor, false);} else if (index > 0 && index < propertyDescriptors.length - 1) {methodVisitor.visitJumpInsn(IFNULL, label1);methodVisitor.visitVarInsn(ALOAD, 1);methodVisitor.visitMethodInsn(INVOKEVIRTUAL, originClassPath, pd.getReadMethod().getName(), descriptor, false);} else {methodVisitor.visitJumpInsn(IFNULL, label1);methodVisitor.visitVarInsn(ALOAD, 1);methodVisitor.visitMethodInsn(INVOKEVIRTUAL, originClassPath, pd.getReadMethod().getName(), descriptor, false);methodVisitor.visitJumpInsn(IFNULL, label1);methodVisitor.visitInsn(ICONST_1);}index++;}Label label2 = new Label();methodVisitor.visitJumpInsn(GOTO, label2);methodVisitor.visitLabel(label1);methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);methodVisitor.visitInsn(ICONST_0);methodVisitor.visitLabel(label2);methodVisitor.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{Opcodes.INTEGER});methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf", "(Z)Ljava/lang/Boolean;", false);methodVisitor.visitInsn(ARETURN);Label label3 = new Label();methodVisitor.visitLabel(label3);methodVisitor.visitLocalVariable("this", "L" + generateClassPatch + ";", null, label0, label3, 0);methodVisitor.visitLocalVariable("origin", "L" + originClassPath + ";", null, label0, label3, 1);methodVisitor.visitMaxs(1, 2);methodVisitor.visitEnd();}{methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_BRIDGE | ACC_SYNTHETIC, "isNull", "(Ljava/lang/Object;)Ljava/lang/Boolean;", null, null);methodVisitor.visitParameter("origin", ACC_SYNTHETIC);methodVisitor.visitCode();Label label0 = new Label();methodVisitor.visitLabel(label0);methodVisitor.visitLineNumber(7, label0);methodVisitor.visitVarInsn(ALOAD, 0);methodVisitor.visitVarInsn(ALOAD, 1);methodVisitor.visitTypeInsn(CHECKCAST, originClassPath);methodVisitor.visitMethodInsn(INVOKEVIRTUAL, generateClassPatch, "isNull", "(L" + originClassPath + ";)Ljava/lang/Boolean;", false);methodVisitor.visitInsn(ARETURN);Label label1 = new Label();methodVisitor.visitLabel(label1);methodVisitor.visitLocalVariable("this", "L" + generateClassPatch + ";", null, label0, label1, 0);methodVisitor.visitMaxs(2, 2);methodVisitor.visitEnd();}classWriter.visitEnd();return classWriter.toByteArray();
}
}
代码有点长,可能你会想还是用chatgpt生成,但这种逻辑性比较强的它就无能为力了。不过我们还有工具可以生成它,我使用的是ASM Bytecode Viewer,idea中安装插件即可。
首先将要实现的结果用代码写出来,然后右键使用ASM Bytecode Viewer,就可以看到对应的asm代码。当然实际我们是要遍历类的所有字段,就是for循环遍历属性的那一部分,这需要自己写,也不难,在插件生成代码后稍微调整下即可。
public class MyPersonGenerated implements NullChecker {
@Override
public Boolean isNull(Person person) {return person.getUserBaseInfo() != null && person.getContactList() != null;
}
}
八股文背多了就知道类生命周期是:加载 -> 链接(验证,准备,解析) -> 初始化 -> 使用 -> 卸载。所以首先要使用ClassLoader将动态类加载到jvm,我们可以定义一个类继承抽象类ClassLoader,调用它的defineClass。
public class MyClassLoader extends ClassLoader {
public Class<?> defineClass(byte[] b) {return super.defineClass(null, b, 0, b.length);
}
}
使用如下,当然实际情况中我们会将生成的NullChecker赋值给一个全局变量缓存,不用每次都newInstance创建。
MyClassLoader myClassLoader = new MyClassLoader();
byte[] bytes = ClassByteGenerator.generate(Person.class);
Class<?> personNullCheckerCls = myClassLoader.defineClass(bytes);
NullChecker personNullChecker = (NullChecker) personNullCheckerCls.newInstance();
boolean result = o.isNull(person);
也可以将生成类的字节保存到文件,然后拖到idea观察结果,如下:
try (FileOutputStream fos = new FileOutputStream("./Person$ASMGenerated.class")) {
fos.write(bytes); // 将字节数组写入.class文件
} catch (IOException e) {
throw e;
}
静态编译
看完动态编译我们再看静态编译。java代码编译和执行的整个过程包含三个主要机制:1.java源码编译机制 2.类加载机制 3.类执行机制。其中java源码编译由3个过程组成:1.分析和输入到符号表 2.注解处理 3.语义分析和生成class文件。如下:
在介绍mapstruct这篇时我们也有提到,其中主要就是在源码编译的注解处理阶段,可以插入我们的自定义代码。
例如我们新建工程,定义如下注解,它标识的类就会对应生成一个含isNull方法的类。其中RetentionPolicy.SOURCE表示在源码阶段生效,在运行时是读不到这个注解的,lombok的注解也是这个道理。
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface GenerateIsNullMethod {
String value() default "";
}
接着编写注解处理器,在发现GenerateIsNullMethod注解时,进入处理逻辑。
@SupportedAnnotationTypes("com.example.mapstruct.processor.GenerateIsNullMethod")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class IsNullAnnotationProcessor extends AbstractProcessor {
private ProcessingEnvironment processingEnv;@Override
public synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);this.processingEnv = processingEnv;
}@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "GenerateIsNullMethod start===");Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(GenerateIsNullMethod.class);for (Element classElement : set) {generateIsNullMethod(classElement);}return true;
}
meipian.cn/5biees11?ntxp=58
meipian.cn/5bif2gsu?dqar=ye
meipian.cn/5bifp8jz?yqcz=fr
meipian.cn/5bihe7sr?ajgi=84
meipian.cn/5bie8w1b?culv=03
meipian.cn/5bieqtoy?rjrp=lb
meipian.cn/5bifepq1?jyws=ou
meipian.cn/5bifpt02?dmqk=42
meipian.cn/5bifqrov?xptx=kj
meipian.cn/5bigaflm?dbmt=fi
meipian.cn/5bifpfg9?ipsw=22
meipian.cn/5biekpl2?djga=02
meipian.cn/5bifybwc?ekcs=78
meipian.cn/5bihrcdk?iuel=56
meipian.cn/5big5era?bgnx=nz
meipian.cn/5bierptb?olqa=dp
meipian.cn/5bigbjja?jvmq=53
meipian.cn/5bieo9pu?lqsj=oc
meipian.cn/5bieq12t?gvsd=ms
meipian.cn/5bihoxeh?hwlg=84
meipian.cn/5bie7hrm?oadb=32
meipian.cn/5bif4zhc?clgr=ye
meipian.cn/5bifru7o?yrom=sy
meipian.cn/5big6rww?bmej=88
meipian.cn/5bifeff8?njad=lj
meipian.cn/5bif79db?hhey=68
meipian.cn/5bifzxdo?qccm=tb
meipian.cn/5bif3mvv?zyli=74
meipian.cn/5bihgdzg?nmxn=iz
meipian.cn/5biffq2o?jmzd=pl
meipian.cn/5bifyiu0?tzzh=qc
meipian.cn/5bigauo2?srpz=lr
meipian.cn/5bie7asd?xxvk=rx
meipian.cn/5bigb7nj?kyxh=69
meipian.cn/5bifvqes?tilp=ev
meipian.cn/5bif5fe8?hftq=jc
meipian.cn/5bif39jd?agda=eq
meipian.cn/5bifn9qs?ohbg=ab
meipian.cn/5bihsv9t?qcgl=tf
meipian.cn/5bifufqg?dpmm=co
meipian.cn/5bie6xqh?axgv=92
meipian.cn/5bifqljs?dcgw=dp
meipian.cn/5bifjmb2?hzii=51
meipian.cn/5bifccgz?qvit=ze
meipian.cn/5bihr1yl?gheo=28
meipian.cn/5bifochf?bguk=54
meipian.cn/5bifnxzo?mzqh=25
meipian.cn/5bifmzse?hbjq=72
meipian.cn/5bif9r2a?szxu=rx
meipian.cn/5bifnmf6?fusj=hu
meipian.cn/5bihehws?asjj=ad
meipian.cn/5bif4isn?reig=tz
meipian.cn/5bigcw4m?fowo=53
meipian.cn/5bif6005?flby=77
meipian.cn/5bif6jni?xqis=fb
meipian.cn/5bigf079?zmxn=52
meipian.cn/5big1d0r?petc=62
meipian.cn/5bierurm?oaxb=fj
meipian.cn/5big25iq?utvp=qh
meipian.cn/5bihsd5m?btdn=71
meipian.cn/5big1jcd?wvtm=ww
meipian.cn/5bifhz36?okgr=rj
meipian.cn/5biftmqj?lqhe=cv
meipian.cn/5bifbwou?yeoy=65
meipian.cn/5bif3fd3?klvs=me
meipian.cn/5bifgjgl?glwu=vs
meipian.cn/5bihq31t?ioyp=41
meipian.cn/5bifkm9g?sacq=62
meipian.cn/5bifzjhm?povr=az
meipian.cn/5bihmy0i?omwn=jn
meipian.cn/5bifyvau?uhky=64
meipian.cn/5bifr2j4?lbsz=lp
meipian.cn/5bif8h3m?kjfi=nc
meipian.cn/5bifjv7g?mypp=67
meipian.cn/5bifubhp?pilj=fe
meipian.cn/5bieemju?rvmx=98
meipian.cn/5bihu8s4?fevs=89
meipian.cn/5bifz7d4?gtyp=71
meipian.cn/5bifk7sw?kjcs=01
meipian.cn/5biepmxl?myoz=dw
meipian.cn/5bifvb0l?vydz=ib
meipian.cn/5bierez9?cmrt=46
meipian.cn/5biffjm2?ldol=50
meipian.cn/5biheuao?apfu=28
meipian.cn/5bieapzn?uznd=wp
meipian.cn/5bier0v8?hzdu=sq
meipian.cn/5biftwa3?nfjm=bh
meipian.cn/5bihnw1u?dpfd=43
meipian.cn/5bifmpoc?bakv=58
meipian.cn/5bifa4c9?yqbf=wv
meipian.cn/5bihsl4g?lqhi=ph
meipian.cn/5bifi5c0?kcda=qr
meipian.cn/5bihidho?clzc=pp
meipian.cn/5bifis9w?lxgd=26
meipian.cn/5bigak22?igcv=55
meipian.cn/5biednyn?gldg=94
meipian.cn/5bifh1qe?ntxg=ug
meipian.cn/5big49ag?kwgr=39
meipian.cn/5bifsb08?cofk=39
meipian.cn/5biescrc?wdge=21
meipian.cn/5bigegs8?dnry=71
meipian.cn/5bifrow4?vkhy=44
meipian.cn/5bifm6i9?lheb=oa
meipian.cn/5bifoxqn?gjbg=67
meipian.cn/5bieddhr?foko=99
meipian.cn/5bif909j?qank=97
meipian.cn/5bifl67d?fevv=eq
meipian.cn/5bif70vu?otku=jc
meipian.cn/5bifwe4i?pbij=og
meipian.cn/5big517b?evgw=81
meipian.cn/5bihb5ku?cvtd=38
meipian.cn/5big3vr9?myja=28
meipian.cn/5bihnchl?drry=ua
meipian.cn/5bihqs5a?dvzx=30
meipian.cn/5bihqlkw?qjix=34
meipian.cn/5bifvy1m?jixa=uh
meipian.cn/5bigd3xv?bxpj=62
meipian.cn/5big2fv7?cusd=oo
meipian.cn/5bieakgi?vjzy=43
meipian.cn/5bif3zm8?giqo=03
meipian.cn/5bifrads?fecw=95
meipian.cn/5bihmrst?zezj=od
meipian.cn/5biht5ok?tyci=66
private void generateIsNullMethod(Element classElement) {//javapoet只能创建新的文件,不能修改
//代码效果参考:https://www.weibow.com/sitemap/post.html
https://github.com/square/javapoet/issues/505
String packageName = processingEnv.getElementUtils().getPackageOf(classElement).toString();
String className = classElement.getSimpleName().toString();
String newClassName = className + "Ext";
MethodSpec.Builder p = MethodSpec.methodBuilder("isNull").addModifiers(Modifier.PUBLIC).addModifiers(Modifier.STATIC).addParameter(ClassName.bestGuess(packageName + "." + className), "p").returns(Boolean.class);String statement = "return ";for (Element ee : classElement.getEnclosedElements()) {if (ee.getKind().isField()) {String eeName = ee.getSimpleName().toString();statement += "p.get" + eeName.substring(0, 1).toUpperCase() + eeName.substring(1, ee.getSimpleName().length()) + "()" + " != null && ";}}statement = statement.substring(0, statement.length() - 4);processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, statement + "===");MethodSpec isNullMethod = p.addStatement(statement).build();TypeSpec updatedClass = TypeSpec.classBuilder(newClassName).addModifiers(Modifier.PUBLIC).addMethod(isNullMethod).build();JavaFile javaFile = JavaFile.builder(packageName, updatedClass).build();try {javaFile.writeTo(processingEnv.getFiler());} catch (IOException e) {processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to generate isNull method: " + e.getMessage());}
}
}
@AutoService是google一个工具包,帮我们在META-INF/services路径下生成配置,注解处理器才会生效。
这里生成代码使用到了javapoet工具,用于生成.java源文件。
其它的就都是在生成代码了,需要注意的是,既然是在编译期,那就不要想用运行时的东西,例如反射,都还没到那个阶段。
导入这个工程使用GenerateIsNullMethod标记Person类,编译后就可以观察到生成一个PersonExt的类,它的isNull方法会判断Person参数每个属性是否为空。
这里我并没有像lombok一样在原类上新增方法,而是新增一个Ext类,因为那样做要解析语法树,比较复杂,我没有实现,有兴趣的可以参考lombok自己实现一下。