[IOC]
1. ioc 简介
1.1 ioc 定义
ioc
(Inversion of Control, 控制反转)把创建对象的操作交给框架,亦被称为 DI
(Dependency Injection, 依赖注入)。
为什么叫做 “控制反转” 呢?之前,我们想要一个对象都是 new
出来的,天天需要 new
对象是不是感觉有点麻烦。有人就想到了,把这些简单重复的工作也交给框架做。本来需要我们向框架 “射入” 对象,现在框架自己能产生对象了,这不正是 控制反转 吗?于是,就有了这个响亮的名字。
1.2 ioc 目标
其实定义中已经把 ioc
的目标说清楚了。这里,再强调一下:把创建对象的操作交给框架,“解放程序员的生产力”。
下面代码是 ioc
的使用用例,它可实现 customerService
变量的自动注入。本文的目标是实现 ioc
,也就是使得下面代码可用。
// 使用了基于注解实现的 ioc 代码
@Controller
public class CustomerController {
// 这里可以自动注入 CustomerService 对象
@InJect
private CustomerService customerService;
...
}
ioc
的实现主要有两种形式:1)声明式;2)注解。其中,声明式是基于 xml
配置文件来做的,这种方式对 jdk
版本无要求,兼容性强,但相注解方式较冗杂;而注解方式,需要 jdk1.5
以上(因为从 jdk1.5 才有了注解),但它更灵活、更简洁。鉴于现在很少有人在使用 jdk1.5
之前的版本,本文将基于注解实现 ioc
。
2. ioc 实现
2.1 实现关键
- 如何标识出类中哪些变量需要注入;
- 怎样生成可用于注入的对象实例以及如何存储它们;
- 何时注入;
2.2 详细过程
1) 创建注解 InJect
用它标识类中需要注入的属性;
// InJect.java
package top.inotwant.annocation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD) // 元注解,表示 InJect 作用于属性之上
@Retention(RetentionPolicy.RUNTIME) // 元注解,用于描述 InJect 生存周期
public @interface InJect {
}
2) 创建 Controller
Service
注解,它们都作用于类上(比如,1.2 节中 @Controller
作用于 CustomerController
之上)。被它们修饰的类将要求产生对象实例,这个过程将会用到 反射,下面再详细描述该过程。(使用过 Spring
框架的是不是对这两个注解很熟悉)
// Controller.java
package top.inotwant.annocation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE) // 元注解,表示 Controller 作用于类之上
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}
// Service.java
package top.inotwant.annocation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
}
3) 自定义类加载器加载包下的所有 java
文件,并从中获取被上面两个注解之一修饰的类的元类 Class<?>
。 有了这些元类,我们就可以使用反射产生对应的对象实例了。
ClassUtil
工具类中的 loadClassSet(String packageName)
静态方法实现了该逻辑。它将返回 packageName
指定的包下所有类的元类。
ClassHelper
帮助类实现了对上面两个注解之一修饰类的元类的抽取。
// ClassUtil.java
package top.inotwant.utils;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
public final class ClassUtil {
/**
* 获取类加载器
*/
public static ClassLoader getClassLoader() {
return Thread.currentThread().getContextClassLoader();
}
/**
* 加载指定类
*/
public static Class<?> loadClass(String className) {
try {
// TODO 注意 class.forName 与 ClassLoader.loadClass 的区别(关于 static 是否静态初始化)
// 注意第二个参数,一定要设置为 true 。否则,在加载类时不会运行 static 块,这将影响一部分类的初始化。
return Class.forName(className, true, getClassLoader());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
/**
* 加载包下所有类
* @param packageName 为包名,比如 “top.inotwant.sample”
*/
public static Set<Class<?>> loadClassSet(String packageName) {
String packagePath = packageName.replace(".", "/");
try {
Set<Class<?>> result = new HashSet<>();
// getResources 的使用请看 API
Enumeration<URL> resources = getClassLoader().getResources(packagePath);
while (resources.hasMoreElements()) {
URL currentURL = resources.nextElement();
if ("file".equals(currentURL.getProtocol())) {
String path = currentURL.getPath().replaceAll("%20", " ");
addClass(path, packageName, result);
} else if ("jar".equals(currentURL.getProtocol())) { // 此处为了加载文件夹下 `jar` 中所包含的所有类文件,若不理解可忽略此处,不影响总体
JarURLConnection conn = (JarURLConnection) currentURL.openConnection();
JarFile jarFile = conn.getJarFile();
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
String jarEntryName = jarEntry.getName();
if (jarEntryName.endsWith(".class")) {
String className = jarEntryName.substring(0, jarEntryName.lastIndexOf(".")).replaceAll("/", ".");
result.add(loadClass(className));
}
}
}
}
return result;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private static void addClass(String path, String packageName, Set<Class<?>> result) {
File[] files = new File(path).listFiles(new FileFilter() {
@Override
public boolean accept(File file) {
return (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory();
}
});
if (files != null) {
for (File file : files) {
if (file.isFile()) {
String subFileName = file.getName();
String className = subFileName.substring(0, subFileName.lastIndexOf("."));
result.add(loadClass(packageName + "." + className));
} else if (file.isDirectory()) {
// 递归
addClass(file.getAbsolutePath(), packageName + "." + file.getName(), result);
}
}
}
}
}
// ClassHelper.java
package top.inotwant.helper;
import top.inotwant.annocation.Controller;
import top.inotwant.annocation.Service;
import top.inotwant.utils.ClassUtil;
import java.util.HashSet;
import java.util.Set;
/**
* Class 帮助类
*/
public final class ClassHelper {
private static Set<Class<?>> CLASS_SET;
static {
String basePackage = "指定包";
CLASS_SET = ClassUtil.loadClassSet(basePackage);
}
/**
* 加载 base package 下的所有类
*/
public static Set<Class<?>> getClassSet() {
return CLASS_SET;
}
/**
* 获取加载的由 service 标注的所有类
*/
public static Set<Class<?>> getServiceClassSet() {
Set<Class<?>> result = new HashSet<>();
for (Class<?> clazz : CLASS_SET) {
if (clazz.isAnnotationPresent(Service.class))
result.add(clazz);
}
return result;
}
/**
* 获取加载的由 controller 标注的所有类
*/
public static Set<Class<?>> getControllerClassSet() {
Set<Class<?>> result = new HashSet<>();
for (Class<?> clazz : CLASS_SET) {
if (clazz.isAnnotationPresent(Controller.class))
result.add(clazz);
}
return result;
}
/**
* 获取框架管理的 bean (包含, service controller)
*/
public static Set<Class<?>> getBeanClassSet() {
Set<Class<?>> result = new HashSet<>();
result.addAll(getServiceClassSet());
result.addAll(getControllerClassSet());
return result;
}
}
4) 利用上面的元类,使用反射生成它们对应的对象实例。
BeanHelper
中的静态块中实现这个逻辑。这些对象实例中包含了可用于注入的(与 @Service
对应的),它们被存储在 map
中,以 <Class,Object>
键值对的形式,是不是很容易理解。
// BeanHelper.java
package top.inotwant.helper;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* bean 帮助类
*/
public final class BeanHelper {
private final static Map<Class<?>, Object> BEAN_MAP = new HashMap<>();
static {
Set<Class<?>> beanClassSet = ClassHelper.getBeanClassSet();
for (Class<?> clazz : beanClassSet) {
BEAN_MAP.put(clazz, getInstance(clazz));
}
}
/**
* 获取类对应的实例
*/
@SuppressWarnings("unchecked")
private static <T> T getInstance(Class<?> clazz) {
try {
return (T) clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/**
* 获取 bean 映射
*/
public static Map<Class<?>, Object> getBeanMap() {
return BEAN_MAP;
}
/**
* 获取 bean
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(Class<T> clazz) {
Object obj = BEAN_MAP.get(clazz);
if (obj != null)
return (T) obj;
else {
throw new RuntimeException("can't get bean by class: " + clazz.getSimpleName());
}
}
}
5) 注入
IocHelper
帮助类的 static 块中完成最后一步 注入。其中的逻辑很简单,我们把所有的 bean
遍历一遍,每一次迭代查看该 bean
的各个属性是否标注 InJect
。如果标注了,就从 beanMap
中获取相应的对象实例进行注入。
package top.inotwant.helper;
import top.inotwant.annocation.InJect;
import top.inotwant.utils.ArrayUtil;
import top.inotwant.utils.CollectionUtil;
import top.inotwant.utils.ReflectUtil;
import java.lang.reflect.Field;
import java.util.Map;
/**
* ioc 实现类
*/
public final class IocHelper {
static {
Map<Class<?>, Object> beanMap = BeanHelper.getBeanMap();
if (CollectionUtil.isNotEmpty(beanMap)) { // 判断集合是否为空
for (Map.Entry<Class<?>, Object> entry : beanMap.entrySet()) {
Class<?> clazz = entry.getKey();
Object obj = entry.getValue();
// TODO 切勿调成 getFields() ,它不能获取私有属性
Field[] fields = clazz.getDeclaredFields();
if (ArrayUtil.isNotEmpty(fields)) { // 判断数组是否
for (Field field : fields) {
if (field.isAnnotationPresent(InJect.class)) {
Object newValue = BeanHelper.getBean(field.getType());
ReflectUtil.modifyField(obj, field, newValue); // 注入
}
}
}
}
}
}
}
6) 初始化 ioc
前面 5 步已经实现了 ioc
,并且可通过系统初始化时加载 IocHelper.class
让它起到作用(想一想为什么只需要加载一个类就可以?)。但是,我建议再创建如下一个类,让加载更集中、初始化的逻辑更清晰。
package top.inotwant;
import top.inotwant.helper.*;
import top.inotwant.utils.ClassUtil;
/**
* 加载相关的 helper 类
*/
public final class HelperLoader {
public static void init() {
Class<?>[] classes = {
ClassHelper.class,
BeanHelper.class,
IocHelper.class
};
for (Class<?> clazz : classes) {
ClassUtil.loadClass(clazz.getName());
}
}
}
至此,1.2 节中的代码已经可以使用。