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