编译动态生成的java文件
前言
之前在做一个项目的时候,遇到过字段经常变动烦不胜烦,最后想是否有办法根据配置文件生成java bean,然后编译生java文件来使用,最后查到到 FreeMarker根据模板来生成java文件,用javax.tools.JavaCompiler或者Eclipse java compiler(ECJ)编译java文件为字节码.
FreeMarker根据模板来生成java文件
maven依赖
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.30</version>
</dependency>
- 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();
}
}
- 生成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();
}
}
- 模板文件
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依赖比较好处理一些。
- 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>
- 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();
}
}
- 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();
}
}
- 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;
}
}
- 自定义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);
}
}
- 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;
}
- 测试
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());
}
}
}
- 项目结构