feat: Add object-size-limit GlobalOptions for ObjectView (#3147)

This commit is contained in:
hengyunabc
2026-03-03 20:45:47 +08:00
committed by GitHub
parent 7647f2f4c7
commit 977ed16645
12 changed files with 192 additions and 42 deletions
@@ -2,6 +2,7 @@ package com.taobao.arthas.core;
import java.lang.reflect.Field;
import com.taobao.arthas.common.ArthasConstants;
import com.taobao.arthas.common.JavaVersionUtils;
import com.taobao.arthas.common.UnsafeUtils;
@@ -66,6 +67,16 @@ public class GlobalOptions {
)
public static volatile boolean isUsingJson = false;
/**
* ObjectView 输出大小限制(字节)
*/
@Option(level = 1,
name = "object-size-limit",
summary = "Option to control ObjectView output size limit",
description = "Upper size limit in bytes for ObjectView output, must be greater than 0. Default value is 10 * 1024 * 1024."
)
public static volatile int objectSizeLimit = ArthasConstants.MAX_HTTP_CONTENT_LENGTH;
/**
* 是否关闭子类
*/
@@ -136,39 +136,55 @@ public class OptionsCommand extends AnnotatedCommand {
try {
// try to case string to type
if (isIn(type, int.class, Integer.class)) {
FieldUtils.writeStaticField(field, afterValue = Integer.valueOf(optionValue));
afterValue = Integer.valueOf(optionValue);
} else if (isIn(type, long.class, Long.class)) {
FieldUtils.writeStaticField(field, afterValue = Long.valueOf(optionValue));
afterValue = Long.valueOf(optionValue);
} else if (isIn(type, boolean.class, Boolean.class)) {
FieldUtils.writeStaticField(field, afterValue = Boolean.valueOf(optionValue));
afterValue = Boolean.valueOf(optionValue);
} else if (isIn(type, double.class, Double.class)) {
FieldUtils.writeStaticField(field, afterValue = Double.valueOf(optionValue));
afterValue = Double.valueOf(optionValue);
} else if (isIn(type, float.class, Float.class)) {
FieldUtils.writeStaticField(field, afterValue = Float.valueOf(optionValue));
afterValue = Float.valueOf(optionValue);
} else if (isIn(type, byte.class, Byte.class)) {
FieldUtils.writeStaticField(field, afterValue = Byte.valueOf(optionValue));
afterValue = Byte.valueOf(optionValue);
} else if (isIn(type, short.class, Short.class)) {
FieldUtils.writeStaticField(field, afterValue = Short.valueOf(optionValue));
afterValue = Short.valueOf(optionValue);
} else if (isIn(type, short.class, String.class)) {
FieldUtils.writeStaticField(field, afterValue = optionValue);
afterValue = optionValue;
} else {
return ExitStatus.failure(-1, format("Options[%s] type[%s] was unsupported.", optionName, type.getSimpleName()));
}
// FIXME hack for ongl strict
if (field.getName().equals("strict")) {
GlobalOptions.updateOnglStrict(Boolean.valueOf(optionValue));
logger.info("update ongl strict to: {}", optionValue);
}
} catch (Throwable t) {
return ExitStatus.failure(-1, format("Cannot cast option value[%s] to type[%s].", optionValue, type.getSimpleName()));
}
String validateError = validateOptionValue(optionAnnotation.name(), afterValue);
if (validateError != null) {
return ExitStatus.failure(-1, validateError);
}
FieldUtils.writeStaticField(field, afterValue);
// FIXME hack for ongl strict
if (field.getName().equals("strict")) {
GlobalOptions.updateOnglStrict(Boolean.valueOf(optionValue));
logger.info("update ongl strict to: {}", optionValue);
}
ChangeResultVO changeResultVO = new ChangeResultVO(optionAnnotation.name(), beforeValue, afterValue);
process.appendResult(new OptionsModel(changeResultVO));
return ExitStatus.success();
}
static String validateOptionValue(String optionName, Object optionValue) {
if ("object-size-limit".equals(optionName)
&& optionValue instanceof Integer
&& ((Integer) optionValue).intValue() <= 0) {
return "options[object-size-limit] must be greater than 0.";
}
return null;
}
/**
* 判断当前动作是否需要展示整个options
@@ -68,7 +68,7 @@ public class TimeTunnelCommand extends EnhancerCommand {
// expand of TimeTunnel
private Integer expand = 1;
// upper size limit
private Integer sizeLimit = 10 * 1024 * 1024;
private Integer sizeLimit;
// watch the index TimeTunnel
private String watchExpress = com.taobao.arthas.core.util.Constants.EMPTY_STRING;
private String searchExpress = com.taobao.arthas.core.util.Constants.EMPTY_STRING;
@@ -131,7 +131,7 @@ public class TimeTunnelCommand extends EnhancerCommand {
}
@Option(shortName = "M", longName = "sizeLimit")
@Description("Upper size limit in bytes for the result (10 * 1024 * 1024 by default)")
@Description("Upper size limit in bytes for the result (must be greater than 0, default value comes from options object-size-limit)")
public void setSizeLimit(Integer sizeLimit) {
this.sizeLimit = sizeLimit;
}
@@ -234,10 +234,22 @@ public class TimeTunnelCommand extends EnhancerCommand {
return !StringUtils.isEmpty(searchExpress);
}
static String validateSizeLimit(Integer sizeLimit) {
if (sizeLimit != null && sizeLimit.intValue() <= 0) {
return "sizeLimit must be greater than 0.";
}
return null;
}
/**
* 检查参数是否合法
*/
private void checkArguments() {
String validateError = validateSizeLimit(sizeLimit);
if (validateError != null) {
throw new IllegalArgumentException(validateError);
}
// 检查d/p参数是否有i参数配套
if ((isDelete || isPlay) && null == index) {
throw new IllegalArgumentException("Time fragment index is expected, please type -i to specify");
@@ -43,7 +43,7 @@ public class WatchCommand extends EnhancerCommand {
private boolean isException = false;
private boolean isSuccess = false;
private Integer expand = 1;
private Integer sizeLimit = 10 * 1024 * 1024;
private Integer sizeLimit;
private boolean isRegEx = false;
private int numberOfLimit = 100;
@@ -97,7 +97,7 @@ public class WatchCommand extends EnhancerCommand {
}
@Option(shortName = "M", longName = "sizeLimit")
@Description("Upper size limit in bytes for the result (10 * 1024 * 1024 by default)")
@Description("Upper size limit in bytes for the result (must be greater than 0, default value comes from options object-size-limit)")
public void setSizeLimit(Integer sizeLimit) {
this.sizeLimit = sizeLimit;
}
@@ -199,6 +199,16 @@ public class WatchCommand extends EnhancerCommand {
return methodNameMatcher;
}
@Override
public void process(CommandProcess process) {
String validateError = validateSizeLimit(sizeLimit);
if (validateError != null) {
process.end(-1, validateError);
return;
}
super.process(process);
}
@Override
protected AdviceListener getAdviceListener(CommandProcess process) {
return new WatchAdviceListener(this, process, GlobalOptions.verbose || this.verbose);
@@ -208,4 +218,11 @@ public class WatchCommand extends EnhancerCommand {
protected void completeArgument3(Completion completion) {
CompletionUtils.complete(completion, Arrays.asList(EXPRESS_EXAMPLES));
}
static String validateSizeLimit(Integer sizeLimit) {
if (sizeLimit != null && sizeLimit.intValue() <= 0) {
return "sizeLimit must be greater than 0.";
}
return null;
}
}
@@ -22,7 +22,7 @@ public class TimeTunnelView extends ResultView<TimeTunnelModel> {
@Override
public void draw(CommandProcess process, TimeTunnelModel timeTunnelModel) {
Integer sizeLimit = timeTunnelModel.getSizeLimit();
int sizeLimitValue = ObjectView.normalizeMaxObjectLength(timeTunnelModel.getSizeLimit());
if (timeTunnelModel.getTimeFragmentList() != null) {
//show list table: tt -l / tt -t
@@ -35,7 +35,7 @@ public class TimeTunnelView extends ResultView<TimeTunnelModel> {
TableElement table = TimeTunnelTable.createDefaultTable();
TimeTunnelTable.drawTimeTunnel(table, tf);
TimeTunnelTable.drawParameters(table, tf.getParams());
TimeTunnelTable.drawReturnObj(table, tf, sizeLimit);
TimeTunnelTable.drawReturnObj(table, tf, sizeLimitValue);
TimeTunnelTable.drawThrowException(table, tf);
process.write(RenderUtil.render(table, process.width()));
@@ -43,7 +43,7 @@ public class TimeTunnelView extends ResultView<TimeTunnelModel> {
//watch single TimeFragment: tt -i 1000 -w 'params'
ObjectVO valueVO = timeTunnelModel.getWatchValue();
if (valueVO.needExpand()) {
process.write(new ObjectView(sizeLimit, valueVO).draw()).write("\n");
process.write(new ObjectView(sizeLimitValue, valueVO).draw()).write("\n");
} else {
process.write(StringUtils.objectToString(valueVO.getObject())).write("\n");
}
@@ -52,7 +52,7 @@ public class TimeTunnelView extends ResultView<TimeTunnelModel> {
//search & watch: tt -s 'returnObj!=null' -w 'returnObj'
TableElement table = TimeTunnelTable.createDefaultTable();
TimeTunnelTable.drawWatchTableHeader(table);
TimeTunnelTable.drawWatchResults(table, timeTunnelModel.getWatchResults(), sizeLimit);
TimeTunnelTable.drawWatchResults(table, timeTunnelModel.getWatchResults(), sizeLimitValue);
process.write(RenderUtil.render(table, process.width()));
} else if (timeTunnelModel.getReplayResult() != null) {
@@ -63,7 +63,7 @@ public class TimeTunnelView extends ResultView<TimeTunnelModel> {
TimeTunnelTable.drawPlayHeader(replayResult.getClassName(), replayResult.getMethodName(), replayResult.getObject(), replayResult.getIndex(), table);
TimeTunnelTable.drawParameters(table, replayResult.getParams());
if (replayResult.isReturn()) {
TimeTunnelTable.drawPlayResult(table, replayResult.getReturnObj(), sizeLimit, replayResult.getCost());
TimeTunnelTable.drawPlayResult(table, replayResult.getReturnObj(), sizeLimitValue, replayResult.getCost());
} else {
TimeTunnelTable.drawPlayException(table, replayResult.getThrowExp());
}
@@ -17,8 +17,9 @@ public class WatchView extends ResultView<WatchModel> {
@Override
public void draw(CommandProcess process, WatchModel model) {
ObjectVO objectVO = model.getValue();
int sizeLimit = ObjectView.normalizeMaxObjectLength(model.getSizeLimit());
String result = StringUtils.objectToString(
objectVO.needExpand() ? new ObjectView(model.getSizeLimit(), objectVO).draw() : objectVO.getObject());
objectVO.needExpand() ? new ObjectView(sizeLimit, objectVO).draw() : objectVO.getObject());
StringBuilder sb = new StringBuilder();
sb.append("method=").append(model.getClassName()).append(".").append(model.getMethodName())
@@ -27,7 +27,6 @@ import static java.lang.String.format;
public class ObjectView implements View {
public static final int MAX_DEEP = 4;
private static final Logger logger = LoggerFactory.getLogger(ObjectView.class);
private final static int MAX_OBJECT_LENGTH = ArthasConstants.MAX_HTTP_CONTENT_LENGTH;
private static final ObjectWriterProvider JSON_OBJECT_WRITER_PROVIDER = new ObjectWriterProvider(
new ObjectWriterCreator() {
@Override
@@ -53,7 +52,7 @@ public class ObjectView implements View {
private final int maxObjectLength;
public ObjectView(ObjectVO objectVO) {
this(MAX_OBJECT_LENGTH, objectVO);
this(defaultMaxObjectLength(), objectVO);
}
// int参数在前面,防止构造函数二义性
@@ -62,7 +61,7 @@ public class ObjectView implements View {
}
public ObjectView(Object object, int deep) {
this(object, deep, MAX_OBJECT_LENGTH);
this(object, deep, defaultMaxObjectLength());
}
public ObjectView(Object object, int deep, int maxObjectLength) {
@@ -83,7 +82,7 @@ public class ObjectView implements View {
} catch (ObjectTooLargeException e) {
buf.append(" Object size exceeds size limit: ")
.append(maxObjectLength)
.append(", try to specify -M size_limit in your command, check the help command for more.");
.append(", try to use `options object-size-limit <bytes>` to increase the limit.");
return buf.toString();
} catch (Throwable t) {
logger.error("ObjectView draw error, object class: {}", object.getClass(), t);
@@ -699,5 +698,19 @@ public class ObjectView implements View {
}
}
public static int normalizeMaxObjectLength(Integer limit) {
if (limit != null && limit > 0) {
return limit;
}
int globalLimit = GlobalOptions.objectSizeLimit;
if (globalLimit > 0) {
return globalLimit;
}
return ArthasConstants.MAX_HTTP_CONTENT_LENGTH;
}
private static int defaultMaxObjectLength() {
return normalizeMaxObjectLength(null);
}
}
@@ -0,0 +1,17 @@
package com.taobao.arthas.core.command.basic1000;
import org.junit.Assert;
import org.junit.Test;
public class OptionsCommandTest {
@Test
public void testValidateObjectSizeLimitOptionValue() {
Assert.assertEquals("options[object-size-limit] must be greater than 0.",
OptionsCommand.validateOptionValue("object-size-limit", Integer.valueOf(0)));
Assert.assertEquals("options[object-size-limit] must be greater than 0.",
OptionsCommand.validateOptionValue("object-size-limit", Integer.valueOf(-1)));
Assert.assertNull(OptionsCommand.validateOptionValue("object-size-limit", Integer.valueOf(1)));
Assert.assertNull(OptionsCommand.validateOptionValue("json-format", Boolean.TRUE));
}
}
@@ -0,0 +1,23 @@
package com.taobao.arthas.core.command.monitor200;
import org.junit.Assert;
import org.junit.Test;
public class SizeLimitValidationTest {
@Test
public void testWatchSizeLimitValidation() {
Assert.assertEquals("sizeLimit must be greater than 0.", WatchCommand.validateSizeLimit(Integer.valueOf(0)));
Assert.assertEquals("sizeLimit must be greater than 0.", WatchCommand.validateSizeLimit(Integer.valueOf(-1)));
Assert.assertNull(WatchCommand.validateSizeLimit(Integer.valueOf(1)));
Assert.assertNull(WatchCommand.validateSizeLimit(null));
}
@Test
public void testTimeTunnelSizeLimitValidation() {
Assert.assertEquals("sizeLimit must be greater than 0.", TimeTunnelCommand.validateSizeLimit(Integer.valueOf(0)));
Assert.assertEquals("sizeLimit must be greater than 0.", TimeTunnelCommand.validateSizeLimit(Integer.valueOf(-1)));
Assert.assertNull(TimeTunnelCommand.validateSizeLimit(Integer.valueOf(1)));
Assert.assertNull(TimeTunnelCommand.validateSizeLimit(null));
}
}
@@ -1,5 +1,6 @@
package com.taobao.arthas.core.view;
import com.taobao.arthas.common.ArthasConstants;
import com.taobao.arthas.core.GlobalOptions;
import org.junit.Assert;
import org.junit.Test;
@@ -223,6 +224,37 @@ public class ObjectViewTest {
}
}
@Test
public void testDefaultSizeLimitFromGlobalOptions() {
int old = GlobalOptions.objectSizeLimit;
try {
GlobalOptions.objectSizeLimit = 5;
ObjectView objectView = new ObjectView("123456", 1);
String output = objectView.draw();
Assert.assertTrue(output.contains("Object size exceeds size limit: 5"));
} finally {
GlobalOptions.objectSizeLimit = old;
}
}
@Test
public void testNormalizeMaxObjectLength() {
int old = GlobalOptions.objectSizeLimit;
try {
GlobalOptions.objectSizeLimit = 9;
Assert.assertEquals(9, ObjectView.normalizeMaxObjectLength(null));
Assert.assertEquals(9, ObjectView.normalizeMaxObjectLength(0));
Assert.assertEquals(9, ObjectView.normalizeMaxObjectLength(-1));
Assert.assertEquals(8, ObjectView.normalizeMaxObjectLength(8));
GlobalOptions.objectSizeLimit = 0;
Assert.assertEquals(ArthasConstants.MAX_HTTP_CONTENT_LENGTH, ObjectView.normalizeMaxObjectLength(null));
Assert.assertEquals(ArthasConstants.MAX_HTTP_CONTENT_LENGTH, ObjectView.normalizeMaxObjectLength(-1));
} finally {
GlobalOptions.objectSizeLimit = old;
}
}
@Test
public void testDate() {
Date d = new Date(1531204354961L - TimeZone.getDefault().getRawOffset()
@@ -276,7 +308,7 @@ public class ObjectViewTest {
" c1=@NestedClass[\n" +
" code=@Integer[1],\n" +
" c1=...\n" +
"... Object size exceeds size limit: 100, try to specify -M size_limit in your command, check the help command for more.";
"... Object size exceeds size limit: 100, try to use `options object-size-limit <bytes>` to increase the limit.";
Assert.assertEquals(expected, objectView.draw());
}
+17 -13
View File
@@ -6,19 +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 化的输出 |
| object-size-limit | 10485760 | ObjectView 输出大小上限(字节),必须大于 0,默认 `10 * 1024 * 1024` |
| 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 模式 |
## 查看所有的 options
@@ -40,6 +41,9 @@ $ options
2 boolea json-format false Option to support JS This option enables to format object outpu
n ON format of object t with JSON when -x option selected.
output
1 int object-size- 10485760 Option to control O Upper size limit in bytes for ObjectView o
limit bjectView output utput, must be greater than 0. Default val
ue is 10 * 1024 * 1024.
1 boolea disable-sub- false Option to control in This option disable to include sub class w
n class clude sub class when hen matching class.
class matching
+4
View File
@@ -12,6 +12,7 @@ Global options
| 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 |
| json-format | false | whether to output in JSON format |
| object-size-limit | 10485760 | upper size limit in bytes for ObjectView output (must be greater than 0, default is `10 * 1024 * 1024`) |
| disable-sub-class | false | whether to enable matching child classes. The default value is `true`. If exact match is desire, turn off this flag |
| support-default-method | true | whether to enable matching default method in interface. The default value is `true`. Refer to [#1105](https://github.com/alibaba/arthas/issues/1105) |
| save-result | false | whether to save execution result. All execution results will be saved to `~/logs/arthas-cache/result.log` when it's turned on |
@@ -40,6 +41,9 @@ $ options
2 boolea json-format false Option to support JS This option enables to format object outpu
n ON format of object t with JSON when -x option selected.
output
1 int object-size- 10485760 Option to control O Upper size limit in bytes for ObjectView o
limit bjectView output utput, must be greater than 0. Default val
ue is 10 * 1024 * 1024.
1 boolea disable-sub- false Option to control in This option disable to include sub class w
n class clude sub class when hen matching class.
class matching