Java Agent【java探针】及AspectJ

在Java 语言中,织入切面分为:编译期织入(CTW: Compile-Time Weaving)、类加载期织入(LTW: Load-Time Weaving)和运行期织入(JDK代理和CGLIB)。编译期织入是指在Java编译期,将切面织入到类字节码中;而类加载期织入则指在类字节码加载到JVM时,织入切面;运行期织入则是采用CGLIB工具或JDK动态代理进行切面的织入。

什么是 Java Agent

Java Agent 又叫做 Java 探针,从JDK1.5 开始引入,可在类加载前修改class字节码。

Jvm启动参数 -javaagent 参数 俗称java探针技术(Premain方式)

使用方法:-javaagent:xxx.jar, 在main方法执行前查找jar中MANIFEST.MF文件的Premain-Class配置项,Premain-Class对应一个java类,然后执行方法premain(String options, Instrumentation instrumentation) premain(String options),如果同时存在,执行前者。

示例说明:

一个普通业务类:

package org.aop.service;

public class UserService {
    public void say() {
        System.out.println("Hello World!!");
    }
}

Premain-Class类:CustomPremainAgent,方法premain只执行一次

package org.aop.agent;

import java.lang.instrument.Instrumentation;

public class CustomPremainAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        if (inst == null) {
            throw new UnsupportedOperationException("JDK5才开始支持!!! preMain -javaagent");
        }

        System.out.println("Running==>premain(String agentArgs, Instrumentation inst)");
        System.out.println("agentArgs : " + agentArgs);

        //加入自定义转换器
        inst.addTransformer(new CustomClassFileTransformer(), true);
    }

    public static void premain(String agentArgs) {
        System.out.println("Running==>premain(String agentArgs)");
    }
}

Instrumentation:premain方法参数,由jvm虚拟机自动传入,class字节码载入jvm时触发执行transform方法(只执行一次)。

ClassFileTransformer类:CustomPremainAgent,拦截UserService类,利用javassist工具修改UserService的class字节码,修改say()方法添加切面功能

package org.aop.agent;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

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

public class CustomClassFileTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if (className.equals("org/aop/service/UserService")) {
            ClassPool classPool = ClassPool.getDefault();
            try {
                CtClass ctClass = classPool.get("org.aop.service.UserService");
                CtMethod ctMethod = ctClass.getDeclaredMethod("say");
                ctMethod.insertBefore("System.out.println(\"-javassist-- before say() ---\");");
                ctMethod.insertAfter("System.out.println(\"-javassist-- after say() ---\");");
                ctClass.detach();
                return ctClass.toBytecode();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return new byte[0];
    }
}

MANIFEST.MF:在类路径META-INF下创建MANIFEST.MF文件

Manifest-Version: 1.0
Premain-Class: org.aop.agent.CustomPremainAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Class-Path: javassist-3.20.0-GA.jar
Build-Jdk-Spec: 1.8
Created-By: Maven Jar Plugin 3.2.0

然后将CustomPremainAgent.java、CustomClassFileTransformer.java、META-INF/MANIFEST.MF编译后打包.

如果用maven打包可以不用手动创建MANIFEST.MF文件

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.2.0</version>
    <configuration>
        <includes>
            <include>org/aop/agent/CustomPremainAgent.class</include>
            <include>org/aop/agent/CustomClassFileTransformer.class</include>
        </includes>
        <archive>
            <!--自动添加META-INF/MANIFEST.MF -->
            <manifest>
                <addClasspath>true</addClasspath>
            </manifest>
            <manifestEntries>
                <Premain-Class>org.aop.agent.CustomPremainAgent</Premain-Class>
                <Can-Redefine-Classes>true</Can-Redefine-Classes>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

最后打包路径:D:/workspace/demo/target/user-service-agent.jar

测试代码:

public static void main(String[] args) {
    UserService service = new UserService();
    service.say();
}

在vm参数中加入:-javaagent:D:/workspace/demo/target/user-service-agent.jar

运行结果:

从JDK6开始,引入一种新的方式:Agentmain方式.
Premain只能在类加载之前修改字节码,类加载之后无能为力。而Agentmain恰恰可以弥补这缺点。Agentmain 可以在类加载之后再次加载这个类。

使用方法:VirtualMachine.loadAgent("XXX.jar", agentArgs), 在main方法执行中查找jar中MANIFEST.MF文件的Agent-Class配置项,Agent-Class对应一个java类,然后执行方法agentmain(String options, Instrumentation instrumentation) agentmain(String options),如果同时存在,执行前者。

示例说明:

Agent-Class类:CustomAgentmainAgent,方法agentmain只执行一次

package org.aop.agent;

import org.aop.service.UserService;

import java.lang.instrument.Instrumentation;

public class CustomAgentmainAgent {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        if (inst == null) {
            throw new UnsupportedOperationException("JDK6才开始支持!!! agentmain -javaagent");
        }

        System.out.println("Running==>agentmain(String agentArgs, Instrumentation inst)");
        System.out.println("agentArgs : " + agentArgs);

        //加入自定义转换器
        inst.addTransformer(new CustomClassFileTransformer(), true);
        try {
            //允许修改字节码
            inst.retransformClasses(UserService.class);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static void agentmain(String agentArgs) {
        System.out.println("Running==>agentmain(String agentArgs)");
    }
}

MANIFEST.MF:在类路径META-INF下创建MANIFEST.MF文件

Manifest-Version: 1.0
Agent-Class: org.aop.agent.CustomAgentmainAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Class-Path: javassist-3.20.0-GA.jar
Build-Jdk-Spec: 1.8
Created-By: Maven Jar Plugin 3.2.0

然后将CustomAgentmainAgent .java、CustomClassFileTransformer.java、META-INF/MANIFEST.MF编译后打包.

最后打包路径:D:/workspace/demo/target/user-service-agent.jar

测试代码:

import com.sun.tools.attach.VirtualMachine;
import org.aop.service.UserService;

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

public class AgentmainTest {
    public static void main(String[] args) throws Exception {
        taskThread.start();
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (Exception e) {
            e.printStackTrace();
        }

        String pid = jvmPid();
        System.out.println("进程pid==>" + pid);
        agent(pid);
    }

    /**
     * 获取jvm进程ID
     * 还可以通过jdk内置命令:jps -l 获取jvm进程ID
     * @return
     */
    private static String jvmPid() {
        String thisJvmName = ManagementFactory.getRuntimeMXBean().getName();
        String thisJvmPid = thisJvmName.split("@")[0];
        return thisJvmPid;
    }

    /**
     * 模拟运行中的系统
     */
    private static Thread taskThread =  new Thread() {
        @Override
        public void run() {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(3);
                    UserService service = new UserService();
                    service.say();
                    System.out.println("\n");
                } catch (Exception e) {}
            }
        }
    };

    /**
     * agent attach
     *
     * @param pid
     */
    public static void agent(String pid) {
        try {
            System.out.println("=========准备增强类=========");
            VirtualMachine vm = VirtualMachine.attach(pid);
            vm.loadAgent("D:/workspace/demo/target/user-service-agent.jar", "org.aop.service.UserService");
            vm.detach();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
编译时,VirtualMachine可能无法编译,可以pom.xml添加如下依赖解决
<dependency>
    <groupId>jdk.tools</groupId>
    <artifactId>jdk.tools</artifactId>
    <version>1.8</version>
    <scope>system</scope>
    <!--<systemPath>${java.home}/lib/tools.jar</systemPath>-->
    <systemPath>C:/Program Files/Java/jdk-1.8/lib/tools.jar</systemPath>
</dependency>

运行结果:

什么是 AspectJ

AspectJ是一个面向切面的框架(AOP框架)。利用 -javaagent 实现的类加载期切面织入(LTW: Load-Time Weaving)

官网地址: Intro to AspectJ | Baeldung

源码地址:GitHub - eclipse-aspectj/aspectj

查看AspectJ jar包中的MANIFEST.MF可得知AspectJ的Premain-Class是 org.aspectj.weaver.loadtime.Agent

AspectJ从1.8.7版本开始支持Agentmain方式。

简单demo示例:

业务类:

package org.aop.service;

public class UserService {
    public void say() {
        System.out.println("Hello World!!");
    }
}

增强类:

package org.aop.aspectj;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AspectConfig {

	@Pointcut("execution(* org.aop.service..*.*(..))")
	public void callAt() {
	}

	@Around("callAt()")
	public Object around(ProceedingJoinPoint pjp) throws Throwable {
		System.out.println("Around==>before");
		Object object = pjp.proceed();
		System.out.println("Around==>after");
		return object;
	}

	public static AspectConfig aspectOf() {
		return new AspectConfig();
	}
}

META-INF新增aop.xml文件

<aspectj>
	<aspects>
		<aspect name="org.aop.aspectj.AspectConfig"/>
		<weaver options="-verbose -showWeaveInfo">
			<include within="org.aop.service.*"/>
		</weaver>
	</aspects>
</aspectj>

测试代码:

package org.aop;

import org.aop.service.UserService;

/*
  AspectJ官网地址:https://www.baeldung.com/aspectj

  在类路径META-INF下创建 aop.xml文件
  内容:
	<aspectj>
		<aspects>
			<aspect name="org.spring.aop.aspectj.AspectConfig"/>
			<weaver options="-verbose -showWeaveInfo">
				<include within="org.spring.aop.service.*"/>
			</weaver>
		</aspects>
	</aspectj>

  jvm启动参数:-javaagent:C:/Users/用户目录/.m2/repository/org/aspectj/aspectjweaver/1.7.4/aspectjweaver-1.7.4.jar
 */
public class AspectJMain {

	public static void main(String[] args) {
		UserService service = new UserService();
		service.say();
	}
}

执行测试:

   添加vm参数:-javaagent:XXXXX/aspectjweaver-1.7.4.jar

可以重写AspectJ的Premain-Class和ClassFileTransformer,将修改后的类字节码写入文件,查看生成情况。

package org.aop.agent;

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

public class AspectAgent {
    /**
     * The instrumentation instance
     */
    private static Instrumentation s_instrumentation;

    /**
     * The ClassFileTransformer wrapping the weaver
     */
    private static ClassFileTransformer s_transformer = new ClassPreProcessorAgentAdapter();

    /**
     * JSR-163 preMain Agent entry method
     *
     * @param options
     * @param instrumentation
     */
    public static void premain(String options, Instrumentation instrumentation) {
        /* Handle duplicate agents */
        if (s_instrumentation != null) {
            return;
        }
        s_instrumentation = instrumentation;
        s_instrumentation.addTransformer(s_transformer);
    }

    /**
     * Returns the Instrumentation system level instance
     */
    public static Instrumentation getInstrumentation() {
        if (s_instrumentation == null) {
            throw new UnsupportedOperationException("Java 5 was not started with preMain -javaagent for AspectJ");
        }
        return s_instrumentation;
    }
}
package org.aop.agent;

import org.aspectj.weaver.loadtime.Aj;
import org.aspectj.weaver.loadtime.ClassPreProcessor;

import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.lang.instrument.ClassFileTransformer;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.ProtectionDomain;

/**
 * Java 1.5 adapter for class pre processor
 *
 * @author <a href="mailto:alex@gnilux.com">Alexandre Vasseur</a>
 */
public class ClassPreProcessorAgentAdapter implements ClassFileTransformer {

    /**
     * Concrete preprocessor.
     */
    private static ClassPreProcessor s_preProcessor;

    static {
        try {
            s_preProcessor = new Aj();
            s_preProcessor.initialize();
        } catch (Exception e) {
            throw new ExceptionInInitializerError("could not initialize JSR163 preprocessor due to: " + e.toString());
        }
    }

    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] bytes) {
        if (classBeingRedefined != null) {
            System.err.println("INFO: (Enh120375):  AspectJ attempting reweave of '" + className + "'");
        }
        byte[] newByte = s_preProcessor.preProcess(className, bytes, loader, protectionDomain);
        if (className.equals("org/aop/service/UserService")) { //写入文件
            this.writeFile(newByte);
        }
        return newByte;
    }

    //写入文件
    private void writeFile(byte[] newByte) {
        try {
            String dirPath = System.getProperty("user.dir") + "/src/proxy";
            Path outDirPath = Paths.get(dirPath);
            Files.createDirectories(outDirPath);
            Path outPath = Paths.get(dirPath + "/UserService$Proxy01.class");
            Files.write(outPath, newByte, new OpenOption[0]);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

记得重新生成MANIFEST.MF配置内容,重新打包,修改 -javaagent:XXX/自己的.jar.

  • 24
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值