简答
- 不要迭代所有从 Instrumentation 加载的类。相反,只需检查传入转换器的类名,如果它与您的目标类匹配,则对其进行转换。否则,只需返回未修改的传递的 classfileBuffer。
- 在变压器外部进行设置调用(即,在您的情况下,从您的代理执行以下操作),因此使用您要转换的类名初始化您的变压器(这将是内部格式所以而不是foo.bar. 搞砸了,您将寻找匹配的富/酒吧/混乱。然后添加变压器,调用 retransform,然后删除变压器。
- 为了调用 retransform,您将需要实际的 [pre-transform] 类,您可以通过调用找到该类名称类(在agentmain中),或者如果你绝对有必要,你可以在Instrumentation.get 加载的类()作为最后的手段。如果目标类尚未加载,则需要调用类加载器Class.forName(名称,布尔值,类加载器)在这种情况下,您可以将 URL 传递到代理主字符串参数中的目标类类路径。
长答案
这里有一些建议:
- Separate out the operation you're calling into 2 separate operations:
- 安装代理。这只需要完成一次。
- 转换目标类。你可能想要这样做n times.
- 我将通过在安装代理时注册一个简单的 JMX MBean 来实现 1.2。这个 MBean 应该提供类似的操作
public void transformClass(String className)
。并且应该参考代理获取的信息进行初始化仪器仪表实例。 MBean 类、接口和任何必需的第三方类应包含在您的代理的加载的.jar。它还应该包含您的修改方法测试类(我认为它已经这样做了)。
- 在安装代理 jar 的同时,还要安装管理代理$JAVA_HOME/lib/management-agent.jar这将激活管理代理,以便您可以在要注册的 MBean 中调用转换操作。
- 参数化您的 DemoTransformer 类以接受内部形式您要转换的类的名称。 (即,如果您的二进制类名称是foo.bar. 搞砸了,内部形式将是富/酒吧/混乱。当您的 DemoTransformer 实例开始获取转换回调时,请忽略与您指定的内部表单类名不匹配的所有类名。 (即简单地返回未修改的classfileBuffer)
- Your tranformer MBean transformClass operation should then:
- 将传递的类名转换为内部形式。
- 创建一个新的 DemoTransformer,传递内部表单类名。
- 使用注册 DemoTransformer 实例
Instrumentation.addTransformer(theNewDemoTransformer, true)
.
- Call
Instrumentation.retransformClasses(ClassForName(className))
(将二进制类名传递给 MBean 操作)。当此调用返回时,您的类将被转换。
- 拆下变压器
Intrumentation.removeTransformer(theNewDemoTransformer)
.
以下是我的意思的未经测试的近似值:
变压器 MBean
public interface TransformerServiceMBean {
/**
* Transforms the target class name
* @param className The binary name of the target class
*/
public void transformClass(String className);
}
变压器服务
public class TransformerService implements TransformerServiceMBean {
/** The JVM's instrumentation instance */
protected final Instrumentation instrumentation;
/**
* Creates a new TransformerService
* @param instrumentation The JVM's instrumentation instance
*/
public TransformerService(Instrumentation instrumentation) {
this.instrumentation = instrumentation;
}
/**
* {@inheritDoc}
* @see com.heliosapm.shorthandexamples.TransformerServiceMBean#transformClass(java.lang.String)
*/
@Override
public void transformClass(String className) {
Class<?> targetClazz = null;
ClassLoader targetClassLoader = null;
// first see if we can locate the class through normal means
try {
targetClazz = Class.forName(className);
targetClassLoader = targetClazz.getClassLoader();
transform(targetClazz, targetClassLoader);
return;
} catch (Exception ex) { /* Nope */ }
// now try the hard/slow way
for(Class<?> clazz: instrumentation.getAllLoadedClasses()) {
if(clazz.getName().equals(className)) {
targetClazz = clazz;
targetClassLoader = targetClazz.getClassLoader();
transform(targetClazz, targetClassLoader);
return;
}
}
throw new RuntimeException("Failed to locate class [" + className + "]");
}
/**
* Registers a transformer and executes the transform
* @param clazz The class to transform
* @param classLoader The classloader the class was loaded from
*/
protected void transform(Class<?> clazz, ClassLoader classLoader) {
DemoTransformer dt = new DemoTransformer(clazz.getName(), classLoader);
instrumentation.addTransformer(dt, true);
try {
instrumentation.retransformClasses(clazz);
} catch (Exception ex) {
throw new RuntimeException("Failed to transform [" + clazz.getName() + "]", ex);
} finally {
instrumentation.removeTransformer(dt);
}
}
}
变压器类
public class DemoTransformer implements ClassFileTransformer {
/** The internal form class name of the class to transform */
protected String className;
/** The class loader of the class */
protected ClassLoader classLoader;
/**
* Creates a new DemoTransformer
* @param className The binary class name of the class to transform
* @param classLoader The class loader of the class
*/
public DemoTransformer(String className, ClassLoader classLoader) {
this.className = className.replace('.', '/');
this.classLoader = classLoader;
}
/**
* {@inheritDoc}
* @see java.lang.instrument.ClassFileTransformer#transform(java.lang.ClassLoader, java.lang.String, java.lang.Class, java.security.ProtectionDomain, byte[])
*/
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
if(className.equals(this.className) && loader.equals(classLoader)) {
return new ModifyMethodTest(classfileBuffer).modiySleepMethod();
}
return classfileBuffer;
}
}
中介
public class AgentMain {
public static void agentmain (String agentArgs, Instrumentation inst) throws Exception {
TransformerService ts = new TransformerService(inst);
ObjectName on = new ObjectName("transformer:service=DemoTransformer");
// Could be a different MBeanServer. If so, pass a JMX Default Domain Name in agentArgs
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
server.registerMBean(ts, on);
// Set this property so the installer knows we're already here
System.setProperty("demo.agent.installed", "true");
}
}
代理安装程序
public class AgentInstaller {
/**
* Installs the loader agent on the target JVM identified in <code>args[0]</code>
* and then transforms all the classes identified in <code>args[1..n]</code>.
* @param args The target JVM pid in [0] followed by the classnames to transform
*/
public static void main(String[] args) {
String agentPath = "D:\\work\\workspace\\myjar\\loaded.jar";
String vid = args[0];
VirtualMachine vm = VirtualMachine.attach(vid);
// Check to see if transformer agent is installed
if(!vm.getSystemProperties().contains("demo.agent.installed")) {
vm.loadAgent(agentPath);
// that property will be set now,
// and the transformer MBean will be installed
}
// Check to see if connector is installed
String connectorAddress = vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress", null);
if(connectorAddress==null) {
// It's not, so install the management agent
String javaHome = vm.getSystemProperties().getProperty("java.home");
File managementAgentJarFile = new File(javaHome + File.separator + "lib" + File.separator + "management-agent.jar");
vm.loadAgent(managementAgentJarFile.getAbsolutePath());
connectorAddress = vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress", null);
// Now it's installed
}
// Now connect and transform the classnames provided in the remaining args.
JMXConnector connector = null;
try {
// This is the ObjectName of the MBean registered when loaded.jar was installed.
ObjectName on = new ObjectName("transformer:service=DemoTransformer");
// Here we're connecting to the target JVM through the management agent
connector = JMXConnectorFactory.connect(new JMXServiceURL(connectorAddress));
MBeanServerConnection server = connector.getMBeanServerConnection();
for(int i = 1; i < args.length; i++) {
String className = args[i];
// Call transformClass on the transformer MBean
server.invoke(on, "transformClass", new Object[]{className}, new String[]{String.class.getName()});
}
} catch (Exception ex) {
ex.printStackTrace(System.err);
} finally {
if(connector!=null) try { connector.close(); } catch (Exception e) {}
}
// Done. (Hopefully)
}
}
=================更新=================
嘿尼克;是的,这是当前(即 Java 5-8)类转换器的限制之一。
引用自仪器javadoc http://docs.oracle.com/javase/7/docs/api/java/lang/instrument/Instrumentation.html:
“重新转换可能会改变方法体、常量池和
属性。重新转换不得添加、删除或重命名字段
或方法,更改方法的签名,或更改继承。
这些限制可能会在未来版本中取消。类文件
直到之后才检查、验证和安装字节
如果结果字节有错误,则已应用转换
这个方法会抛出异常。”
顺便说一句,同样的限制也被逐字记录在重新定义类中。
因此,您有 2 个选择:
不要添加新方法。这通常是非常有限的,并且取消了非常常见的字节码 AOP 模式(如方法)的使用资格
包装。根据您使用的字节码操作库,您也许能够注入您想要的所有功能
现有的方法。有些库比其他库更容易做到这一点。或者,我应该说,有些库会让这比其他库更容易。
在类加载之前转换类。这使用了我们已经讨论过的代码的相同通用模式,只是您不触发
通过调用 retransformClasses 进行转换。相反,您注册 ClassFileTransformer 来执行转换before类已加载
并且您的目标类将在加载第一个类时被修改。在这种情况下,您几乎可以自由地以任何方式修改该类
就像,只要最终产品仍然可以被验证。击败应用程序(即让你的 ClassFileTransformer
在应用程序加载类之前注册)很可能需要像这样的命令java代理,尽管如果你有严格的控制
在应用程序的生命周期中,可以在更传统的应用程序层代码中执行此操作。正如我所说,你只需要做
确保在加载目标类之前注册变压器。
您可以使用的#2 的另一种变体是simulate使用新的类加载器创建一个全新的类。如果您创建一个新的
隔离的类加载器不会委托给现有的[已加载]类,但可以访问[已卸载]目标类字节码,
你本质上是在重现上面#2 的要求,因为 JVM 认为这是一个全新的类。
================更新================
在你最后的评论中,我觉得我有点不知道你在哪里了。无论如何,Oracle JDK 1.6 绝对支持重新转换。我对 ASM 不太熟悉,但您发布的最后一个错误表明 ASM 转换以某种方式修改了不允许的类模式,因此重新转换失败。
我认为一个工作示例会更加清晰。与上面相同的类(加上一个名为 Person 的测试类)是here https://gist.github.com/nickman/6494990。有一些修改/添加:
- The transform operation in the TransformerService https://gist.github.com/nickman/6494990#file-transformerservice-java now has 3 parameters:
- 二进制类名
- 仪器的方法名称
- 与方法签名匹配的[正则]表达式。 (如果为 null 或为空,则匹配所有签名)
- 实际的字节码修改是使用完成的Java助手 http://www.csg.ci.i.u-tokyo.ac.jp/~chiba/javassist/ in the 修改方法测试 https://gist.github.com/nickman/6494990#file-modifymethodtest-java班级。所有仪器所做的就是添加一个系统输出打印文件看起来像这样:
-->Invoked method [com.heliosapm.shorthandexamples.Person.sayHello((I)V)]
- The 代理安装程序 https://gist.github.com/nickman/6494990#file-agentinstaller-java(其中有演示的 Main)只需自行安装代理和转换服务。 (更容易用于开发/演示目的,但仍可与其他 JVM 一起使用)
- 一旦代理自行安装,主线程就会创建一个Person https://gist.github.com/nickman/6494990#file-person-java实例并只是循环,调用 Person 的两个sayHello方法。
在转换之前,该输出如下所示。
Temp File:c:\temp\com.heliosapm.shorthandexamples.AgentMain8724970986698386534.jar
Installing AgentMain...
AgentMain Installed
Agent Loaded
Instrumentation Deployed:true
Hello [0]
Hello [0]
Hello [1]
Hello [-1]
Hello [2]
Hello [-2]
人有2sayHello方法,需要一种int,另一个需要一个String。 (字符串一仅打印循环索引的负数)。
一旦启动 AgentInstaller,代理就会安装完毕,Person 就会在循环中被调用,我会使用 JConsole 连接到 JVM:
我导航到 TransformerService MBean 并调用变换类手术。我提供完全限定的类 [二进制] 名称、仪器的方法名称以及正则表达式(I)V哪个匹配only the sayHello以 int 作为参数的方法。 (或者我可以提供.*,或者没有任何东西可以匹配所有重载)。我执行操作。
现在,当我返回正在运行的 JVM 并检查输出时:
Examining class [com/heliosapm/shorthandexamples/Person]
Instrumenting class [com/heliosapm/shorthandexamples/Person]
[ModifyMethodTest] Adding [System.out.println("\n\t-->Invoked method [com.heliosapm.shorthandexamples.Person.sayHello((I)V)]");]
[ModifyMethodTest] Intrumented [1] methods
-->Invoked method [com.heliosapm.shorthandexamples.Person.sayHello((I)V)]
Hello [108]
Hello [-108]
-->Invoked method [com.heliosapm.shorthandexamples.Person.sayHello((I)V)]
Hello [109]
Hello [-109]
完毕。方法仪器化。
请记住,允许重新转换的原因是 Javassist 字节码修改除了将代码注入现有方法之外没有进行任何更改。
合理 ?