文章目录
- 前言
- Tool和ToolProvider
- 编译器工具:JavaCompiler
- 文件管理
- 文件:FileObject
- 文件管理器:JavaFileManager
- 诊断监听器:Diagnostic
- Demo:allPowerfulInterface
- 具体实现
- 测试
- 结语
前言
当生产环境出现问题时,经常需要走一个很繁琐的上线流程进行部署,然后再解决问题,于是我想写一个万能接口来避免这个流程,这个接口的思想是:
- 将写好逻辑的Java源程序上传到指定的线上HTTP接口
- 线上编译并运行上传的源代码
这实际上就是Java编译器API的使用,本文就带着大家来学习和实现一下,本文实现的Demo具有以下特点:
- 实现了MyBatis万能查询接口
- 实现了与Spring框架的完美融合,丝滑在上传源代码中使用IOC中的Bean
Tool和ToolProvider
Tool
是所有可以从程序中调用的工具的公共接口,这些工具通常是命令行程序,例如编译器等。该接口的run
方法使用给定的I/O通道和参数运行该工具。返回0
表示成功,返回非0
表示错误。生成的任何诊断信息将以某种未指定的格式写入输出流或错误流。
/**
*in:如果为空则使用标准输入
*out:如果为空则使用标准输出
*err:如果为空则使用标准错误
*arguments:该工具运行时需要的参数
*/
int run(InputStream in, OutputStream out, OutputStream err, String... arguments)
ToolProvider
是一个工具提供类,用于提供一些实现了Tool
接口的工具:
static DocumentationTool getSystemDocumentationTool()
static JavaCompiler getSystemJavaCompiler()
编译器工具:JavaCompiler
JavaCompiler
是用于从代码中调用 Java 编译器的工具。该工具依赖于以下两个服务:
- 文件管理器:编译器工具有一个相关联的标准文件管理器,可以通过调用
getStandardFileManager
方法获取,文件管理器有以下两方面的作用:- 解决自定义编译任务如何的读取和写入文件的问题
- 可以在多个自定义编译任务之间共享,以此减少扫描和读取文件的开销
- 诊断监听器:在编译过程中,编译器可能会生成诊断信息(例如错误消息)。如果提供了诊断监听器,这些诊断信息将会提供给监听器。如果未提供监听器,则诊断信息将以未指定的格式进行格式化,并写入默认输出流。即使提供了诊断监听器,某些诊断信息可能也不适合放在一个诊断对象中,而会被写入默认输出。
该接口提供以下方法:
/**
*diagnosticListener:用于非致命诊断的诊断侦听器,如果为空,则使用编译器的默认方法来报告诊断
*locale:格式化诊断时要应用的语言环境;Null表示默认区域设置。
*charset:用于解码字节的字符集,如果为空,则使用平台默认值。
*/
StandardJavaFileManager getStandardFileManager(DiagnosticListener<? super JavaFileObject> diagnosticListener, Locale locale, Charset charset);
/**
*out:一个Writer用于编译器的额外输出,如果为零则默认为标准错误流
*fileManager:文件管理器,如果为空则默认使用编译器的标准管理器
*diagnosticListener:诊断侦听器;如果为空,则使用编译器的默认方法来报告诊断
*options:编译器选项
*classes:注解处理要处理的类名,空表示没有类名
*compilationUnits:要编译的编译单元,null表示没有编译单元
*/
JavaCompiler.CompilationTask getTask(Writer out, JavaFileManager fileManager, DiagnosticListener<? super JavaFileObject> diagnosticListener, Iterable<String> options, Iterable<String> classes, Iterable<? extends JavaFileObject> compilationUnits)
其中getTask
方法可以建立一个CompilationTask
编译任务,该任务接口继承了Callabel
接口,并提供了以下方法设置一个注解处理器:
void setProcessors(Iterable<? extends Processor> processors)
文件管理
文件:FileObject
FileObject
用于表示Java编程语言中的文件,这些文件包括:
JavaFileObject.Kind.CLASS
:字节码文件JavaFileObject.Kind.SOURCE
:源文件JavaFileObject.Kind.HTML
:HTMLJavaFileObject.Kind.OTHER
:除此之外的其他文件
它有两个子实现,其中:
ForwardingFileObject<F extends FileObject>
:是一个FileObject
的包装类,可以将调用转发给指定的文件对象。JavaFileObject
:Java源文件和Java字节码文件的抽象。
文件管理器:JavaFileManager
JavaFileManager
是用于操作 Java 编程语言中文件的文件管理器。在构建新的 JavaFileObjects
时,文件管理器必须确定创建它们的位置。例如,如果文件管理器管理文件系统上的常规文件,它很可能会有一个当前/工作目录作为创建或查找文件的默认位置。可以向文件管理器提供一些提示,以确定创建文件的位置,但任何文件管理器都可能选择忽略这些提示。它有以下两个实现类:
ForwardingJavaFileManager<M extends JavaFileManager>
:文件管理器的包装类,可以将调用转发给指定的文件管理器。StandardJavaFileManager
:基于java.io.File
和java.nio.file.Path
的文件管理器。
诊断监听器:Diagnostic
和诊断监听器有关的类如下,它们简单易懂,在此不再赘述。
Demo:allPowerfulInterface
通过以下四个类就可以实现该功能了,具体代码放到下边了。
具体实现
AllPowerfulController.java
:请求入口
@Slf4j
@RestController
@RequestMapping("/manual/allPowerful")
@RequiredArgsConstructor
public class AllPowerfulController {
private final CompileLoadAndRun compileLoadAndRun;
@PostMapping("/go")
public CommonRespDTO test(@RequestParam("sourceFile") MultipartFile sourceFile) throws IOException {
return compileLoadAndRun.go(sourceFile.getBytes());
}
}
CompileLoadAndRun.java
:负责编译、加载并运行上传的源文件
@Slf4j
@Component
@RequiredArgsConstructor
public class CompileLoadAndRun {
private final ApplicationContext applicationContext;
private static final String CLASS_PATH = "./target/classes/generated";
//换成你自己的
private static final String CLASS_NAME = "xxx.allPowerfulInterface.handler.AllPowerFulHandler";
public CommonRespDTO go(byte[] sourceCode) {
//获取编译器和文件管理器
JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(null, null, StandardCharsets.UTF_8);
//创建编译任务,将编译后的类放到类路径下
JavaCompiler.CompilationTask task = javaCompiler.getTask(
null,
fileManager,
null,
List.of("-d", CLASS_PATH),
null,
List.of(new MemoryJavaFileObject(CLASS_NAME, sourceCode)));
Boolean success = task.call();
//编译成功后执行
if (success) {
try {
//加载类
File classPath = new File(CLASS_PATH);
URLClassLoader classLoader = URLClassLoader.newInstance(new URL[]{classPath.toURI().toURL()});
Class<?> clazz = Class.forName(CLASS_NAME, true, classLoader);
//构建对象,通过构造函数构造,构造函数参数要从Spring容器中获取
ArrayList<Class<?>> constructorParamClass = new ArrayList<>();
ArrayList<Object> constructorParamObj = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
constructorParamClass.add(field.getType());
constructorParamObj.add(applicationContext.getBean(field.getType()));
}
//获取构造函数并构造对象
Constructor<?> constructor = clazz.getConstructor(constructorParamClass.toArray(new Class[]{}));
Object target = constructor.newInstance(constructorParamObj.toArray(new Object[]{}));
//获取并执行方法
Method method = clazz.getDeclaredMethod("handle");
method.invoke(target);
} catch (MalformedURLException | ClassNotFoundException | InvocationTargetException |
NoSuchMethodException | IllegalAccessException | InstantiationException e) {
log.error("执行失败", e);
return CommonRespDTO.error(1, "执行失败");
}
return CommonRespDTO.success("执行成功");
} else {
return CommonRespDTO.error(1, "编译失败");
}
}
private static class MemoryJavaFileObject extends SimpleJavaFileObject {
private final CharSequence sourceCode;
public MemoryJavaFileObject(String className, byte[] sourceCode) {
super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.sourceCode = new String(sourceCode, StandardCharsets.UTF_8);
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return sourceCode;
}
}
}
IHandler.java
:用来约束上传源文件的行为,上传的源文件必须实现该接口
public interface IHandler {
void handle();
}
AllPowerFulManager.java
:封装Mapper并实现查询结果的转换
@Slf4j
@Service
@RequiredArgsConstructor
public class AllPowerFulManager {
private final AllPowerFulMapper allPowerFulMapper;
public void insert(String table, String fields, String values) {
allPowerFulMapper.insert(table, fields, values);
}
public void delete(String table, String condition) {
allPowerFulMapper.delete(table, condition);
}
public void update(String table, String fieldValues, String condition) {
allPowerFulMapper.update(table, fieldValues, condition);
}
public <T> List<T> select(String table, String fields, String condition, Class<T> clazz) {
ArrayList<T> result = new ArrayList<>();
List<Map<String, Object>> unMappedList = allPowerFulMapper.select(table, fields, condition);
for (Map<String, Object> unMappedMap : unMappedList) {
try {
T target = clazz.getConstructor().newInstance();
for (String key : unMappedMap.keySet()) {
String fieldName = this.toCamelCase(key);
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
if (field.getType() == Byte.class) {
field.set(target, Byte.valueOf(unMappedMap.get(key).toString()));
} else if (field.getType() == byte.class) {
field.set(target, Byte.parseByte(unMappedMap.get(key).toString()));
} else if (field.getType() == Integer.class) {
field.set(target, Integer.valueOf(unMappedMap.get(key).toString()));
} else if (field.getType() == int.class) {
field.set(target, Integer.parseInt(unMappedMap.get(key).toString()));
} else if (field.getType() == Long.class) {
field.set(target, Long.valueOf(unMappedMap.get(key).toString()));
} else if (field.getType() == long.class) {
field.set(target, Long.parseLong(unMappedMap.get(key).toString()));
} else if (field.getType() == Float.class) {
field.set(target, Float.valueOf(unMappedMap.get(key).toString()));
} else if (field.getType() == float.class) {
field.set(target, Float.parseFloat(unMappedMap.get(key).toString()));
} else if (field.getType() == Double.class) {
field.set(target, Double.valueOf(unMappedMap.get(key).toString()));
} else if (field.getType() == double.class) {
field.set(target, Double.parseDouble(unMappedMap.get(key).toString()));
} else {
field.set(target, unMappedMap.get(key));
}
} catch (NoSuchFieldException e) {
log.error("字段不存在", e);
}
}
result.add(target);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException |
NoSuchMethodException e) {
log.error("实例化失败", e);
}
}
return result;
}
private String toCamelCase(String str) {
if (str == null || str.isEmpty()) {
return str;
}
StringBuilder result = new StringBuilder();
boolean upperCase = false;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (c == '_') {
upperCase = true;
} else {
if (upperCase) {
result.append(Character.toUpperCase(c));
upperCase = false;
} else {
result.append(Character.toLowerCase(c));
}
}
}
return result.toString();
}
}
AllPowerFulMapper.java
:MyBatis万能接口:
@Mapper
public interface AllPowerFulMapper {
@Insert("INSERT INTO ${table}(${fields}) VALUES(${values})")
void insert(@Param("table") String table, @Param("fields") String fields, @Param("values") String values);
@Delete("DELETE FROM ${table} WHERE ${condition}")
void delete(@Param("table") String table, @Param("condition") String condition);
@Update("UPDATE ${table} SET ${fieldValues} WHERE ${condition}")
void update(@Param("table") String table, @Param("fieldValues") String fieldValues, @Param("condition") String condition);
@Select("SELECT ${fields} FROM ${table} WHERE ${condition}")
List<Map<String, Object>> select(@Param("table") String table, @Param("fields") String fields, @Param("condition") String condition);
}
测试
我们实现一个类:
@Component
@RequiredArgsConstructor
public class AllPowerFulHandler implements IHandler {
private final AllPowerFulManager allPowerFulManager;
public void handle() {
List<SkuEntity> skuList = allPowerFulManager.select("t_sku", "*", "1=1 limit 10", SkuEntity.class);
System.out.println(JSON.toJSONString(skuList));
}
}
然后使用IDEA的.http文件(manual.http)测试一下:
### 万能接口
POST http://localhost:8089/manual/allPowerful/go
Content-Type: multipart/form-data; boundary=WebAppBoundary
--WebAppBoundary
Content-Disposition: form-data; name="sourceFile";filename="AllPowerFulHandler.java";
Content-Type: application/json
< ./AllPowerFulHandler.java
--WebAppBoundary--
结果非常的完美。
结语
注意,该工具具有严重的安全隐患,请谨慎使用。