Spring框架的核心内容中,主要有两个功能:IoC和AOP。IoC又称为:控制反转,或者叫“自动注入”,而AOP则称为“面向切面编程”方案,或者称为:非侵入式逻辑更改/扩展。IoC的控制反转
中所谓的反转是指:非反转情况下,类的对象应该由代码自身实现实例化,而控制反转意味着,类的对象不是由代码本身完成实例化的,而是由某个“外在”系统实现实例化。下面我们来看一个非反转经典代码:
class Some {
private UserClass obj;
// ...
某个方法() {
this.obj = new UserClass();
}
}
这是由代码自身实现对象实例化。那么,IoC是使用“配置、自动注入”这两个配套手段实现对对象的实例化过程,即DI。代码中不会出现实例化过程。这样做的好处至少可以使得用户代码更加容易替换。
在学习了SpringIoC的相关配置以及一些简单的实现原理后,开始试着模拟实现一个简单的容器来模拟IoC的自动装配以及依赖注入。
模拟实现的几个部分如下所示:
1、可以实现对于需要管理的bean对象的包进行扫描,得到需要管理的bean对象。
2、对于bean对象的获取,采用了注解+包扫描的方式,注解借鉴了spring的注解命名,但是我们也可以使用XML的方式来配置。
3、采用“懒汉模式”来获取bean对象,即在调用getBean时才进行DI,最终返回一个完成依赖注入的bean对象。
4、对于依赖注入采用两种不同的方式来进行,一种是通过属性,另一种是通过属性的setting方法来进行注入。
5、对于循环依赖进行了简单的处理,解决了循环依赖的问题
6、对于无法操作的jar包中的对象,采用Bean注解的方式,调用get方法来返回这个bean对象,对于无参与参数中存在依赖的情况进行了简单处理
下面对这几点进行简单的介绍:
1、容器对bean对象的管理以及注解的配置
采用工厂模式,使用BeanFactory作为容器来产生并且管理bean对象,容器的真正实现在与BeanFactory中的beanPool成员,采用HashMap的方式以bean对象的Class名为键,BeanDefinition为值来存储bean对象。
而BeanDefinition的作用在于存储bean的类以及反射得到的实例化对象,以及是否完成了依赖注入,对于这个成员在后面的解决循环依赖的部分中介绍。
public class BeanDefinition {
private Class<?> klass;
private Object object;
private boolean isInject;
public BeanDefinition() {
this.isInject = false;
BeanFactory中定义了一系列的对bean对象的管理以及获取操作,它的主体代码如下:
private static Map<String, BeanDefinition> beanPool;
static {
beanPool = new HashMap<>();
}
public BeanFactory() {
}
public static void scanBeanByAnnotation(String packageName) throws ClassNotFoundException, IOException, URISyntaxException {
new PackageScanner() {
@Override
public void dealClass(Class<?> klass) {
//对带有Component注解的类进行bean管理
if (klass.isAnnotationPresent(Component.class)) {
try {
BeanDefinition beanDefinition = new BeanDefinition();
beanDefinition.setKlass(klass);
Object object = klass.newInstance();
beanDefinition.setObject(object);
String beanId = klass.getName();
beanPool.put(beanId, beanDefinition);
//处理Component的类中带Bean注解的方法,即把带有Bean注解的方法也加入到容器中
dealBeanMethod(beanDefinition);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}.scanPackage(packageName);
}
上面的代码就是对bean对象获取后管理在容器中,里面使用到了一个包扫描类,PackageScanner类是我自己定义的用来得到一个包下的所有文件的抽象工具类,且不局限于在此模块中使用。这个点在下面会展示出来。我们通过扫描之后得到包下的所有文件,且通过PackageScanner工具预留的dealClass抽象方法,得到带有@Component注解的类,即想管理的bean对象的类。
对于注解的配置我参考Spring的命名方式,@Component注解只能用来修饰类,标识需要管理的bean对象;@Autowired注解可以修饰方法和成员,标识采用何种依赖注入式来注入依赖;@Bean注解可以修饰方法,它的作用是标识那些无法直接设置注解但是我们又需要管理的类,通过get方法获得bean对象来进一步管理。
2、bean对象的获取-包扫描
bean对象的获取是基于包扫描抽象类的来得以实现的,这里主要讲述包扫描的具体实现。主体代码如下所示:
//提供给使用此工具的类来使用,处理扫描到的类
public abstract void dealClass(Class<?> klass);
public void scanPackage(String packageName) throws URISyntaxException, ClassNotFoundException, IOException {
//得到包路径
String pathName = packageName.replace(".", "/");
//由包路径得到Uniform Resource Locator - 统一资源定位符
URL url = Thread.currentThread().getContextClassLoader().getResource(pathName);
//得到资源的协议,这里主要用来区分jar包与普通包
String protocol = url.getProtocol();
if ("jar".equals(protocol)) {
dealJar(packageName, url);
} else {
//通过URL得到文件根目录,即文件的路径
File root = new File(url.toURI());
dealDir(packageName, root);
}
}
包扫描是通过对传入的包名进行解析与分割,分为jar包与非jar包来处理,对于非jar包采取下面的方式来处理:
private void dealDir(String packageName, File root) throws ClassNotFoundException {
//得到此根目录下的所有文件即文件夹
File[] files = root.listFiles();
for(File file : files) {
if (file.isDirectory()) {
//递归此文件夹的所有文件
dealDir(packageName + file.getName(), file);
} else {
//找出所有的.class
if (file.getName().endsWith(".class")) {
String className = file.getName().replace(".class", "");
//得到class文件全类名
className = packageName + "." + className;
Class<?> klass = Class.forName(className);
dealClass(klass);
}
}
}
}
采用递归的方式得到这个包路径下的所有文件,自动调用dealClass这个抽象方法来处理得到的class文件。
而对于jar包我的处理方式大致类似与处理非jar包,主要调用了jdk中的util包下的jar包下的一些方法来得到具体文件,如下所示:
//对jar包的处理
private void dealJar(String packageName, URL url) throws IOException, ClassNotFoundException {
//得到jar包的URLConnection,之后得到jar文件
JarURLConnection connection = (JarURLConnection) url.openConnection();
JarFile jarFile = connection.getJarFile();
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String entryName = entry.getName();
if (!entryName.endsWith(".class")) {
continue;
}
entryName = entryName.replace(".class", "");
String className = entryName.replace("/", ".");
if (!className.startsWith(packageName)) {
continue;
}
Class<?> klass = Class.forName(className);
dealClass(klass);
}
}
3、bean对象的获取
对于bean对象的获取是采用getBean方法,以想得到的class名为键,来得到完成依赖注入的bean对象结果。在返回bean对象之前完成依赖注入:
/**
* 完成两个任务
* 1、给需要得到的bean对象进行依赖注入
* 2、返回需要的bean的对象
* @param beanId
* @param <T>
* @return
*/
public static <T> T getBean(String beanId) throws Exception {
BeanDefinition beanDefinition = beanPool.get(beanId);
if (beanDefinition == null) {
throw new RuntimeException("Bean:" + beanId + "不存在");
}
T bean = (T) beanDefinition.getObject();
if (!beanDefinition.isInject()) {
//依赖注入
beanDefinition.setInject(true);
inject(beanDefinition);
}
return bean;
}
//注入依赖
private static void inject(BeanDefinition beanDefinition) throws Exception {
Class<?> klass = beanDefinition.getKlass();
Object object = beanDefinition.getObject();
//通过成员注入
injectByField(klass, object);
//通过方法注入
injectByMethod(klass, object);
}
还提供了一个使用类来得到bean对象,返回执行方法的返回值,即使用类.getName()转化为beanId来得到bean对象。
public static <T> T getBean(Class<?> klass) throws Exception {
return getBean(klass.getName());
}
4、依赖注入以及简单解决循环依赖问题
对于依赖注入采用两种方式来注入,即使用@Autowired标识成员变量,或者标识set方法来注入依赖。但是二者最终的实现方式均是采用执行set方法进行依赖的注入,实现代码及注释如下:
//注入依赖
private static void inject(BeanDefinition beanDefinition) throws Exception {
Class<?> klass = beanDefinition.getKlass();
Object object = beanDefinition.getObject();
//通过成员注入
injectByField(klass, object);
//通过方法注入
injectByMethod(klass, object);
}
private static void injectByField(Class<?> klass, Object object) throws Exception {
Field[] fields = klass.getFields();
for (Field field : fields) {
if(!field.isAnnotationPresent(Autowired.class)) {
continue;
}
String fieldName = field.getName();
Class<?> fieldType = field.getType();
String fieldMethodName = "set" + fieldName.substring(0,1).toUpperCase() + fieldName.substring(1);
Method fieldMethod = klass.getDeclaredMethod(fieldMethodName,fieldType);
//注入的属性,即注入的依赖bean的类型
setterMethodInvoke(fieldMethod, object, fieldType);
}
}
private static void injectByMethod(Class<?> klass, Object object) throws Exception {
Method[] methods = klass.getDeclaredMethods();
for (Method method : methods) {
String methodName = method.getName();
if(!methodName.startsWith("set") || !method.isAnnotationPresent(Autowired.class) || method.getParameterCount() != 1) {
continue;
}
//得到set方法的参数类型,传递方法、对象以及参数类型给doSetterMethod
Class<?> parameterType = method.getParameterTypes()[0];
setterMethodInvoke(method, object, parameterType);
}
}
private static void setterMethodInvoke(Method fieldMethod, Object object, Class<?> fieldType) throws Exception {
Object parameter = getBean(fieldType);
if(parameter == null) {
throw new Exception("需要注入的类【" + parameter.getClass().getName() + "】");
}
//执行setter方法注入了依赖
fieldMethod.invoke(object, parameter);
}
这些方法都是私有方法,换句话说,不需要使用者进行其他操作,由容器自动装配以及依赖注入,对容器进行了封装,简化了容器的使用。
对于循环依赖的解释及处理如下:
@Component
public class ClassOne {
@Autowired
private ClassTwo two;
...
}
@Component
public class ClassTwo {
@Autowired
private ClassThird third;
...
}
public class ClassThird {
@Autowired
private ClassOne one;
...
}
明显,上述三个类的各个成员之间形成了循环依赖关系,则,对三个类中的任何一个对象进行“注入”操作时,都会产生递归!
首先看一种简单的循环依赖关系:
A -> B 表示,A类中存在B类的成员,且,需要注入;
B -> A 表示,B类中存在A类的成员,且,需要注入。
假设getBean(A.class);
则,对A的对象a进行注入操作,即,需要对a.b进行注入;但是,B的对象是通过getBean(B.class)获取的;而getBean()方法,又会
继续实现对b对象的注入;即,需要对b.a进行注入;发现,b.a的注入过程已经出现循环递归了!
但是,如果先:
beanDefinition.setInject(true);
inject(beanDefinition);
这个过程详细变量跟踪一次:
getBean(A.class);在这个过程中,需要完成对a的注入,也就是对a.b实现注入;但是,先设置A已经完成注入了!a.b的获取是通过getBean(B.class)获得的,这个过程需要实现对b.a进行注入;但是,先设置B已经完成注入了!再次对A的对象进行注入时,发现A类的注入操作已经完成了,就不需要再对A进行注入了,这就避免了无限递归!
简单的对设置依赖已经注入提前到真正注入依赖之前,便可解决循环递归依赖注入问题。
6、对于jar包内的bean对象的获取以及依赖注入问题
包内类处理:
对于Jar包内的类,采取由用户书写一个函数来实例化,但是由IoC调用的方法。即在本身包中新建一个类用于配置,其中书写的方法由新增的Bean注解标记,扫描到该方法,由IoC调用,即可将Jar包内的类实例出来。对该方法应该如下理解:
1.方法的返回值是一个BeanDefinition,方便与放入pool
2.在方法中实例化相关类,这是主要目的
3.允许方法的参数是其他Bean,这点是处理难点,当所需的Bean未生成甚至该类在其他包内需要额外处理
此时重点问题是依赖存在于参数,那么如何判断依赖是否满足及当满足时运行该方法。这种主要的应用场景是需要的Bean存在于其他Jar包内,无法获取该参数,即依赖此时无法完成,应利用Map先存储。额外新建一个定义类BeanMethod,用于被Bean标记得方法,对应类的实例化对象,需要注入数目(即参数类型个数)
总共生成两个Map,一个List
第一个Map:用于存储方法BeanMethod和该类所需参数类型(使用Map存储,虽然值没有用处,但是借助Map键只能出现一次的特性可以解决参数重复出现的情况)的对应关系。
List:用于存储依赖关系以满足(即BeanMethod中的注入数目为0)的方法
第二个Map:该Map主要是为了效率考虑,我们每生成一个Bean,会生成其自己所需的依赖关系,那么依赖于这个Bean的方法,我们应该进行相应的处理,利用存储了该Bean及依赖于该Bean的BeanMethodDefinition列表,可以简化寻找的过程,按键找到对应的值,进行注入数目减一
包内循环依赖:
jar包内的循环依赖是无解的,因为循环依赖的存在(即相关方法的参数发生了循环关系),所设置的方法参数不全,无法调用,即无法实例化出对象。此时解决方法是抛出一个异常。
故如何查找到某个对象依赖不完全并识别出来是重点:
static BeanMethodDefinition getBeanMethodDefinitionByClassName(String className) {
if (beanMethodDepandent.isEmpty()) {
return null;
}
for (BeanMethodDefinition beanMethodDefinition : beanMethodDepandent.keySet()) {
Class<?> klass = beanMethodDefinition.getKlass();
if (klass.getName().equals(className)) {
return beanMethodDefinition;
}
}
return null;
}
在beanMethodDepandent图中,存储了该Bean方法定义及其依赖的参数对应关系,当在pool内查找不到该类的对象,说明该类没有生成,那么就需要获得该类,已知类名称,就可以在图中依据方法定义查找该类,并返回。
上面就是我对于spring中的IoC的简单模拟实现,对于加深sping的理解与使用有了更大一部分的体会。