背景:html
在使用谷歌开源的本地缓存解决常常查询数据库致使的查询效率低下,将从数据库查询好的数据放入到缓存中,而后设计过时时间,接着设计一个get方法缓存汇总获取数据,进一步将整个流程封装成一个CacheSerice,而后在Controller层调用这个Service,从Service中获取数据。java
问题:数据库
须要对CacheService进行初始化,设计的初衷是:当Service的bean被加载以后,其中的缓存数据就已经被初始化(即利用数据库查询Service获取数据,并塞入缓存),而这个初始化的过程被我放到了CacheService类的构造函数中。结果在发布的时候就一直报空指针。缓存
@Service("test")
public class Test implements IAppnameCache {
@Autowired
IAppnameService iAppnameService;
public Test(){
iAppnameService.queryAppname();// 抛出空指针
}
@Override
public List<AppnameViewModel> get(String app){
return iAppnameService.queryAppname();
}
}
问题定位:安全
通过查询日志,发现是CacheService的构造函数在执行的时候发生空指针问题。那么有多是引入的谷歌开源库的问题有可能不是,采用排除法很快就发现了不是这个库的问题,不含谷歌开源库的测试类采用这种写法也发生了空指针的问题。多线程
问题思考:app
既然跟引入的谷歌开源库没有关系,那就说明当CacheService被构造的时候(采用构造函数),里面依赖的其余bean尚未被构造出来,于是致使空指针问题。针对这个问题进一步对Spring的bean构造过程进行研究。ide
Spring的bean加载过程:函数
bean的主要生成过程以下:工具
1,AbstractBeanFactory.getBean(String) 2,AbstractBeanFactory.doGetBean(String, Class<T>, Object[], boolean) 3,DefaultSingletonBeanRegistry.getSingleton(String) 4,AbstractAutowireCapableBeanFactory.createBean(String, RootBeanDefinition, Object[]) 5,AbstractAutowireCapableBeanFactory.doCreateBean(String, RootBeanDefinition, Object[]) 6,AbstractAutowireCapableBeanFactory.createBeanInstance(String, RootBeanDefinition, Object[]) 7,AbstractAutowireCapableBeanFactory.instantiateBean(String, RootBeanDefinition) 8,SimpleInstantiationStrategy.instantiate(RootBeanDefinition, String, BeanFactory) 9, AbstractAutowireCapableBeanFactory.populateBean(String, RootBeanDefinition, BeanWrapper) 10,AbstractAutowireCapableBeanFactory.initializeBean(String, Object, RootBeanDefinition) 11,AbstractAutowireCapableBeanFactory.invokeAwareMethods(String, Object) 12,AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(Object, String) 13,AbstractAutowireCapableBeanFactory.invokeInitMethods(String, Object, RootBeanDefinition) 14,AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(Object, String)
(1)在经过BeanFactory获取bean实例对象的时候,会先去单例集合中找是否已经建立了对应的实例,若是有就直接返回了,这里是第一次获取,因此没有拿到;
(2)而后AbastractBeanFactory会根据bean的名称获取对应的BeanDefinition对象,BeanDefinition对象表明了对应类的各类元数据,因此根据BeanDefinition对象就能够判断是不是单例,是否依赖其余对象,若是依赖了其余对象那么先生成其依赖,这里是递归调用。
在步骤7以前都是为了生成bean作准备,真正生成bean是在AbstractAutowireCapableBeanFactory的instantiateBean方法:
protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) { try { Object beanInstance; final BeanFactory parent = this; if (System.getSecurityManager() != null) { beanInstance = AccessController.doPrivileged(new PrivilegedAction<Object>() { @Override public Object run() { return getInstantiationStrategy().instantiate(mbd, beanName, parent); } }, getAccessControlContext()); } else { beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent); } BeanWrapper bw = new BeanWrapperImpl(beanInstance); initBeanWrapper(bw); return bw; } catch (Throwable ex) { throw new BeanCreationException( mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex); } }
-----------------------------------------------------------------------------------------------------
@Override public Object instantiate(RootBeanDefinition bd, String beanName, BeanFactory owner) { // Don't override the class with CGLIB if no overrides. if (bd.getMethodOverrides().isEmpty()) { Constructor<?> constructorToUse; synchronized (bd.constructorArgumentLock) { //这里一堆安全检查 } //默认使用构造函数利用反射实例化bean return BeanUtils.instantiateClass(constructorToUse); } else { // Must generate CGLIB subclass. return instantiateWithMethodInjection(bd, beanName, owner); } }
能够看到实际上bean的生成是直接使用BeanUtils工具类经过反射获取类的实例。
而反射获取类实例的过程以下:
Class<?> cls = Class.forName("cn.mldn.demo.Person"); // 取得Class对象 Object obj = cls.newInstance() //反射实例化对象 Constructor<?> cons = cls.getConstructor(String.class, int.class);//得到构造方法 Method m3 = cls.getDeclaredMethod("getName"); //得到get方法 Field nameField = cls.getDeclaredField("name"); // 得到name属性
同时在JVM进行类加载的时,再进行到初始化这一步骤的时候,首先会调用默认构造器进行变量初始化:
- 类构造器<clinit>()方法是由编译器自动收集类中的全部类变量的赋值动做和静态语句块(static块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块以前的变量,定义在它以后的变量,在前面的静态语句快能够赋值,可是不能访问。
- 类构造器<clinit>()方法与类的构造函数(实例构造函数<init>()方法)不一样,它不须要显式调用父类构造,虚拟机会保证在子类<clinit>()方法执行以前,父类的<clinit>()方法已经执行完毕。所以在虚拟机中的第一个执行的<clinit>()方法的类确定是java.lang.Object。
- 因为父类的<clinit>()方法先执行,也就意味着父类中定义的静态语句快要优先于子类的变量赋值操做。
- <clinit>()方法对于类或接口来讲并非必须的,若是一个类中没有静态语句,也没有变量赋值的操做,那么编译器能够不为这个类生成<clinit>()方法。(默认值是内存分配的时候赋予的,与初始化过程无关)
- 接口中不能使用静态语句块,但接口与类不一样是,执行接口的<clinit>()方法不须要先执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也同样不会执行接口的<clinit>()方法。
- 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁和同步,若是多个线程同时去初始化一个类,那么只会有一个线程执行这个类的<clinit>()方法,其余线程都须要阻塞等待,直到活动线程执行<clinit>()方法完毕。若是一个类的<clinit>()方法中有耗时很长的操做,那就可能形成多个进程阻塞。
CacheService类中没有赋值行为,而后则会调用默认的构造函数,因此在CacheService类中,被反射获取构造器的时候会调用明确的构造器。回归到本次的问题,在构造器中使用了其余的bean,而Spring的bean生成实际上是没有规律的(也就是依赖的bean尚未被注入),因此抛出空指针的异常。
那么问题来了,Spring说好的有自动检测依赖的功能呢?
请列位看官慢慢往下看,请小生为各位一一分解。
咱们把目光往前看,若是在容器中没有拿到目标bean,而后AbastractBeanFactory会根据bean的名称获取对应的BeanDefinition对象,BeanDefinition对象表明了对应类的各类元数据,
// 运行到这里说明bean没有被建立,先获取此bean依赖的bean String[] dependsOn = mbd.getDependsOn(); if (dependsOn != null) { for (String dep : dependsOn) { if (isDependent(beanName, dep)) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'"); } registerDependentBean(dep, beanName); //实例化依赖的bean getBean(dep); } }
能够看到是经过getDependsOn方法获取依赖的bean,而这个过程是经过Setter将CacheService的属性bean进行注入,而后获取bean。那么既然属性注入的时候
IAppnameService就已经完成bean注入了,为什么构造器仍是抛出了异常呢?原来上述过程注入明确指出的依赖,即在bean的配置中加入depends-on(也支持注解),
若是没有配置,那么CacheService的属性注入是在getbean()完成以后。因此在执行CacheService的构造函数时固然抛出异常啦!那么也就是说Spring的依赖检查其实没有开启的,
是须要手动在配置文件中开启的,在Spring中有四种依赖检查的方式。依赖检查有四种模式:simple,objects,all,none,都经过bean的dependency-check属性进行模式设置。
固然Spring中的加载过程当中,其加载过程仍是遵循JVM的加载过程。
JVM的主要加载过程
在JVM中是经过类加载器以及类的全限定名来保证类的惟一性的,也就是说若是两个类的路径名称彻底同样,可是只要是加载它们的类加载器不同就能够认为是两个不同的类。而在JVM中含有以下几种类加载器:
JVM中包括集中类加载器:
1 BootStrapClassLoader 引导类加载器
2 ExtClassLoader 扩展类加载器
3 AppClassLoader 应用类加载器
4 CustomClassLoader 用户自定义类加载器
而且在JVM中使用双亲委派模型进行加载。什么是双亲委派模型呢?就是在2,3,4的类加载器加载类的时候,都会向上调用父类加载器来实现,也就是说最后都是交给BootStrapClassLoader加载器完成加载的。这样作的好处一是由于安全性,由于JVM的类加载过程当中有验证这一步骤,会对class文件进行校验,判断是否符合JVM规范。二是由于保证类不会被重复加载,由于在执行new的时候,会首先从元数据区查找类符号,若是没有则会加载相应的文件。因此为了不在这个过程当中重复加载的现象,最终都是经过系统提供的类加载器完成加载。
加载细节:
(未完待续)参考资料:
1. 深刻理解JVM虚拟机
2. https://blog.51cto.com/wenshengzhu/1950146
3. https://blog.csdn.net/h12kjgj/article/details/54312766
4. https://www.cnblogs.com/kjitboy/p/12076303.html [Spring Bean的装配方式