手写IOC容器
我们先来看一下IOC容器的整体流程:
首先,我们先创建一个Maven项目,然后在项目的resources目录下添加一个配置文件application.properties,在配置文件中指定需要扫描的包路径
然后我们定义一些注解,分别表示访问控制层、业务服务层、数据持久层、依赖注入注解、获取配置文件注解,代码如下
依赖注入注解@MyAutowired
访问控制层注解@MyController
业务服务层注解@MyService
数据持久层注解@MyMapping
配置文件获取注解@Value
定义完注解后,我们可以开始编写代码了。根据上面的流程图,此时应该先获取读取配置文件,从配置文件中获取需要扫描的包路径
我们先写一个配置文件工具类ConfigurationUtils,代码如下:
package cn.xiaosong.platform.config;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* 类 名: ConfigurationUtils
* @author xiaosong
*/
public class ConfigurationUtils {
/**
* 项目配置文件信息
*/
public static Properties properties;
public ConfigurationUtils(String propertiesPath) {
properties = this.getBeanScanPath(propertiesPath);
}
/**
* @demand: 读取配置文件
*/
private Properties getBeanScanPath(String propertiesPath) {
if (StringUtils.isEmpty(propertiesPath)) {
propertiesPath = "/application.properties";
}
Properties properties = new Properties();
// 通过类的加载器获取具有给定名称的资源
InputStream in = ConfigurationUtils.class.getResourceAsStream(propertiesPath);
try {
System.out.println("正在加载配置文件application.properties");
properties.load(in);
return properties;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return properties;
}
/**
* @demand: 根据配置文件的key获取value的值
*/
public static Object getPropertiesByKey(String propertiesKey) {
if (properties.size() > 0) {
return properties.get(propertiesKey);
}
return null;
}
}
上述代码中,我们通过读取配置文件获取到配置文件信息的key-value键值对;然后我们再根据配置文件中指定的扫描包路径进行包扫描 拿到包扫描路径后,我们就可以获取到当前路径下的文件信息及文件夹信息,我们将当前路径下所有以.class结尾的文件添加到一个Set集合中进行存储。代码如下:
private void classLoader() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
// 加载配置文件所有配置信息
new ConfigurationUtils(null);
// 获取扫描包路径
String classScanPath = (String) ConfigurationUtils.properties.get("ioc.scan.path");
if (StringUtils.isNotEmpty(classScanPath)) {
classScanPath = classScanPath.replace(".", "/");
} else {
throw new RuntimeException("请配置项目包扫描路径 ioc.scan.path");
}
// 扫描项目根目录中所有的class文件
getPackageClassFile(classScanPath);
for (String className : classSet) {
addServiceToIoc(Class.forName(className));
}
// 获取带有MyService注解类的所有的带MyAutowired注解的属性并对其进行实例化
Set<String> beanKeySet = iocBeanMap.keySet();
for (String beanName : beanKeySet) {
addAutowiredToField(iocBeanMap.get(beanName));
}
}
我们需要扫描项目路径中所有以.class结尾的文件,将其添加到一个全局的Set集合中,代码如下:
private Set<String> classSet = new HashSet();
private void getPackageClassFile(String packageName) {
URL url = this.getClass().getClassLoader().getResource(packageName);
File file = new File(url.getFile());
if (file.exists() && file.isDirectory()) {
File[] files = file.listFiles();
for (File fileSon : files) {
if (fileSon.isDirectory()) {
// 递归扫描
getPackageClassFile(packageName + "/" + fileSon.getName());
} else {
// 是文件并且是以 .class结尾
if (fileSon.getName().endsWith(".class")) {
System.out.println("正在加载: " + packageName.replace("/", ".") + "." + fileSon.getName());
classSet.add(packageName.replace("/", ".") + "." + fileSon.getName().replace(".class", ""));
}
}
}
} else {
throw new RuntimeException("没有找到需要扫描的文件目录");
}
}
我们将所有的类的字节码对象都存储到一个全局的Set集合中之后,遍历这个set集合,获取在类上有指定注解的类,并将其交给IOC容器;我们先定义一个安全的Map用来存储这些对象
```java
/**
* @demand: 控制反转
*/
private void addServiceToIoc(Class classZ) throws IllegalAccessException, InstantiationException {
// 预留位置,之后优化
if (classZ.getAnnotation(MyController.class) != null) {
iocBeanMap.put(toLowercaseIndex(classZ.getSimpleName()), classZ.newInstance());
System.out.println("控制反转访问控制层:" + toLowercaseIndex(classZ.getSimpleName()));
} else if (classZ.getAnnotation(MyService.class) != null) {
// 将当前类交由IOC管理
MyService myService = (MyService) classZ.getAnnotation(MyService.class);
iocBeanMap.put(StringUtils.isEmpty(myService.value()) ? toLowercaseIndex(classZ.getSimpleName()) : toLowercaseIndex(myService.value()), classZ.newInstance());
System.out.println("控制反转服务层:" + toLowercaseIndex(classZ.getSimpleName()));
} else if (classZ.getAnnotation(MyMapping.class) != null) {
MyMapping myMapping = (MyMapping) classZ.getAnnotation(MyMapping.class);
iocBeanMap.put(StringUtils.isEmpty(myMapping.value()) ? toLowercaseIndex(classZ.getSimpleName()) : toLowercaseIndex(myMapping.value()), classZ.newInstance());
System.out.println("控制反转持久层:" + toLowercaseIndex(classZ.getSimpleName()));
}
}
/**
* @demand: 类名首字母转小写
*/
public static String toLowercaseIndex(String name) {
if (StringUtils.isNotEmpty(name)) {
return name.substring(0, 1).toLowerCase() + name.substring(1, name.length());
}
return name;
}
然后我们再遍历这个IOC容器,获取到每一个类的实例,判断里面是有有依赖其他的类的实例,即依赖注入;
/**
* @demand: 依赖注入
*/
private void addAutowiredToField(Object obj) throws IllegalAccessException, InstantiationException, ClassNotFoundException {
Field[] fields = obj.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.getAnnotation(MyAutowired.class) != null) {
field.setAccessible(true);
MyAutowired myAutowired = field.getAnnotation(MyAutowired.class);
Class<?> fieldClass = field.getType();
// 接口不能被实例化,需要对接口进行特殊处理获取其子类,获取所有实现类
if (fieldClass.isInterface()) {
// 如果有指定获取子类名
if (StringUtils.isNotEmpty(myAutowired.value())) {
field.set(obj, iocBeanMap.get(myAutowired.value()));
} else {
// 当注入接口时,属性的名字与接口实现类名一致则直接从容器中获取
Object objByName = iocBeanMap.get(field.getName());
if (objByName != null) {
field.set(obj, objByName);
// 递归依赖注入
addAutowiredToField(field.getType());
} else {
// 注入接口时,如果属性名称与接口实现类名不一致的情况下
List<Object> list = findSuperInterfaceByIoc(field.getType());
if (list != null && list.size() > 0) {
if (list.size() > 1) {
throw new RuntimeException(obj.getClass() + " 注入接口 " + field.getType() + " 失败,请在注解中指定需要注入的具体实现类");
} else {
field.set(obj, list.get(0));
// 递归依赖注入
addAutowiredToField(field.getType());
}
} else {
throw new RuntimeException("当前类" + obj.getClass() + " 不能注入接口 " + field.getType().getClass() + " , 接口没有实现类不能被实例化");
}
}
}
} else {
String beanName = StringUtils.isEmpty(myAutowired.value()) ? toLowercaseIndex(field.getName()) : toLowercaseIndex(myAutowired.value());
Object beanObj = iocBeanMap.get(beanName);
field.set(obj, beanObj == null ? field.getType().newInstance() : beanObj);
System.out.println("依赖注入" + field.getName());
// 递归依赖注入
}
addAutowiredToField(field.getType());
}
if (field.getAnnotation(Value.class) != null) {
field.setAccessible(true);
Value value = field.getAnnotation(Value.class);
field.set(obj, StringUtils.isNotEmpty(value.value()) ? ConfigurationUtils.getPropertiesByKey(value.value()) : null);
System.out.println("注入配置文件 " + obj.getClass() + " 加载配置属性" + value.value());
}
}
}
上述代码中,我们通过判断带指定注解的类里面是否有注入其它的类,然后进行递归注入;但是有一个问题,接口和抽象类不能被实例化,所以在处理接口时,就出现了一个难题。通常我们习惯注入接口,但是接口不能被实例化,我们需要对接口赋值它的子类,如何获取到接口的实现类呢? 翻遍了JDK1.8的API,没有找到能够提供这样的方法。于是这里做了写了一个循环,遍历IOC容器中的每一个类是否有实现接口,如果是相同的接口则记录,但是这样做会非常消耗性能的,其代码如下:
/**
* @demand: 判断需要注入的接口所有的实现类
*/
private List<Object> findSuperInterfaceByIoc(Class classz) {
Set<String> beanNameList = iocBeanMap.keySet();
ArrayList<Object> objectArrayList = new ArrayList<>();
for (String beanName : beanNameList) {
Object obj = iocBeanMap.get(beanName);
Class<?>[] interfaces = obj.getClass().getInterfaces();
if (useArrayUtils(interfaces, classz)) {
objectArrayList.add(obj);
}
}
return objectArrayList;
}
对于接口的注入,暂时还没有想到有什么好的方式可以优化,也不知道Spring是怎么做的。当一个接口有多个实现类时,需要用过自定义名称进行交给IOC管理和注入注解进行获取。 截至到这里,我们就完成了整个IOC容器的创建以及依赖注入功能了。我们可以写一个简单的测试类来试一下我们写的这个IOC容器;测试代码:访问控制层
@MyController
public class LoginController {
@Value(value = "ioc.scan.pathTest")
private String test;
@MyAutowired(value = "test")
private LoginService loginService;
public String login() {
return loginService.login();
}
}
测试代码:业务服务接口层
public interface LoginService {
String login();
}
``
测试代码:具体服务层(这里尝试了写两个实现类,多态情况下)`
```java
@MyService(value = "test")
public class LoginServiceImpl implements LoginService {
@MyAutowired
private LoginMapping loginMapping;
@Override
public String login() {
return loginMapping.login();
}
}
@MyService
public class TestLoginServiceImpl implements LoginService {
@Override
public String login() {
return "测试多态情况下依赖注入";
}
}
测试代码:数据持久层接口层public interface LoginMapping {
String login();
}
测试代码:数据持久层具体持久层
@MyMapping
public class LoginMappingImpl implements LoginMapping {
@Override
public String login() {
return "项目启动成功";
}
}
然后我们写一个启动类PlatformApplication
public class PlatformApplication {
public static void main(String[] args) throws Exception {
// 从容器中获取对象(自动首字母小写)
MyApplicationContext applicationContext = new MyApplicationContext();
LoginController loginController = (LoginController) applicationContext.getIocBean("LoginController");
String login = loginController.login();
System.out.println(login);
}
}
控制台输出日志信息如下:
正在加载配置文件application.properties
正在加载: cn.jiayao.platform.common.MyApplicationContext.class
正在加载: cn.jiayao.platform.config.ConfigurationUtils.class
正在加载: cn.jiayao.platform.core.annotation.MyAutowired.class
正在加载: cn.jiayao.platform.core.annotation.MyController.class
正在加载: cn.jiayao.platform.core.annotation.MyMapping.class
正在加载: cn.jiayao.platform.core.annotation.MyService.class
正在加载: cn.jiayao.platform.core.annotation.Value.class
正在加载: cn.jiayao.platform.modular.controller.LoginController.class
正在加载: cn.jiayao.platform.modular.dao.impl.LoginMappingImpl.class
正在加载: cn.jiayao.platform.modular.dao.LoginMapping.class
正在加载: cn.jiayao.platform.modular.service.impl.LoginServiceImpl.class
正在加载: cn.jiayao.platform.modular.service.impl.TestLoginServiceImpl.class
正在加载: cn.jiayao.platform.modular.service.LoginService.class
正在加载: cn.jiayao.platform.PlatformApplication.class
正在加载: cn.jiayao.platform.utils.MyArrayUtils.class
控制反转访问控制层:loginController
控制反转持久层:loginMappingImpl
控制反转服务层:loginServiceImpl
控制反转服务层:testLoginServiceImpl
注入配置文件 class cn.jiayao.platform.modular.controller.LoginController 加载配置属性ioc.scan.pathTest
项目启动成功
这样,一个简单的IOC容器就创建完成了;
Github地址:https://github.com/smile960207/IOC.git