diff --git a/core/pom.xml b/core/pom.xml index 69c39b47e04..7bbe0004b4b 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -211,6 +211,10 @@ ognl ognl + + com.alibaba + QLExpress4 + org.junit.vintage junit-vintage-engine @@ -263,6 +267,10 @@ provided true + + ognl + ognl + 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..62ae94e288d 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,26 @@ public class GlobalOptions { description = STRICT_MESSAGE ) public static volatile boolean strict = true; + /** + * 是否切换使用表达式ognl/qlexpress开关 + */ + @Option(level = 1, + name = "express-type", + summary = "Option to use ognl/qlexpress", + description = "Option to use ognl/qlexpress in commands, default ognl, can change to qlexpress" + ) + public static volatile String ExpressType = "ognl"; + + + /** + * qlexpress使用参数 + */ + @Option(level = 1, + name = "qlexpress-config", + summary = "config init when use qlexpress, with json-simple, for example: {\"precise\": true }", + description = "" + ) + public static volatile String QLExpressConfig = ""; public static void updateOnglStrict(boolean strict) { try { 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..207f7aac848 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,23 @@ 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 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..5c4da297397 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/express/QLExpress.java @@ -0,0 +1,109 @@ +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.fastjson2.JSON; +import com.alibaba.qlexpress4.ClassSupplier; +import com.alibaba.qlexpress4.Express4Runner; +import com.alibaba.qlexpress4.InitOptions; +import com.alibaba.qlexpress4.QLOptions; +import com.alibaba.qlexpress4.runtime.ReflectLoader; +import com.alibaba.qlexpress4.security.QLSecurityStrategy; +import com.taobao.arthas.core.GlobalOptions; +import com.taobao.arthas.core.command.model.QLExpressConfigModel; + + +/** + * @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) { + initQLExpress(classResolver); + initConfig(); + initContext(); + } + + private void initConfig() { + try { + if (GlobalOptions.QLExpressConfig.length() > 0) { + QLOptions.Builder qlOptionsBuilder = QLOptions.builder(); + QLExpressConfigModel qlExpressConfigModel = JSON.parseObject(GlobalOptions.QLExpressConfig, QLExpressConfigModel.class); + qlOptionsBuilder.cache(qlExpressConfigModel.isCache()); + qlOptionsBuilder.avoidNullPointer(qlExpressConfigModel.isAvoidNullPointer()); + qlOptionsBuilder.maxArrLength(qlExpressConfigModel.getMaxArrLength()); + qlOptionsBuilder.polluteUserContext(qlExpressConfigModel.isPolluteUserContext()); + qlOptionsBuilder.precise(qlExpressConfigModel.isPrecise()); + qlOptionsBuilder.timeoutMillis(qlExpressConfigModel.getTimeoutMillis()); + qlOptions = qlOptionsBuilder.build(); + }else { + qlOptions = QLOptions.DEFAULT_OPTIONS; + } + //4.0设置InitOptions + }catch (Throwable t){ + //异常不设置options + logger.error("Error Init Options For QLExpress:", t); + } + } + + private void initQLExpress(ClassSupplier classResolver) { + InitOptions.Builder initOptionsBuilder = InitOptions.builder(); + initOptionsBuilder.securityStrategy(QLSecurityStrategy.open()); + initOptionsBuilder.allowPrivateAccess(true); + initOptionsBuilder.classSupplier(classResolver); + initOptions = initOptionsBuilder.build(); + expressRunner = QLExpressRunner.getInstance(initOptions); + } + + private void initContext() { + ReflectLoader reflectLoader = new ReflectLoader(initOptions.getSecurityStrategy(), initOptions.getExtensionFunctions(), initOptions.isAllowPrivateAccess()); + qlGlobalContext = new QLGlobalContext(reflectLoader); + } + + @Override + public Object get(String express) throws ExpressException { + try { + Object result = expressRunner.execute(express, qlGlobalContext, qlOptions); + return result; + } 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..fa6e46f9054 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/express/QLGlobalContext.java @@ -0,0 +1,70 @@ +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.fastjson2.JSON; +import com.alibaba.qlexpress4.exception.PureErrReporter; +import com.alibaba.qlexpress4.runtime.ReflectLoader; +import com.alibaba.qlexpress4.runtime.Value; +import com.alibaba.qlexpress4.runtime.context.ExpressContext; +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 Object object; + private ReflectLoader reflectLoader; + + public QLGlobalContext(ReflectLoader reflectLoader) { + this.context = new ConcurrentHashMap<>(); + this.reflectLoader = reflectLoader; + } + + public void put(String name, Object value){ + context.put(name, value); + } + + public void clear() { + context.clear(); + this.context.put("reflectLoader",reflectLoader); + } + + public void bindObj(Object object) { + this.object = object; + context.put("object",object); + } + @Override + public Value get(Map attachments, String variableName) { + if ((this.reflectLoader != null) && (this.object != null) && !variableName.startsWith("#")) { + return this.reflectLoader.loadField(this.object, variableName, true, PureErrReporter.INSTANCE); + } + String newVariableName = variableName.replace("#",""); + return new MapItemValue(this.context, newVariableName); + } + + + public Map getContext() { + return context; + } + + public void setContext(Map context) { + this.context = context; + } + + public Object getObject() { + return object; + } + + public void setObject(Object object) { + this.object = object; + } + +} 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/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..b701c276b2a --- /dev/null +++ b/core/src/test/java/com/taobao/arthas/core/command/express/QLExpressTest.java @@ -0,0 +1,50 @@ +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 ognl.OgnlException; +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() throws ExpressException { + 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 OgnlException, ExpressException { + String conditionExpress = "\"aaa\".equals(params[0].flowAttribute.getBxApp())"; + boolean result = express.is(conditionExpress); + assertTrue(result); + } + + @Test + public void testObjectEquals() throws OgnlException, ExpressException { + String conditionExpress = "params[0].flowAttribute.getBxApp().equals(\"aaa\")"; + boolean result = express.is(conditionExpress); + assertTrue(result); + } + + @Test + public void testEqualSign() throws OgnlException, 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..bba957fec9d 100644 --- a/pom.xml +++ b/pom.xml @@ -163,6 +163,11 @@ ognl 3.3.5 + + com.alibaba + QLExpress4 + 4.0.0-beta.2 + org.junit junit-bom