Nicksxs's Blog

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

上次说了可以改写类,那进一步的我们可以做一下类似于之前提过的通过字节码来做切面的工作
首先我们有一个很简单的类和方法

1
2
3
4
5
6
7
8
public class DemoService {
public List<String> queryList(String name, String title, String n3) {
List<String> list = new ArrayList<>();
list.add(name);
list.add(title);
return list;
}
}

就是一个方法,输入几个字符串,然后加到list里,那么比如我们想知道打印入参和返回结果
这里比较类似于我们在用aop的时候就是织入代码
正好javassist有这样的操作接口
就是我们可以找到方法,然后使用

1
javassist.CtBehavior#insertBefore(java.lang.String)

1
javassist.CtBehavior#insertAfter(java.lang.String)

就可以在我们找到类和方法以后

1
2
3
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get(targetClass);
CtMethod method = cc.getDeclaredMethod(targetMethod);

对方法的前后进行逻辑织入

1
2
3
4
5
6
7
8
9
10
11
12
method.insertBefore(
"{ java.lang.StringBuilder _paramValues = new StringBuilder();" +
" Object[] params = $args;" +
" if(params != null) {" +
" for(int i=0; i<params.length; i++) {" +
" if(i > 0) _paramValues.append(\", \");" +
" _paramValues.append(String.valueOf(params[i]));" +
" }" +
" }" +
" System.out.println(\"[Method Entry] " + targetMethod + " params: \" + _paramValues.toString());" +
"}"
);

这里我们使用了直接织入代码的形式,在javassist中可以用 $args 来获取参数,然后循环参数, 把每个参数拼接以后作为字符串输出
然后我们想要在函数返回前获取返回值,这个也是有个语法糖 $_

1
2
3
4
5
6
7
StringBuilder after = new StringBuilder();
after.append("{")
.append(" java.util.List _result = $_;\n")
.append(" String _resultStr = (_result != null ? _result.toString() : \"null\");\n")
.append(" int _size = (_result != null ? _result.size() : 0);\n")
.append(" System.out.println(\"[Method Exit] " + targetMethod + " returns: \" + _resultStr + \" size: \" + _size);\n")
.append("}");

这里刚好我是list返回的,就直接这么处理了, 我们通过一个简单的调用来验证下

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
35
36
37
38
39
40
41
public static void main(String[] args) throws Exception {
// 假设要监控 org.example.DemoService 类中的 queryList 方法
addMonitoringNew("org.example.DemoService", "queryList");
// 2. 使用自定义类加载器加载增强后的类
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name.equals("org.example.DemoService")) {
try {
// 读取增强后的类文件
byte[] bytes = java.nio.file.Files.readAllBytes(
java.nio.file.Paths.get("org/example/DemoService.class")
);
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
throw new ClassNotFoundException("Failed to load enhanced class", e);
}
}
return super.loadClass(name);
}
};

// 3. 加载增强后的类
Class<?> enhancedClass = classLoader.loadClass("org.example.DemoService");

// 4. 创建实例
Object instance = enhancedClass.newInstance();

// 5. 获取并调用方法
Method processMethod = enhancedClass.getMethod("queryList", String.class, String.class, String.class); // 假设方法接收一个String参数
Object result = processMethod.invoke(instance, "name1", "title1", "n3");

System.out.println("Method returned: " + result);
}
}
可以看到我们的输出结果
```java
Successfully enhanced method: queryList
[Method Entry] queryList params: name1, title1, n3
[Method Exit] queryList returns: [name1, title1] size: 2
Method returned: [name1, title1]

这里其实核心很依赖两个语法糖,类似这样的还有一些

1
2
3
4
5
6
7
8
9
10
11
12
$0, $1, $2, ...    	this and actual parameters
$args An array of parameters. The type of $args is Object[].
$$ All actual parameters.
For example, m($$) is equivalent to m($1,$2,...)

$cflow(...) cflow variable
$r The result type. It is used in a cast expression.
$w The wrapper type. It is used in a cast expression.
$_ The resulting value
$sig An array of java.lang.Class objects representing the formal parameter types.
$type A java.lang.Class object representing the formal result type.
$class A java.lang.Class object representing the class currently edited.

可以结合文档仔细学习下

前面那篇在讲agent的时候用到了javassist,我们就来简单讲个demo
我想用javassist来创建一个类

1
2
3
4
5
6
7
public static void createEntity() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cl = pool.makeClass("Entity");
CtConstructor cons = new CtConstructor(new CtClass[]{}, cl);
cl.addConstructor(cons);
cl.writeFile();
}

这个代码可以给我们创建一个Entity类, 带有一个无参的构造函数,默认在项目根目录下会生成Entity类的class文件
但是其实这个也是不必要的,默认会给生成一个无参的构造函数
然后我们可以给这个类加个字段, 加在 writeFile 方法调用前

1
2
3
CtField param = new CtField(pool.get("java.lang.String"), "name", cl);
param.setModifiers(Modifier.PRIVATE);
cl.addField(param, CtField.Initializer.constant("ent1"));


我们就生成了这样一个类
javassist也已经提供好了

1
2
cl.addMethod(CtNewMethod.setter("setName", param));
cl.addMethod(CtNewMethod.getter("getName", param));

我们生成的类就变成这样了

接下去我们可以再加一个默认赋值的无参构造函数

1
2
3
4
CtConstructor con = new CtConstructor(new CtClass[]{}, cl);
// 需要转义下
con.setBody("{name = \"nick\";}");
cl.addConstructor(con);


然后我们就有了这样的类,带有一个默认赋值的构造函数,接下去可以来个带参数的构造函数

1
2
3
4
con = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, cl);
// $0=this / $1,$2,$3...指的是参数,$1代表第一个参数
con.setBody("{$0.name = $1;}");
cl.addConstructor(con);


最后我们可以再加个打印该字段的方法

1
2
3
4
CtMethod ctMethod = new CtMethod(CtClass.voidType, "printName", new CtClass[]{}, cl);
ctMethod.setModifiers(Modifier.PUBLIC);
ctMethod.setBody("{System.out.println(name);}");
cl.addMethod(ctMethod);

这样我们就获得了这样一个类

这只是javassist的简单使用,它还有很多强大的功能可以在官方文档和网上的资料进行学习

在上次的基础上我们可以通过一些方法来获取参数的参数名,以此我们又可以达到类似于切面的功能逻辑,
首先我在之前的代码里做一点修改

1
2
3
4
5
6
7
8
public static void start(String input) {
try {
System.out.println("Demo starting ...");
System.out.println(input);
Thread.sleep(123);
} catch (InterruptedException ignore) {
}
}

就是带了个参数,这边是我把参数打了出来,如果没有打,我想通过agent来知道这个参数值是啥就可以用agent技术
获取代码中的变量名以及类型

1
2
3
4
MethodInfo methodInfo = declaredMethod.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attribute = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
System.out.println(attribute.variableName(0));

这边就是个string类型,我们就能够获取到 input 这个变量名,然后我们就可以在这里插入一段代码来打印这个参数

1
declaredMethod.insertAfter("System.out.println( " + attribute.variableName(0)+ " );");

这样我们就能做到类似于java中切面的技术

之前在用到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


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

0%