java動態編譯
阿新 • • 發佈:2021-06-22
1.背景
在某些應用中,我們的程式碼可能需要動態去執行...
意思就是說,你可以傳一段程式碼去執行
比如:假設我們的業務需要對接上游渠道,而這個上游渠道非常多,隨著業務的發展隨時都在增加渠道....
這就意味這個,如果不能動態載入程式碼的話,每上一個渠道,我們就要打包釋出一次專案...這樣很麻煩,而卻也不安全...容易出錯
直接程式碼
2.實現程式碼
package com.XXX.cashier.hw; import javax.tools.*; import java.io.ByteArrayOutputStream; import java.io.IOException; importjava.io.OutputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.net.URI; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @Copyright (C) XXXXXXXXXXX科技股份技有限公司 * * @Date: 2021-06-22 11:48 * @Description:*/ public class CustomStringJavaCompiler { //類全名 private String fullClassName; private String sourceCode; //存放編譯之後的位元組碼(key:類全名,value:編譯之後輸出的位元組碼) 也可以存放在磁碟上 private static Map<String, ByteJavaFileObject> javaFileObjectMap = new ConcurrentHashMap<>(); //獲取java的編譯器 privateJavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); //存放編譯過程中輸出的資訊 private DiagnosticCollector<JavaFileObject> diagnosticsCollector = new DiagnosticCollector<>(); //編譯耗時(單位ms) private long compilerTakeTime; public CustomStringJavaCompiler(String sourceCode) { this.sourceCode = sourceCode; this.fullClassName = getFullClassName(sourceCode); } /** * 編譯字串原始碼,編譯失敗在 diagnosticsCollector 中獲取提示資訊 * * @return true:編譯成功 false:編譯失敗 */ public boolean compiler() { long startTime = System.currentTimeMillis(); Object o = javaFileObjectMap.get(fullClassName); if (o != null) { //設定編譯耗時 compilerTakeTime = System.currentTimeMillis() - startTime; System.out.println("使用已有的編譯............"); return true; } //標準的內容管理器,更換成自己的實現,覆蓋部分方法 StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(diagnosticsCollector, null, null); JavaFileManager javaFileManager = new StringJavaFileManage(standardFileManager); //構造原始碼物件 JavaFileObject javaFileObject = new StringJavaFileObject(fullClassName, sourceCode); //獲取一個編譯任務 JavaCompiler.CompilationTask task = compiler.getTask(null, javaFileManager, diagnosticsCollector, null, null, Arrays.asList(javaFileObject)); //設定編譯耗時 compilerTakeTime = System.currentTimeMillis() - startTime; Boolean call = task.call(); return call; } /** * 執行send 方法 */ public String runMainMethod(String json) throws Exception { StringClassLoader stringClassLoader = new StringClassLoader(); Class<?> obj = stringClassLoader.findClass(fullClassName); Constructor<?> constructor = obj.getConstructor(); Object newInstance = constructor.newInstance(); // 獲取方法並執行 Method method01 = obj.getMethod("send", String.class); Object invoke = method01.invoke(newInstance, json); System.out.println("執行結束-->" + invoke); return invoke.toString(); } /** * @return 編譯資訊(錯誤 警告) */ public String getCompilerMessage() { StringBuilder sb = new StringBuilder(); List<Diagnostic<? extends JavaFileObject>> diagnostics = diagnosticsCollector.getDiagnostics(); for (Diagnostic diagnostic : diagnostics) { sb.append(diagnostic.toString()).append("\r\n"); } return sb.toString(); } public long getCompilerTakeTime() { return compilerTakeTime; } /** * 獲取類的全名稱 * * @param sourceCode 原始碼 * @return 類的全名稱 */ public static String getFullClassName(String sourceCode) { String className = ""; Pattern pattern = Pattern.compile("package\\s+\\S+\\s*;"); Matcher matcher = pattern.matcher(sourceCode); if (matcher.find()) { className = matcher.group().replaceFirst("package", "").replace(";", "").trim() + "."; } pattern = Pattern.compile("class\\s+\\S+\\s+\\{"); matcher = pattern.matcher(sourceCode); if (matcher.find()) { className += matcher.group().replaceFirst("class", "").replace("{", "").trim(); } return className; } /** * 自定義一個字串的原始碼物件 */ private class StringJavaFileObject extends SimpleJavaFileObject { //等待編譯的原始碼欄位 private String contents; //java原始碼 => StringJavaFileObject物件 的時候使用 public StringJavaFileObject(String className, String contents) { super(URI.create("string:///" + className.replaceAll("\\.", "/") + Kind.SOURCE.extension), Kind.SOURCE); this.contents = contents; } //字串原始碼會呼叫該方法 @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { return contents; } } /** * 自定義一個編譯之後的位元組碼物件 */ private class ByteJavaFileObject extends SimpleJavaFileObject { //存放編譯後的位元組碼 private ByteArrayOutputStream outPutStream; public ByteJavaFileObject(String className, Kind kind) { super(URI.create("string:///" + className.replaceAll("\\.", "/") + Kind.SOURCE.extension), kind); } //StringJavaFileManage 編譯之後的位元組碼輸出會呼叫該方法(把位元組碼輸出到outputStream) @Override public OutputStream openOutputStream() { outPutStream = new ByteArrayOutputStream(); return outPutStream; } //在類載入器載入的時候需要用到 public byte[] getCompiledBytes() { return outPutStream.toByteArray(); } } /** * 自定義一個JavaFileManage來控制編譯之後位元組碼的輸出位置 */ private class StringJavaFileManage extends ForwardingJavaFileManager { StringJavaFileManage(JavaFileManager fileManager) { super(fileManager); } //獲取輸出的檔案物件,它表示給定位置處指定型別的指定類。 @Override public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException { ByteJavaFileObject javaFileObject = new ByteJavaFileObject(className, kind); javaFileObjectMap.put(className, javaFileObject); return javaFileObject; } } /** * 自定義類載入器, 用來載入動態的位元組碼 */ private class StringClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { ByteJavaFileObject fileObject = javaFileObjectMap.get(name); if (fileObject != null) { byte[] bytes = fileObject.getCompiledBytes(); return defineClass(name, bytes, 0, bytes.length); } try { return ClassLoader.getSystemClassLoader().loadClass(name); } catch (Exception e) { return super.findClass(name); } } } }
3.測試
@Test public void test() { // 這段程式碼可以是資料庫或者磁碟檔案讀取 String code = "package com.XXXXX.cashier.order.testCode;\n" + "import com.alibaba.fastjson.JSONObject;\n" + "import lombok.extern.slf4j.Slf4j;\n" + "@Slf4j\n" + "public class Demo01 {\n" + " public String send(String json) {\n" + " log.info(\"json=\" + json);\n" + " log.info(\"run method01-------------\");\n" + " JSONObject jsonObject = JSONObject.parseObject(json);\n" + " String orderNO = jsonObject.getString(\"orderNo\");\n" + " JSONObject object = new JSONObject();\n" + " object.put(\"code\", \"100\");\n" + " object.put(\"msg\", \"success\");\n" + " object.put(\"orderNo\", orderNO);\n" + " object.put(\"createTime\", System.currentTimeMillis());\n" + " log.info(\"run end-------------createTime=\"+object.get(\"createTime\"));\n" + " log.info(\"run end-------------orderNO=\"+orderNO);\n" + " return object.toJSONString();\n" + " }\n" + "}"; // 第一次執行 method(code); System.out.println("-------------------"); // 第二次執行 method(code); } private void method(String code) { CustomStringJavaCompiler compiler = new CustomStringJavaCompiler(code); boolean res = compiler.compiler(); if (res) { try { JSONObject object = new JSONObject(); object.put("productNO", "P001203"); object.put("orderNo", "001203"); String result = compiler.runMainMethod(object.toJSONString()); System.out.println("-----------------result=" + result); } catch (Exception e) { e.printStackTrace(); } System.out.println("診斷資訊:" + compiler.getCompilerMessage()); } else { System.out.println("編譯失敗"); System.out.println(compiler.getCompilerMessage()); } }
執行結果:
4.注意
如果報錯找不到:
java.lang.NoClassDefFoundError: com/sun/tools/javac/processing/JavacProcessingEnvironment
需要引入tools.jar包