学习Java Agent

介绍

JavaAgent技术是什么:自行搜索

为什么要学JavaAgent:破解Java软件,Agent内存马,RASP等

其中Agent的Jar由:Manifest,Agent Class,ClassFile Transformer组成

他们之间的关联大致如下:

Manifest->AgentClass->Instrumentation->ClassFileTransformer->ASM

根据Mainifest中定义的属性找到具体的AgentClass,调用对应的方法

在方法中通过Instrumentation对象使用ClassFileTransformer

而操作字节码则需要了解Class文件和ASM或Javassist的使用

Manifest

首先来看Manifest中与Java Agent相关的属性

这里做一个简单的介绍,对相关属性有大体的了解,结合后续实例进一步理解

(1)Premain-Class

使用Java Agent有两种方式:Load-Time和Dynamic,也就是说一种是启动时,一种是运行时。当我们使用启动时Agent需要加入-javaagent参数,这时候就会在Manifest中寻找Premain-Class属性

这个属于记录的是一个类名,要求这个类中必须包含premain方法

(2)Agent-Class

使用另一种Dynamic方式的Java Agent需要寻找Manifest的Agent-Class属性

该属性记录的也是一个类名,要求这个类中必须包含agentmain方法

(3)Can-Redefine-Classes

这是一个Boolean类型的属性,默认False

判断当前JVM的配置是否支持类的重新定义(后续介绍)

(4)Can-Retransform-Classes

这是一个Boolean类型的属性,默认False

如果需要调用Instrumentation.retransformClasses等方法需要设置为True

(5)Can-Set-Native-Method-Prefix

这是一个Boolean类型的属性,默认False

是否支持设置本地方法前(后续介绍)

(6)Class-Path与Boot-Class-Path

JVM有三个ClassLoader:Bootstrap,Extension,Application(System)

其中Class-Path指定的Jar包将由Application(System) ClassLoader加载

其中Boot-Class-Path指定的Jar包将由Bootstrap ClassLoader加载

之所以要提到Boot-Class-Path属性,因为在某些情况下必须使用该属性

(7)Launcher-Agent-Class

该属性是Java9引用的属性,记录的是一个类名,类似Premain-Class和Agent-Class

可用于在main方法执行之前做一些事情

Agent Class

(1)LoadTimeAgent

注意到上文:LoadTimeAgent需要配置Premain-Class属性且要求premain方法存在

这个方法有两种使用方式

public static void premain(String agentArgs, Instrumentation inst);

另一种

public static void premain(String agentArgs);

实际上大多数情况下需要使用的是第一种方式,因为我们的目的是修改某个Class文件,如果需要修改Class文件则离不开Instrumentation对象

其实JVM在加载Agent的时候,也是优先选择第一种方法,如果无法找到则会选择第二种

(2)DynamicAgent

注意到上文:DynamicAgent需要配置Agent-Class属性且要求agentmain方法存在

这个方法也有两种使用方式

public static void agentmain(String agentArgs, Instrumentation inst);

另一种

public static void agentmain(String agentArgs);

可以发现与LoadTimeAgent几乎一致

ClassFileTransformer

上文提到:如果需要修改Class文件则离不开Instrumentation对象

Instrumentation是一个接口,关于这个接口更多的方法暂不介绍

先来看最基本的两个方法:添加和移除ClassFileTransformer的方法

public interface Instrumentation {
    void
    addTransformer(ClassFileTransformer transformer, boolean canRetransform);

    boolean
    removeTransformer(ClassFileTransformer transformer);
}

ClassFileTransformer中有一个至关重要的方法:transform

如果我们需要修改字节码,需要实现ClassFileTransformer接口并重写transform方法

public interface ClassFileTransformer {
    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;
}

LoadTime示例

目标:利用JavaAgent技术打印正在加载的类(LoadTimeAgent)

测试程序

一个提供加法和减法的方法

package sample;

public class HelloWorld {
    public static int add(int a, int b) {
        return a + b;
    }

    public static int sub(int a, int b) {
        return a - b;
    }
}

一个简单的类:随机生成数字并做加减的运算

package sample;

import java.lang.management.ManagementFactory;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class Program {
    public static void main(String[] args) throws Exception {
        String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
        System.out.println(nameOfRunningVM);
        int count = 600;
        for (int i = 0; i < count; i++) {
            String info = String.format("|%03d| %s remains %03d seconds", i, nameOfRunningVM, (count - i));
            System.out.println(info);

            Random rand = new Random(System.currentTimeMillis());
            int a = rand.nextInt(10);
            int b = rand.nextInt(10);
            boolean flag = rand.nextBoolean();
            String message;
            if (flag) {
                message = String.format("a + b = %d", HelloWorld.add(a, b));
            } else {
                message = String.format("a - b = %d", HelloWorld.sub(a, b));
            }
            System.out.println(message);

            TimeUnit.SECONDS.sleep(1);
        }
    }
}

编译运行

javac sample/*.java
java sample.Program

观察到如下的打印

|000| 31968@DESKTOP-FP02BKH remains 060 seconds
a - b = 0
|001| 31968@DESKTOP-FP02BKH remains 059 seconds
a + b = 10
|002| 31968@DESKTOP-FP02BKH remains 058 seconds
a - b = 2
|003| 31968@DESKTOP-FP02BKH remains 057 seconds
a - b = 6

编写Agent

首先编写manifest.txt文件(结尾必须是一个换行)

Premain-Class: agent.LoadTimeAgent

编写AgentClass

package agent;

import instrument.InfoTransformer;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;

public class LoadTimeAgent {
    public static void premain(String agentArgs, Instrumentation inst){
        System.out.println("Premain-Class: "+LoadTimeAgent.class.getName());
        ClassFileTransformer transformer = new InfoTransformer();
        inst.addTransformer(transformer);
    }
}

编写ClassFileTransformer实现简单的打印功能

package instrument;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import java.util.Formatter;

public class InfoTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader,
                            String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) {
        StringBuilder sb = new StringBuilder();
        Formatter fm = new Formatter(sb);
        fm.format("ClassName: %s%n", className);
        fm.format("\tClassLoader: %s%n", loader);
        fm.format("\tClassBeingRedefined: %s%n", classBeingRedefined);
        fm.format("\tProtectionDomain: %s%n", protectionDomain);
        System.out.println(sb);
        return null;
    }
}

手动打包

找到所有的Java文件并将文件名写入sources.txt

find ./src/main/java/ -name "*.java" > sources.txt

编译

mkdir out && javac -d out/ @sources.txt

复制manifest.txt文件到输出目录

cp ./src/main/java/manifest.txt ./out/

使用命令生成Agent Jar

jar -cvfm TheAgent.jar manifest.txt .

jar -cvmf manifest.txt TheAgent.jar .

使用-javaagnet启动,观察到很多打印内容说明成功

java -javaagent:TheAgent.jar sample.Program

修改字节码示例

注意到上文的LoadTime示例并没有修改字节码

目标:打印出方法接收的参数,使用LoadTimeAgent实现

ASM代码

测试程序不变,由于调用打印方法需要修改字节码,所以编写ASM代码

这里使用JDK自带的ASM框架,对于JDK8而言,ASM对应版本是5

package cst;


import jdk.internal.org.objectweb.asm.Opcodes;

public class Const {
    public static final int ASM_VERSION = Opcodes.ASM5;
}

该ClassVisitor的目的是遇到非构造方法和非静态代码块时,交给自定义MethodAdapter处理

package asm;

import cst.Const;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.MethodVisitor;

public class MethodInfoVisitor extends ClassVisitor {
    private String owner;

    public MethodInfoVisitor(ClassVisitor classVisitor) {
        super(Const.ASM_VERSION, classVisitor);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this.owner = name;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if (mv != null && !name.equals("<init>") && !name.equals("<clinit>")) {
            mv = new MethodInfoAdapter(mv, owner, access, name, descriptor);
        }
        return mv;
    }
}

在对应的MethodAdapter中,逻辑是这样:一开始进入方法时,打印方法名等信息,然后遍历方法参数,使用ILOAD指令将每个参数入栈。之所以这样做是因为后续打印方法的调用需要弹栈,取出这个参数

package asm;

import cst.Const;
import jdk.internal.org.objectweb.asm.MethodVisitor;
import jdk.internal.org.objectweb.asm.Opcodes;
import jdk.internal.org.objectweb.asm.Type;

public class MethodInfoAdapter extends MethodVisitor {
    private final String owner;
    private final int methodAccess;
    private final String methodName;
    private final String methodDesc;

    public MethodInfoAdapter(MethodVisitor methodVisitor, String owner,
                             int methodAccess, String methodName, String methodDesc) {
        super(Const.ASM_VERSION, methodVisitor);
        this.owner = owner;
        this.methodAccess = methodAccess;
        this.methodName = methodName;
        this.methodDesc = methodDesc;
    }

    @Override
    public void visitCode() {
        if (mv != null) {
            String line = String.format("Method Enter: %s.%s:%s", owner, methodName, methodDesc);
            printMessage(line);
            int slotIndex = (methodAccess & Opcodes.ACC_STATIC) != 0 ? 0 : 1;
            Type methodType = Type.getMethodType(methodDesc);
            Type[] argumentTypes = methodType.getArgumentTypes();
            for (Type t : argumentTypes) {
                int sort = t.getSort();
                int size = t.getSize();
                int opcode = t.getOpcode(Opcodes.ILOAD);
                super.visitVarInsn(opcode, slotIndex);

                if (sort >= Type.BOOLEAN && sort <= Type.DOUBLE) {
                    String desc = t.getDescriptor();
                    printValueOnStack("(" + desc + ")V");
                } else {
                    printValueOnStack("(Ljava/lang/Object;)V");
                }
                slotIndex += size;
            }
        }

        super.visitCode();
    }

    private void printMessage(String str) {
        super.visitLdcInsn(str);
        super.visitMethodInsn(Opcodes.INVOKESTATIC, "utils/ParameterUtils", "printText", "(Ljava/lang/String;)V", false);
    }

    private void printValueOnStack(String descriptor) {
        super.visitMethodInsn(Opcodes.INVOKESTATIC, "utils/ParameterUtils", "printValueOnStack", descriptor, false);
    }

    private void printStackTrace() {
        super.visitMethodInsn(Opcodes.INVOKESTATIC, "utils/ParameterUtils", "printStackTrace", "()V", false);
    }
}

由于System.out.println打印方法实际上对应的字节码指令比较复杂,所以自行写了一个ParameterUtils提供静态打印方法

package utils;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;

public class ParameterUtils {
    private static final DateFormat fm = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    private ParameterUtils() {
        throw new UnsupportedOperationException();
    }

    public static void printValueOnStack(boolean value) {
        System.out.println("    " + value);
    }

    public static void printValueOnStack(byte value) {
        System.out.println("    " + value);
    }

    public static void printValueOnStack(char value) {
        System.out.println("    " + value);
    }

    public static void printValueOnStack(short value) {
        System.out.println("    " + value);
    }

    public static void printValueOnStack(int value) {
        System.out.println("    " + value);
    }

    public static void printValueOnStack(float value) {
        System.out.println("    " + value);
    }

    public static void printValueOnStack(long value) {
        System.out.println("    " + value);
    }

    public static void printValueOnStack(double value) {
        System.out.println("    " + value);
    }

    public static void printValueOnStack(Object value) {
        if (value == null) {
            System.out.println("    " + null);
        }
        else if (value instanceof String) {
            System.out.println("    " + value);
        }
        else if (value instanceof Date) {
            System.out.println("    " + fm.format(value));
        }
        else if (value instanceof char[]) {
            System.out.println("    " + Arrays.toString((char[]) value));
        }
        else if (value instanceof Object[]) {
            System.out.println("    " + Arrays.toString((Object[]) value));
        }
        else {
            System.out.println("    " + value.getClass() + ": " + value);
        }
    }

    public static void printText(String str) {
        System.out.println(str);
    }

    public static void printStackTrace() {
        Exception ex = new Exception();
        ex.printStackTrace(System.out);
    }
}

编写Agent

其中manifest.txt文件不变(结尾必须是一个换行)

Premain-Class: agent.LoadTimeAgent

修改LoadTimeAgent类的代码:使用inst.addTransformer

package agent;

import instrument.ASMTransformer;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;

public class LoadTimeAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("Premain-Class: " + LoadTimeAgent.class.getName());
        ClassFileTransformer transformer = new ASMTransformer();
        inst.addTransformer(transformer);
    }
}

编写ASMTransformer代码:由于我们只处理sample/HelloWorld类所以判断类名是否equals然后用ASM的一套流程对字节码进行修改即可

package instrument;

import asm.MethodInfoVisitor;
import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.ClassWriter;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class ASMTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader,
                            String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) {
        if (className.equals("sample/HelloWorld")) {
            ClassReader cr = new ClassReader(classfileBuffer);
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
            ClassVisitor cv = new MethodInfoVisitor(cw);
            int parsingOptions = 0;
            cr.accept(cv, parsingOptions);
            return cw.toByteArray();
        }
        return null;
    }
}

手动打包

找到所有的Java文件并将文件名写入sources.txt

find ./src/main/java/ -name "*.java" > sources.txt

编译(当我们使用javac编译时候并没有直接和rt.jar做关联,而是使用到ct.sym,如果需要指定使用rt.jar则加上-XDignore.symbol.file参数才可以,否则会报错)

mkdir out && javac -XDignore.symbol.file -d out/ @sources.txt

复制manifest.txt文件到输出目录

cp ./src/main/java/manifest.txt ./out/

使用命令生成Agent Jar

jar -cvfm TheAgent.jar manifest.txt .

jar -cvmf manifest.txt TheAgent.jar .

使用-javaagnet启动,观察到很多打印内容说明成功

java -javaagent:TheAgent.jar sample.Program

打印如下

Premain-Class: agent.LoadTimeAgent
60692@DESKTOP-FP02BKH
|000| 60692@DESKTOP-FP02BKH remains 060 seconds
Method Enter: sample/HelloWorld.add:(II)I
    5
    4
a + b = 9

Dynamic示例

以上两个示例都是LoadTimeAgent

这个示例将使用DynamicAgent来实现和示例二一样的功能:打印方法参数

大部分代码都是相同的,区别在于Agent的编写

编写Agent

编写DynamicAgent需要在manifest.txt中添加两个属性

Agent-Class: agent.DynamicAgent
Can-Retransform-Classes: true

编写DynamicAgent代码:注意retransform之后要remove了防止影响过多

package agent;

import instrument.ASMTransformer;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;

public class DynamicAgent {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        ClassFileTransformer transformer = new ASMTransformer();
        try {
            inst.addTransformer(transformer, true);
            Class<?> targetClass = Class.forName("sample.HelloWorld");
            inst.retransformClasses(targetClass);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        finally {
            inst.removeTransformer(transformer);
        }
    }
}

使用DynamicAgent需要特殊的一个类VMAttach

关于这个类相关的介绍将放在后文,本文只是使用

package attach;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

public class VMAttach {
    public static void main(String[] args) throws Exception {
        String agent = "TheAgent.jar";
        System.out.println("Agent Path: " + agent);
        List<VirtualMachineDescriptor> vmds = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : vmds) {
            if (vmd.displayName().equals("sample.Program")) {
                VirtualMachine vm = VirtualMachine.attach(vmd.id());
                System.out.println("Load Agent");
                vm.loadAgent(agent);
                System.out.println("Detach");
                vm.detach();
            }
        }
    }
}

手动打包

对于DynamicAgent而言想要打包其实比较麻烦,例如上文的VMAttach类需要tools.jar

所以建议分来来编译和打包

首先对测试程序本身进行编译

mkdir out && javac src/main/java/sample/*.java -d out/

可以进入out目录运行测试

java  sample.Program

对Agent进行打包

find ./src/main/java/ -name "*.java" > sources.txt

这时候sources.txt里会有VMAttach的部分,这个编译会报错,所以手动删除

然后进行编译

javac -XDignore.symbol.file -d out/ @sources.txt

复制manifest.txt文件

cp ./src/main/java/manifest.txt ./out/

使用命令生成Agent Jar

jar -cvfm TheAgent.jar manifest.txt .

jar -cvmf manifest.txt TheAgent.jar .

最后是对VMAttach进行编译(这是针对GIT模拟的Linux命令行MINGW64

javac -cp "${JAVA_HOME}/lib/tools.jar"\;. -d out/ src/main/java/attach/VMAttach.java

在Linux中应该是这样

javac -cp "${JAVA_HOME}/lib/tools.jar":. -d out/ src/main/java/attach/VMAttach.java

纯Windows应该是这样

javac -cp "%JAVA_HOME%/lib/tools.jar";. -d out/ src/main/java/attach/VMAttach.java

最后运行(同样分三种)

java -cp "${JAVA_HOME}/lib/tools.jar"\;. attach.VMAttach

纯Linux

java -cp "${JAVA_HOME}/lib/tools.jar":. attach.VMAttach

纯Windows

java -cp "%JAVA_HOME%/lib/tools.jar";. attach.VMAttach

注意:需要先跑起来测试程序,然后再跑VMAttach程序

如果VMAttach打印如下且测试程序开始输出参数则说明成功

Agent Path: TheAgent.jar
Load Agent
Detach

Maven打包

不难发现,自行编译打包非常复杂,所以迫切需要学习自动打包方式

  • 第一种:maven-jar-plugin + maven-dependency-plugin
  • 第二种:maven-assembly-plugin
  • 第三种:maven-shade-plugin

加入必须的依赖(可以用ASM库而不是JDK内置了)

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>${asm.version}</version>
</dependency>

一些属性配置

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
    <maven.compiler.source>${java.version}</maven.compiler.source>
    <maven.compiler.target>${java.version}</maven.compiler.target>
    <asm.version>9.2</asm.version>
</properties>

由于DynamicAgent需要tools.jar所以另外添加

<dependency>
    <groupId>com.sun</groupId>
    <artifactId>tools</artifactId>
    <version>8</version>
    <scope>system</scope>
    <systemPath>${env.JAVA_HOME}/lib/tools.jar</systemPath>
</dependency>

编译插件(必须)

<build>
    <!-- 最终编译JAR名字 -->
    <finalName>TheAgent</finalName>
    <plugins>
        <!-- Java Compiler -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>${java.version}</source>
                <target>${java.version}</target>
                <fork>true</fork>
                <compilerArgs>
                    <!-- 生成所有调试信息 -->
                    <arg>-g</arg>
                    <!-- 生成MethodParameter属性 -->
                    <arg>-parameters</arg>
                    <!-- 之前介绍过使用rt.jar -->
                    <arg>-XDignore.symbol.file</arg>
                </compilerArgs>
            </configuration>
        </plugin>
    </plugins>
</build>

dependency

JAR插件

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.2.0</version>
    <configuration>
        <archive>
            <manifest>
                <!-- 主类名(不是必须) -->
                <mainClass>myagent.Main</mainClass>
                <addClasspath>true</addClasspath>
                <classpathPrefix>lib/</classpathPrefix>
                <addDefaultImplementationEntries>false</addDefaultImplementationEntries>
                <addDefaultSpecificationEntries>false</addDefaultSpecificationEntries>
            </manifest>
            <!-- Agent相关的属性 -->
            <manifestEntries>
                <Premain-Class>myagent.agent.LoadTimeAgent</Premain-Class>
                <Agent-Class>myagent.agent.DynamicAgent</Agent-Class>
                <Launcher-Agent-Class>myagent.agent.LauncherAgent</Launcher-Agent-Class>
                <!-- 防止出问题直接给三个属性都设为true -->
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
                <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
            </manifestEntries>
            <addMavenDescriptor>false</addMavenDescriptor>
        </archive>
        <!-- 只将指定目录打包 -->
        <includes>
            <include>myagent/**</include>
        </includes>
    </configuration>
</plugin>

这时候如果执行代码会出问题,找不到依赖

所以需要另一个插件:作用是把所有依赖复制过来

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <version>3.2.0</version>
    <executions>
        <execution>
            <id>lib-copy-dependencies</id>
            <phase>package</phase>
            <goals>
                <goal>copy-dependencies</goal>
            </goals>
            <configuration>
                <excludeArtifactIds>tools</excludeArtifactIds>
                <outputDirectory>${project.build.directory}/lib</outputDirectory>
                <overWriteReleases>false</overWriteReleases>
                <overWriteSnapshots>false</overWriteSnapshots>
                <overWriteIfNewer>true</overWriteIfNewer>
            </configuration>
        </execution>
    </executions>
</plugin>

这时候就可以运行Agent观察效果了

assembly

另一种办法是使用assembly把所有jar都打包进去

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <version>3.3.0</version>
    <configuration>
        <archive>
            <manifest>
                <mainClass>myagent.Main</mainClass>
                <addDefaultEntries>false</addDefaultEntries>
            </manifest>
            <manifestEntries>
                <Premain-Class>myagent.agent.LoadTimeAgent</Premain-Class>
                <Agent-Class>myagent.agent.DynamicAgent</Agent-Class>
                <Launcher-Agent-Class>myagent.agent.LauncherAgent</Launcher-Agent-Class>
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
                <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
            </manifestEntries>
        </archive>
        <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
        </descriptorRefs>
    </configuration>
    <executions>
        <execution>
            <id>make-assembly</id>
            <phase>package</phase>
            <goals>
                <goal>single</goal>
            </goals>
        </execution>
    </executions>
</plugin>

shade

使用shade插件,可以在assembly基础上进一步缩小体积

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>3.2.4</version>
    <configuration>
        <minimizeJar>true</minimizeJar>
        <filters>
            <filter>
                <artifact>*:*</artifact>
                <!-- 排除 -->
                <excludes>
                    <exclude>run/*</exclude>
                    <exclude>sample/*</exclude>
                </excludes>
            </filter>
        </filters>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <manifestEntries>
                            <Main-Class>myagent.Main</Main-Class>
                            <Premain-Class>myagent.agent.LoadTimeAgent</Premain-Class>
                            <Agent-Class>myagent.agent.DynamicAgent</Agent-Class>
                            <Launcher-Agent-Class>myagent.agent.LauncherAgent</Launcher-Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                            <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
                        </manifestEntries>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

运行测试

对于LoadTimeAgent的测试

java -cp ./target/classes/ -javaagent:./target/TheAgent.jar sample.Program

对于DynamicAgent的测试

先启动测试程序

java -cp ./target/classes/ sample.Program

再启动Agent

java -cp "${JAVA_HOME}/lib/tools.jar"\;./target/classes/ run.DynamicInstrumentation

测试成功,用Maven自动打包的确比手动方便很多

(DynamicInstrumentation类就是上文的VMAttach类)