Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

之前在用到arthas就想到过可以研究下java的agent,这里算是个初入门
首先我们有个应用,需要挂上agent来探测一些事情
比如就是简单的主方法

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
Demo.start();
Demo.start2();
}
}

然后有个Demo类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Demo {
public static void start() {
try {
System.out.println("Demo starting ...");
Thread.sleep(123);
} catch (InterruptedException ignore) {
}
}

public static void start2() {
try {
System.out.println("Demo starting 2...");
Thread.sleep(567);
} catch (InterruptedException ignore) {
}
}
}

然后我们想要像切面一样在start方法前后织入一些前后置的逻辑
我们需要将一个maven的module
然后agent代码是这样, premain 方法会在我们指定挂载agent执行,并且就是入口方法

1
2
3
4
5
6
7
8
9
public class DemoAgent {

public static void premain(String arg, Instrumentation instrumentation) {
System.out.println("agent start");
System.out.println("arg is " + arg);
instrumentation.addTransformer(new DemoTransformer());
}

}

然后这个 transformer 就是我要怎么在目标类里如何织入代码的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class DemoTransformer implements ClassFileTransformer {
// 这里是我们要织入代码的目标类
private static final String INJECTED_CLASS = "org.example.Demo";

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
String realClassName = className.replace("/", ".");
// 匹配到目标类
if (realClassName.equals(INJECTED_CLASS)) {
System.out.println("injected class :" + realClassName);
CtClass ctClass;
try {
// 使用javassist,获取字节码类
ClassPool classPool = ClassPool.getDefault();
ctClass = classPool.get(realClassName);

// 获取目标类所有的方法,也可指定方法,进行增强
CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
for (CtMethod declaredMethod : declaredMethods) {
System.out.println(declaredMethod.getName() + "method be inject");
declaredMethod.addLocalVariable("time", CtClass.longType);
declaredMethod.insertBefore("System.out.println(\"--- start ---\");");
declaredMethod.insertBefore("time = System.currentTimeMillis();");
declaredMethod.insertAfter("System.out.println(\"--- end ---\");");
declaredMethod.insertAfter("System.out.println(\"cost: \" + (System.currentTimeMillis() - time));");
}
return ctClass.toBytecode();
} catch (Throwable ignore) {
}
}
return classfileBuffer;
}
}

接下去要在pom.xml里配置如何打包agent的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<!-- 指定maven编译的jdk版本。若不指定,maven3默认用jdk 1.5 maven2默认用jdk1.3 -->
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<!--自动添加META-INF/MANIFEST.MF -->
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Menifest-Version>1.0</Menifest-Version>
<Premain-Class>org.example.DemoAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>

需要指定 Premain-Class 来指定入口方法所在的类
然后用 mvn package 进行打包
打包后在我们的目标代码的启动vm参数里加上

1
-javaagent:/{path}/demo-agent-1.0-SNAPSHOT.jar

{path} 需要替换成agent的jar包所在路径
然后执行目标main方法

就可以看到结果了,这样我们第一个agent的demo就跑起来了

之前在使用grep来匹配内容,想看下匹配的结果数量都是用管道,然后用wc命令的
最近看了下似乎没必要这么麻烦
grep命令自带了这个功能
原来在用grep的时候还有个额外的功能就是看匹配行的前后几行,
使用

1
grep -B 4

是匹配行的前4行,可以用before来记

1
grep -A 4

是匹配行的后4行,可以用after来记
然后

1
grep -C 4

是看匹配行的前后4行
而这次的主角是小写的c

1
grep -c 'something' filename.txt


类似于这个效果,这又是个可以少打几个字符的偷懒技巧

我们日常在服务器上查看日志的时候用的很多的就是tail命令,使用tail最基本的就是

1
tail filename.log

这种,可以看到日志文件的最后10行
我们一般会使用

1
tail -f filename.log

可以用 -f来看到后续新生成的日志,可以用来观察是否有新增的异常日志或者是否有没有按预期打印新的日志
然后对于我们对输出的日志行数有具体要求的

1
tail -f -n 100 filename.log

可以用 -n 参数,可以指定显示行数,比如想要显示100行
而有一个技巧就是之前看到别的同学用过

1
tail -100f filename.log

可以和上面产生一样的效果
这样可以少输几个字符,也算是一个偷懒小技巧了,如果对于经常需要登录服务器看日志的也算是个效率小窍门
只是这样子对于比如我只想看新增的特定内容的日志来说,好像还不行
这里我们就可以使用组合技,结合 tailgrep 命令

1
tail -f filename.log | grep something 

这样就可以关注 something 的新增了

我们在日常使用grep这个强大的命令行工具时有时候会碰到一个问题,就是grep在识别文件的时候在碰到 ‘\000 NUL’的时候会认为文件是二进制文件,就不进行识别了,碰到这种情况我们可以使用 grep -a 或者 grep --text
比如我们做个小实验

1
2
3
4
<?php
$content = file_get_contents('input.txt');
$binary = "\0" . $content; // 添加 NUL 字符到开头
file_put_contents('output.bin', $binary);

然后我们就有了 output.bin 这个二进制文件
本身 input.txt 的内容也很简单

1
2
12345
12345

我们尝试用 grep 查找其中的内容,

就会出现这个问题
当我们加上了 -a 或者 --text 就是指定 grep 需要在包含二进制文件格式中或者将文件当做是文本文件来识别匹配

这个话题应该首先明确,这不是类似于 open tracing 的应用间调用路径,而是应用内部,举个例子,我有一个底层方法,比如DAO层的一个方法,有很多调用方,那么有什么方法,第一种是最简单,但是比较麻烦,我去每个调用方那打个日志,然后分析下日志,这个办法肯定是没错的,只是如果我的调用方很多,可能需要加日志的地方就很多了,会比较麻烦
第二种是我可以在这个被调用的DAO层方法里加个日志,但是要怎么获取调用方呢,这是我们可以用代码堆栈trace来实现,这个思路可以从异常的角度出发,我们在出现异常的时候,如何定位异常的代码,就是通过异常堆栈,那么堆栈是不是只有在抛异常的时候才有呢,答案是否定的,了解一点java虚拟机的应该知道我们在内存中有一个区域放的就是我们的代码调用栈,用来保存我们目前程序的调用层次逻辑以及数据,之前在php中也讲到过这个,那时候也是帮助理清调用逻辑和调用来源,
在java中如何使用这个功能呢,

1
2
3
4
5
6
7
8
9
10
11
12
public class StackTrace {
public static void main(String[] args) {
method1();
}
public static void method1() {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
System.out.println(stackTraceElement);
}
}

}

这个方法其实也比较简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public StackTraceElement[] getStackTrace() {
if (this != Thread.currentThread()) {
// check for getStackTrace permission
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkPermission(
SecurityConstants.GET_STACK_TRACE_PERMISSION);
}
// optimization so we do not call into the vm for threads that
// have not yet started or have terminated
if (!isAlive()) {
return EMPTY_STACK_TRACE;
}
StackTraceElement[][] stackTraceArray = dumpThreads(new Thread[] {this});
StackTraceElement[] stackTrace = stackTraceArray[0];
// a thread that was alive during the previous isAlive call may have
// since terminated, therefore not having a stacktrace.
if (stackTrace == null) {
stackTrace = EMPTY_STACK_TRACE;
}
return stackTrace;
} else {
// Don't need JVM help for current thread
return (new Exception()).getStackTrace();
}
}

这里面返回的 StackTraceElement 的内容

1
2
3
4
5
6
public final class StackTraceElement implements java.io.Serializable {
// Normally initialized by VM (public constructor added in 1.5)
private String declaringClass;
private String methodName;
private String fileName;
private int lineNumber;

分别是定义的类,方法名,文件名,以及代码行数

我们就能看到这样的结果

1
2
3
java.lang.Thread.getStackTrace(Thread.java:1564)
StackTrace.StackTrace.method1(StackTrace.java:14)
StackTrace.StackTrace.main(StackTrace.java:11)

因为是栈,所以就是最上面的是最近的方法,而第二行其实是我的目标方法,就是前面描述的逻辑其实应该找到第3个元素,因为第1个是调用 getStackTrace 方法,
第2个是目标方法,第3个才是我们想要找的调用方方法,以及所在的行数

0%