Java 如何在运行时重新转换类
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/18567552/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
how to retransform a class at runtime
提问by Nick Dong
I am tring modify class which already loaded in a jvm. The solution which I found is:
我正在修改已经加载到 jvm 中的类。我找到的解决方案是:
- 1st Attach an agent to a jvm specified by pid. (e.g. 8191)(Codes: AttachTest)
- 2nd Find the class which you want modified from those that have already been loaded in the jvm(e.g. 8191).
- 3rd Add transformer using Instrument (Codes: AgentMain)
- 4th Modify the class(e.g. Person) in
transform
method(Codes: DemoTransformer) - 5th Retransform the class using retransformClasses
- 1st 将代理附加到由 pid 指定的 jvm。(例如 8191)(代码:AttachTest)
- 2nd 从那些已经加载到 jvm 中的类中找到你想要修改的类(例如 8191)。
- 3rd 使用 Instrument 添加变压器(代码:AgentMain)
- 4th 修改方法中的类(例如 Person)
transform
(代码:DemoTransformer) - 5th 使用retransformClasses重新转换类
It works fine from 1st step to 5th step, but there are problems at retransformClasses
. It called transform
again which contains codes to modify class. And It modify other classes which I never wanna modify.
I think the problem may occured during addTransformer
or retransformClasses
. But I still confused. Well, how to retransform a class? Any ideas? thx
从第 1 步到第 5 步都可以正常工作,但在retransformClasses
. 它transform
再次调用包含修改类的代码。它修改了我不想修改的其他类。我认为问题可能发生在addTransformer
或期间retransformClasses
。但我还是一头雾水。那么,如何重新转换一个类呢?有任何想法吗?谢谢
public class AttachTest {
public static void main(String[] args) throws AttachNotSupportedException,
IOException, AgentLoadException, AgentInitializationException {
String agentPath = "D:\work\workspace\myjar\loaded.jar";
String vid = args[0];
VirtualMachine vm = VirtualMachine.attach(vid);
vm.loadAgent(agentPath);
}
}
//Agent
//代理人
public class AgentMain {
public static void agentmain (String agentArgs, Instrumentation inst)
throws ClassNotFoundException, UnmodifiableClassException,
InterruptedException {
Class<?> [] allLoadedClasses = inst.getAllLoadedClasses();
String tmpString = null;
for (int i = 0; i<allLoadedClasses.length; i++) {
tmpString = allLoadedClasses[i].getName();
if (0 != tmpString.length()) {
if (-1 != tmpString.lastIndexOf(".")) {
tmpString = tmpString.substring(tmpString.lastIndexOf(".")+1,tmpString.length());
}
if (tmpString.equals("Person")) {
inst.addTransformer(new DemoTransformer(), true);
inst.retransformClasses(allLoadedClasses[i]);
}
}
}
}
}
|
|
public class DemoTransformer implements ClassFileTransformer {
@Override
public byte[] transform (ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
ModifyMethodTest tm = new ModifyMethodTest(classfileBuffer);
byte[] byteArray = null;
try {
byteArray = tm.modiySleepMethod();
} catch (Exception e) {
e.printStackTrace();
}
return byteArray;
}
}
OUTPUTS:THE ATTACH PROGRAM
输出:附加程序
javax.management.RuntimeMBeanException: java.lang.RuntimeException: Failed to transform [Person]
at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.rethrow(DefaultMBeanServerInterceptor.java:856)
at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.rethrowMaybeMBeanException(DefaultMBeanServerInterceptor.java:869)
at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.invoke(DefaultMBeanServerInterceptor.java:838)
at com.sun.jmx.mbeanserver.JmxMBeanServer.invoke(JmxMBeanServer.java:761)
at javax.management.remote.rmi.RMIConnectionImpl.doOperation(RMIConnectionImpl.java:1427)
at javax.management.remote.rmi.RMIConnectionImpl.access0(RMIConnectionImpl.java:72)
at javax.management.remote.rmi.RMIConnectionImpl$PrivilegedOperation.run(RMIConnectionImpl.java:1265)
at javax.management.remote.rmi.RMIConnectionImpl.doPrivilegedOperation(RMIConnectionImpl.java:1360)
at javax.management.remote.rmi.RMIConnectionImpl.invoke(RMIConnectionImpl.java:788)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:305)
at sun.rmi.transport.Transport.run(Transport.java:159)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:155)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:535)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:790)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:649)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:619)
at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:255)
at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:233)
at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:142)
at com.sun.jmx.remote.internal.PRef.invoke(Unknown Source)
at javax.management.remote.rmi.RMIConnectionImpl_Stub.invoke(Unknown Source)
at javax.management.remote.rmi.RMIConnector$RemoteMBeanServerConnection.invoke(RMIConnector.java:993)
at AttachStackOverflow.main(AttachStackOverflow.java:57)
Caused by: java.lang.RuntimeException: Failed to transform [Person]
at loaded3.TransformerService.transform(TransformerService.java:75)
at loaded3.TransformerService.transformClass(TransformerService.java:38)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at com.sun.jmx.mbeanserver.StandardMBeanIntrospector.invokeM2(StandardMBeanIntrospector.java:93)
at com.sun.jmx.mbeanserver.StandardMBeanIntrospector.invokeM2(StandardMBeanIntrospector.java:27)
at com.sun.jmx.mbeanserver.MBeanIntrospector.invokeM(MBeanIntrospector.java:208)
at com.sun.jmx.mbeanserver.PerInterface.invoke(PerInterface.java:120)
at com.sun.jmx.mbeanserver.MBeanSupport.invoke(MBeanSupport.java:262)
at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.invoke(DefaultMBeanServerInterceptor.java:836)
at com.sun.jmx.mbeanserver.JmxMBeanServer.invoke(JmxMBeanServer.java:761)
at javax.management.remote.rmi.RMIConnectionImpl.doOperation(RMIConnectionImpl.java:1427)
at javax.management.remote.rmi.RMIConnectionImpl.access0(RMIConnectionImpl.java:72)
at javax.management.remote.rmi.RMIConnectionImpl$PrivilegedOperation.run(RMIConnectionImpl.java:1265)
at javax.management.remote.rmi.RMIConnectionImpl.doPrivilegedOperation(RMIConnectionImpl.java:1360)
at javax.management.remote.rmi.RMIConnectionImpl.invoke(RMIConnectionImpl.java:788)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:305)
at sun.rmi.transport.Transport.run(Transport.java:159)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:155)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:535)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:790)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:649)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:619)
Caused by: java.lang.UnsupportedOperationException: class redefinition failed: attempted to change the schema (add/remove fields)
at sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
at sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:124)
at loaded3.TransformerService.transform(TransformerService.java:72)
... 31 more
OUTPUTS:TARGET PROGRAM
产出:目标计划
print Call sayHello()
print Hello World!
Supported Redefine
Supported Retransform
Call transform() in TransformerService
Add transformer
support redefine. return TRUE
support retransforme. return TRUE
IsModifiable class "class Person". return TRUE
Retransform classes
Number of times to Call transform() in DemoTransformer:1
####ASM CODE####
consturct ModifyMethodTest
Call modifySleepMethod
new classreader
new classwriter
construct ModifyClassAdapter
sayHello
consturct Modifymethod
[arg1] = java/io/PrintStream [arg2] = println #5
[arg1] = java/io/PrintStream [arg2] = println #13
[arg1] = java/util/concurrent/TimeUnit [arg2] = sleep #22
[arg1] = java/io/PrintStream [arg2] = println #30
sayHello2
consturct Modifymethod
[arg1] = java/io/PrintStream [arg2] = println #5
[arg1] = java/io/PrintStream [arg2] = println #13
<init>
consturct Modifymethod
[arg1] = java/lang/Object [arg2] = <init> #1
main
consturct Modifymethod
[arg1] = Person [arg2] = <init> #4
[arg1] = Person [arg2] = sayHello #9
[arg1] = Person [arg2] = sayHello2 #13
[arg1] = java/lang/InterruptedException [arg2] = printStackTrace #21
getName
consturct Modifymethod
setName
consturct Modifymethod
Call visitend
Finished to call modifymethodtest
####End of ASM CODE
Remove transformer
Call transform() in TransformerService
Add transformer
support redefine. return TRUE
support retransforme. return TRUE
IsModifiable class "class Person". return TRUE
Retransform classes
Number of times to Call transform() in DemoTransformer:2
####ASM CODE####
consturct ModifyMethodTest
Call modifySleepMethod
new classreader
new classwriter
construct ModifyClassAdapter
sayHello
consturct Modifymethod
[arg1] = java/io/PrintStream [arg2] = println #5
[arg1] = java/io/PrintStream [arg2] = println #13
[arg1] = java/util/concurrent/TimeUnit [arg2] = sleep #22
[arg1] = java/io/PrintStream [arg2] = println #30
sayHello2
consturct Modifymethod
[arg1] = java/io/PrintStream [arg2] = println #5
[arg1] = java/io/PrintStream [arg2] = println #13
<init>
consturct Modifymethod
[arg1] = java/lang/Object [arg2] = <init> #1
main
consturct Modifymethod
[arg1] = Person [arg2] = <init> #4
[arg1] = Person [arg2] = sayHello #9
[arg1] = Person [arg2] = sayHello2 #13
[arg1] = java/lang/InterruptedException [arg2] = printStackTrace #21
getName
consturct Modifymethod
setName
consturct Modifymethod
Call visitend
Finished to call modifymethodtest
####End of ASM CODE
Remove transformer
print in sayHello()
print Call sayHello2()
print Hello World!2
采纳答案by Nicholas
Short Answer
简答
- Don't iterate through all the loaded classes from Instrumentation. Instead, just examine the class name passed in to the transformer and if it matches your target class, then transform it. Otherwise, simply return the passed classfileBuffer unmodified.
- Make the set up calls outside the transformer, (i.e. in your case, do the following from your agent) so initialize your transformer with the class name you're looking to transform (this will be the inner formatso instead of foo.bar.Snafu, you will be looking to match against foo/bar/Snafu. Then add the transformer, call retransform and then remove the transformer.
- In order to call retransform, you will need the actual [pre-transform] class which you can find by calling Class.forName(in the agentmain), or if you absolutely have to, you could find it in Intrumentation.getAllLoadedClasses()as a last resort. If the target class has not been loaded, you will need the classloader to call Class.forName(name, boolean, classloader)in which case you can pass the URL to the target class class-path in the agent main string args.
- 不要遍历所有从 Instrumentation 加载的类。相反,只需检查传递给转换器的类名,如果它与您的目标类匹配,则对其进行转换。否则,只需返回未修改的传递的 classfileBuffer。
- 在转换器外部进行设置调用(即,在您的情况下,从您的代理执行以下操作),因此使用您要转换的类名初始化转换器(这将是内部格式,而不是foo.bar。 Snafu,您将寻找匹配foo/bar/Snafu。然后添加转换器,调用 retransform 然后删除转换器。
- 为了调用 retransform,您需要实际的 [pre-transform] 类,您可以通过调用Class.forName(在 agentmain 中)找到该类,或者如果您绝对需要,您可以在Intrumentation.getAllLoadedClasses() 中找到它作为最后的手段。如果目标类尚未加载,您将需要类加载器调用Class.forName(name, boolean, classloader),在这种情况下,您可以将 URL 传递到代理主字符串 args 中的目标类类路径。
Long Answer
长答案
Here's a few recommendations:
这里有一些建议:
- Separate out the operation you're calling into 2 separate operations:
- Install the agent. This only needs to be done once.
- Transform the target class[es]. You might want to do this ntimes.
- I would implement 1.2 by registering a simple JMX MBean when you install the agent. This MBean should provide an operation like
public void transformClass(String className)
. and should be initialized with a reference to the agent's acquired Instrumentationinstance. The MBean class, interface and any required 3rd party classes should be included in your agent's loaded.jar. It should also contain your ModifyMethodTestclass (which I assume it already does). - At the same time that you install your agent jar, also install the management agent from $JAVA_HOME/lib/management-agent.jarwhich will activate the management agent so you can invoke the transform operation in the MBean you're about to register.
- Parameterize your DemoTransformer class to accept the internal formof the name of the class you want to transform. (i.e. if your binary class name is foo.bar.Snafu, the internal form will be foo/bar/Snafu. As your DemoTransformer instance starts getting transform callbacks, ignore all class names that do not match the internal form class name you specified. (i.e. simply return the classfileBuffer unmodified)
- Your tranformer MBean transformClassoperation should then:
- Convert the passed class name to internal form.
- Create a new DemoTransformer, passing the internal form class name.
- Register the DemoTransformer instance using
Instrumentation.addTransformer(theNewDemoTransformer, true)
. - Call
Instrumentation.retransformClasses(ClassForName(className))
(with the binary class name passed to the MBean operation). When this call returns, your class will be transformed. - Remove the transformer with
Intrumentation.removeTransformer(theNewDemoTransformer)
.
- 将您正在调用的操作分成 2 个单独的操作:
- 安装代理。这只需要做一次。
- 转换目标类[es]。您可能想要这样做n次。
- 我将通过在安装代理时注册一个简单的 JMX MBean 来实现 1.2。此 MBean 应提供类似
public void transformClass(String className)
. 并且应该使用对代理获取的Instrumentation实例的引用进行初始化。MBean 类、接口和任何需要的第 3 方类都应包含在您的代理的loaded.jar 中。它还应该包含您的ModifyMethodTest类(我认为它已经这样做了)。 - 在安装代理 jar 的同时,还从$JAVA_HOME/lib/management-agent.jar安装管理代理,这将激活管理代理,以便您可以在将要注册的 MBean 中调用转换操作。
- 参数化您的 DemoTransformer 类以接受要转换的类名称的内部形式。(即,如果您的二进制类名是foo.bar.Snafu,则内部表单将是foo/bar/Snafu。当您的 DemoTransformer 实例开始获取转换回调时,忽略所有与您指定的内部表单类名不匹配的类名。 (即简单地返回未修改的 classfileBuffer)
- 然后,您的转换器 MBean transformClass操作应该:
- 将传递的类名转换为内部形式。
- 创建一个新的 DemoTransformer,传递内部表单类名。
- 使用 注册 DemoTransformer 实例
Instrumentation.addTransformer(theNewDemoTransformer, true)
。 - 调用
Instrumentation.retransformClasses(ClassForName(className))
(使用传递给 MBean 操作的二进制类名)。当此调用返回时,您的类将被转换。 - 用 拆下变压器
Intrumentation.removeTransformer(theNewDemoTransformer)
。
The following is an untested approximation of what I mean:
以下是我的意思的未经测试的近似值:
Transformer MBean
转换器 MBean
public interface TransformerServiceMBean {
/**
* Transforms the target class name
* @param className The binary name of the target class
*/
public void transformClass(String className);
}
Transformer Service
变压器服务
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);
}
}
}
The Class Transformer
类转换器
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;
}
}
The Agent
中介
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");
}
}
The Agent Installer
代理安装程序
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)
}
}
================= UPDATE =================
================ 更新 ==================
Hey Nick; Yep, that's one of the limitations of the current (i.e. Java 5-8) class transformers. To quote from the Instrumentation javadoc:
嘿尼克;是的,这是当前(即 Java 5-8)类转换器的局限性之一。引用Instrumentation javadoc:
"The retransformation may change method bodies, the constant pool and attributes. The retransformation must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. These restrictions maybe be lifted in future versions. The class file bytes are not checked, verified and installed until after the transformations have been applied, if the resultant bytes are in error this method will throw an exception."
“重新转换可能会更改方法体、常量池和属性。重新转换不得添加、删除或重命名字段或方法,更改方法的签名或更改继承。这些限制可能会在未来版本中取消。类文件字节在应用转换之后才检查、验证和安装,如果结果字节错误,则此方法将引发异常。”
As an aside, this same limitation is documented verbatim for redefining classes too.
顺便说一句,同样的限制也被逐字记录用于重新定义类。
As such, you have 2 options:
因此,您有 2 个选择:
Don't add new methods. This is commonly very limiting and disqualifies the use of very common byte-code AOP patterns like method wrapping. Depending on which byte-code manipulation library you're using, you may be able to inject all the functionality you want into the existing methods. Some libraries make this easier than others. Or, I should say, some libraries will make this easier than others.
Transform the class before it is class loaded. This uses the same general pattern of the code we already discussed, except you don't trigger the transform through calling retransformClasses. Rather, you register the ClassFileTransformer to perform the transform beforethe class is loaded and your target class will be modified when it is first class loaded. In this case, you're pretty much free to modify the class any way you like, provided the end product can still be validated. Beating the application to the punch (i.e. getting your ClassFileTransformer registered before the application loads the class) will most likely require a command like javaagent, although if you have tight control of the lifecycle of your application, it is possible to do this in more traditional application layer code. As I said, you just need to make sure you get the transformer registered before the target class is loaded.
不要添加新方法。这通常是非常有限的,并且无法使用非常常见的字节码 AOP 模式,例如方法包装。根据您使用的字节码操作库,您可以将所需的所有功能注入现有方法中。有些库比其他库更容易做到这一点。或者,我应该说,有些库会使这比其他库更容易。
在类加载之前转换类。这使用了与我们已经讨论过的代码相同的通用模式,只是您不通过调用 retransformClasses 来触发转换。相反,您在加载类之前注册 ClassFileTransformer 以执行转换,并且您的目标类将在加载第一个类时进行修改。在这种情况下,您可以随意修改类,只要最终产品仍然可以验证。击败应用程序(即在应用程序加载类之前注册您的 ClassFileTransformer)很可能需要像javaagent这样的命令,尽管如果您对应用程序的生命周期有严格的控制,则可以在更传统的应用程序层代码中执行此操作。正如我所说,您只需要确保在加载目标类之前注册转换器即可。
One other variation on #2 you may be able to use is to simulatea brand new class by using a new classloader. If you create a new isolated classloader which will not delegate to the existing [loaded] class, but does have access to the [unloaded] target class byte-code, you're essentially reproducing the requirements of #2 above since the JVM considers this to be a brand new class.
您可以使用的 #2 的另一种变体是通过使用新的类加载器来模拟全新的类。如果您创建一个新的隔离类加载器,它不会委托给现有的 [loaded] 类,但确实可以访问 [unloaded] 目标类字节码,那么您实际上是在复制上面 #2 的要求,因为 JVM 考虑了这一点成为一个全新的班级。
================ UPDATE ================
================ 更新 ================
In your last comments, I feel like I've lost track a bit of where you are. At any rate, Oracle JDK 1.6 most definitely supports retransform. I am not too familiar with ASM, but the last error you posted indicates that the ASM transformation somehow modified the class schema which is not allowed, so the retransform failed.
在你最后的评论中,我觉得我有点忘记你在哪里了。无论如何,Oracle JDK 1.6 绝对支持重新转换。我对 ASM 不太熟悉,但是您发布的最后一个错误表明 ASM 转换以某种方式修改了不允许的类架构,因此重新转换失败。
I figured a working example would add more clarity. The same classes as above (plus one test class called Person) are here. There's a couple of modifications/additions:
我认为一个有效的例子会增加更多的清晰度。与上面相同的类(加上一个名为 Person 的测试类)在这里。有一些修改/添加:
- The transform operation in the TransformerServicenow has 3 parameters:
- The binary class name
- The method name to instrument
- A [regular] expression to match the method signature. (if null or empty, matches all signatures)
- The actual bytecode modification is done using Javassistin the ModifyMethodTestclass. All the instrumentation does is add a System.out.printlnthat looks like this:
-->Invoked method [com.heliosapm.shorthandexamples.Person.sayHello((I)V)]
- The AgentInstaller(which has the demo's Main) just self-installs the agent and the transform service. (Easier for Dev/Demo purposes, but will still work with other JVMs)
- Once the agent is self-installed, the main thread creates a Personinstance and just loops, calling the Person's two sayHellomethods.
- TransformerService 中的转换操作现在有 3 个参数:
- 二进制类名
- 要检测的方法名称
- 一个 [regular] 表达式来匹配方法签名。(如果为 null 或为空,则匹配所有签名)
- 实际字节码修改使用做了Javassist在ModifyMethodTest类。检测所做的只是添加一个System.out.println,如下所示:
-->Invoked method [com.heliosapm.shorthandexamples.Person.sayHello((I)V)]
- 该AgentInstaller(其中有演示的主)刚自安装代理和改造服务。(更容易用于开发/演示目的,但仍可与其他 JVM 一起使用)
- 代理自安装后,主线程会创建一个Person实例并循环调用 Person 的两个sayHello方法。
Prior to transform, that output looks as follows.
在转换之前,该输出如下所示。
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]
Person has 2 sayHellomethods, one takes an int, the other takes a String. (The String one just prints the negative of the loop index).
Person 有 2 个sayHello方法,一个采用int,另一个采用String。(字符串一只是打印循环索引的负数)。
Once I start the AgentInstaller, the agent is installed and Person is being invoked in a loop, I connect to the JVM using JConsole:
一旦我启动了 AgentInstaller,代理就被安装并且 Person 被循环调用,我使用 JConsole 连接到 JVM:
I navigate to the TransformerService MBean and invoke the transformClassoperation. I supply the fully qualified class [binary] name, the method name to instrument, and a regex expression (I)Vwhich matches onlythe sayHellomethod which takes an int as an argument. (Or I could supply .*, or nothing to match all overloads). I execute the operation.
我导航到 TransformerService MBean 并调用transformClass操作。我提供了完全限定的类 [binary] 名称、要检测的方法名称和一个正则表达式(I)V,它仅匹配以 int 作为参数的sayHello方法。(或者我可以提供.*,或者不匹配所有重载)。我执行操作。
Now when I go back to the running JVM and examine the output:
现在,当我回到正在运行的 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]
Done. Method instrumented.
完毕。方法检测。
Keep in mind, the reason the retransform is allowed is because the Javassist bytecode modification made no changes other than to inject code into an existing method.
请记住,允许重新转换的原因是 Javassist 字节码修改除了将代码注入现有方法之外没有进行任何更改。
Make sense ?
有道理 ?