Java注解和反射简单实现IoC
之前学spring boot时, 就一直对依赖注入的实现很好奇, 当时没有学会注解和反射, 大学Java教材只到1.7的版本, 都不知道注解是什么玩意儿。之后学了注解和反射后, 就想着自己实现一个用注解自动装配的样例当作练习。
IoC
我们传统的方式创建对象是通过new来主动创建对象的, 但这样会造成代码耦合度过高: 假设我们有100个地方使用了A
接口的某个实现类B
, 代码经过版本跟新后我们废弃掉了实现类B
, 而改用新的实现类C
, 那么我们怎么改呢? 用这种传统的方式我们就只能够手动的一个一个修改, 但这样太麻烦了, 而且违反了开闭原则:
IoC不是主动的new对象, 而是在初始化的时候, IoC 容器通过反射将对象注入进去, 所以在修改代码的时候, 我们不需要修改每个使用了该接口的地方, 而是只要将IoC容器中的对象替换掉即可:
在spring boot中, 我们只需要在需要注入的地方加入@Autowired
注解spring容器就会自动为其装配对象。
实例编写
为了更好的理解IoC, 同时作为对Java 注解及反射的练习, 编写了一个用注解注入对象的实例:
首先是注解部分:
/**
* 注解扫描
* 扫描value中的包下的所有注解
*
* @author Bobasyu
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyScan {
String value() default "";
}
/**
* 标有该注解的类会在IoC容器中产生相对应的对象
*
* @author bobasyu
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCompetent {
}
/**
* 标有这个注解的变量将会被注入相对应的对象
* @author Bobasyu
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAutoWired {
}
@MyScan
注解标记在启动类上, 在启动后IoC容器会递归扫描该注解的值所对应的包目录及其子目录(默认为启动类对应目录), 检查里面的类是否标记有@MyCompetent
注解, 若标有该注解, 则将该类实例化存入容器, 同时, 检查容器中的对象实例, 若对象有属性标记有@MyAutoWired
注解则从容器中找到其对应的对象注入进去。
其次是IoC容器类, 该容器使用一个HashMap存放IoC容器中的对象实例, 可以通过类名来获取对象:
/**
* IoC容器
*
* @author Bobasyu
*/
public class MyContext {
/**
* 装有 [类名 : 类对应的实例] 的容器
*/
private Map<Class, Object> objectMap;
protected MyContext() {
this.objectMap = new HashMap<>();
}
/**
* 添加类,并创建相应的实例对象到map容器中
*
* @param clazz 所添加的类对象
*/
protected void push(Class clazz) {
try {
objectMap.put(clazz, clazz.newInstance());
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 获取类所继承的接口
*
* @param clazz 通过类实例反射来所获取类所继承的的接口
* @return 所获取的类所继承的接口
*/
public Object getObject(Class clazz) {
if (objectMap.containsKey(clazz)) {
return this.getObjectMap().get(clazz);
}
for (Class c : objectMap.keySet()) {
for (Class i : c.getInterfaces()) {
if (clazz.equals(i)) {
return objectMap.get(c);
}
}
}
return null;
}
/**
* 获取mao容器键的集合
*
* @return
*/
protected Set<Class> classSet() {
return objectMap.keySet();
}
public Map<Class, Object> getObjectMap() {
return this.objectMap;
}
}
然后是IoC的功能实现, 其内部有IoC容器, 主要功能就是扫描目录结构并注入对象:
/**
* IoC 功能实现
* @author Bobasyu
*/
public class MyApplication {
private MyContext myContext;
public MyApplication() {
this.myContext = new MyContext();
}
public void run(Class clazz) {
MyScan myScan = (MyScan) clazz.getAnnotation(MyScan.class);
String pack = myScan.value();
if (pack == null || "".equals(pack)) {
pack = clazz.getPackage().toString().replace("package ", "");
}
// 先把包名转换为路径,首先得到项目的classpath
String classpath = clazz.getResource("/").getPath().replace("/", File.separator);
//然后把我们的包名basPath转换为路径名
String basePath = classpath + pack.replace(".", File.separator);
init(pack, basePath);
}
/**
* 初始化, 获取pack包目录下所有的类的名称
*
* @param pack 被扫描的包的目录
* @param basePath pack所在的绝对路径
*/
private void init(String pack, String basePath) {
List<String> filePath = new ArrayList<>();
FileUtil.findFileList(new File(basePath), filePath);
basePath = FileUtil.replaceBasePath(basePath);
List<Class> classList = getClassList(filePath, pack, basePath);
addObject(classList);
injectionObject();
}
/**
* 根据Class列表将带有 @MyCompetent 注解的类创建实体添加到容器中
*
* @param classList 需要添加到容器的Class列表
*/
private void addObject(List<Class> classList) {
classList.parallelStream().forEach(clazz -> {
Annotation annotation = clazz.getAnnotation(MyCompetent.class);
if (annotation != null && clazz != null) {
this.getMyContext().push(clazz);
}
});
}
/**
* 将容器中的实例中标记有@MyAutoWired注解的属性注入相应的对象
*/
private void injectionObject() {
this.getMyContext().classSet().parallelStream().forEach(clazz -> {
Field[] fields = clazz.getDeclaredFields();
Arrays.stream(fields).filter(field -> field.getAnnotation(MyAutoWired.class) != null)
.forEach(field -> {
try {
field.setAccessible(true);
Object fieldObject = this.myContext.getObject(field.getType());
Object clazzObject = this.myContext.getObject(clazz);
field.set(clazzObject, fieldObject);
} catch (Exception e) {
e.printStackTrace();
}
});
});
}
/**
* 根据路径获取类对象
*
* @param fileName 类的文件名列表
* @param pack 包名
* @param basePath 包的目录的绝对路径
* @return 类对象列表
*/
private List<Class> getClassList(List<String> fileName, String pack, String basePath) {
return fileName.parallelStream().map(str -> {
String className = str.replace(basePath, pack + ".")
.replace(".class", "")
.replace(File.separator, ".");
try {
return Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}).collect(Collectors.toList());
}
private MyContext getMyContext() {
return myContext;
}
}
下面编写测试用接口及其实现类:
/**
* @author Bobasyu
*/
public interface MyTestInterface {
/**
* 测试用
*/
void test();
}
/**
* @author Bobasyu
*/
@MyCompetent
public class MyTestInterfaceImpl implements MyTestInterface {
@Override
public void test() {
System.out.println("test");
}
}
启动类:
/**
* @author Boba
*/
@MyScan("cn.bobasyu.test")
@MyCompetent
public class Main {
@MyAutoWired
private static MyTestAutowired myTestAutowired;
public static void main(String[] args) {
new MyApplication().run(Main.class);
myTestAutowired.test();
}
}
我们在启动类上标记了@MyScan("cn.bobasyu.test")
注解, 其会在cn.bobasyu.test
包对应目录及其子目录进行扫描, 发现类Main
和MyTestInterfaceImpl
标记有@MyCompetent
注解, IoC容器会将其注入到容器中, 键为该类实现的接口, 值为其实例对象。接着对容器中的实例进行检查, 发现Main
的实例中存在属性MyTestAutowired myTestAutowired
标有@MyAutoWired
注解, 所以IoC容器会将内部存有的实现了MyTestAutowired
接口的对象实例注入进去, 所以在调用过程中没有new新对象但不会报空指针异常。
此外, 还用到了一个工具处理类:
/**
* 和文件有关的工具类
*
* @author Bobasyu
*/
public class FileUtil {
public static String replaceBasePath(String basePath) {
if (basePath.charAt(0) == File.separatorChar) {
String str = "";
for (int i = 1; i < basePath.length(); i++) {
str += basePath.charAt(i);
}
basePath = str;
}
if (basePath.charAt(basePath.length() - 1) != File.separatorChar) {
basePath += File.separator;
}
return basePath;
}
/**
* 获取目录下的所有文件名称
*
* @param dir 目标文件或目录
* @param fileNames 文件名列表, 包含文件路径
*/
public static void findFileList(File dir, List<String> fileNames) {
try {
// 判断是否存在目录
if (!dir.exists() || !dir.isDirectory()) {
throw new FileNotFoundException("file path" + dir.getPath() + "not exist.");
}
// 读取目录下的所有目录文件信息
String[] files = dir.list();
// 循环,添加文件名或回调自身
for (int i = 0; i < files.length; i++) {
File file = new File(dir, files[i]);
if (file.isFile()) {
// 如果文件是, 添加文件全路径名
fileNames.add(dir + "\\" + file.getName());
} else {
// 如果是目录, 回调自身继续查询
findFileList(file, fileNames);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
代码链接:
https://github.com/Boba-Syu/Ioc_learning