如何使用Java检测替换新类?

我需要创建一个java代理,当启用它时,它获取jar文件的路径作为参数,然后它将任何加载的类替换为jar文件中的一个,如果它们的名称匹配。

例如,我们有一个名为com.something.ClassTest的类的应用程序。 现在,如果提到的jar(不在类路径中)有一个与com.something.ClassTest完全相同的类,我想用jar中的那个替换它。

我有这类变压器,但不确定这是否正确。 我收到IOException,找不到消息Class。

@Override public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if(classNames.contains(className.replace("/", "."))) { System.out.format("\n==> Found %s \n", className); try { Class c = urlClassLoader.loadClass(className.replace("/", ".")); InputStream is = urlClassLoader.getResourceAsStream(className.replace("/", ".")); System.out.println("Loaded class " + c); ClassReader reader = new ClassReader(is); ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); byte[] content = writer.toByteArray(); System.out.println("Redifned " + new String(content)); System.out.println("Orig " + new String(classfileBuffer)); ClassDefinition cd = new ClassDefinition(c, content); instrumentation.redefineClasses(cd); return content; } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (UnmodifiableClassException e) { e.printStackTrace(); } } return classfileBuffer; } 

我得到的错误是在实例化ClassReader的行。 我想这个错误是因为urlClassloader在某种程度上位于当前类加载器的层次结构中……但我不知道我怎么能做到这一点。

这是初始化URL类加载的代码

  public SimpleClassTransformer(Instrumentation instrumentation, String jarFileName) { this.jarFileName = jarFileName; if(jarFileName != null) { JarFile jarFile = null; try { jarFile = new JarFile(this.jarFileName); Enumeration e = jarFile.entries(); System.out.println("Jar file: " + this.jarFileName); URL[] urls = { new URL("jar:file:" + this.jarFileName+"!/") }; urlClassLoader = URLClassLoader.newInstance(urls); while (e.hasMoreElements()) { JarEntry je = (JarEntry) e.nextElement(); if(je.isDirectory() || !je.getName().endsWith(".class")){ continue; } // -6 because of .class String jarClassName = je.getName().substring(0,je.getName().length()-6); jarClassName = jarClassName.replace('/', '.'); System.out.println("Adding class " + jarClassName); this.classNames.add(jarClassName); } } catch (IOException e) { e.printStackTrace(); } } } 

在premain()方法中实例化变换器时设置了Instrumentation。 我试图避免使用Javassist。 你能帮我解决这个问题。

这是我得到的例外:

 java.io.IOException: Class not found at jdk.internal.org.objectweb.asm.ClassReader.readClass(ClassReader.java:484) at jdk.internal.org.objectweb.asm.ClassReader.(ClassReader.java:453) at com.agent.SimpleClassTransformer.transform(SimpleClassTransformer.java:79) at sun.instrument.TransformerManager.transform(TransformerManager.java:188) at sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:428) at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:760) at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) at java.net.URLClassLoader.defineClass(URLClassLoader.java:467) at java.net.URLClassLoader.access$100(URLClassLoader.java:73) at java.net.URLClassLoader$1.run(URLClassLoader.java:368) at java.net.URLClassLoader$1.run(URLClassLoader.java:362) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:361) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) at org.springframework.util.ClassUtils.forName(ClassUtils.java:250) at org.springframework.beans.factory.support.AbstractBeanDefinition.resolveBeanClass(AbstractBeanDefinition.java:394) at org.springframework.beans.factory.support.AbstractBeanFactory.doResolveBeanClass(AbstractBeanFactory.java:1397) at org.springframework.beans.factory.support.AbstractBeanFactory.resolveBeanClass(AbstractBeanFactory.java:1344) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineTargetType(AbstractAutowireCapableBeanFactory.java:628) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.predictBeanType(AbstractAutowireCapableBeanFactory.java:597) at org.springframework.beans.factory.support.AbstractBeanFactory.isFactoryBean(AbstractBeanFactory.java:1445) at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBeanNamesForType(DefaultListableBeanFactory.java:445) at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:415) at org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration$DefaultDispatcherServletCondition.checkServlets(DispatcherServletAutoConfiguration.java:141) at org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration$DefaultDispatcherServletCondition.getMatchOutcome(DispatcherServletAutoConfiguration.java:131) at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:47) at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:102) at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:203) at org.springframework.context.annotation.ConfigurationClassParser.processMemberClasses(ConfigurationClassParser.java:336) at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:248) at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:231) at org.springframework.context.annotation.ConfigurationClassParser.processImports(ConfigurationClassParser.java:509) at org.springframework.context.annotation.ConfigurationClassParser.processDeferredImportSelectors(ConfigurationClassParser.java:454) at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:185) at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:321) at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:243) at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:273) at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:98) at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:677) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:519) at org.springframework.boot.context.embedded.EmbeddedWebApplicationContext.refresh(EmbeddedWebApplicationContext.java:118) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) at org.springframework.boot.SpringApplication.doRun(SpringApplication.java:347) at org.springframework.boot.SpringApplication.run(SpringApplication.java:295) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1112) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1101) 

====编辑====

在解决了url类加载器的问题后,我现在得到Spring在尝试刷新其上下文时发生的错误:

 TestClass has been compiled by a more recent version of the Java Runtime (class file version 0.0), this version of the Java Runtime only recognizes class file versions up to 52.0 

另一种解决问题的方法是使用认可的目录。 保留要在库中加载的类,并将-Djava.endorsed.dirs=作为程序的JVM参数提供。

在加载类时,JVM首先检查此目录中的类可用性,如果未找到,则它将检查应用程序类。 这完全正常,没有任何问题,没有任何编码。

  1. 您的UrlClassLoader应包含要从中加载类的jar文件。
  2. 如果该类已经加载,则无法重新加载。
  3. 只有在将类加载到JVM时,检测和重新定义类才有效。

代码看起来不错,但是您需要仔细检查urlClassLoader是否包含要从中加载类的jar文件,jar是否具有所需的类。

您可以调试应用程序以确保上述条件。

我设法解决了这个问题。 如果有人在这里有同样的问题是问题:

我使用的是ClassReader和ClassWriter。 出于某种原因,ClassWriter填充了字节代码,或许将已经编译的类传递给类编写器是错误的,但无论如何以下代码:

  ClassReader reader = new ClassReader(is); ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); byte[] content = writer.toByteArray(); 

替换为:

  InputStream is = urlClassLoader.getResourceAsStream(className + ".class"); byte[] content = new byte[is.available()]; is.read(content); System.out.println ("Original class version: " + ((classfileBuffer[6]&0xff)<<8 | (classfileBuffer[7]&0xff))); System.out.println ("Redefined class version: " + ((content[6]&0xff)<<8 | (content[7]&0xff))); 

如您所见,我正在使用InputStream直接检索字节代码。 这解决了问题,如果你感兴趣,Spring很好地检测了差异并刷新了上下文。

====编辑====

我注意到在这里使用URLClassLoader是不可靠的,因为某些原因,它可能返回应用程序本身中已加载的类而不是JAR文件中的类。 它是随机的,有时返回jar中的类,有时返回原始类,所以我决定删除URLClassLoader,而是在遍历jar文件时将类文件作为InputStream。 对于需要它的人来说,这是我的变压器的最终代码:

 public class JarFileClassTransformer implements ClassFileTransformer { private String jarFileName = null; protected Map classNames = new HashMap<>(); static Instrumentation instrumentation = null; /** * Constructor. * @param jarFileName */ public JarFileClassTransformer(String jarFileName) { this.jarFileName = jarFileName; File file = new File(jarFileName); System.out.println("Jar file '" + this.jarFileName + "' " + (file.exists() ? "exists" : "doesn't exists!")); if(file.exists()) { try { JarFile jarFile = new JarFile(file); Enumeration e = jarFile.entries(); while (e.hasMoreElements()) { JarEntry je = (JarEntry) e.nextElement(); if(je.isDirectory() || !je.getName().endsWith(".class")){ continue; } // -6 because of .class String jarClassName = je.getName().substring(0,je.getName().length()-6); jarClassName = jarClassName.replace('/', '.'); System.out.println("Adding class " + jarClassName); this.classNames.put(jarClassName, jarFile.getInputStream(je)); } } catch (IOException e) { e.printStackTrace(); } } } @Override public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if(classNames.containsKey(className.replace("/", "."))) { System.out.format("\n==> Found %s to replace with the existing version\n", className); try { Class c = loader.loadClass(className.replace("/", ".")); System.out.println("Existing class: " + c); InputStream is = classNames.get(className.replace("/", ".")); byte[] content = new byte[is.available()]; is.read(content); System.out.println("Original class version: " + ((classfileBuffer[6]&0xff)<<8 | (classfileBuffer[7]&0xff))); System.out.println("Redefined class version: " + ((content[6]&0xff)<<8 | (content[7]&0xff))); System.out.println("Original bytecode: " + new String(classfileBuffer)); System.out.println("Redefined byte code: " + new String(content)); ClassDefinition cd = new ClassDefinition(c, content); instrumentation.redefineClasses(cd); return content; } catch (Throwable e) { e.printStackTrace(); } } return classfileBuffer; } 

}