diff --git a/core/pom.xml b/core/pom.xml
index 69c39b47e04..0d260ded280 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -211,6 +211,10 @@
ognl
ognl
+
+ com.alibaba
+ qlexpress4
+
org.junit.vintage
junit-vintage-engine
diff --git a/core/src/main/java/com/taobao/arthas/core/GlobalOptions.java b/core/src/main/java/com/taobao/arthas/core/GlobalOptions.java
index f9b9f0df36c..ec91af0870a 100644
--- a/core/src/main/java/com/taobao/arthas/core/GlobalOptions.java
+++ b/core/src/main/java/com/taobao/arthas/core/GlobalOptions.java
@@ -142,6 +142,15 @@ public class GlobalOptions {
description = STRICT_MESSAGE
)
public static volatile boolean strict = true;
+ /**
+ * 是否切换使用表达式ognl/qlexpress开关
+ */
+ @Option(level = 1,
+ name = "el",
+ summary = "expression language",
+ description = "Option to use ognl/qlexpress in commands, default ognl, can change to qlexpress"
+ )
+ public static volatile String ExpressType = "ognl";
public static void updateOnglStrict(boolean strict) {
try {
diff --git a/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java b/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java
index 262793812e1..f1f153b6a35 100644
--- a/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java
+++ b/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java
@@ -77,6 +77,7 @@ private void initCommands(List disabledCommands) {
commandClassList.add(PerfCounterCommand.class);
// commandClassList.add(GroovyScriptCommand.class);
commandClassList.add(OgnlCommand.class);
+ commandClassList.add(QLExpressCommand.class);
commandClassList.add(MemoryCompilerCommand.class);
commandClassList.add(RedefineCommand.class);
commandClassList.add(RetransformCommand.class);
diff --git a/core/src/main/java/com/taobao/arthas/core/command/basic1000/QLExpressCommand.java b/core/src/main/java/com/taobao/arthas/core/command/basic1000/QLExpressCommand.java
new file mode 100644
index 00000000000..fe2a9e3b262
--- /dev/null
+++ b/core/src/main/java/com/taobao/arthas/core/command/basic1000/QLExpressCommand.java
@@ -0,0 +1,110 @@
+package com.taobao.arthas.core.command.basic1000;
+
+import com.alibaba.arthas.deps.org.slf4j.Logger;
+import com.alibaba.arthas.deps.org.slf4j.LoggerFactory;
+import com.taobao.arthas.core.command.Constants;
+import com.taobao.arthas.core.command.express.Express;
+import com.taobao.arthas.core.command.express.ExpressException;
+import com.taobao.arthas.core.command.express.ExpressFactory;
+import com.taobao.arthas.core.command.model.ClassLoaderVO;
+import com.taobao.arthas.core.command.model.ObjectVO;
+import com.taobao.arthas.core.command.model.QLExpressModel;
+import com.taobao.arthas.core.shell.command.AnnotatedCommand;
+import com.taobao.arthas.core.shell.command.CommandProcess;
+import com.taobao.arthas.core.util.ClassLoaderUtils;
+import com.taobao.arthas.core.util.ClassUtils;
+import com.taobao.middleware.cli.annotations.*;
+
+import java.lang.instrument.Instrumentation;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @Author TaoKan
+ * @Date 2025/3/23 8:59 PM
+ */
+@Name("qlexpress")
+@Summary("Execute qlexpress expression.")
+@Description(Constants.EXAMPLE
+ + " qlexpress 'java.lang.System.out.println(\"hello~\")' \n"
+ + " qlexpress -x 2 'java.lang.Math.abs(1)' \n"
+ + " qlexpress -c 5d113a51 'com.taobao.arthas.core.GlobalOptions.isDump' \n"
+ + Constants.WIKI + Constants.WIKI_HOME + "qlexpress\n"
+ + " https://github.com/alibaba/QLExpress/tree/v4.0.0-beta.1")
+public class QLExpressCommand extends AnnotatedCommand {
+ private static final Logger logger = LoggerFactory.getLogger(QLExpressCommand.class);
+
+ private String express;
+ private String hashCode;
+ private String classLoaderClass;
+ private int expand = 1;
+
+ @Argument(argName = "express", index = 0, required = true)
+ @Description("The qlexpress expression.")
+ public void setExpress(String express) {
+ this.express = express;
+ }
+
+ @Option(shortName = "c", longName = "classLoader")
+ @Description("The hash code of the special class's classLoader, default classLoader is SystemClassLoader.")
+ public void setHashCode(String hashCode) {
+ this.hashCode = hashCode;
+ }
+
+
+ @Option(longName = "classLoaderClass")
+ @Description("The class name of the special class's classLoader.")
+ public void setClassLoaderClass(String classLoaderClass) {
+ this.classLoaderClass = classLoaderClass;
+ }
+
+
+ @Option(shortName = "x", longName = "expand")
+ @Description("Expand level of object (1 by default).")
+ public void setExpand(Integer expand) {
+ this.expand = expand;
+ }
+ @Override
+ public void process(CommandProcess process) {
+ Instrumentation inst = process.session().getInstrumentation();
+ ClassLoader classLoader = null;
+ if (hashCode != null) {
+ classLoader = ClassLoaderUtils.getClassLoader(inst, hashCode);
+ if (classLoader == null) {
+ process.end(-1, "Can not find classloader with hashCode: " + hashCode + ".");
+ return;
+ }
+ } else if (classLoaderClass != null) {
+ List matchedClassLoaders = ClassLoaderUtils.getClassLoaderByClassName(inst, classLoaderClass);
+ if (matchedClassLoaders.size() == 1) {
+ classLoader = matchedClassLoaders.get(0);
+ } else if (matchedClassLoaders.size() > 1) {
+ Collection classLoaderVOList = ClassUtils.createClassLoaderVOList(matchedClassLoaders);
+ QLExpressModel qlModel = new QLExpressModel()
+ .setClassLoaderClass(classLoaderClass)
+ .setMatchedClassLoaders(classLoaderVOList);
+ process.appendResult(qlModel);
+ process.end(-1, "Found more than one classloader by class name, please specify classloader with '-c '");
+ return;
+ } else {
+ process.end(-1, "Can not find classloader by class name: " + classLoaderClass + ".");
+ return;
+ }
+ } else {
+ classLoader = ClassLoader.getSystemClassLoader();
+ }
+
+ Express unpooledExpress = ExpressFactory.unpooledExpressByQL(classLoader);
+ try {
+ Object value = unpooledExpress.bind(new Object()).get(express);
+ QLExpressModel qlModel = new QLExpressModel()
+ .setValue(new ObjectVO(value, expand));
+ process.appendResult(qlModel);
+ process.end();
+ } catch (ExpressException e) {
+ logger.warn("qlexpress: failed execute express: " + express, e);
+ process.end(-1, "Failed to execute qlexpress, exception message: " + e.getMessage()
+ + ", please check $HOME/logs/arthas/arthas.log for more details. ");
+ }
+ }
+}
diff --git a/core/src/main/java/com/taobao/arthas/core/command/express/ExpressFactory.java b/core/src/main/java/com/taobao/arthas/core/command/express/ExpressFactory.java
index d2d578bc17f..5e66e3c5984 100644
--- a/core/src/main/java/com/taobao/arthas/core/command/express/ExpressFactory.java
+++ b/core/src/main/java/com/taobao/arthas/core/command/express/ExpressFactory.java
@@ -1,5 +1,9 @@
package com.taobao.arthas.core.command.express;
+import com.taobao.arthas.core.GlobalOptions;
+import com.taobao.arthas.core.command.model.ExpressTypeEnum;
+
+
/**
* ExpressFactory
* @author ralf0131 2017-01-04 14:40.
@@ -7,12 +11,8 @@
*/
public class ExpressFactory {
- private static final ThreadLocal expressRef = new ThreadLocal() {
- @Override
- protected Express initialValue() {
- return new OgnlExpress();
- }
- };
+ private static final ThreadLocal expressRef = ThreadLocal.withInitial(() -> new OgnlExpress());
+ private static final ThreadLocal expressRefQLExpress = ThreadLocal.withInitial(() -> new QLExpress());
/**
* get ThreadLocal Express Object
@@ -20,10 +20,31 @@ protected Express initialValue() {
* @return
*/
public static Express threadLocalExpress(Object object) {
+ if (GlobalOptions.ExpressType.equals(ExpressTypeEnum.QLEXPRESS.getExpressType())) {
+ return expressRefQLExpress.get().reset().bind(object);
+ }
return expressRef.get().reset().bind(object);
}
public static Express unpooledExpress(ClassLoader classloader) {
+ if (classloader == null) {
+ classloader = ClassLoader.getSystemClassLoader();
+ }
+ if (GlobalOptions.ExpressType.equals(ExpressTypeEnum.QLEXPRESS.getExpressType())) {
+ return new QLExpress(new QLExpressClassLoaderClassResolver(classloader));
+ }
+ return new OgnlExpress(new ClassLoaderClassResolver(classloader));
+ }
+
+
+ public static Express unpooledExpressByQL(ClassLoader classloader) {
+ if (classloader == null) {
+ classloader = ClassLoader.getSystemClassLoader();
+ }
+ return new QLExpress(new QLExpressClassLoaderClassResolver(classloader));
+ }
+
+ public static Express unpooledExpressByOGNL(ClassLoader classloader) {
if (classloader == null) {
classloader = ClassLoader.getSystemClassLoader();
}
diff --git a/core/src/main/java/com/taobao/arthas/core/command/express/QLExpress.java b/core/src/main/java/com/taobao/arthas/core/command/express/QLExpress.java
new file mode 100644
index 00000000000..eac806f08d5
--- /dev/null
+++ b/core/src/main/java/com/taobao/arthas/core/command/express/QLExpress.java
@@ -0,0 +1,78 @@
+package com.taobao.arthas.core.command.express;
+
+import com.alibaba.arthas.deps.org.slf4j.Logger;
+import com.alibaba.arthas.deps.org.slf4j.LoggerFactory;
+import com.alibaba.qlexpress4.*;
+import com.alibaba.qlexpress4.security.QLSecurityStrategy;
+
+
+/**
+ * @Author TaoKan
+ * @Date 2024/9/17 6:01 PM
+ */
+public class QLExpress implements Express {
+ private static final Logger logger = LoggerFactory.getLogger(QLExpress.class);
+ private Express4Runner expressRunner;
+ private QLGlobalContext qlGlobalContext;
+
+ private QLOptions qlOptions;
+
+ private InitOptions initOptions;
+
+ public QLExpress() {
+ this(QLExpressCustomClassResolver.customClassResolver);
+ }
+
+ public QLExpress(ClassSupplier classResolver) {
+ this.initOptions = initQLExpress(classResolver);
+ this.expressRunner = QLExpressRunner.getInstance(initOptions);
+ this.qlOptions = initConfig();
+ this.qlGlobalContext = new QLGlobalContext(expressRunner);
+ }
+
+ private QLOptions initConfig() {
+ return QLOptions.DEFAULT_OPTIONS;
+ }
+
+ private InitOptions initQLExpress(ClassSupplier classResolver) {
+ InitOptions.Builder initOptionsBuilder = InitOptions.builder();
+ initOptionsBuilder.securityStrategy(QLSecurityStrategy.open());
+ initOptionsBuilder.allowPrivateAccess(true);
+ initOptionsBuilder.classSupplier(classResolver);
+ return initOptionsBuilder.build();
+ }
+
+ @Override
+ public Object get(String express) throws ExpressException {
+ try {
+ return expressRunner.execute(express, qlGlobalContext, qlOptions).getResult();
+ } catch (Exception e) {
+ logger.error("Error during evaluating the expression with QLExpress:", e);
+ throw new ExpressException(express, e);
+ }
+ }
+
+ @Override
+ public boolean is(String express) throws ExpressException {
+ final Object ret = get(express);
+ return ret instanceof Boolean && (Boolean) ret;
+ }
+
+ @Override
+ public Express bind(Object object) {
+ qlGlobalContext.bindObj(object);
+ return this;
+ }
+
+ @Override
+ public Express bind(String name, Object value) {
+ qlGlobalContext.put(name, value);
+ return this;
+ }
+
+ @Override
+ public Express reset() {
+ qlGlobalContext.clear();
+ return this;
+ }
+}
diff --git a/core/src/main/java/com/taobao/arthas/core/command/express/QLExpressClassLoaderClassResolver.java b/core/src/main/java/com/taobao/arthas/core/command/express/QLExpressClassLoaderClassResolver.java
new file mode 100644
index 00000000000..70a469c3b63
--- /dev/null
+++ b/core/src/main/java/com/taobao/arthas/core/command/express/QLExpressClassLoaderClassResolver.java
@@ -0,0 +1,40 @@
+package com.taobao.arthas.core.command.express;
+
+import com.alibaba.qlexpress4.ClassSupplier;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @Author TaoKan
+ * @Date 2024/12/1 7:07 PM
+ */
+public class QLExpressClassLoaderClassResolver implements ClassSupplier {
+
+ private ClassLoader classLoader;
+
+ private final Map>> cache = new ConcurrentHashMap<>();
+
+ public QLExpressClassLoaderClassResolver(ClassLoader classLoader) {
+ this.classLoader = classLoader;
+ }
+
+ private Optional> loadClsInner(String clsQualifiedName) {
+ try {
+ Class> aClass = null;
+ if (classLoader != null) {
+ aClass = classLoader.loadClass(clsQualifiedName);
+ }else {
+ aClass = Class.forName(clsQualifiedName);
+ }
+ return Optional.of(aClass);
+ } catch (ClassNotFoundException | NoClassDefFoundError e) {
+ return Optional.empty();
+ }
+ }
+ @Override
+ public Class> loadCls(String className) {
+ Optional> clsOp = cache.computeIfAbsent(className, this::loadClsInner);
+ return clsOp.orElse(null);
+ }
+}
diff --git a/core/src/main/java/com/taobao/arthas/core/command/express/QLExpressCustomClassResolver.java b/core/src/main/java/com/taobao/arthas/core/command/express/QLExpressCustomClassResolver.java
new file mode 100644
index 00000000000..9c127fe50e4
--- /dev/null
+++ b/core/src/main/java/com/taobao/arthas/core/command/express/QLExpressCustomClassResolver.java
@@ -0,0 +1,44 @@
+package com.taobao.arthas.core.command.express;
+
+import com.alibaba.arthas.deps.org.slf4j.Logger;
+import com.alibaba.arthas.deps.org.slf4j.LoggerFactory;
+import com.alibaba.qlexpress4.ClassSupplier;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @Author TaoKan
+ * @Date 2024/12/1 7:06 PM
+ */
+public class QLExpressCustomClassResolver implements ClassSupplier {
+
+ public static final QLExpressCustomClassResolver customClassResolver = new QLExpressCustomClassResolver();
+
+ private final Map>> cache = new ConcurrentHashMap<>();
+
+ private QLExpressCustomClassResolver() {
+
+ }
+
+ private Optional> loadClsInner(String clsQualifiedName) {
+ try {
+ ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ Class> aClass = null;
+ if (classLoader != null) {
+ aClass = classLoader.loadClass(clsQualifiedName);
+ } else {
+ aClass = Class.forName(clsQualifiedName);
+ }
+ return Optional.of(aClass);
+ } catch (ClassNotFoundException | NoClassDefFoundError e) {
+ return Optional.empty();
+ }
+ }
+ @Override
+ public Class> loadCls(String clsQualifiedName) {
+ Optional> clsOp = cache.computeIfAbsent(clsQualifiedName, this::loadClsInner);
+ return clsOp.orElse(null);
+ }
+
+}
diff --git a/core/src/main/java/com/taobao/arthas/core/command/express/QLExpressRunner.java b/core/src/main/java/com/taobao/arthas/core/command/express/QLExpressRunner.java
new file mode 100644
index 00000000000..c9c18f603b8
--- /dev/null
+++ b/core/src/main/java/com/taobao/arthas/core/command/express/QLExpressRunner.java
@@ -0,0 +1,34 @@
+package com.taobao.arthas.core.command.express;
+
+
+import com.alibaba.qlexpress4.Express4Runner;
+import com.alibaba.qlexpress4.InitOptions;
+
+/**
+ * @Author TaoKan
+ * @Date 2024/9/22 12:20 PM
+ */
+public class QLExpressRunner {
+ private volatile static QLExpressRunner instance = null;
+ private Express4Runner expressRunner;
+
+ private QLExpressRunner(InitOptions initOptions){
+ expressRunner = new Express4Runner(initOptions);
+ }
+
+ //对外提供静态方法获取对象
+ public static Express4Runner getInstance(InitOptions initOptions){
+ //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
+ if(instance == null){
+ synchronized (QLExpressRunner.class){
+ //抢到锁之后再次进行判断是否为null
+ if(instance == null){
+ instance = new QLExpressRunner(initOptions);
+ }
+ }
+ }
+ return instance.expressRunner;
+ }
+
+
+}
diff --git a/core/src/main/java/com/taobao/arthas/core/command/express/QLGlobalContext.java b/core/src/main/java/com/taobao/arthas/core/command/express/QLGlobalContext.java
new file mode 100644
index 00000000000..85ac3840aa6
--- /dev/null
+++ b/core/src/main/java/com/taobao/arthas/core/command/express/QLGlobalContext.java
@@ -0,0 +1,59 @@
+package com.taobao.arthas.core.command.express;
+
+import com.alibaba.arthas.deps.org.slf4j.Logger;
+import com.alibaba.arthas.deps.org.slf4j.LoggerFactory;
+import com.alibaba.qlexpress4.Express4Runner;
+import com.alibaba.qlexpress4.runtime.Value;
+import com.alibaba.qlexpress4.runtime.context.ExpressContext;
+import com.alibaba.qlexpress4.runtime.context.ObjectFieldExpressContext;
+import com.alibaba.qlexpress4.runtime.data.MapItemValue;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @Author TaoKan
+ * @Date 2024/9/22 12:39 PM
+ */
+public class QLGlobalContext implements ExpressContext {
+ private static final Logger logger = LoggerFactory.getLogger(QLGlobalContext.class);
+ private Map context;
+ private ObjectFieldExpressContext objectFieldExpressContext;
+ private Express4Runner express4Runner;
+
+ public QLGlobalContext(Express4Runner expressRunner) {
+ this.context = new ConcurrentHashMap<>();
+ this.express4Runner = expressRunner;
+ }
+
+ public void put(String name, Object value){
+ context.put(name, value);
+ }
+
+ public void clear() {
+ context.clear();
+ }
+
+ public void bindObj(Object object) {
+ this.objectFieldExpressContext = new ObjectFieldExpressContext(object, express4Runner);
+ context.put("object",object);
+ }
+ @Override
+ public Value get(Map attachments, String variableName) {
+ Value getFromLoadField = objectFieldExpressContext.get(attachments, variableName);
+ if (getFromLoadField == null || getFromLoadField.get() == null) {
+ return new MapItemValue(this.context, variableName);
+ }
+ return getFromLoadField;
+ }
+
+
+ public Map getContext() {
+ return context;
+ }
+
+ public void setContext(Map context) {
+ this.context = context;
+ }
+
+}
diff --git a/core/src/main/java/com/taobao/arthas/core/command/klass100/OgnlCommand.java b/core/src/main/java/com/taobao/arthas/core/command/klass100/OgnlCommand.java
index b2dd6125dcd..e2216b1b684 100644
--- a/core/src/main/java/com/taobao/arthas/core/command/klass100/OgnlCommand.java
+++ b/core/src/main/java/com/taobao/arthas/core/command/klass100/OgnlCommand.java
@@ -31,13 +31,13 @@
@Name("ognl")
@Summary("Execute ognl expression.")
@Description(Constants.EXAMPLE
- + " ognl '@java.lang.System@out.println(\"hello \\u4e2d\\u6587\")' \n"
- + " ognl -x 2 '@Singleton@getInstance()' \n"
- + " ognl '@Demo@staticFiled' \n"
- + " ognl '#value1=@System@getProperty(\"java.home\"), #value2=@System@getProperty(\"java.runtime.name\"), {#value1, #value2}'\n"
- + " ognl -c 5d113a51 '@com.taobao.arthas.core.GlobalOptions@isDump' \n"
- + Constants.WIKI + Constants.WIKI_HOME + "ognl\n"
- + " https://commons.apache.org/proper/commons-ognl/language-guide.html")
+ + " ognl '@java.lang.System@out.println(\"hello \\u4e2d\\u6587\")' \n"
+ + " ognl -x 2 '@Singleton@getInstance()' \n"
+ + " ognl '@Demo@staticFiled' \n"
+ + " ognl '#value1=@System@getProperty(\"java.home\"), #value2=@System@getProperty(\"java.runtime.name\"), {#value1, #value2}'\n"
+ + " ognl -c 5d113a51 '@com.taobao.arthas.core.GlobalOptions@isDump' \n"
+ + Constants.WIKI + Constants.WIKI_HOME + "ognl\n"
+ + " https://commons.apache.org/proper/commons-ognl/language-guide.html")
public class OgnlCommand extends AnnotatedCommand {
private static final Logger logger = LoggerFactory.getLogger(OgnlCommand.class);
@@ -100,7 +100,7 @@ public void process(CommandProcess process) {
classLoader = ClassLoader.getSystemClassLoader();
}
- Express unpooledExpress = ExpressFactory.unpooledExpress(classLoader);
+ Express unpooledExpress = ExpressFactory.unpooledExpressByOGNL(classLoader);
try {
// https://github.com/alibaba/arthas/issues/2892
Object value = unpooledExpress.bind(new Object()).get(express);
diff --git a/core/src/main/java/com/taobao/arthas/core/command/model/ExpressTypeEnum.java b/core/src/main/java/com/taobao/arthas/core/command/model/ExpressTypeEnum.java
new file mode 100644
index 00000000000..5191f875d04
--- /dev/null
+++ b/core/src/main/java/com/taobao/arthas/core/command/model/ExpressTypeEnum.java
@@ -0,0 +1,21 @@
+package com.taobao.arthas.core.command.model;
+
+/**
+ * @Author TaoKan
+ * @Date 2024/9/22 7:32 AM
+ */
+public enum ExpressTypeEnum
+{
+ OGNL("ognl"),
+ QLEXPRESS("qlexpress");
+
+ private String expressType;
+
+ ExpressTypeEnum(String expressType) {
+ this.expressType = expressType;
+ }
+
+ public String getExpressType() {
+ return expressType;
+ }
+}
diff --git a/core/src/main/java/com/taobao/arthas/core/command/model/QLExpressConfigModel.java b/core/src/main/java/com/taobao/arthas/core/command/model/QLExpressConfigModel.java
new file mode 100644
index 00000000000..05348c6213d
--- /dev/null
+++ b/core/src/main/java/com/taobao/arthas/core/command/model/QLExpressConfigModel.java
@@ -0,0 +1,97 @@
+package com.taobao.arthas.core.command.model;
+
+/**
+ * @Author TaoKan
+ * @Date 2024/9/22 7:51 AM
+ */
+public class QLExpressConfigModel {
+ //QL_OPTIONS
+ private boolean precise = false;
+ private boolean polluteUserContext = false;
+ private long timeoutMillis = -1L;
+
+ private boolean cache = false;
+ private boolean avoidNullPointer = false;
+ private int maxArrLength = -1;
+
+
+ //INIT_OPTIONS
+ private boolean allowPrivateAccess = true;
+ private boolean debug;
+ private boolean useCacheClear;
+
+ public boolean isAllowPrivateAccess() {
+ return allowPrivateAccess;
+ }
+
+ public void setAllowPrivateAccess(boolean allowPrivateAccess) {
+ this.allowPrivateAccess = allowPrivateAccess;
+ }
+
+ public boolean isDebug() {
+ return debug;
+ }
+
+ public void setDebug(boolean debug) {
+ this.debug = debug;
+ }
+
+ public boolean isUseCacheClear() {
+ return useCacheClear;
+ }
+
+ public void setUseCacheClear(boolean useCacheClear) {
+ this.useCacheClear = useCacheClear;
+ }
+
+
+
+ public boolean isPrecise() {
+ return precise;
+ }
+
+ public void setPrecise(boolean precise) {
+ this.precise = precise;
+ }
+
+ public boolean isPolluteUserContext() {
+ return polluteUserContext;
+ }
+
+ public void setPolluteUserContext(boolean polluteUserContext) {
+ this.polluteUserContext = polluteUserContext;
+ }
+
+ public long getTimeoutMillis() {
+ return timeoutMillis;
+ }
+
+ public void setTimeoutMillis(long timeoutMillis) {
+ this.timeoutMillis = timeoutMillis;
+ }
+
+ public boolean isAvoidNullPointer() {
+ return avoidNullPointer;
+ }
+
+ public void setAvoidNullPointer(boolean avoidNullPointer) {
+ this.avoidNullPointer = avoidNullPointer;
+ }
+
+ public int getMaxArrLength() {
+ return maxArrLength;
+ }
+
+ public void setMaxArrLength(int maxArrLength) {
+ this.maxArrLength = maxArrLength;
+ }
+
+
+ public boolean isCache() {
+ return cache;
+ }
+
+ public void setCache(boolean cache) {
+ this.cache = cache;
+ }
+}
diff --git a/core/src/main/java/com/taobao/arthas/core/command/model/QLExpressModel.java b/core/src/main/java/com/taobao/arthas/core/command/model/QLExpressModel.java
new file mode 100644
index 00000000000..9394d89b80c
--- /dev/null
+++ b/core/src/main/java/com/taobao/arthas/core/command/model/QLExpressModel.java
@@ -0,0 +1,47 @@
+package com.taobao.arthas.core.command.model;
+
+import java.util.Collection;
+
+/**
+ * @Author TaoKan
+ * @Date 2025/3/23 9:12 PM
+ */
+public class QLExpressModel extends ResultModel {
+ private ObjectVO value;
+
+ private Collection matchedClassLoaders;
+ private String classLoaderClass;
+
+
+ @Override
+ public String getType() {
+ return "ql";
+ }
+
+ public ObjectVO getValue() {
+ return value;
+ }
+
+ public QLExpressModel setValue(ObjectVO value) {
+ this.value = value;
+ return this;
+ }
+
+ public String getClassLoaderClass() {
+ return classLoaderClass;
+ }
+
+ public QLExpressModel setClassLoaderClass(String classLoaderClass) {
+ this.classLoaderClass = classLoaderClass;
+ return this;
+ }
+
+ public Collection getMatchedClassLoaders() {
+ return matchedClassLoaders;
+ }
+
+ public QLExpressModel setMatchedClassLoaders(Collection matchedClassLoaders) {
+ this.matchedClassLoaders = matchedClassLoaders;
+ return this;
+ }
+}
diff --git a/core/src/main/java/com/taobao/arthas/core/command/view/QLExpressView.java b/core/src/main/java/com/taobao/arthas/core/command/view/QLExpressView.java
new file mode 100644
index 00000000000..8df3a3e6a55
--- /dev/null
+++ b/core/src/main/java/com/taobao/arthas/core/command/view/QLExpressView.java
@@ -0,0 +1,28 @@
+package com.taobao.arthas.core.command.view;
+
+import com.taobao.arthas.core.command.model.ObjectVO;
+import com.taobao.arthas.core.command.model.OgnlModel;
+import com.taobao.arthas.core.command.model.QLExpressModel;
+import com.taobao.arthas.core.shell.command.CommandProcess;
+import com.taobao.arthas.core.util.StringUtils;
+import com.taobao.arthas.core.view.ObjectView;
+
+/**
+ * @Author TaoKan
+ * @Date 2025/9/26 10:57 PM
+ */
+public class QLExpressView extends ResultView {
+ @Override
+ public void draw(CommandProcess process, QLExpressModel model) {
+ if (model.getMatchedClassLoaders() != null) {
+ process.write("Matched classloaders: \n");
+ ClassLoaderView.drawClassLoaders(process, model.getMatchedClassLoaders(), false);
+ process.write("\n");
+ return;
+ }
+
+ ObjectVO objectVO = model.getValue();
+ String resultStr = StringUtils.objectToString(objectVO.needExpand() ? new ObjectView(objectVO).draw() : objectVO.getObject());
+ process.write(resultStr).write("\n");
+ }
+}
diff --git a/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java b/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java
index 947b60a1ee6..f5527952273 100644
--- a/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java
+++ b/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java
@@ -56,6 +56,7 @@ private void initResultViews() {
registerView(JadView.class);
registerView(MemoryCompilerView.class);
registerView(OgnlView.class);
+ registerView(QLExpressView.class);
registerView(RedefineView.class);
registerView(RetransformView.class);
registerView(SearchClassView.class);
diff --git a/core/src/test/java/com/taobao/arthas/core/command/express/QLExpressTest.java b/core/src/test/java/com/taobao/arthas/core/command/express/QLExpressTest.java
new file mode 100644
index 00000000000..1d8ff8ecb1b
--- /dev/null
+++ b/core/src/test/java/com/taobao/arthas/core/command/express/QLExpressTest.java
@@ -0,0 +1,49 @@
+package com.taobao.arthas.core.command.express;
+
+import com.taobao.arthas.core.GlobalOptions;
+import com.taobao.arthas.core.advisor.Advice;
+import com.taobao.arthas.core.command.model.ExpressTypeEnum;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @Author TaoKan
+ * @Date 2025/1/12 5:31 PM
+ */
+public class QLExpressTest {
+
+ private Express express;
+
+ @BeforeEach
+ public void setUp() {
+ FlowContext context = new FlowContext();
+ Object[] params = new Object[4];
+ params[0] = context;
+ Advice advice = Advice.newForAfterReturning(null, getClass(), null, null, params, null);
+ GlobalOptions.ExpressType = ExpressTypeEnum.QLEXPRESS.getExpressType();
+ express = ExpressFactory.unpooledExpress(null).bind(advice).bind("cost", 123);
+ }
+
+ @Test
+ public void testStringEquals() throws ExpressException {
+ String conditionExpress = "\"aaa\".equals(params[0].flowAttribute.getBxApp())";
+ boolean result = express.is(conditionExpress);
+ assertTrue(result);
+ }
+
+ @Test
+ public void testObjectEquals() throws ExpressException {
+ String conditionExpress = "params[0].flowAttribute.getBxApp().equals(\"aaa\")";
+ boolean result = express.is(conditionExpress);
+ assertTrue(result);
+ }
+
+ @Test
+ public void testEqualSign() throws ExpressException {
+ String conditionExpress = "\"aaa\" == params[0].flowAttribute.getBxApp()";
+ boolean result = express.is(conditionExpress);
+ assertTrue(result);
+ }
+}
diff --git a/pom.xml b/pom.xml
index 3fabb644ba2..05fb7d21b7c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -163,6 +163,11 @@
ognl
3.3.5
+
+ com.alibaba
+ qlexpress4
+ 4.0.3
+
org.junit
junit-bom
diff --git a/site/docs/doc/commands.md b/site/docs/doc/commands.md
index 41b87c3685c..a78ae6bd94e 100644
--- a/site/docs/doc/commands.md
+++ b/site/docs/doc/commands.md
@@ -16,6 +16,7 @@
- [thread](thread.md) - 查看当前 JVM 的线程堆栈信息
- [vmoption](vmoption.md) - 查看和修改 JVM 里诊断相关的 option
- [vmtool](vmtool.md) - 从 jvm 里查询对象,执行 forceGc
+- [qlexpress](qlexpress.md) - 执行 qlexpress 表达式
## class/classloader 相关
diff --git a/site/docs/doc/options.md b/site/docs/doc/options.md
index 910ed728181..a6e26679763 100644
--- a/site/docs/doc/options.md
+++ b/site/docs/doc/options.md
@@ -6,20 +6,20 @@
全局开关
:::
-| 名称 | 默认值 | 描述 |
+| 名称 | 默认值 | 描述 |
| ---------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| unsafe | false | 是否支持对系统级别的类进行增强,打开该开关可能导致把 JVM 搞挂,请慎重选择! |
-| dump | false | 是否支持被增强了的类 dump 到外部文件中,如果打开开关,class 文件会被 dump 到`/${application working dir}/arthas-class-dump/`目录下,具体位置详见控制台输出 |
-| batch-re-transform | true | 是否支持批量对匹配到的类执行 retransform 操作 |
-| json-format | false | 是否支持 json 化的输出 |
-| disable-sub-class | false | 是否禁用子类匹配,默认在匹配目标类的时候会默认匹配到其子类,如果想精确匹配,可以关闭此开关 |
-| support-default-method | true | 是否支持匹配到 default method, 默认会查找 interface,匹配里面的 default method。参考 [#1105](https://github.com/alibaba/arthas/issues/1105) |
-| save-result | false | 是否打开执行结果存日志功能,打开之后所有命令的运行结果都将保存到`~/logs/arthas-cache/result.log`中 |
-| job-timeout | 1d | 异步后台任务的默认超时时间,超过这个时间,任务自动停止;比如设置 1d, 2h, 3m, 25s,分别代表天、小时、分、秒 |
-| print-parent-fields | true | 是否打印在 parent class 里的 filed |
-| verbose | false | 是否打印更多详细信息 |
-| strict | true | 是否启用 strict 模式 |
-
+| unsafe | false | 是否支持对系统级别的类进行增强,打开该开关可能导致把 JVM 搞挂,请慎重选择! |
+| dump | false | 是否支持被增强了的类 dump 到外部文件中,如果打开开关,class 文件会被 dump 到`/${application working dir}/arthas-class-dump/`目录下,具体位置详见控制台输出 |
+| batch-re-transform | true | 是否支持批量对匹配到的类执行 retransform 操作 |
+| json-format | false | 是否支持 json 化的输出 |
+| disable-sub-class | false | 是否禁用子类匹配,默认在匹配目标类的时候会默认匹配到其子类,如果想精确匹配,可以关闭此开关 |
+| support-default-method | true | 是否支持匹配到 default method, 默认会查找 interface,匹配里面的 default method。参考 [#1105](https://github.com/alibaba/arthas/issues/1105) |
+| save-result | false | 是否打开执行结果存日志功能,打开之后所有命令的运行结果都将保存到`~/logs/arthas-cache/result.log`中 |
+| job-timeout | 1d | 异步后台任务的默认超时时间,超过这个时间,任务自动停止;比如设置 1d, 2h, 3m, 25s,分别代表天、小时、分、秒 |
+| print-parent-fields | true | 是否打印在 parent class 里的 filed |
+| verbose | false | 是否打印更多详细信息 |
+| strict | true | 是否启用 strict 模式 |
+| el | ognl | 表达式语言,是否切换使用表达式ognl/qlexpress开关 |
## 查看所有的 options
```bash
@@ -62,6 +62,8 @@ $ options
n e ed to set object properties. Want to set o
bject properties, execute `options strict
false`
+ 1 String el ognl expression language Option to use ognl/qlexpress in commands,
+ express default ognl, can change to qlexpress.
```
## 获取 option 的值
diff --git a/site/docs/doc/qlexpress.md b/site/docs/doc/qlexpress.md
new file mode 100644
index 00000000000..dcf919a8b19
--- /dev/null
+++ b/site/docs/doc/qlexpress.md
@@ -0,0 +1,72 @@
+# qlexpress
+
+[`qlexpress`在线教程](https://github.com/alibaba/QLExpress)
+
+::: tip
+执行 qlexpress 表达式
+:::
+
+## 参数说明
+
+| 参数名称 | 参数说明 |
+|-----------------------:| :--------------------------------------------------------------- |
+| _express_ | 执行的表达式 |
+| `[c:]` | 执行表达式的 ClassLoader 的 hashcode,默认值是 SystemClassLoader |
+| `[classLoaderClass:]` | 指定执行表达式的 ClassLoader 的 class name |
+| `[x]` | 结果对象的展开层次,默认值 1 |
+
+## 使用参考
+
+- QLExpress 表达式官方指南:[https://github.com/alibaba/QLExpress](https://github.com/alibaba/QLExpress)
+
+调用静态函数:
+
+```bash
+$ qlexpress 'java.lang.System.out.println("hello~")'
+null
+```
+
+获取静态类的静态字段:
+
+```bash
+$ qlexpress 'com.taobao.arthas.core.GlobalOptions.isDump'
+```
+
+导入应用中的类并且使用:
+
+```bash
+$ qlexpress 'import com.alibaba.qlexpress4.QLImportTester; QLImportTester.add(1,2);'
+```
+
+
+通过 hashcode 指定 ClassLoader:
+
+```bash
+$ classloader -t
++-BootstrapClassLoader
++-jdk.internal.loader.ClassLoaders$PlatformClassLoader@301ec38b
+ +-com.taobao.arthas.agent.ArthasClassloader@472067c7
+ +-jdk.internal.loader.ClassLoaders$AppClassLoader@4b85612c
+ +-org.springframework.boot.loader.LaunchedURLClassLoader@7f9a81e8
+
+$ qlexpress -c 7f9a81e8 org.springframework.boot.SpringApplication.logger
+@Slf4jLocationAwareLog[
+ FQCN=@String[org.apache.commons.logging.LogAdapter$Slf4jLocationAwareLog],
+ name=@String[org.springframework.boot.SpringApplication],
+ logger=@Logger[Logger[org.springframework.boot.SpringApplication]],
+]
+$
+```
+
+注意 hashcode 是变化的,需要先查看当前的 ClassLoader 信息,提取对应 ClassLoader 的 hashcode。
+
+对于只有唯一实例的 ClassLoader 可以通过 class name 指定,使用起来更加方便:
+
+```bash
+$ qlexpress --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader org.springframework.boot.SpringApplication.logger
+@Slf4jLocationAwareLog[
+ FQCN=@String[org.apache.commons.logging.LogAdapter$Slf4jLocationAwareLog],
+ name=@String[org.springframework.boot.SpringApplication],
+ logger=@Logger[Logger[org.springframework.boot.SpringApplication]],
+]
+```
\ No newline at end of file
diff --git a/site/docs/en/doc/commands.md b/site/docs/en/doc/commands.md
index 799b6418bce..189569b9412 100644
--- a/site/docs/en/doc/commands.md
+++ b/site/docs/en/doc/commands.md
@@ -16,6 +16,7 @@
- [thread](thread.md) - show java thread information
- [vmoption](vmoption.md) - view/modify the vm diagnostic options.
- [vmtool](vmtool.md) - jvm tool, getInstances in jvm, forceGc
+- [qlexpress](qlexpress.md) - execute qlexpress expression
## class/classloader - related
diff --git a/site/docs/en/doc/options.md b/site/docs/en/doc/options.md
index 59c38070135..e0cee74d40a 100644
--- a/site/docs/en/doc/options.md
+++ b/site/docs/en/doc/options.md
@@ -7,7 +7,7 @@ Global options
:::
| Name | Default Value | Description |
-| ---------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+|------------------------| ------------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| unsafe | false | whether to enhance to system-level class. Use it with caution since JVM may hang |
| dump | false | whether to dump enhanced class to the external files. If it's on, enhanced class will be dumped into `/${application dir}/arthas-class-dump/`, the specific output path will be output in the console |
| batch-re-transform | true | whether to re-transform matched classes in batch |
@@ -19,7 +19,7 @@ Global options
| print-parent-fields | true | This option enables print files in parent class, default value true. |
| verbose | false | This option enables print verbose information |
| strict | true | whether to enable strict mode |
-
+| el | ognl | Option to use ognl/qlexpress in commands, default ognl, can change to qlexpress |
## View all options
```bash
@@ -62,6 +62,8 @@ $ options
n e ed to set object properties. Want to set o
bject properties, execute `options strict
false`
+ 1 String el ognl expression language Option to use ognl/qlexpress in commands,
+ express default ognl, can change to qlexpress.
```
## Get special option value