点击蓝色“程序猿DD”关注我哟
加个“星标”,不忘签到哦
来源:字节观
关注我,回复口令获取可获取独家整理的学习资料:
- 001 :领取《Spring Boot基础教程》
- 002 :领取《Spring Cloud基础教程》
>>当当大促,160买400的书,点击进入<<
从事java开发的小伙伴们,应该基本都用过Spring,但是估计很多人可能都不知道本文要说的这个知识点(标题中的80%是拍脑袋得出来的^_^),什么知识点这么神秘呢?且听我从一个实际问题慢慢说起。
这个问题是我一个朋友碰到的,问题的知识背景是这样的(为了大家更容易看明白,我用一些示意类进行说明):
有这样一个接口类HelloService:
public interface HelloService { public void sayHello(); }
我朋友提供这样一个实现类HelloServiceImpl (这里需要大家注意:它加了@Component注解):
package com.xx.yy;@org.springframework.stereotype.Componentpublic class HelloServiceImpl extends HelloService {@Overridepublic void sayHello() { System.out.println("Hello world!"); }}
编译完后得到HelloServiceImpl.class,然后再使用加密工具对HelloServiceImpl.class文件内容加密后打包成一个jar包丢给业务方使用。请大家注意这里:HelloServiceImpl.class文件内容是经过加密的
业务方项目基于SpringBoot搭建,在使用的时候将HelloServiceImpl所在jar包依赖加进来,并且将它的包路径 com.xx.yy 加到了bean扫描路径里面,简单示意如下:
@Configuration@EnableAutoConfiguration@ComponentScan("com.xx.yy")public class Application { @Autowiredprivate HelloService helloService; public static void main(String[] args) { SpringApplication.run(Application.class,args); }}
业务方将该SpringBoot项目打包成demo.jar,然后使用类似下面的命令启动:
java -jar -agentpath:/usr/lib/xxdecrypt.so demo.jar
之前说过,HelloServiceImpl.class 这个文件的内容是加密过的,如果在这个类加载过程中不进行解密,那么在jvm加载这个类的校验阶段是通不过的进而引发加载报错。
那么在什么地方对class进行解密呢?请大家注意这个启动参数:
-agentpath:/usr/lib/xxdecrypt.so,这是用C++写的一个native agent。
Agent可以利用JVM提供的JVMTI(JVM Tool Interface)接口来和JVM进行通讯,它可以订阅自己感兴趣的JVM事件(比如类加载卸载、方法出入、线程始末等等),当这些事件发生时JVM会回调Agent的代码。利用这个机制我们可以编写一个Agent,在JVM加载class文件的时候进行拦截,对字节码进行解密后返回给JVM,从而达到对调用方透明加解密的目的。详细的JVMTI相关知识,大家可以参考@江南白衣写的《入门科普,围绕JVM的各种外挂技术》
到这里,背景知识介绍完毕。这时候问题出现了,上面的启动命令报错了,程序启动失败,报错信息如下:
2019-06-04 11:38:37.357 ERROR 10219 --- [ main] o.s.boot.SpringApplication : Application run failedorg.springframework.beans.factory.BeanDefinitionStoreException: Failed to read candidate component class: URL [jar:file:/home/sample/demo.jar!/com/xx/yy/HelloServiceImpl.class]; nested exception is java.lang.ArrayIndexOutOfBoundsException: 15467at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.scanCandidateComponents(ClassPathScanningCandidateComponentProvider.java:454) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.findCandidateComponents(ClassPathScanningCandidateComponentProvider.java:316) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:275) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.context.annotation.ComponentScanAnnotationParser.parse(ComponentScanAnnotationParser.java:132) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.context.annotation.ConfigurationClassParser.doProcessConfigurationClass(ConfigurationClassParser.java:288) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.context.annotation.ConfigurationClassParser.processConfigurationClass(ConfigurationClassParser.java:245) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:202) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.context.annotation.ConfigurationClassParser.parse(ConfigurationClassParser.java:170) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:316) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:233) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:271) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:91) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:694) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:532) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:140) ~[spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:762) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:398) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]at org.springframework.boot.SpringApplication.run(SpringApplication.java:330) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]at org.springframework.boot.SpringApplication.run(SpringApplication.java:1258) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]at org.springframework.boot.SpringApplication.run(SpringApplication.java:1246) [spring-boot-2.0.4.RELEASE.jar:2.0.4.RELEASE]at com.xht.Sample5App.main(Sample5App.java:28) [xht-jni-sample5_encrypt.jar:na]Caused by: java.lang.ArrayIndexOutOfBoundsException: 15467at org.springframework.asm.ClassReader.<init>(ClassReader.java:198) ~[spring-core-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.asm.ClassReader.<init>(ClassReader.java:168) ~[spring-core-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.asm.ClassReader.<init>(ClassReader.java:445) ~[spring-core-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.core.type.classreading.SimpleMetadataReader.<init>(SimpleMetadataReader.java:54) ~[spring-core-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.core.type.classreading.SimpleMetadataReaderFactory.getMetadataReader(SimpleMetadataReaderFactory.java:103) ~[spring-core-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.core.type.classreading.CachingMetadataReaderFactory.getMetadataReader(CachingMetadataReaderFactory.java:123) ~[spring-core-5.0.8.RELEASE.jar:5.0.8.RELEASE]at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.scanCandidateComponents(ClassPathScanningCandidateComponentProvider.java:430) ~[spring-context-5.0.8.RELEASE.jar:5.0.8.RELEASE] ... 20 common frames omitted
从报错日志来看,是Spring加载HelloServiceImpl.class这个类失败了。可能原因是什么呢?
很自然地,即使你不知道本文即将揭晓的这个Spring知识点,按照逻辑也能想到问题的原因可能有如下两个:
1、HelloServiceImpl.class这个文件是不是解密出错导致解密后的字节码不符合jvm规范?
2、HelloServiceImpl.class的加载没有走Agent的解密逻辑,导致没有解密?
朋友反馈说如果执行了解密逻辑的话会打印日志,而这个case没有看到相关日志,说明没有执行解密逻辑。难道启动命令参数不对导致Agent没起作用?朋友尝试了一下不把HelloServiceImpl所在的包路径加到Spring扫描范围,由业务代码自己new HelloServiceImpl的话,用同样的启动命令是可以成功启动并正常运行的。所以问题到底是啥呢?朋友百思不得其解找来求助于我。
我反问他:你知道Spring是怎么找到加了@Component等注解的类的么?
朋友回答:不就是遍历指定的包路径下的所有类,然后用类加载器加载这个类,通过反射判断这个类是否有相应的注解么?
我说:不是的。试想一下,如果Spring都把指定的包路径下的类都一一用类加载器加载一遍,那如果程序中其实没有使用到这些类,那不是白白加载了很多无用类么?
好了,不卖关子了,现在公布本文要说的Spring的知识点:Spring在扫描指定包路径下的类时,并不会一一用类加载器加载它们,而是自己把类文件当成普通文件从本地磁盘中读进来变成一个字节数组(并没有经过JVM类加载过程),然后用ASM去解析这个字节数组得到这个类的元数据,然后判断这个类的元数据里面是否有@Component等相关Spring的注解。如果有的话后面才会进一步使用类加载器去加载这个类,没有的话就不会尝试去加载。Spring相关源代码如下:
SimpleMetadataReader(Resource resource, ClassLoader classLoader) throws IOException { InputStream is = new BufferedInputStream(resource.getInputStream()); ClassReader classReader;try { classReader = new ClassReader(is); }catch (IllegalArgumentException ex) {throw new NestedIOException("ASM ClassReader failed to parse class file - " +"probably due to a new Java class file version that isn't supported yet: " + resource, ex); }finally {is.close(); } AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(classLoader); classReader.accept(visitor, ClassReader.SKIP_DEBUG);this.annotationMetadata = visitor;// (since AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor)this.classMetadata = visitor;this.resource = resource; }
本文的case中看到的错误信息就是从classReader = new ClassReader(is) 抛出来的:因为直接读取的类文件二进制内容,是已经经过加密的,肯定是不符合JVM规范的,所以按照正常的JVM规范去解析一个不符合规范的字节码,那就很容易出错,进而报了数组越界的异常。
掌握这点知识之后,朋友就恍然大悟找到了解决办法。在此就不公布解决办法了,相信小伙伴们在了解这个知识点后自己能找到解决方案。没找到的话可以尝试在公众号发送”help“四个字母获取答案。
新群招募
如果你喜欢自己装机、刷系统、玩黑群,或对各类科技产品有浓厚的兴趣!那么赶紧加入,一起聊聊喜欢的东西,在大促的时候种种草。亦或是聊聊你想买的东西,让大伙给你拔拔草。^_^
添加微信:zyc_enjoy,根据指引,加入讨论群
推荐阅读:
如何像技术高手一样解决问题
Maven 虐我千百遍,我待 Maven 如初恋
初探性能优化:2个月到4小时的性能提升
MySQL跑在CentOS 6 和 7上的性能比较
Spring Boot 配置文件中的花样,看这一篇足矣!
“自律到极致 - 人生才精致:第9期”
明日即将发布!
关注我,加个星标,不忘签到哦~
2019
与大家聊聊技术人的斜杠生活
点一点“”小惊喜在等你