前言
在这一篇文章中,我们将会手动实现一个和spring包扫描类似的功能,这里先说明一下这篇文章中要使用到的一些核心技术,主要用到了注解、I/O、反射,不说废话了,直接开始吧
总体设计思路
我们会参照spring的实现思路,实现一个类似的,总体设计图如下
具体功能实现
实现自定义注解
首先新建一个项目,然后创建一个包,存放自定义的一些注解
自定义注解都会放到myAnnotation下面,下面开始写5个自定义注解,由于它们功能基本都一样,所以仅对@MyRepository注解进行详细的说明
首先先创建该注解类
public @interface MyRepository {
}
设置@Target注解,表示修饰到哪些元素上。设置@Retention注解,表示注解保留时间
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
创建一个value属性,用于保存用户自定义bean的名称
String value() default "";
最终该注解全部内容如下
@MyRepository
package com.ttpfx.mySpring.myAnnotation;
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 MyRepository {
String value() default "";
}
另外四个注解类似的,换个名称即可,如下
@MyService
package com.ttpfx.mySpring.myAnnotation;
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 MyService {
String value() default "";
}
@MyController
package com.ttpfx.mySpring.myAnnotation;
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 MyController {
String value() default "";
}
@MyComponent
package com.ttpfx.mySpring.myAnnotation;
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 MyComponent {
String value() default "";
}
@MyComponentScan
package com.ttpfx.mySpring.myAnnotation;
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 MyComponentScan {
String path() default "";
}
实现配置类
创建一个config包,然后在里面创建MySpringConfig类
加上@MyComponentScan注解,最终代码如下
package com.ttpfx.mySpring.config;
import com.ttpfx.mySpring.myAnnotation.MyComponentScan;
@MyComponentScan
public class MySpringConfig {
}
实现MySpring容器
第一步先创建MySpringApplicationContext类
设置3个成员属性
//要传入的配置类
private Class<?> configClass;
//单例对象池
private final ConcurrentHashMap<String, Object> singletonObjects;
//存放要实例化的一些自定义注解
private final Class<? extends Annotation>[] classes;
然后在代码块中进行初始化
{
//初始化
singletonObjects = new ConcurrentHashMap<>();
classes = new Class[]{MyRepository.class, MyService.class,
MyController.class, MyComponent.class};
}
下一步就是创建构造器,在创建容器时要传入一个配置类
public MySpringApplicationContext(Class<?> configClass) {
this.configClass = configClass;
}
创建一个方法,在这个方法中会根据配置类来完成初始化工作,也就是扫描包,我就创建了一个init方法
public void init(){
}
在这个方法中,首先通过配置类拿到@MyConponentScan注解,然后通过该注解获取到要进行包扫描的路径
//得到配置类上的MyComponentScan注解
MyComponentScan annotation = configClass.getAnnotation(MyComponentScan.class);
//获取包扫描路径
String path = annotation.path();
由于配置路径的形式是以.进行分割的,我们要将其变为/,还要注意路径中的中文问题
//处理中文
path = URLDecoder.decode(path,"UTF-8");
//用/替换.
String newPath = path.replace(".", "/");
这个路径只是相对于该项目的,我们通过配置类获取到ClassLoader,然后获取到实际的路径,并进行拼接,获取到最终路径
//得到配置类的ClassLoader
ClassLoader classLoader = configClass.getClassLoader();
//得到最终的绝对扫描路径
String fullPath = classLoader.getResource(newPath).getPath();
//处理中文
fullPath = URLDecoder.decode(fullPath,"UTF-8");
然后创建文件,遍历该目录下的所有文件,对于子目录,这里不进行处理
//创建文件
File rootFile = new File(fullPath);
//判断是不是一个目录
if (rootFile.isDirectory()) {
//得到该目录下的所以文件
File[] files = rootFile.listFiles();
if (files == null)return;
//遍历所以文件
for (File file : files) {
}
}
判断该文件是不是一个java文件
//得到文件名
String fileName = file.getName();
//判断该文件是不是java文件
if (fileName.endsWith(".class")) {
}
得到类的全路径
//得到类名
String className = fileName.substring(0, fileName.lastIndexOf(".class"));
//得到类的全路径
String fullClassName = path + "." + className;
得到类的加载器
//得到类的加载器
Class<?> loadClass = classLoader.loadClass(fullClassName);
判断该类是否存在我们自定义的那几个注解
Class<? extends Annotation> legalAnnotation = null;
//对所有我们自己定义的合法注解进行遍历
for (Class<? extends Annotation> aClass : classes) {
//判断该类是否存在我们自定义的合法注解
if (loadClass.isAnnotationPresent(aClass)){
legalAnnotation = aClass;
break;
}
}
if (legalAnnotation == null)return;
下一步就是设置一个id变量,表示用户给bean设置的名字
//id表示用户给bean设置的名称
String id = "";
然后判断该注解是那一个注解,然后获取注解的value
//判断该注解是哪一个注解
if (legalAnnotation == MyRepository.class) {
//获取该注解
MyRepository loadClassAnnotation = loadClass.getAnnotation(MyRepository.class);
//获取注解的值
id = loadClassAnnotation.value();
} else if (legalAnnotation == MyService.class) {
MyService loadClassAnnotation = loadClass.getAnnotation(MyService.class);
id = loadClassAnnotation.value();
} else if (legalAnnotation == MyController.class) {
MyController loadClassAnnotation = loadClass.getAnnotation(MyController.class);
id = loadClassAnnotation.value();
} else if (legalAnnotation == MyComponent.class) {
MyComponent loadClassAnnotation = loadClass.getAnnotation(MyComponent.class);
id = loadClassAnnotation.value();
}
如果用户没有设置bean的名称,那么就默认该类目首字母小写作为名称
//如果id为空,那么就默认该类目首字母小写作为名称
if ("".equals(id)){
id = className.substring(0,1).toLowerCase()+className.substring(1);
}
下一步就是实例化该对象
//得到该类的实例化对象
Object o = Class.forName(fullClassName).newInstance();
然后将该对象放入单例对象池
//将对象放入单例对象池
singletonObjects.put(id, o);
最后,我们在构造器方法中调用init方法,然后我们的初始化容器就结束了,init方法的完整代码如下
public void init() throws UnsupportedEncodingException, ClassNotFoundException, InstantiationException, IllegalAccessException {
//得到配置类上的MyComponentScan注解
MyComponentScan annotation = configClass.getAnnotation(MyComponentScan.class);
//获取包扫描路径
String path = annotation.path();
//处理中文
path = URLDecoder.decode(path, "UTF-8");
//用/替换.
String newPath = path.replace(".", "/");
//得到配置类的ClassLoader
ClassLoader classLoader = configClass.getClassLoader();
//得到最终的绝对扫描路径
String fullPath = classLoader.getResource(newPath).getPath();
//处理中文
fullPath = URLDecoder.decode(fullPath, "UTF-8");
//创建文件
File rootFile = new File(fullPath);
//判断是不是一个目录
if (rootFile.isDirectory()) {
//得到该目录下的所以文件
File[] files = rootFile.listFiles();
if (files == null) return;
//遍历所以文件
for (File file : files) {
//得到文件名
String fileName = file.getName();
//判断该文件是不是java文件
if (fileName.endsWith(".class")) {
//得到类名
String className = fileName.substring(0, fileName.lastIndexOf(".class"));
//得到类的全路径
String fullClassName = path + "." + className;
//得到类的加载器
Class<?> loadClass = classLoader.loadClass(fullClassName);
Class<? extends Annotation> legalAnnotation = null;
//对所有我们自己定义的合法注解进行遍历
for (Class<? extends Annotation> aClass : classes) {
//判断该类是否存在我们自定义的合法注解
if (loadClass.isAnnotationPresent(aClass)) {
legalAnnotation = aClass;
break;
}
}
if (legalAnnotation == null) return;
//id表示用户给bean设置的名称
String id = "";
//判断该注解是哪一个注解
if (legalAnnotation == MyRepository.class) {
//获取该注解
MyRepository loadClassAnnotation = loadClass.getAnnotation(MyRepository.class);
//获取注解的值
id = loadClassAnnotation.value();
} else if (legalAnnotation == MyService.class) {
MyService loadClassAnnotation = loadClass.getAnnotation(MyService.class);
id = loadClassAnnotation.value();
} else if (legalAnnotation == MyController.class) {
MyController loadClassAnnotation = loadClass.getAnnotation(MyController.class);
id = loadClassAnnotation.value();
} else if (legalAnnotation == MyComponent.class) {
MyComponent loadClassAnnotation = loadClass.getAnnotation(MyComponent.class);
id = loadClassAnnotation.value();
}
//如果id为空,那么就默认该类目首字母小写作为名称
if ("".equals(id)) {
id = className.substring(0, 1).toLowerCase() + className.substring(1);
}
//得到该类的实例化对象
Object o = Class.forName(fullClassName).newInstance();
//将对象放入单例对象池
singletonObjects.put(id, o);
}
}
}
}
提供getBean方法
我们像spring那样提供getBean方法,很简单,就是根据key返回value,下面直接上代码
public Object getBean(String name){
return singletonObjects.get(name);
}
public <T>T getBean(String name,Class<T> aclass){
return (T)singletonObjects.get(name);
}
提供一个getAllObjectName的方法
为了方便测试,我们提供一个显示所有bean名称的方法
public String[] getAllObjectName(){
//存放所有对象的名称
String[] names = new String[singletonObjects.size()];
//设置当前索引
int index = 0;
//获取所有的名称
ConcurrentHashMap.KeySetView<String, Object> keySet = singletonObjects.keySet();
for (String s : keySet) {
//将名称加入到数组中
names[index++] = s;
}
//返回所有对象名称
return names;
}
测试
创建一个use包,里面创建4个类,分别是UserDao,UserService,UserController,UserComponent,每个类加上相应的注解。如下
UserDao
package com.ttpfx.mySpring.use;
import com.ttpfx.mySpring.myAnnotation.MyRepository;
@MyRepository
public class UserDao {
}
UserService
package com.ttpfx.mySpring.use;
import com.ttpfx.mySpring.myAnnotation.MyService;
@MyService
public class UserService {
}
UserController
package com.ttpfx.mySpring.use;
import com.ttpfx.mySpring.myAnnotation.MyController;
@MyController
public class UserController {
}
UserComponent
package com.ttpfx.mySpring.use;
import com.ttpfx.mySpring.myAnnotation.MyController;
@MyController
public class UserController {
}
这时我们的项目结构如下
在MySpringConfig中配置包扫描路径,如下
package com.ttpfx.mySpring.config;
import com.ttpfx.mySpring.myAnnotation.MyComponentScan;
@MyComponentScan(path = "com.ttpfx.mySpring.use")
public class MySpringConfig {
}
创建一个测试包,然后里面创建一个测试类,然后创建我们自己的spring容器
package com.ttpfx.mySpring.test;
import com.ttpfx.mySpring.config.MySpringConfig;
import com.ttpfx.mySpring.spring.MySpringApplicationContext;
public class MySpringTest {
public static void main(String[] args) throws Exception {
MySpringApplicationContext ioc = new MySpringApplicationContext(MySpringConfig.class);
}
}
获取所有bean对象名称
通过前面我们编写的方法获取全部bean的名称,看是否成功将我们的对象装入到容器中
public static void main(String[] args) throws Exception {
MySpringApplicationContext ioc = new MySpringApplicationContext(MySpringConfig.class);
String[] names = ioc.getAllObjectName();
for (String name : names) {
System.out.println(name);
}
}
输入如下
由于我们没有设置bean的名称,所以默认首字母小写作为bean的名称,下面我们将每个bean的名称都设置为my+类名,输出如下
可以发现,我们编写的MySpring容器没有问题
测试getBean方法
我们上面测试了获取所有bean名称的方法,现在测试获取bean的方法,由于我们的bean名称都设置为了my+类名的形式,所以我们就按照设置的来获取名称,代码如下
public static void main(String[] args) throws Exception {
MySpringApplicationContext ioc = new MySpringApplicationContext(MySpringConfig.class);
UserDao userDao = ioc.getBean("myUserDao", UserDao.class);
UserService myUserService = ioc.getBean("myUserService", UserService.class);
System.out.println("userDao:"+userDao);
System.out.println("userService:"+myUserService);
}
控制台输出如下
可以发现,我们的getBean也没有问题,到此,我们的MySpring就测试完毕了
总结
在这篇文章中,我们自己手动实现了spring基于包扫描创建bean的机制,虽然不是很完善,但是核心功能都已经实现了,相信在自己手动实现之后,对spring又有了更深的认识。这里只是简单实现了下,在后面介绍完AOP之后,将会手动实现一个相对完善的spring,解开spring的神秘面纱,深入理解spring