首先我想总结一下:
为什么要使用ioc:
为什么要使用ioc来进行依赖注入,主要的目的是为了解耦,原先的代码controller层依赖于Service层,Service层依赖于DAO层,service需要new dao,controller需要new service。层层依赖使得代码的耦合度很高,同时增加了维护成本和难度,ioc本质上就是将原先controller要通过new一个service,现在通过控制反转后将控制权交给了第三方ioc容器,也就是说通过ioc接管控制权,主动帮controller注入service对象,从而避免controller自己new一个对象,这就是控制反转的过程。
随便提一下控制反转是一种思想,而依赖注入是实现的手段,两者并不等价。
从中得到的理解与认识:
通过手写ioc容器,加强了我对低耦合编程的理解,同时对里氏替换原则,依赖倒置原则等SOLID六大基本原则的编程思想理解加深。
对我印象尤为深刻的是面向接口编程思想,在刚接触oop时,始终不明白接口的作用,认为多此一举,但随着我对编程的深入以及手写ioc容器的过程,逐渐加深了面向接口编程思想的理解。
关于面向接口编程的个人拙见:
简而言之面向接口编程是一种编程思想,其核心是基于抽象而不是实现编程,从而提高代码的灵活性、可扩展性和可维护性。而里氏替换原则和依赖倒置原则是实现面向接口编程的重要原则。相较于面向对象,面向接口更注重接口与抽象,里氏替换原则提出接口的实现类应该能替换接口使用,同时依赖倒置提出高层模块不应该依赖于低层模块,它们都应该依赖于抽象。这意味着,应该基于接口而不是实现进行编程,从而提高代码的可维护性和可扩展性。在依赖倒置原则中,抽象应该不依赖于具体实现,而具体实现应该依赖于抽象。这可以通过依赖注入来实现。所以我在进行依赖注入时注入的是接口而非其实现类,在注入时将将实现类实例注入进接口,这一点里氏替换原则给出了可行性。以上两个原则是面向接口编程思想的重要原则,其规定了一种优秀的编程方式,这种模式可以提高代码的灵活性、可扩展性和可维护性,从而使得代码更加易于开发、测试和维护。
由ioc与di深入到面向接口编程的应用:
再举一个具体的例子,如果注入的是某个接口的具体的一个实现类,现在因为业务需求需要扩展功能,以至于有多个实现类实现了该接口,此时要将其他实现类注入原先的上层代码,就会报错,这一点在单一实现类问题不大,而一旦业务复杂出现多个实现类或扩展功能,注入实现类的弊端就暴露无遗了,因此要遵循面向接口编程的原则,将实现类实例注入接口,接口作为实现类的依赖,根据里氏替换原则,注入的实现类实例可以完全代替接口的作用,从而达到易扩展可维护,低耦合的编程要求。
接口也相当于代码规范,其规定了其实现类应该遵循的规则和方法规范,注入接口就不会因随意更改实现类代码而导致出错,也体现出面向接口编程的优点。同时将实现类实例注入接口也是一种多态的体现。
接下来是代码流程的解析 :
依赖注入有两种手段,其一是通过xml配置文件解析出相应的bean并注入,其二是通过注解实现,本篇小记重点讲述后者。
要实现注解方式,首先要自定义注解类:
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 依赖注入 注解标识
**/
@Target(ElementType.FIELD) // 暂时只用于属性
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAutowired {
}
此注解类作用是标记方法属性等,暂时只用于属性,具体用法后续会提及
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标识类是一个bean
**/
@Target(ElementType.TYPE) // 暂时只用于类上
@Retention(RetentionPolicy.RUNTIME)
public @interface MyComponent {
String value() default "";
}
此注解类用于标记被依赖的类,将其注册成bean实例
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解,用于解析获取 bean 扫描路径
**/
@Target(ElementType.TYPE) // 只能用于类
@Retention(RetentionPolicy.RUNTIME) // 运行时有效
public @interface MyComponentScan {
String value() default "";
}
此注解用于解析获取 bean 扫描路径
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 标识bean是单例还是多例
**/
@Target(ElementType.TYPE) // 用于类上
@Retention(RetentionPolicy.RUNTIME) // 运行时有效
public @interface MyScope {
String value() default "";
}
此注解类用于标识bean是单例还是多例,现阶段主要还是单例使用较多,算是扩展功能
接下来我们要定义一个bean实体类,用于存储bean的实例信息,并对单例和多例操作节省代码量和内存
(单例直接生成新的bean,多例继续使用原先生成的bean实体)
java
/**
* bean 的定义修饰类,用于保存bean的修饰信息
*
**/
public class BeanDefinition {
// 是哪个类
private Class clazz;
// 单例还是多例
private String scopeType;
public Class getClazz() {
return clazz;
}
public void setClazz(Class clazz) {
this.clazz = clazz;
}
public String getScopeType() {
return scopeType;
}
public void setScopeType(String scopeType) {
this.scopeType = scopeType;
}
}
然后我们开始写ioc容器
大致思路是
1.根据注解的扫包路径,进行扫包
2.将带有MyComponentScan注解的类的类名转为class对象
3.获取bean的别名称,将别名作为key,beanDefinition实例作为value存入beanDefinitionMap集合中
4.找到存在@MyAutowired注解标记的属性,通过反射进行注入
现在开始定义一个ioc容器,完成bean扫描,通常放在构造函数中,自动执行 减少重复代码
java
public class MySpringAppAnnotationContext {
// 指定配置类属性
private Class aClass;
// bean定义类的集合
Map<String,BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>();
//bean的单列池 保存bean对象
private Map<String,Object> singletonObjMap = new ConcurrentHashMap<>();
public MySpringAppAnnotationContext(Class aClass) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
this.aClass = aClass;
// 容器初始化,解析对应的配置类
// 1、 扫描
// 判断对象上是否有对应的注解
if (aClass.isAnnotationPresent(MyComponentScan.class)) {
// getDeclaredAnnotation 会忽略继承
// getAnnotation 包括继承的所有注解
//MyComponentScan myComponentScan = (MyComponentScan) aClass.getAnnotation(MyComponentScan.class);
MyComponentScan myComponentScan = (MyComponentScan) aClass.getDeclaredAnnotation(MyComponentScan.class);
String value = myComponentScan.value();
//System.out.println(value);
// 如果未设定扫描的路径值,则默认 配置类 所在的包作为父包
if(value == null || value.length() == 0){
value = aClass.getPackage().getName();
}
// 当前value是注解中的value自定义路径,属于相对路径。
// 需要获取绝对路径,可以依据classpath
// 获取ClassLoader
ClassLoader classLoader = this.getClass().getClassLoader();
// getResource需要传递路径,但value只是包(xx.xx.xx),需要转换成路径(xx/xx/xx)
value = value.replace(".","/");
// 获取相对于 -classpath 的路径
URL resource = classLoader.getResource(value);
// 为了判断当前的包是否是文件夹(filePath 为 绝对路径)
String filePath = resource.getFile();
//System.out.println(filePath); // /E:/study/my_spring/target/classes/com/test
// 迭代扫描子包路径
Set<String> classNameFromDir = getClassNameFromDir(filePath, value, true);
for (String className : classNameFromDir) {
// 如何将包名+类名 变为class对象,就需要使用到类加载器
Class<?> loadClass = classLoader.loadClass(className);
// 并非是每个class文件都是我们所需要的,bean只需要保证携带@Component注解即可
if (!loadClass.isAnnotationPresent(MyComponent.class)) {
continue;
}
// 满足要求,那么接下来就是将bean构建出来
// 这里其实还需要判断是否是单例 需要定义 @MyScope 暂时不考虑,统一为单例
MyComponent myComponent = loadClass.getAnnotation(MyComponent.class);
String beanName = myComponent.value();
String newBeanName = null;
// 如果在 MyComponent 注解中未指定value属性,此处如何获取?
if(beanName == null || beanName.length() == 0){
// 将类名的首字母小写处理后,将其作为bean的别名称
beanName = Introspector.decapitalize(loadClass.getSimpleName());
newBeanName = beanName.substring(0,beanName.length() - "Impl".length());
}
// 扫描中只负责处理类的加载,但不生成bean,考虑到多例bean的获取形式,会在getbean中获取
// 所以此处是将bean的修饰信息进行保存
BeanDefinition beanDefinition = new BeanDefinition();
beanDefinition.setClazz(loadClass);
// 获取类的scope属性
if (loadClass.isAnnotationPresent(MyScope.class)) {
// 存在注解,则获取注解中设定的值,根据值进行判断是单例还是多例
MyScope myScope = loadClass.getAnnotation(MyScope.class);
String scopeVal = myScope.value();
if(ScopeEnum.SINGLETON.getValue().equalsIgnoreCase(scopeVal)){
beanDefinition.setScopeType(ScopeEnum.SINGLETON.getValue());
}else{
// 设置的多例值或者其他值的话,就设置为多例
beanDefinition.setScopeType(ScopeEnum.PROTOTYPE.getValue());
}
}else{
// 没有这个注解,默认就是单例
beanDefinition.setScopeType(ScopeEnum.SINGLETON.getValue());
}
// 修饰好了类,就将修饰类放入集合
beanDefinitionMap.put(newBeanName,beanDefinition);
}
}
要注意的是,扫包获取的是相对路径,而根据类路径创建类实例要通过类的绝对路径
先将相对路径的"."替换成"/" 再迭代扫描是否为文件夹,进行下一步操作,这里是迭代方法
java
/**
* 遍历文件夹,判断文件夹中是否包含文件夹,如果 isRecursion 是true,则遍历到子文件夹继续遍历
* @param filePath
* @param packageName
* @param isRecursion
* @return
*/
public Set<String> getClassNameFromDir(String filePath, String packageName, boolean isRecursion){
Set<String> className = new HashSet<>();
// 将文件路径转换为对象
File file = new File(filePath);
// 获取该File对象下的所有文件信息(含文件夹)
File[] files = file.listFiles();
// 遍历判断是文件还是文件夹
for (File chileFile : files) {
if(chileFile.isDirectory()){
// 是文件夹
if(isRecursion){
className.addAll(getClassNameFromDir(chileFile.getPath(),packageName+"."+chileFile.getName(),isRecursion));
}
}else{
// 如果是文件,则判断是否是class文件
String fileName = chileFile.getAbsolutePath();
//System.out.println(fileName);
// 启动可能存在非class文件,需要过滤
if(!fileName.endsWith(".class")){
continue;
}
//System.out.println("是class文件");
// 截取包名------类名
// 不能把参数写死,依据传递的包名称进行拆分
String[] split = packageName.split("/");
String classFileName = fileName.substring(fileName.indexOf(split[0]), fileName.indexOf(".class"));
//System.out.println(className);
// 因为名称是文件路径,并不是包名称
classFileName = classFileName.replace("\\", ".");
className.add(classFileName);
}
}
return className;
}
之后对beanDefinitionMap进行遍历,创建Bean对应的class实例放入singletonObjMap集合中
java
// 2、将bean的修饰对象进行遍历,创建bean
for (String beanName : beanDefinitionMap.keySet()) {
BeanDefinition beanDefinition = beanDefinitionMap.get(beanName);
// 判断单例还是多例
if (ScopeEnum.SINGLETON.getValue().equalsIgnoreCase(beanDefinition.getScopeType())) {
// 单例bean全局只会有一个对象
Object bean = createBean(beanName,beanDefinition);
// 放入bean的单例池中
singletonObjMap.put(beanName,bean);
}
}
这里是创建Bean的代码
java
private Object createBean(String beanName,BeanDefinition beanDefinition){
Class clazz = beanDefinition.getClazz();
Object obj = null;
// 获取无参构造,实例化出对象
try {
//通过反射创建一个类的实例对象
obj = clazz.getDeclaredConstructor().newInstance();
// 获取实例化对象的属性,判断哪些属性需要依赖注入,
//获取 clazz 类中声明的所有成员变量(即属性)
Field[] fields = clazz.getDeclaredFields();
//遍历每个属性
for (Field field : fields) {
// 并不是每个属性都需要注入
if (field.isAnnotationPresent(MyAutowired.class)) {
// 存在 MyAutowired 注解修饰的属性,需要进行属性注入
// 暂不考虑循环依赖注入问题
Object fieldBean = getBean(field.getName());
// 设置至父类中
// 这里设置为true,只是为了不进行繁杂的安全校验,耗费时间
field.setAccessible(true);
field.set(obj,fieldBean);
}
}
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return obj;
}
调用getBean获取与该属性同名的Bean对象,getBean代码
java
// 获取bean对象
public Object getBean(String className) {
// 通过bean的别名称,获取对应的bean实例化对象
// 这里需要判断是否是单例还是多例,暂时不考虑复杂的,统一按照单例处理
BeanDefinition beanDefinition = beanDefinitionMap.get(className);
if(Objects.isNull(beanDefinition)){
// 说明bean没有被扫描到
throw new NullPointerException("没有这个bean");
}
// 获取他的作用域
String scopeType = beanDefinition.getScopeType();
if(ScopeEnum.SINGLETON.getValue().equalsIgnoreCase(scopeType)){
// 单例的
// 判断bean实例池中是否存在bean
Object obj = singletonObjMap.get(className);
if(Objects.isNull(obj)){
obj = createBean(className,beanDefinition);
// 保存bean池
singletonObjMap.put(className,obj);
}
return obj;
}else{
// 多例每次都创建
return createBean(className,beanDefinition);
}
}
这样一个基本的ioc容器就创建成功了。
关于如何手写出来,我不否认代码有部分参考,但比起参考到代码,我认为将代码模块化运用到实际项目中并熟练掌握代码调试能力发现其中的bug与缺陷,从而更好地优化代码的能力更重要。本文有问题的地方欢迎指出,讨论,本人一定会虚心学习!