闽公网安备 35020302035485号
@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。因为这个条件判断并不会自动随着我们新增字段而自动修改,完全靠人记忆,容易遗漏。@Data
public class PersonBase {
// 堆代码 duidaima.com
//第一部分
private Integer countryId;
private Integer companyId;
private String uid;
}
@Data
public class Person extend PersonBase {
//第二部分...
private User userBaseInfo;
private List<UserContact> contactList;
}
要动态生成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;
}
}
这就是我们本篇要解决的问题,通过静态/动态编译技术生成代码。这里静态是指“编译期”,也就是类和方法在编译期间就存在了,动态是指“运行时”,意思编译期间类还不存在,等程序运行时才被加载,链接,初始化。
public class HelloWorld {
public static void main(String[] args) {
System.out.println("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,并注释每行代码写明它的作用。”
public interface NullChecker<T> {
/**
* 参数固定为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();
//动态生成类的名称:原类$ASMGenerated
String 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中安装插件即可。
public class MyPersonGenerated implements NullChecker < Person > {
@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;
}

@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;
// 堆代码 duidaima.com
@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;
}
private void generateIsNullMethod(Element classElement) {
//javapoet只能创建新的文件,不能修改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路径下生成配置,注解处理器才会生效。导入这个工程使用GenerateIsNullMethod标记Person类,编译后就可以观察到生成一个PersonExt的类,它的isNull方法会判断Person参数每个属性是否为空。