写在前面
源码 。
在开发过程中为了调试代码我们就可能就需要知道某个方法入参的值是什么,或者是返回值是什么。此时,我们的解决办法一般都是debug,但是debug的效率说实话其实是不高的,特别是不断的调试,不断的debug。所以为了解决(😅,彻底解决不用debug,不太可能,但肯定能在一定程度上缓解吧)这个痛点问题,我们就来尝试开发一个idea插件吧。
1:编写agent程序
通过bytebuddy来实现字节码插桩,源码如下:
public class MyPreAgent {
// 如果有该方法则优先调用这个有两个参数的方法
public static void premain(String agentArgs, Instrumentation inst) {
AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
return builder
// .method(ElementMatchers.named("executeInternal")) // 拦截任意方法
.method(ElementMatchers.named("m1").or(ElementMatchers.named("m2"))) // 拦截任意方法
.intercept(MethodDelegation.to(MonitorMethod.class)); // 委托
};
new AgentBuilder
.Default()
// .type(ElementMatchers.nameStartsWith("com.mysql.cj.jdbc.ClientPreparedStatement"))
.type(ElementMatchers.nameStartsWith("com.dahuyou.agent.test.MyCls"))
.transform(transformer)
.installOn(inst);
}
// 如果是没有上面的方法则调用这个一个参数的方法
public static void premain(String agentArgs) {
}
}
在代码中我们设置了满足什么规则才拦截,这里是固定写死的拦截类com.dahuyou.agent.test.MyCls的方法m1和m2,当然可以随意改,或者是通过某种配置的方式做成活的,上述的类MonitorMethod即为插装的代码,源码如下:
public class MonitorMethod {
@RuntimeType
public static Object intercept(@This Object obj, @Origin Method method, @SuperCall Callable<?> callable, @AllArguments Object... args) throws Exception {
System.out.println("MonitorMethod.intercept...");
long start = System.currentTimeMillis();
Object resultObj = null;
try {
resultObj = callable.call();
return resultObj;
} finally {
System.out.println("方法名称:" + method.getName() + "入参个数:" + method.getParameterCount());
for (int i = 0; i < method.getParameterCount(); i++) {
System.out.println("入参 Idx:" + (i + 1) + " 类型:" + method.getParameterTypes()[i].getName() + " 内容:" + args[i]);
}
System.out.println("出参类型:" + method.getReturnType().getName());
System.out.println("出参结果:" + resultObj);
System.out.println("方法耗时:" + (System.currentTimeMillis() - start) + "ms");
}
}
}
接着我们就可以生成agent的jar包来测试了,如下:
然后创建一个满足agent匹配规则的类:
public class MyCls {
public String m1(String name) {
System.out.println("m1方法执行了啊: " + name);
this.m2(30, "一个啊神秘的地方");
return name;
}
private int m2(int age, String addr) {
System.out.println("m2方法执行了啊: " + age);
return ++age;
}
}
测试类:
public class MyPreAgentTest {
public static void main(String[] args) {
String name = "张三";
new MyCls().m1(name);
}
}
然后配置VM option添加-javaagent,如下:
最后运行测试:
接着就可以来继续开发插件了。
2:编写idea插件
要想让agent工作,必须通过-javaagent添加到java运行命令中,idea提供了java.programPatcher的扩展可以让我们实现这个需求,具体是继承抽象类JavaProgramPatcher,然后修改其中的入参RunProfile:
/**
* 向Java cmd中动态添加-javaagent:xxx
*/
public class JavaCmdEditor extends JavaProgramPatcher {
@Override
public void patchJavaParameters(Executor executor, RunProfile configuration, JavaParameters javaParameters) {
String agentCoreJarPath = PluginUtil.getAgentCoreJarPath();
RunConfiguration runConfiguration = (RunConfiguration) configuration;
ParametersList vmParametersList = javaParameters.getVMParametersList();
// 向运行参数中增加-javaagent:/path/to/the-show-sql-agent.jar
vmParametersList.addParametersString("-javaagent:" + agentCoreJarPath);
vmParametersList.addNotEmptyProperty("com.dahuyou.the-probe-plugin", runConfiguration.getProject().getLocationHash());
}
}
这样还不行,为了让idea加载运行,还需要配置到plugin.xml中的java.ProgramPatcher中:
<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
<java.programPatcher implementation="com.dahuyou.probe.plugin.JavaCmdEditor"/>
</extensions>
然后就可以来测试了:
运行:
到这里还有一个问题需要解决,即插装的类和方法是固定写死的,接下来我们开发一个UI来允许配置想要拦截的类和方法,就基本全了。
3:插桩代码支持配置
首先开发一个action,在file中增加一个按钮入口:
public class AgentSettingAction extends AnAction {
@Override
public void actionPerformed(AnActionEvent e) {
Messages.showInfoMessage("即将跳转到agent设置页面", "提示");
// 弹出一个设置(implements Configurable)页面(extends JPanel)
ShowSettingsUtil.getInstance().editConfigurable(e.getProject(), new AgentSettingUI());
}
}
这里AgentSettingUI是需要我们自定义的页面,继承了Japnel,并实现了Configurable接口:
public class AgentSettingUI extends JPanel implements Configurable {}
效果如下:
点击ok会调用apply方法,在该方法中会将用户设置的信息写到本地文件中:
接着还需要改造agent程序,动态的从文件中读取数据来进行拦截配置:
public static void premain(String agentArgs, Instrumentation inst) {
String agentSettingFilePath = "D:/agentsetting.txt";
String packagePrefix = "";
List<String> methodList = null;
try {
BufferedReader br = new BufferedReader(new FileReader(agentSettingFilePath));
String agentSettingInfo = br.readLine();
// com.dahuyou3343#tttmyFn1,myFn2
System.out.println("agentSettingInfo: " + agentSettingInfo);
String[] split = agentSettingInfo.split("#");
packagePrefix = split[0];
methodList = Arrays.asList(split[1].split(","));
System.out.println("packagePrefix: " + packagePrefix);
System.out.println("methodList111: " + methodList);
} catch (Exception e) {
e.printStackTrace();
}
List<String> finalMethodList = methodList;
AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
ElementMatcher.Junction<NamedElement> named = ElementMatchers.named(finalMethodList.get(0));
for (int i = 1; i < finalMethodList.size(); i++) {
named = named.or(ElementMatchers.named(finalMethodList.get(i)));
}
return builder
.method(named) // 拦截任意方法
.intercept(MethodDelegation.to(MonitorMethod.class)); // 委托
};
new AgentBuilder
.Default()
.type(ElementMatchers.nameStartsWith(packagePrefix))
.transform(transformer)
.installOn(inst);
}
注意打一个新的agent jar包放在插件模块的libs目录中,接着我们来测试,只拦截m1,不拦截m2:
设置一个错误的包前缀,都不拦截:
4:在idea中安装
首先打包:
安装到idea也比较简单,你可以直接下载 然后安装到你的idea中,安装也很简单,可以直接拖拽到idea中松手,会提示你重启,也可以通过file->settings->plugins:
然后选择zip包安装,也会提示你重启idea,重启后就可以使用该插件了,对了插件的名字叫:debug小助手
。
5:实际使用
假设有如下的springboot项目:
@SpringBootApplication
@RestController
public class AAApplication {
public static void main(String[] args) {
SpringApplication.run(AAApplication.class);
}
@RequestMapping("/sayHi")
@ResponseBody
public String sayHi(String name) {
new MyCls().m1(name);
return name + ",hi!";
}
}
测试同学说你的sayHi接口,错误,此时如果你怀疑是入参问题,就可以用这个插件了(是不是就不用debug了🤭🤭🤭,提高效率杠杆的)
,如下:
你安装使用了吗?是的话就点个赞让我知道下吧🙁🙁🙁!!!如果你有对这个插件有什么建议,或者其他的想法,就留言告诉我吧,我们一起进步,让自己在这互联网寒潮中不做那批被淘汰的人💪💪💪!!!
写在后面
参考文章列表
Java Agent 介绍和实战 。