perf: GroovyClassLoader 全局单例,避免每次执行创建新 ClassLoader

This commit is contained in:
mediabot-pt
2026-07-01 10:35:33 +08:00
parent 3073653296
commit 83f6a729c2

View File

@@ -2,6 +2,7 @@ package com.par.core.adapter;
import com.fasterxml.jackson.databind.ObjectMapper;
import groovy.lang.Binding;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyShell;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
@@ -11,19 +12,16 @@ import java.util.*;
/**
* Groovy 脚本解析引擎。
* 脚本可访问以下内置变量:
* content - String,页面内容(HTML 或 JSON)
* url - String,访问的 URL
* jsoup - 可直接调用 Jsoup.parse() 等
* json - ObjectMapper,可直接调用 readTree() 等
* utils - 工具方法集合
* 脚本应返回 Map<String, Object> 或 List<Map<String, Object>>
* 共享 GroovyClassLoader 避免频繁创建 ClassLoader,每次执行复用同一个。
* 脚本可访问内置变量:content, url, jsoup, json, utils
*/
@Slf4j
@Service
public class GroovyScriptEngine {
private static final ObjectMapper objectMapper = new ObjectMapper();
/** 全局单例 ClassLoader,避免每次 new GroovyShell 产生新 ClassLoader */
private static final GroovyClassLoader classLoader = new GroovyClassLoader();
@SuppressWarnings("unchecked")
public Map<String, Object> execute(String groovyScript, String content, String url) {
@@ -37,61 +35,67 @@ public class GroovyScriptEngine {
binding.setVariable("json", objectMapper);
binding.setVariable("utils", new ScriptUtils());
GroovyShell shell = new GroovyShell(binding);
GroovyShell shell = new GroovyShell(classLoader, binding);
Object scriptResult = shell.evaluate(groovyScript);
if (scriptResult instanceof Map) {
result.put("matched", true);
Map<String, Object> m = (Map<String, Object>) scriptResult;
result.put("pageName", m.getOrDefault("pageName", "Groovy脚本"));
result.put("fieldCount", m.containsKey("fields") ? ((Map<?, ?>) m.get("fields")).size() : 0);
// 有 fields map → 展示为一行
if (m.containsKey("fields")) {
result.put("extracted", List.of(m.get("fields")));
result.put("rowCount", 1);
} else if (m.containsKey("extracted")) {
result.put("extracted", m.get("extracted"));
result.put("rowCount", m.getOrDefault("rowCount", ((List<?>) m.get("extracted")).size()));
} else {
result.put("extracted", List.of(m));
result.put("rowCount", 1);
}
result.put("message", m.getOrDefault("message", "解析成功"));
} else if (scriptResult instanceof List) {
result.put("matched", true);
result.put("pageName", "Groovy脚本");
result.put("extracted", (List<?>) scriptResult);
result.put("rowCount", ((List<?>) scriptResult).size());
result.put("fieldCount", !((List<?>) scriptResult).isEmpty() && ((List<?>) scriptResult).get(0) instanceof Map
? ((Map<?, ?>) ((List<?>) scriptResult).get(0)).size() : 0);
result.put("message", "共 " + ((List<?>) scriptResult).size() + " 行");
} else if (scriptResult != null) {
result.put("matched", true);
result.put("pageName", "Groovy脚本");
result.put("extracted", List.of(Map.of("result", scriptResult)));
result.put("rowCount", 1);
result.put("message", String.valueOf(scriptResult));
} else {
result.put("matched", true);
result.put("message", "脚本返回 null");
result.put("extracted", List.of());
result.put("rowCount", 0);
}
return buildResult(result, scriptResult);
} catch (Exception e) {
result.put("matched", false);
result.put("message", "Groovy 脚本执行异常: " + e.getMessage());
log.error("Groovy script error for url={}", url, e);
log.error("Groovy error for url={}", url, e);
}
return result;
}
private Map<String, Object> buildResult(Map<String, Object> result, Object scriptResult) {
if (scriptResult instanceof Map) {
result.put("matched", true);
Map<String, Object> m = (Map<String, Object>) scriptResult;
result.put("pageName", m.getOrDefault("pageName", "Groovy脚本"));
if (m.containsKey("fields")) {
result.put("extracted", List.of(m.get("fields")));
result.put("rowCount", 1);
result.put("fieldCount", ((Map<?, ?>) m.get("fields")).size());
} else if (m.containsKey("extracted")) {
List<?> ex = (List<?>) m.get("extracted");
result.put("extracted", ex);
result.put("rowCount", ex.size());
result.put("fieldCount", !ex.isEmpty() && ex.get(0) instanceof Map
? ((Map<?, ?>) ex.get(0)).size() : 0);
} else {
result.put("extracted", List.of(m));
result.put("rowCount", 1);
result.put("fieldCount", m.size());
}
result.put("message", m.getOrDefault("message", "解析成功"));
} else if (scriptResult instanceof List) {
result.put("matched", true);
result.put("pageName", "Groovy脚本");
List<?> list = (List<?>) scriptResult;
result.put("extracted", list);
result.put("rowCount", list.size());
result.put("fieldCount", !list.isEmpty() && list.get(0) instanceof Map
? ((Map<?, ?>) list.get(0)).size() : 0);
result.put("message", "共 " + list.size() + " 行");
} else if (scriptResult != null) {
result.put("matched", true);
result.put("pageName", "Groovy脚本");
result.put("extracted", List.of(Map.of("result", scriptResult)));
result.put("rowCount", 1);
} else {
result.put("matched", true);
result.put("extracted", List.of());
result.put("rowCount", 0);
result.put("message", "脚本返回 null");
}
return result;
}
/** Jsoup 辅助类 */
public static class JsoupHelper {
public Object parse(String html) { return Jsoup.parse(html); }
public Object parse(String html, String baseUri) { return Jsoup.parse(html, baseUri); }
}
/** 脚本工具方法 */
public static class ScriptUtils {
public String text(Object node) {
try { return (String) node.getClass().getMethod("text").invoke(node); }
@@ -101,13 +105,9 @@ public class GroovyScriptEngine {
try { return (String) node.getClass().getMethod("attr", String.class).invoke(node, attr); }
catch (Exception e) { return null; }
}
public Object select(Object node, String cssQuery) {
try { return node.getClass().getMethod("select", String.class).invoke(node, cssQuery); }
catch (Exception e) { return null; }
}
public Object selectFirst(Object node, String cssQuery) {
try {
Object elements = select(node, cssQuery);
Object elements = node.getClass().getMethod("select", String.class).invoke(node, cssQuery);
if (elements instanceof List && !((List<?>) elements).isEmpty())
return ((List<?>) elements).get(0);
return null;