ioc的模拟实现
Spring框架在Java中绝对是大名鼎鼎,ioc(反转控制)更是spring的核心。所谓临渊羡鱼不如退而结网,Spring框架的使用固然非常重要,但如果我们能更弄清楚spring框架的原理,使用自然不在话下。因此我做了一个ioc的模拟框架 ,希望能帮助到大家更深层的学习spring。
## 整体思路分析
1.构建容器
2.懒汉模式完成注入。
如何构建容器
1.加注解:
给需要存放在容器中的类加上@Component注解;给该类需要注入的成员加上@Autowired,表示在getBean的时候需要对这个成员进行自动的注入。如果我们想得到jar包中某个类的对象,当然无法给jar包中的类加注解,但是我们可以写一个方法,让它的返回值为jar包中的类,并给该方法加上@Bean注解,执行这个方法就能得到该类的对象。
这是普通类加注解的方式
@Component
public class ClassOne {
@Autowired
private Complex complex;
private String str;
这是给jar包里面的类加注解的方式,当然假设Point类是jar包里的类啦,通过执行Bean注解的方法就能得到一个Point类的对象。
@Component
public class Configuration {
public Configuration() {
}
@Bean
public Point getPoint( PointOne c) {
Point point = new Point();
return point;
}
2.包扫描收集带注解的类和方法。
通过包扫描收集所有带@Component注解的类,把它放在我们事先准备好的BeanPool里面,BeanPool是一个Map,其中键为类名,值是BeanDefinitation类的对象,而BeanDefinitation的三个成员为
private Class<?> klass;
private Object object;
private boolean isinject//是否注入
其中通过包扫描,每次扫描到带@Component注解的类,就New一个BeanDefinitation对象,并设置它的成员为相关带注解的类的信息,如下是包扫描的相关代码
new PackageScanner() {
@Override
public void dealClass(Class<?> klass) {
if(klass.isPrimitive()
||klass.isAnnotation()
||klass.isInterface()
||klass.isEnum()
||klass.isArray()
||!klass.isAnnotationPresent(Component.class)){
return;
}
BeanDefinitation bd = new BeanDefinitation();
bd.setKalss(klass);
try {
bd.setObject(klass.newInstance());
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
bd.setisInject(false);
BeanPool.put(klass.getName(), bd);
collectMethod(bd,alreadList);//收集相关带Bean注解的方法
}
}.packageScanner(packageName);
dealBean(alreadList);//处理相关方法。
}
collectMethod(bd,alreadList) 是 收集带Bean注解的方法,因为某些方法的参数可能需要在BeanPool里面获取,因此我们通过包扫描只收集相关方法,等到扫描结束形成了相关的BeanPool后统一执行
collectMethod(BeanDefinitation bd,AlreadMethod alreadList) 收集方法:
private void collectMethod(BeanDefinitation bd,AlreadMethod alreadList) {
Method method[] = bd.getKalss().getDeclaredMethods();
for(Method meth : method){
if(!meth.isAnnotationPresent(Bean.class)){
continue;
}
BeanMethodDefinitation bmd = new BeanMethodDefinitation();
bmd.setKlass(bd.getKalss());
bmd.setMethod(meth);
bmd.setObj(bd.getObject());
bmd.setParaCount();
boolean hasDepen = bdpen.dealDependecheShip(bmd);//判断该方法是否还有依赖关系
if(!hasDepen){
alreadList.add(bmd);
}
}
}
可以看出来,每扫描到一个带注解的类时,我们就得到这个类的所有带Bean注解的方法,并New一个BeanMethodDefinitation,与BeanDefinitation相似,用来保存执行该方法的相关信息。每一个方法的执行都需要有相关参数的依赖关系,通过调用bdpen.dealDependecheShip(bmd)( BeanDependence:专门用来处理方法参数之间依赖关系的类。bdpen为它的一个实例),如果判断依赖关系全部成立就把对应的BeanMethodDefinitation加入到alreadList 里面 (AlreadMethod 类的实例,用来处理可以直接执行的方法,alreadList.add(BeanMethodDefinitation),是把可执行的方法加到AlreadMethod中存储可执行方法的一个list里面) 如果不能直接执行就稍后处理。
## 如何建立依赖关系
一个类的实例可以作为参数和多个方法建立依赖关系,即很多方法的执行都需要这个参数,如下代码:
@Bean
public Point getPoint(Complex c) {
Point point = new Point(c);
return point;
}
@Bean
public PointOne getPointONe(Complex c, Point p1,Point p2,){
return new PointOne();
}
依赖参数 | 方法 |
---|---|
Complex | getPoint[1], getPointONe[2] |
Point | getPointONe [2] |
中括号中为方法参数类型的个数。也是该方法依赖参数的个数
类型个数的获取还是有个小技巧的,代码如下:
void setParaCount() {
int count = method.getParameterCount();
if(count <= 0){
return;
}
Map<Class<?>, Object> paraMap = new HashMap<Class<?>, Object>();
for(Parameter para : method.getParameters()){
paraMap.put(para.getType(), null);
}
this.paraCount = paraMap.size();
}
我们先获取方法的所有的参数,把参数的类型作为键添加进去,如果键值相同就会进行覆盖,因此paraMap的大小就是方法参数类型的个数。
有了依赖参数和依赖这个参数的所有方法之后,我们就可以构建依赖关系的表了,代码入下:
boolean dealDependecheShip( BeanMethodDefinitation bmd){
Method meth = bmd.getMethod();
int count = bmd.getParaCount();
if(count <= 0){
return false;//无参方法直接返回false
}
List<BeanMethodDefinitation> methList = null;
for (Parameter para : meth.getParameters()){//得到方法的所有参数
if(!dependenceShip.containsKey(para.getType())){
//判断这个参数是否已经作为键值与某些方法形成依赖关系,
//如果没有,就申请一个methList用来存放依赖该参数的所有方法,
//并把参数类型作为键,methList作为值加入到Map中去形成依赖对应
methList = new ArrayList<BeanMethodDefinitation>();
dependenceShip.put(para.getType(), methList);
}
methList = dependenceShip.get(para.getType());
methList.add(bmd);//把这个方法加到对应参数依赖关系的List里面去
}
return true;//返回true,表示存在依赖关系
}
由此生成的dependenceShip形成了参数和方法对应关系的Map;
3.处理待执行方法
通过以上两步,我们收集了一些带有@Component注解的Bean和待执行的方法,接下来我们看看怎么处理这些待执行的方法,让我们的BeanPool丰富起来
每个方法执行之后都会对BeanPool进行拓展,依赖关系也会发生改变,因此每执行一个方法之前都需要检查一下依赖关系,需要把依赖关系列表中的存在的参数类型和BeanPool里面的类型进行比较,如果BeanPool里面已经有了 dependenceShip的参数类型,则所有方法对该参数的依赖关系都成立,每一个方法依赖的参数个数都会减少一个,此时如果该方法依赖的参数个数为0,说明该方法需要的所有参数都已经在BeanPool中存在了,就可以把它加入到readList里面去。下面是检测依赖关系的代码:
private void checkDependence(AlreadMethod almethod){
Set<String> keys = BeanPool.keySet();//得到BeanPool的所有键值
for(String key : keys){
bdpen.isDependence(key, almethod);//遍历键值,检测该值是否在依赖 关系的列表中也存在
}
}
void isDependence(String key, AlreadMethod almenth) {
List<BeanMethodDefinitation> bmdlist;
try {
bmdlist = dependenceShip.get(Class.forName(key));//根据传过来的BeanPool的键值,寻找dependenceShip中对应参数的list
if(bmdlist == null){
return;//参数没有对应的依赖关系
}
for(BeanMethodDefinitation bmd : bmdlist){
if(bmd.decrease()<= 0){//每一个方法依赖的参数个数都会减1,
almenth.add(bmd);//如果该方法依赖的参数个数为0,说明该方法需要的所有参数都已经在BeanPool中存在了,就把它加入到可执行方法列表。
}
}
dependenceShip.remove(Class.forName(key));//所有方法对该参数的依赖关系都成立,dependenceShip删除这个键。
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
举例说明:
@Bean
public Point getPoint(Complex c) {
Point point = new Point(c);
return point;
}
@Bean
public PointOne getPointONe(Complex c, Point p1,Point p2,){
return new PointOne();
}
如果我们要执行带注解的方法,把BeanPool中的键依次传递过来,在dependenceShip中对比有没有Point,和Complex参数,若Complex即在BeanPool里又在dependenceShip里,则Complex对应的依赖关系就会全部成立,
依赖关系就会变成
依赖参数 | 方法 |
---|---|
删除Complex | getPoint[1 - 1], getPointONe[2 -1] |
Point | getPointONe [2-1] |
此时getPoint依赖的参数全部成立,可以列为可执行方法。
检查完依赖关系我们就可以执行方法了:
private <T> void dealBean(AlreadMethod readyMethod) {
Object paraValue[];
checkDependence(readyMethod);
while(readyMethod.hasNext()){//判断可执行方法的列表是否还有
BeanMethodDefinitation bmd = readyMethod.out();//取出第一个方法
Parameter parameter[] = bmd.getMethod().getParameters();
int count = parameter.length;
if(count<=0){
paraValue = new Object[]{};
}else{
paraValue = new Object[count];
for(int i = 0; i < count; i++){
paraValue[i] = BeanPool.get(parameter[i].getType().getName()).getObject
//在BeanPool中获取参数,构造方法的参数数组
//我们在执行方法时候没有对的它参数成员进行注入,通过直接getObject浅注入
//这样做存在着一些不合理的地方,为简化期间,先进性这样的操作。
}
}
Object result = bmd.invokeMeth(paraValue);//反射机制指向该方法
BeanDefinitation bd = new BeanDefinitation();//用方法的返回值构造一个Bean
bd.setKalss(result.getClass());
bd.setObject(result);
bd.setisInject(true);
BeanPool.put(result.getClass().getName(), bd);//把Bean加入到BeanPool中
checkDependence(readyMethod);//再次检查依赖关系是否变化
}
}
到此我们就完成了容器的构造工作!!!!!
懒汉模式注入
通过上面的操作,我们已经完成了容器的构建,接下来我们可以从容器中得到Bean了。下面是getBean方法:
public <T> T getBean(String className){
if(bdpen.hasNext()){//检查方法中的循环依赖
System.out.println("存在循坏依赖");
System.out.println(bdpen.dependenceShip);
}
BeanDefinitation bd = BeanPool.get(className);
if(bd == null){
throw new HasNoBeanException("没有找到" + className + "的Bean");
}
if(!bd.isIsinject()){//注入参数
bd.setisInject(true);
injectBean(bd);
}
return (T) bd.getObject();
}
在得到Bean之前,我们先要判断所有的依赖关系是否都处理完成了,如果依赖关系的列表不为空,说明存在循环依赖,比如下面的情况:
@Bean
public Point getPoint(PointOne pointOne) {
Point point = new Point();
return point;
}
@Bean
public PointOne getPointONe(Point point){
return new PointOne();
}
getPoint方法的执行需要PointOne,作为参数,getPointONe的执行需要Point作为参数,两个方法形成了循环依赖,无法执行,在第一次getBean的时候抛出异常,并输出无法正常指向的依赖关系,提示使用者。
循环依赖抛出异常:
如果能够正常执行就开始给这个Bean注入参数,一下是注入参数的代码:
private void injectBean(BeanDefinitation bd){
Field[] fields = bd.getKalss().getDeclaredFields();
//得到Bean的所有成员
for(Field field : fields){
field.setAccessible(true);
if(!field.isAnnotationPresent(Autowired.class)){
continue;
}
//遍历它的所有成员得到带Autowired注解的方法
String valueName = field.getType().getName();
Object obj = getBean(valueName);
//得到该类成员的Bean,递归调用完成对参数成员的注入
if(obj == null){
throw new HasNoBeanException("没有找到" + valueName + "的Bean");
}
try {
field.set(bd.getObject(), obj);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
## 下面举例说明一下有关getBean部分的递归调用。
@Component
public class ClassOne {
@Autowired
private Complex complex;
private String str;
@Component
public class Complex {
private double real;
private double vir;
@Autowired
private ClassTwo calssTwo;
@Component
public class ClassTwo {
private int number;
以上三个类,如果我们想通过getBean的方式得到ClassOne 的对象,就必须对它带@Autowired的成员进行注入,再对ClassOne的成员complex进行注入的过程中,发现complex中还有需要注入的成员classTwo,先完成对classTwo的注入,然后再注入Complex,最后完成对ClassOne的注入,由此就形成了getBean方法和inject方法的递归调用。
如何处理参数之间的循环依赖
@Component
public class ClassOne {
@Autowired
private Complex complex;
private String str;
@Component
public class Complex {
private double real;
private double vir;
@Autowired
private ClassOne classOne;
如果要通过getBean得到ClassOne的对象,与习惯不同,我们先把该对象标记为注入,再对其进行注入这样就能避免循环递归。
if(!bd.isIsinject()){//注入参数
bd.setisInject(true);
injectBean(bd);
}
最后测试类是这个样子的:
public class Test {
public static void main(String[] args) {
BeanFactory2 bf = new BeanFactory2();
bf.scanPackage("com.mec.spring.imitate.someclass");
System.out.println(bf.getBean(Point.class));
}
}
以上就是我对spring ioc 的模拟实现,虽然做得比较粗糙,还有很多不完善的地方,但是对IOC的总体思路是对的,以后也会一遍学习一遍继续完善,希望这篇文章能过帮助到大家学习spring框架。