-
premain是Java SE5开始就提供的代理方式,由于其
必须在命令行指定代理jar,并且代理类必须在main方法前启动
。因此,要求开发者在应用前就必须确认代理的处理逻辑和参数内容等等。在有些场合下,premain代理方式不能满足需求:- premain 必须启动时在命令行指定代理jar,
- 并且代理类必须在main方法前启动,只执行一次
- 类加载之后无能为力,只能通过重新创建ClassLoader 这种方式重新加载
为解决运行时启动代理类的问题,Java SE6开始提供了在应用程序的VM启动后在动态添加代理的方式,即agentmain方式。 agentmain 可以在类加载之后再次加载一个类,也就是重定义,你就可以通过在重定义的时候进行修改类了
-
与Permain类似,agent方式同样需要提供一个agent jar,并且这个jar需要满足:
- 代理类需要提供agentmain方法。并且再二者同时存在时以前者优先。args和inst和premain中的一致:
public static void agentmain(String args, Instrumentation inst) public static void agentmain(String args)
- 定义一个MANIFEST.MF 文件,文件中必须包含 Agent-Class
- 创建一个 Agent-Class 指定的类,该类必须包含 agentmain 方法
- 将MANIFEST.MF 和 Agent 类打成 jar 包
- 将 jar 包载入目标虚拟机,目标虚拟机将会自动执行 agentmain 方法执行方法逻辑
- 代理类需要提供agentmain方法。并且再二者同时存在时以前者优先。args和inst和premain中的一致:
-
重定义类的对类的修改是有限制的:
- 父类是同一个;
- 实习那的接口数也要相同;
- 类访问符必须一致;
- 字段数和字段名必须一致;
- 新增的方法必须是 private static/final 的;
- 可以删除修改方法;
计划:每一次类修改重加载后,给每个方法添加计时程序
-
计时程序(注意:所有程序我都没有写包名)
import org.apache.log4j.Logger; public class InvokeTimer { private static Logger logger = Logger.getLogger(InvokeTimer.class); static ThreadLocal<Long> threadLocal = new ThreadLocal<Long>(); public static void start() { threadLocal.set(System.currentTimeMillis()); } public static void end() { long time = System.currentTimeMillis() - threadLocal.get(); StackTraceElement element = Thread.currentThread().getStackTrace()[2]; logger.debug("Class name:"+element.getClassName() +",Method name:"+ element.getMethodName() + ",Line number:"+ element.getLineNumber()+ ",耗费时间:" + time + "ms."); } }
获取app根路径
import java.io.InputStream;
import java.net.URL;
/**
* Created by micocube
* ProjectName: coding
* PackageName: com.coding.Path
* User: micocube
* Email: ldscube@gmail.com
* CreateTime: 2019/1/11下午4:01
* ModifyTime: 2019/1/11下午4:01
* Version: 0.1
* Description:
**/
public class PathUtils {
public static void main(String[] args) {
System.out.println(PathUtils.getRootPath());
}
public static String getRootPath(){
URL resource = ClassLoader.getSystemResource("");
// ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// URL resource = classLoader.getResource("");
String path = resource.getPath();
// System.out.println("RootPath:"+path);
return path;
}
public static String getRootPath(String name){
URL resource = ClassLoader.getSystemResource("");
// URL resource = Thread.currentThread().getContextClassLoader().getResource(name);
String path = resource.getPath();
return path;
}
public static String getClassRealPath(Class clazz){
String rootPath = getRootPath();
String path = rootPath + class2ClassPath(clazz);
return path;
}
private static String class2ClassPath(Class clazz) {
return clazz.getName().replace('.', '/') + ".class";
}
public static InputStream getClassInputStream(Class clazz){
InputStream is = Thread.currentThread().getContextClassLoader()
.getResourceAsStream(class2ClassPath(clazz));
return is;
}
}
- AgentMain 程序
import org.apache.log4j.Logger;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Created by micocube
* ProjectName: coding
* PackageName: com.coding.instrumentation
* User: micocube
* Email: ldscube@gmail.com
* CreateTime: 2019/1/17下午3:02
* ModifyTime: 2019/1/17下午3:02
* Version: 0.1
* Description:
* <p>
* System.out.println("agentmain load Class :" + className);
* Class[] classes = inst.getInitiatedClasses(loader);
* // 获取所有已经被初始化过了的类
* System.out.println("initiated classes:");
* Arrays.stream(classes).forEach(System.out::println);
* // 获取所有已经被加载的类
* Class[] allLoadedClasses = inst.getAllLoadedClasses();
* System.out.println("loaded classes:");
* Arrays.stream(allLoadedClasses).forEach(System.out::println);
**/
public class HotSwapAgent {
private static Logger logger = Logger.getLogger(HotSwapAgent.class);
private static Map<String, Content> classes = new HashMap<String, Content>();
public static void premain(String args, Instrumentation inst)
{
System.out.println("PreMain Args:" + args);
}
public static void agentmain(String agentArgs, Instrumentation inst){
// Metric.printInfo();
Class<?>[] allClass = inst.getAllLoadedClasses();
String rootPath = PathUtils.getRootPath();
Content.getAllClasses(rootPath, classes);
logger.debug("Scan Modified Classes:\n");
Set<String> clazz = classes.keySet();
for(String cl : clazz){
Content content = classes.get(cl);
if(content.isModified()){
System.out.println(content);
}
}
for (Class<?> c : allClass) {
//System.out.println("Loaded class:" + c.getName());
if (classes.containsKey(c.getName())) {
Content content = classes.get(c.getName());
if (content.isModified()) {
byte[] classFile = content.getContent();
// if(c.getName().equals("TestService")){
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassReader reader = new ClassReader(classFile);
reader.accept(new AopClassAdapter(Opcodes.ASM5,cw), 0);
classFile = cw.toByteArray();
//new FileOutputStream("/Users/micocube/Documents/AgentMain/out/"+c.getName()+".class").write(classFile);
// }
ClassDefinition classDefinition = new ClassDefinition(c, classFile);
try {
inst.redefineClasses(classDefinition);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (UnmodifiableClassException e) {
e.printStackTrace();
}
}
}
}
// HotSwapTransformer transformer = new HotSwapTransformer();
// inst.addTransformer(transformer, true);
// inst.retransformClasses(Account.class);
}
}
- ASM给method添加计时程序
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class AopClassAdapter extends ClassVisitor implements Opcodes {
public AopClassAdapter(int api, ClassWriter classWriter) {
super(api, classWriter);
}
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
super.visit(version,access,name,signature,superName,interfaces);
}
public MethodVisitor visitMethod(int access, String name,
String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name,
desc, signature, exceptions);
MethodVisitor visitor = new MethodVisitor(this.api, mv) {
/**
* 访问方法头,只访问一次
*/
@Override
public void visitCode() {
super.visitCode();
this.visitMethodInsn(INVOKESTATIC, "InvokeTimer", "start", "()V",false);
}
/**
* 返回
* @param opcode
*/
@Override
public void visitInsn(int opcode) {
if (opcode == RETURN) {//在返回之前安插after 代码。
mv.visitMethodInsn(INVOKESTATIC, "InvokeTimer", "end", "()V",false);
}
super.visitInsn(opcode);
}
};
return visitor;
}
}
- 扫描app根路径,获取更改过的class文件
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Map;
public class Content {
private File file;
private Long lastModified = -1L;
private boolean modified = false;
private byte[] content = new byte[0];
public Content(File file) {
this.file = file;
}
public static void getAllClasses(String rootPath, Map<String, Content> classes) {
File file = new File(rootPath);
File[] list = file.listFiles();
for (int i = 0; i < list.length; i++) {
File f = list[i];
if (f.isDirectory()) {
getAllClasses(f.getPath(), classes);
}
if (f.getPath().endsWith(".class")) {
String path = f.getPath();
String key = path.replace(PathUtils.getRootPath(), "").replace(".class", "");
Content content;
if (classes.containsKey(key)) {
content = classes.get(key);
} else {
content = new Content(f);
classes.put(key, content);
}
try {
Content.fileContent(content);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
}
public static Content fileContent(Content content) throws Exception {
File file = content.getFile();
Long lastModified = content.getLastModified();
if (lastModified == file.lastModified()) {
content.setModified(false);
} else {
content.setModified(true);
content.setLastModified(file.lastModified());
InputStream inputStream = new FileInputStream((file));
byte[] buffer = new byte[inputStream.available()];
int read = inputStream.read(buffer);
if (-1 == read) {
System.out.println("empty file :" + file.getName());
content.setContent(null);
} else {
content.setContent(buffer);
}
}
return content;
}
public boolean isModified() {
return modified;
}
public Content setModified(boolean modified) {
this.modified = modified;
return this;
}
public File getFile() {
return file;
}
public Content setFile(File file) {
this.file = file;
return this;
}
public Long getLastModified() {
return lastModified;
}
public Content setLastModified(Long lastModified) {
this.lastModified = lastModified;
return this;
}
public byte[] getContent() {
return content;
}
public Content setContent(byte[] content) {
this.content = content;
return this;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("Content{\n");
sb.append("file=").append(file).append(",\n");
sb.append("lastModified=").append(lastModified).append(",\n");
sb.append("modified=").append(modified).append(",\n");
sb.append("contentLength=").append(content.length).append("\n");
sb.append("}\n");
return sb.toString();
}
}
-
逻辑很简单 :
- 获取所有已经加载过的类,
- 扫描app的根路径,检测是否有更改过的class文件
- 如果class文件被更改,使用ASM给该类的所有method添加计时程序
- 调用Instrumentation的redefineClasses方法,重定义类
-
执行程序:
- 模拟app运行期间的执行程序[把它想象成tomcat运行用户定义的方法向外提供服务的app]
public class Main { public static void main(String[] args) throws InterruptedException { for (; ; ) { new TestService().operation(); Thread.sleep(5000); } } }
public class TestService { public void operation() { System.out.println("Hello World!"); try { Thread.sleep(10); } catch (Exception e) { e.printStackTrace(); } } }
- 使用外部程序定时加载agentmain的jar,每隔3秒扫描一次class
import com.sun.tools.attach.*; import java.io.IOException; import java.util.List; import java.util.Timer; import java.util.TimerTask; /** * Created by micocube * ProjectName: coding * PackageName: com.coding.instrumentation * User: micocube * Email: ldscube@gmail.com * CreateTime: 2019/1/17下午3:15 * ModifyTime: 2019/1/17下午3:15 * Version: 0.1 * Description: **/ public class LoadAgent { public static void main(String[] args) throws Exception { new Timer().schedule(new TimerTask() { @Override public void run() { try { LoadAgent.load("Main", "/Users/micocube/Documents/AgentMain/src/AgentMain.jar", "cxs"); } catch (IOException e) { e.printStackTrace(); } catch (AgentLoadException e) { e.printStackTrace(); } catch (AgentInitializationException e) { e.printStackTrace(); } catch (AttachNotSupportedException e) { e.printStackTrace(); } } },0,3000L); } public static void load(String appName, String agentJarPath, String agentArgs) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException { System.out.println("agentJarPath"+agentJarPath+",args:"+agentArgs); List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list) { System.out.println("VirtualMachineDescriptorName:" + vmd.displayName()); if (vmd.displayName().endsWith(appName)) { VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent(agentJarPath, agentArgs); virtualMachine.detach(); } } } }
MANIFEST.MF,将该文件和上面所有的class打包成jar
Manifest-Version: 1.0
Can-Redefine-Classes: true
Agent-Class: HotSwapAgent
Premain-Class: HotSwapAgent
Can-Retransform-Classes: true
-
运行
- 运行Main.java
- 运行LoadAgent.java,注意agentJarPath参数,换成你的jar的路径
- 输出Hello World!
- 修改TestService.operation方法,修改为输出
Hello World! QQQQQ
,重新编译,覆盖旧class,3秒后class被检测到修改,输出了新的内容