编译动态生成的java文件

前言

之前在做一个项目的时候,遇到过字段经常变动烦不胜烦,最后想是否有办法根据配置文件生成java bean,然后编译生java文件来使用,最后查到到 FreeMarker根据模板来生成java文件,用javax.tools.JavaCompiler或者Eclipse java compiler(ECJ)编译java文件为字节码.

git仓库地址

FreeMarker根据模板来生成java文件

maven依赖

<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.30</version>
</dependency>
  1. FreeMarker获取模板的帮助类
package com.thghh.bcode.ftl;

import freemarker.cache.ClassTemplateLoader;
import freemarker.cache.NullCacheStorage;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;

import java.io.IOException;

/**
 * @author Zhikang.Peng
 * @version 1.0
 * @email thghh@qq.com
 * @date 2020/12/8 16:37
 */
public class FreeMarkerTemplateUtils {

    private static final Configuration configuration = new Configuration(Configuration.getVersion());

    private FreeMarkerTemplateUtils(){}

    static{
        //这里比较重要,用来指定加载模板所在的路径
        configuration.setTemplateLoader(new ClassTemplateLoader(FreeMarkerTemplateUtils.class, "templates"));
        configuration.setDefaultEncoding("UTF-8");
        configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        configuration.setCacheStorage(NullCacheStorage.INSTANCE);
    }

    public static Template getTemplate(String templateName) throws IOException {
        try {
            return configuration.getTemplate(templateName);
        } catch (IOException e) {
            throw e;
        }
    }

    public static void clearCache() {
        configuration.clearTemplateCache();
    }
}
  1. 生成java源码的类
package com.thghh.bcode.ftl;

import com.thghh.bcode.model.PropertyModel;
import com.thghh.bcode.model.SourceFileModel;
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.beans.BeansWrapperBuilder;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;

import java.io.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Zhikang.Peng
 * @version 1.0
 * @email thghh@qq.com
 * @date 2020/12/9 16:47
 */
public class GenerateJavaSource {

    public GenerateJavaSource() {
    }

    public byte[] generateFile(SourceFileModel sourceFileModel, List<PropertyModel> properties) throws IOException, TemplateException {
        Template template = FreeMarkerTemplateUtils.getTemplate("PropertyModel.ftl");
        Map<String, Object> dataMap = new HashMap<>();
        dataMap.put("package_name", sourceFileModel.getPackageName());
        dataMap.put("class_name", sourceFileModel.getClassName());
        dataMap.put("properties", properties);
        ByteArrayOutputStream output = new ByteArrayOutputStream(1024);
        Writer writer = new OutputStreamWriter(new BufferedOutputStream(output));

        // Create the builder:
        BeansWrapperBuilder builder = new BeansWrapperBuilder(Configuration.getVersion());
        // Set desired BeansWrapper configuration properties:
        builder.setUseModelCache(true);
        builder.setExposeFields(true);
        template.process(dataMap, writer, builder.build());
        writer.flush();
        return output.toByteArray();
    }
}
  1. 模板文件
package ${package_name};

import lombok.Data;
import lombok.EqualsAndHashCode;
import com.thghh.table.BeanColumn;
import com.thghh.bcode.model.Property;

@Data
@EqualsAndHashCode(callSuper=false)
public class ${class_name} extends Property
{
    <#if properties?exists>
        <#list properties as property>
            <#if property.property>
     @BeanColumn(name = "${property.displayPropertyName}", index = ${property.propertyIndex}, editable = ${property.editable?string('true','false')})
            </#if>
     private ${property.propertyType} ${property.propertyName};

        </#list>
    </#if>
}

动态java文件编译

使用的是ToolProvider.getSystemJavaCompiler()或ECJ(Eclipse java compiler)编译源码,好处是不用控制台命令来执行,class依赖比较好处理一些。

  1. Maven依赖
    tools.jar在java10好像合并到主包里了,不需要明确指定,可根据实际情况修改
<dependency>
	<groupId>org.eclipse.jdt.core.compiler</groupId>
	<artifactId>ecj</artifactId>
	<version>4.6.1</version>
</dependency>

<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<version>1.18.10</version>
	<scope>provided</scope>
</dependency>

<dependency>
	<groupId>tools</groupId>
	<artifactId>tools</artifactId>
	<version>1.8</version>
	<scope>system</scope>
	<systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath>
</dependency>
  1. DynamicJavaCompiler
package com.thghh.bcode.compiler;

import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler;

import javax.tools.*;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * @author Zhikang.Peng
 * @version 1.0
 * @email thghh@qq.com
 * @date 2020/12/4 13:35
 */
public class DynamicJavaCompiler implements Closeable {

    // sum java compiler
    public static final String JAVAC = "JAVAC";
    // eclipse java compiler, not support JSR 269 API
    public static final String ECJ = "ECJ";
    private ByteCodeFileManager fileManager;
    private JavaCompiler compiler;
    private DiagnosticCollector<JavaFileObject> diagnostics;


    public DynamicJavaCompiler() {
        this(JAVAC);
    }

    public DynamicJavaCompiler(String model) {
        initFileManager(model);
    }

    private void initFileManager(String model) {
        if (fileManager == null) {
            if (JAVAC.equals(model)) {
                compiler = ToolProvider.getSystemJavaCompiler();
                diagnostics = new DiagnosticCollector<>();
                fileManager = new ByteCodeFileManager(compiler.getStandardFileManager(diagnostics, null, null));
            } else {
                compiler = new EclipseCompiler();
                diagnostics = new DiagnosticCollector<>();
                fileManager = new ByteCodeFileManager(compiler.getStandardFileManager(diagnostics, null, null));
            }
        }
    }

    public boolean javaCompiler(String className, String sourceCode) {
        List<JavaFileObject> compilationUnits = new ArrayList<>();
        compilationUnits.add(new StringJavaFileObject(className, sourceCode));
        JavaCompiler.CompilationTask compilationTask = compiler.getTask(null, fileManager, diagnostics, null, null, compilationUnits);

        boolean result = compilationTask.call();
        for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
            System.out.format("Error on line %d in %s", diagnostic.getLineNumber(), diagnostic.getMessage(null));
        }
        return result;
    }

    public Class loadClass(String className) throws ClassNotFoundException {
        return fileManager.getClassLoader(null).loadClass(className);
    }

    public byte[] getByteCode() {
        return fileManager.getByteCode();
    }

    @Override
    public void close() throws IOException {
        this.fileManager.close();
    }
}

  1. ByteCodeFileManager
package com.thghh.bcode.compiler;

import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.security.SecureClassLoader;

/**
 * @author Zhikang.Peng
 * @version 1.0
 * @email thghh@qq.com
 * @date 2020/12/4 16:56
 */
public class ByteCodeFileManager extends ForwardingJavaFileManager {
    private ByteCodeJavaFileObject byteCodeJavaFileObject;
    private ByteCodeClassLoader byteCodeClassLoader;
    /**
     * Creates a new instance of ForwardingJavaFileManager.
     *
     * @param fileManager delegate to this file manager
     */
    protected ByteCodeFileManager(JavaFileManager fileManager) {
        super(fileManager);
        byteCodeClassLoader = new ByteCodeClassLoader(getClass().getClassLoader());
    }

    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
        this.byteCodeJavaFileObject = new ByteCodeJavaFileObject(className, kind);
        this.byteCodeClassLoader.setByteCodeJavaFileObject(byteCodeJavaFileObject);
        return this.byteCodeJavaFileObject;
    }

    @Override
    public ClassLoader getClassLoader(Location location) {
        return byteCodeClassLoader;
    }

    public byte[] getByteCode() {
        return byteCodeJavaFileObject.getBytes();
    }
}

  1. ByteCodeJavaFileObject ,StringJavaFileObject
    StringJavaFileObject用于传递要编译的java源码
    ByteCodeJavaFileObject用于获取编译之后的字节码
package com.thghh.bcode.compiler;

import javax.tools.SimpleJavaFileObject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;

/**
 * @author Zhikang.Peng
 * @version 1.0
 * @email thghh@qq.com
 * @date 2020/12/4 16:51
 */
public class ByteCodeJavaFileObject extends SimpleJavaFileObject {

    private ByteArrayOutputStream outputStream;
    private String className;

    /**
     * Construct a SimpleJavaFileObject of the given kind and with the
     * given class name.
     *
     * @param className class name
     * @param kind      the kind of this file object
     */
    protected ByteCodeJavaFileObject(String className, Kind kind) {
        super(URI.create("bytecode://" + className.replaceAll("\\.", "/") + kind.extension), kind);
        this.className = className;
        this.outputStream = new ByteArrayOutputStream(512);
    }

    @Override
    public OutputStream openOutputStream() throws IOException {
        return this.outputStream;
    }

    /**
     * FileManager会使用该方法获取编译后的byte,然后将类加载到JVM
     */
    public byte[] getBytes() {
        return this.outputStream.toByteArray();
    }

    public String getClassName() {
        return className;
    }
}

package com.thghh.bcode.compiler;

import javax.tools.SimpleJavaFileObject;
import java.io.IOException;
import java.net.URI;

/**
 * @author Zhikang.Peng
 * @version 1.0
 * @email thghh@qq.com
 * @date 2020/12/4 16:42
 */
public class StringJavaFileObject extends SimpleJavaFileObject {

    private CharSequence content;
    /**
     * Construct a SimpleJavaFileObject of the given className and with the
     * given content.
     *
     * @param className class name
     * @param content java source string
     */
    protected StringJavaFileObject(String className, String content) {
        super(URI.create("bytecode://" + className.replaceAll("\\.", "/") + Kind.SOURCE.extension),  Kind.SOURCE);
        this.content = content;
    }

    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
        return content;
    }

}

  1. 自定义class文件加载器
    在findClass方法最后一定要判断是否是自己编译的class文件,不然你会遇到非常意外的错误,我在Property使用内省机制的时候就出现一个问题,在Introspector.getBeanInfo(clazz)代码里面会莫名奇妙的生成一个PropertyBeanInfo的className去查有没有这个类,最后debug发现在
    com.sun.beans.finder.InstanceFinder类中find方法有一个拼接操作
    String var2 = var1.getName() + this.suffix; 我很奇怪这BeanInfo后缀是怎么出现的,最后在com.sun.beans.finder.BeanInfoFinder的构造函数中有,我直呼好家伙
com.sun.beans.finder.BeanInfoFinder
public BeanInfoFinder() {
        super(BeanInfo.class, true, "BeanInfo", new String[]{"sun.beans.infos"});
    }
    
com.sun.beans.finder.InstanceFinder
public T find(Class<?> var1) {
        if (var1 == null) {
            return null;
        } else {
            String var2 = var1.getName() + this.suffix;
            Object var3 = this.instantiate(var1, var2);
            if (var3 != null) {
                return var3;
            } else {
                if (this.allow) {
                    var3 = this.instantiate(var1, (String)null);
                    if (var3 != null) {
                        return var3;
                    }
                }

                int var4 = var2.lastIndexOf(46) + 1;
                if (var4 > 0) {
                    var2 = var2.substring(var4);
                }

                String[] var5 = this.packages;
                int var6 = var5.length;

                for(int var7 = 0; var7 < var6; ++var7) {
                    String var8 = var5[var7];
                    var3 = this.instantiate(var1, var8, var2);
                    if (var3 != null) {
                        return var3;
                    }
                }

                return null;
            }
        }
    }
package com.thghh.bcode.compiler;

/**
 * @author Zhikang.Peng
 * @version 1.0
 * @email thghh@qq.com
 * @date 2020/12/10 17:11
 */
public class ByteCodeClassLoader extends ClassLoader {

    private ByteCodeJavaFileObject byteCodeJavaFileObject;

    public ByteCodeClassLoader(ClassLoader parent) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
    }

    public void setByteCodeJavaFileObject(ByteCodeJavaFileObject byteCodeJavaFileObject) {
        this.byteCodeJavaFileObject = byteCodeJavaFileObject;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if (byteCodeJavaFileObject.getClassName().equals(name)) {
            byte[] bytecode = byteCodeJavaFileObject.getBytes();
            return defineClass(name, bytecode, 0, bytecode.length);
        }
        throw new ClassNotFoundException(name);
    }
}

  1. java内省机制获取字段的读写方法
package com.thghh.bcode.model;

import com.thghh.table.BeanColumn;
import com.thghh.table.CheckException;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author Zhikang.Peng
 * @version 1.0
 * @email thghh@qq.com
 * @date 2020/12/8 10:46
 */
public class Property {

    @Getter(AccessLevel.NONE)
    @Setter(AccessLevel.NONE)
    protected Map<String, PropertyDescriptor> propertyMap;

    public Property() {
        propertyMap = new HashMap<>();
        analyze();
    }

    /**
     * 对当前类进行分析
     */
    public void analyze() {
        try {
            Class clazz = this.getClass();
            Field[] fields = clazz.getDeclaredFields();
            BeanInfo beanInfo = Introspector.getBeanInfo(clazz);
            PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
            for (Field field : fields) {
                // A comment of the specified type exists
                if (field.isAnnotationPresent(BeanColumn.class)) {
                    BeanColumn beanColumn = field.getAnnotation(BeanColumn.class);
                    // title name and column index
                    String name = beanColumn.name();
                    for (int i = 0; i < propertyDescriptors.length; i++) {
                        String propertyName = propertyDescriptors[i].getName();
                        if (field.getName().equalsIgnoreCase(propertyName)) {
                            propertyMap.put(name, propertyDescriptors[i]);
                        }
                    }
                }
            }
        } catch (Exception e) {
            throw new CheckException(e);
        }
    }

    /**
     * 对指定名称的字段设置值
     *
     * @param propertyName
     * @param value
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     */
    public void setValue(String propertyName, Object value) throws IllegalAccessException, InvocationTargetException {
        propertyMap.get(propertyName).getWriteMethod().invoke(this, value);
    }

    /**
     * 获取指定字段的值
     *
     * @param propertyName
     * @return
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     */
    public Object getValue(String propertyName) throws IllegalAccessException, InvocationTargetException {
        return propertyMap.get(propertyName).getReadMethod().invoke(this, null);
    }
}

package com.thghh.table;

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

/**
 * @author Zhikang.Peng
 * @version 1.0
 * @email thghh@qq.com
 * @date 2020/9/11 10:39
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BeanColumn {
    /**
     * Table name
     */
    String name();
    /**
     * Table title name index
     */
    int index();
    /**
     * is modify
     */
    boolean editable() default false;
}
package com.thghh.bcode.model;

import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author Zhikang.Peng
 * @version 1.0
 * @email thghh@qq.com
 * @date 2020/12/8 15:27
 */
@Data
@NoArgsConstructor
public class PropertyModel {

    private String propertyName;
    private String propertyType;
    private boolean property;
    // annotation
    private String displayPropertyName;
    private int propertyIndex;
    private boolean editable;
}
package com.thghh.bcode.model;

import lombok.Data;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Zhikang.Peng
 * @version 1.0
 * @email thghh@qq.com
 * @date 2020/12/8 15:39
 */
@Data
public class SourceFileModel {
    private String packageName;
    private String className;
}

  1. 测试
package com.thghh.bcode.ftl;

import com.thghh.bcode.compiler.DynamicJavaCompiler;
import com.thghh.bcode.model.Property;
import com.thghh.bcode.model.PropertyModel;
import com.thghh.bcode.model.SourceFileModel;
import org.junit.Test;

import java.util.ArrayList;
import java.util.List;

public class GenerateJavaSourceTest {

    @Test
    public void generateFile() throws Exception {
        GenerateJavaSource generateJavaSource = new GenerateJavaSource();

        SourceFileModel sourceFileModel = new SourceFileModel();
        sourceFileModel.setPackageName("com.thghh");
        sourceFileModel.setClassName("TestJava");

        PropertyModel propertyModel = new PropertyModel();
        propertyModel.setPropertyName("name");
        propertyModel.setPropertyType(String.class.getName());
        propertyModel.setProperty(true);
        propertyModel.setDisplayPropertyName("Name");
        propertyModel.setPropertyIndex(0);
        propertyModel.setEditable(true);

        List<PropertyModel> properties = new ArrayList<>();
        properties.add(propertyModel);

        byte[] source = generateJavaSource.generateFile(sourceFileModel, properties);
        System.out.println(new String(source));
        DynamicJavaCompiler compiler = new DynamicJavaCompiler();
        boolean isCompile = compiler.javaCompiler("com.thghh.TestJava", new String(source));
        if (isCompile) {
            Class<?> clazz = compiler.loadClass("com.thghh.TestJava");
            Property p = (Property) clazz.newInstance();
            p.setValue("name", "kang");
            System.out.println(p.toString());
        }
    }
}
  1. 项目结构
    项目结构
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值