模拟Spring核心IOC实现类的注入
注:本片文章简单模拟Spring的整个思路,完整代码在下一篇。建议两个对比着看。
我们平常使用对象的时候,一般都是直接使用关键字类new一个对象,使用new就表示当前模块已经不知不觉的和 new的对象耦合了,为了降低对象耦合关系,Spring框架编写者提出了IOC和AOP的核心思想。
DI:(Dependency Injection)依赖注入;
IOC(Inverse of Contro)控制反转,有时候也被称为DI依赖注入,它是一种降低对象耦合关系的一种设计思想。
AOP(Aspect Oriented Programming『面向切面编程』。其提倡的是针对同一类问题的统一处理,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。它是一种新的方法论,是对传统OOP编程的一种补充。
今天,我们就来简单模拟一下Spring。
首先我们先思考一个类中成员的初始化,不需要让编写者自己实现成员初始化的代码,而是有一套机制悄悄提前完成了并放入一个池子中,我们只需要从里面取出来即可。
我们可以通过注解或者XML文件的方式,表明需要初始化的类,以及类中需要初始化的成员。再将某应用需要的类及其对象,都集中存储到一个集合(池子)中;凡是在这个集合中的类,尤其是这些类的成员类型也在这个池子中,则,这些成员的初始化都从池子中的对象来给予!
可能上面的一段话比较抽象,先不急。
我们采用注解形式来完成。
首先我们先新建两个注解类,一个是用在类上面的,用来表示该类是否需要加入池子里,这个注解类叫Component。
@Retention(RUNTIME)
@Target(TYPE)
public @interface Component {
boolean singleton() default true;
}
第二个注解类叫AutoWired,它用在成员和方法身上,用来表示该成员或方法是否需要被注入(初始化)。
@Retention(RUNTIME)
@Target({ FIELD, METHOD })
public @interface AutoWired {
}
我们新建个类举例说明一下,如下图,该类带了Component注解,表示这个类就需要放入池子中,它的成员有AutoWired注解,就表示这个ClassTwo类型的成员two需要被初始化。
@Component
public class ClassThree {
@AutoWired
ClassTwo two;
public ClassTwo getTwo() {
return two;
}
public void setTwo(ClassTwo two) {
this.two = two;
}
}
下面,我们编写BeanDefinition类以及BeanFactory类。
BeanDefinition类中主要封装 该类的Class类、该类的对象、该类是否已经被注入、该类是否是单例的。
![](https://i-blog.csdnimg.cn/blog_migrate/2d30934a125dbca3b24701d04ccd5661.png)
BeanFactory类中有一个池子beanPool,该Map的键是该类的类名,值是该类对应的BeanDefinition对象。
![](https://i-blog.csdnimg.cn/blog_migrate/326315a3ccebc5d0b16e06f2c7a4e691.png)
这是扫描包的代码,主要功能就是在众多类中找到带有Component注解的类,并将这个类实例化,放入池子中,以便我们日后获取出对应的对象。
public static void scanPackage(String packageName) {
new PackageScanner() {
@Override
public void dealClass(Class<?> klass) {
if (klass.isPrimitive()
|| klass == String.class
|| klass.isAnnotation()
|| klass.isArray()
|| klass.isInterface()
|| !klass.isAnnotationPresent(Component.class)) {
return;
}
// 将这个类实例化,并将其放到beanPool中
try {
Object object = klass.newInstance();
BeanDefinition beanDefinition = new BeanDefinition();
beanDefinition.setKlass(klass);
beanDefinition.setObject(object);
beanPool.put(klass.getName(), beanDefinition);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}.scanPackage(packageName);
}
这时就有一个问题了!我们存入的ClassThree类里面还有一个ClassTwo类型的成员或方法加有AutoWired注解,加AutoWired注解就意味着需要被初始化,那万一ClassTwo类此时还没有放入池子中,肯定取不到其对象啊!取不到对象就没法初始化!总不能让它的值为null吧,这样一定会影响我们后续的操作的!
所以,这就需要考虑,到底在哪里给带有AutoWired注解的成员进行注入!
最保险的方案是:在获取该类对象用的时候再去执行注入操作!因为我们无法知道ClassTwo类到底在什么时候被put进池子里。
但是!有没有发现Object object = klass.newInstance();这条语句有猫腻!
有些类并不是用无参构造new出来的!比如Calendar rightNow = Calendar.getInstance();如果遇到这种情况怎么办呢?
还有,如果我们要用的类是jar包里的类,不能够给类上加Component注解,不加注解就不能扫描到该类,就不能加到池子里,又怎么办?
我们可以新建一个Config类,再新建一个Bean注解类。
我们给Config类上加上Component注解,使得在包扫描时能够扫描到它。
在Config类中,全是带有Bean注解的方法,每个方法的返回值都将会被put进池子中。这样我们就可以解决刚才说的问题了。
Config类举例:
@Component
public class Config {
public Config() {
}
@Bean
public ClassTwo getTwoClass() {
ClassTwo two = new ClassTwo();
return two;
}
@Bean
public Two getTwo(One one) {
Two two = new Two();
two.setOne(one);
return two;
}
@Bean
public One getOne(Two two) {
One one = new One();
one.setTwo(two);
return one;
}
@Bean
public ClassForth getClassForth(Complex com) {
ClassForth forth = new ClassForth();
forth.setComplex(com);
return forth;
}
@Bean
public Calendar getDate() {
Calendar date = Calendar.getInstance();
return date;
}
}
可以看到上面的Config类有很多方法啊!
有一个getClassForth方法带有参数引起了我的注意!
其实这就是Bean注解的强大之处,它可以带很多参数,然后把进行包装后的完美对象放入池中,而不是简单的反射机制调用无参构造!
![](https://i-blog.csdnimg.cn/blog_migrate/2de29848d3a97e8bbe624379901e225f.png)
当然,功能越强大意味着我们需要考虑的越多,实现的越多!
这里是一个难点哦!
Config类中方法的处理:
得到所有方法并遍历所有方法,若有方法带有bean注解,则判断该方法有无参数。
如果是无参的,我们只需要执行该方法并得到返回值和返回值类型,new一个BeanDefinition并设置BeanDefinition的相关成员,最后放入池子中。
这里处理循环依赖
有参的话!就麻烦了哦!
我先举个麻烦的例子,比如一个方法有4个参数(A、A、B、C),其中2个A类型相同,另外两个类型不同。且A类已经在池子中了(即可以取出现成的对象),但B、C类并没有在池子中,意味着这个方法不能执行,因为他们参数连值都没有获取到!那不能执行怎么办呢?如果其他方法需要以这个方法执行完后的返回值作为参数值,岂不是也不能执行!哈哈哈,放轻松,我们慢慢来。
首先,我们应该明确该方法的参数总共有几类,因为A最终都是从池子里取出的,所以取出的对象一定相同,所以参数类型个数只用算3个!
我们可以通过一个小技巧来实现这步操作!HashMap默认,如果put的键之前put过了,后面再put,前面的值将会被覆盖。所以通过下面的代码后,paraPool中就只剩下不重复的类类型了,后面的Integer是随便给的,没有意义。
Class<?>[] paraType = method.getParameterTypes();
Map<Class<?>, Integer> paraPool = new HashMap<Class<?>, Integer>();
for(Class<?> paraKlass : paraType) {
paraPool.put(paraKlass, 0);
}
下面先介绍我们整个流程,再来逐个分析!
先看下面的图~
uninvokeList(列表): 存放当下不能执行的方法,这些方法的特点是有些形参值无法从BeanPool中取到。
invokeList(列表): 存放可以执行的方法,方法特点是每个方法的所有形参类型的对象都能在BeanPool中取到。
dependenceMethodPool(Map):这个池中表示未满足的参数与方法之间的对应关系。该Map的键是参数类型,值是一个需要该参数的Method方法集合,一个List。
我们当时记录了每个方法的缺少类型个数,我们称为count。
大概就是这么个流程,而且还要探讨一个问题,就是现在BeanPool里面虽然没有参数B类型,但是说不定下一次put就能加进来了,所以我们每put一个键值对进BeanPool,都要去dependenceMethodPool中检查是否刚put进的键就是dependenceMethodPool里的某个键。
若B是dependenceMethodPool的键,那么键B所对应的List里的所有缺少B类型的方法的count都要减一,且若某个方法的count减到0,就表示此方法可以执行了,就把它放入invokeList中等待执行;之后把dependenceMethodPool中的键B remove掉!
后面就还有一个大问题,就是从BeanPool中取出对应类的对象。getBean()方法!
这里牵扯一个单例非单例的问题
单例对象意味着,我们只用注入一次,我们每次取出的对象都是同一个!
非单例意味着,每次取对象时都要重新进行注入(注入就是将加AutoWired注解的成员初始化)
@Retention(RUNTIME)
@Target(TYPE)
public @interface Component {
boolean singleton() default true;
}
单例与否写在Component注解上,默认是单例的!
还有如果该类是单例的,意味着只需要注入一次,那么我们就需要定义一个标志来表示该类的对象是否已经被注入过!
于是有了BeanDefinition类的四个成员
private Class<?> klass; //类类型
private Object object; //该类的实例化对象
private boolean inject; //表示该类是否被注入
private boolean singleton; //表示该类是否是单例的
我们上面也说过,注入的工作是在getBean时才做的,这也是所谓的懒汉模式!与之对应的有饿汉模式!这里不再赘述!
其实这个getBean方法里有递归吼吼!看代码就成!
总结Bean注解的用途:
关于Bean注解:
Autowired注解的缺陷:Autowired只能获取池子中的对象,而池中对象
都是需要给对应的类以@Component注解;对于不可更改的Jar包中的类,
就没有办法增加@Component注解,也就不能实现“注入”操作。
Bean注解就是为了解决这样的问题存在的。
给一个方法增加Bean注解,而将这个方法的返回值对象和返回值对象类型
作为键值对,存储到池子中!
Bean注解的第二个应用场合:
若相关类没有提供可用的构造方法;所谓的没有提供可用的构造方法包括
相关构造方法是private的,或者,构造方法不能直接调用,或者,构造
方法不能直接生成对象!在这种情况下,由于对于Component注解的处理
是通过调用相关类的无参构造产生的,那么,对于上述情况,就不能产生
这个类的对象!此时,可以通过Bean注解,调用合适的获取该类对象的
方法,取得这个类的对象,并加入BeanPool中!
Bean注解的第三个应用场合:
相关类的对象,不是简单无参构造就能直接使用的;意思是:这个类虽然
存在无参构造,但是,无参构造出来的对象不能直接使用。那么,在这种
情况下,通过Bean注解的方法,完成其对象所必须的基础数据,从而使得
该对象可用!
通过XML方式,也可配置Bean和注入;
XML能实现的,注解方式也可以实现;
XML配置的优点是:不侵害源代码,保证了“开闭原则”;
注解配置的优点:程序可读性强,无需额外代码(开发效率高);
这篇就先介绍到这里吧!代码在下一篇博文里,建议对比着看!
戳这里!直接跳转!