第一章:前言
现在用Java做Web开发已经无法避免使用Spring框架了,Spring框架给开发者提供了很多功能,其中就包含了IOC。今天就学习学习IOC的思想,然后做一个简单的Demo。
首先由两个概念:IOC和DI。IOC(Inversion of Control ,控制反转)和DI(Dependency Inject,依赖注入)。
假设现在有两个类A和B,A需要引用B。在传统的设计模式中,我们在编写A的时候会new一个B的实例出来供A调用,是程序主动去创建对象。IOC的设计思想是,单独做一个容器,在程序运行时创建一个B的实例对象,A的程序里面在容器里面去请求B的实例而不是去创建实例,将引用对象交与容器控制。这样相对于传统设计,可以有效减少对象的创建。这个是我理解的IOC思想。
还是上面提到的A和B,B是A的一个属性,这是添加一个类C,C的程序里面需要引用A,按照上面的思想,在程序启动时,IOC容器要创建A的实例,因为A需要引用B的实例所以在实例化A时容器需要将B的实例注入到A的实例里面。其中A,B,C之间的关系是体现在IOC容器的配置中,当程序启动时,容器根据配置文件去实例每个实例,并为每个实例注入需要的资源。这是对DI的理解
第二章:制作简单的IOC Demo
第一步:计划
使用HashMap作为IOC容器,用注解作为配置文件,在程序启动时根据配置创建实例对象;创建具有引用关系的两个类。
第二步:创建注解
/**
* 需要IOC容器实例化的注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Bean {
String name(); //对象实例之后的名称
}
|
/**
* 请求IOC容器中对象的注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Require {
String name(); //需要注入的实例的注解
}
|
上面的注解,Bean表示需要IOC托管的类,name表示这个类在容器中的名称;Require表示需要从IOC容器获取的属性,name对应属性对应实例在IOC中的名词;所以在IOC容器创建实例时,名称不能重复,类之间不能循环引用,找不到属性对应的实例会报错。
第三步:创建IOC容器
/**
* 使用一个HashMap作为一个IOC容器
*/
public class IOCMap {
private HashMap<String, Object> map;
public IOCMap() {
this(new String[0]);
}
public IOCMap(String[] packages) {
map = new HashMap<>();
//开始扫描包
if (packages.length == 0) {
String path = this.getClass().getPackage().getName();
packages = new String[]{path};
}
//获取到包下面的类
Set<Class> set = new HashSet<>();
for (String path : packages) {
set.addAll(Utils.getClasses(path));
}
setBeans(map, set, true);
}
private void setBeans(HashMap<String, Object> map, Set<Class> clazzs, boolean first) {
if (clazzs.isEmpty()) return;
Set<Class> nextSet = new HashSet<>();
for (Class clazz : clazzs) {
Bean beanAnno = (Bean) clazz.getDeclaredAnnotation(Bean.class);
if (beanAnno != null) {
String name = beanAnno.name();
if (map.containsKey(name) && map.get(name) != null) {
throw new RuntimeException("实例名称配置重复:" + name);
}
Field[] fields = clazz.getDeclaredFields();
boolean enough = true;
//判断当前类的属性依赖
for (Field field : fields) {
Require declaredAnno = field.getDeclaredAnnotation(Require.class);
if (declaredAnno != null) {
String name1 = declaredAnno.name();
if (!first && !map.containsKey(name1)) {
throw new RuntimeException("没有这个实例:" + name1);
}
if (map.get(name1) == null) {
nextSet.add(clazz);
enough = false;
break;
}
}
}
if (enough) {
Object object;
try {
Constructor constructor = clazz.getDeclaredConstructor();
object = constructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(clazz.getName() + ": 无参构造函数出错");
}
for (Field field : fields) {
Require declaredAnno = field.getDeclaredAnnotation(Require.class);
String name1 = declaredAnno.name();
field.setAccessible(true);
try {
field.set(object, map.get(name1));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
map.put(name, object);
} else if (!map.containsKey(name)) {
map.put(name, null);
}
}
}
//当一次处理没有减少Class数,则判断引用重复
if (nextSet.size() == clazzs.size()) {
throw new RuntimeException("引用关系出现循环");
}
setBeans(map, nextSet, false);
}
public Object getBean(String name) {
return map.get(name);
}
}
|
/**
* 工具类
*/
public class Utils {
//扫描包路径下的类
public static Set<Class> getClasses(String packagePath) {
Set<Class> res = new HashSet<>();
String path = packagePath.replace(".", "/");
URL url = Thread.currentThread().getContextClassLoader().getResource(path);
if (url == null) {
System.out.println(packagePath + " is not exit");
return res;
}
String protocol = url.getProtocol();
if ("jar".equalsIgnoreCase(protocol)) {
try {
res.addAll(getJarClasses(url, packagePath));
} catch (IOException e) {
e.printStackTrace();
return res;
}
} else if ("file".equalsIgnoreCase(protocol)) {
res.addAll(getFileClasses(url, packagePath));
}
return res;
}
//获取file路径下的class文件
private static Set<Class> getFileClasses(URL url, String packagePath) {
Set<Class> res = new HashSet<>();
String filePath = url.getFile();
File dir = new File(filePath);
String[] list = dir.list();
if (list == null) return res;
for (String classPath : list) {
if (classPath.endsWith(".class")) {
classPath = classPath.replace(".class", "");
try {
Class<?> aClass = Class.forName(packagePath + "." + classPath);
res.add(aClass);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} else {
res.addAll(getClasses(packagePath + "." + classPath));
}
}
return res;
}
//使用JarURLConnection类获取路径下的所有类
private static Set<Class> getJarClasses(URL url, String packagePath) throws IOException {
Set<Class> res = new HashSet<>();
JarURLConnection conn = (JarURLConnection) url.openConnection();
if (conn != null) {
JarFile jarFile = conn.getJarFile();
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
String name = jarEntry.getName();
if (name.contains(".class") && name.replaceAll("/", ".").startsWith(packagePath)) {
String className = name.substring(0, name.lastIndexOf(".")).replace("/", ".");
try {
Class clazz = Class.forName(className);
res.add(clazz);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
return res;
}
public static void main(String[] args) {
Set<Class> classes = Utils.getClasses("com.ioc");
classes.addAll(Utils.getClasses("net.sf"));
for (Class clazz : classes) {
System.out.println(clazz.getName());
}
}
}
|
在IOCMap中,使用一个HashMap作为对象存储空间,在IOCMap构造函数里面去进行对象创建:先找到需要扫描包路径下的所有类,遍历所有的类查询注解是否需要实例化,对于要实例化的类查询是否有需要注入的属性,如果有需要注入的属性,查询注入名称是否有实例在Map中,如果没有则将类放在下一次遍历Set中。如果类需要注入的对象全部存在,则创建。如果这一次需要创建的类数目和下一次需要创建的类数目一样,判断为循环引用。
第三步:创建对象
/**
* ClassA 需要被IOC容器实例化 名称为 class_a
*/
@Bean(name = "class_a")
public class ClassA {
public String call() {
return " I am A";
}
}
|
/**
* ClassB 需要被IOC创建 名称为class_b
* 具有一个属性 ClassA 需要注入名称为class_a的实例
*/
@Bean(name = "class_b")
public class ClassB {
@Require(name = "class_a")
private ClassA classA;
public String call() {
return classA.call();
}
}
|
在上面的ClassA和ClassB都需要被容器实例化,其中ClassB 具有一个ClassA的属性。
第四步:测试
/**
* 测试,从IOCMap中获取叫 classb_的实例
*/
public class App {
public static void main(String[] args) {
IOCMap iocMap = new IOCMap();
ClassB classB = (ClassB) iocMap.getBean("class_b");
System.out.println(classB.call());
}
}
|
从容器中获取名叫 class_b 的实例,调用call方法
执行结果:IOCMap实例化之后的数据
输出:I am A
扩展考虑:如果需要想实现单例,多例的区别,增加注解属性,实现工厂接口。HashMap中就存放 name 和factory,获取的时候就调用factory的create方法就行。如果想要根据Class进行注入,那map的key使用Class就行了,获取的时候判断子父类就OK