目录
前言
在阅读本文之前,你必须:
- 掌握Java语法
- 掌握Java反射的用法
- 掌握IOC的概念,并用过Spring的IOC功能
预期达到的效果:
- 能正确加载配置文件来确定扫描包的范围
- 能正确识别我们自定义的注解,初始化自定义的IOC容器
- 能正确装配Bean
全部代码皆可戳本github仓库链接找到。
一、定义注解
我们首先定义4个注解,分别是@Repository、@Service、@Autowired和@Qualifier
,熟悉Spring的同学肯定也对这些注解很熟悉。
@Repository和@Service
都是用来将类标识为Bean,功能上区别不大,只是方便更好区分不同的类各自的功能,分别对应存储层Bean和业务层Bean@Autowired
根据对象的类名来查找IOC容器中的Bean,并装配@Qualifier
根据指定的BeanName
来查找IOC容器中的Bean,并装配
@Target(value = ElementType.TYPE) // 该注解只能用在类上
@Retention(value = RetentionPolicy.RUNTIME) // 运行时加载注解
public @interface Repository {
String name() default ""; // 配置BeanName
}
@Target(value = ElementType.TYPE) // 该注解只能用在类上
@Retention(value = RetentionPolicy.RUNTIME) // 运行时加载注解
public @interface Service {
String name() default ""; // 配置BeanName
}
@Target(value = ElementType.FIELD) // 该注解只能用在成员变量上
@Retention(value = RetentionPolicy.RUNTIME) // 运行时加载注解
public @interface Autowired {
}
@Target(value = ElementType.FIELD) // 该注解只能用在成员变量上
@Retention(value = RetentionPolicy.RUNTIME) // 运行时加载注解
public @interface Qualifier {
String value() default ""; // 配置BeanName
}
二、实现IOC功能
写具体代码之前,首先分析一下实现一个IOC功能,需要划分为几个步骤:
- 加载配置文件
- 扫描指定包下的.class文件
- 初始化IOC容器
- 装配Bean
将步骤划分清楚后,我们可以先把对应的函数创建出来,并在构造函数里调用:
public class AnnotationBeanFactory{
private Properties properties; // 配置信息
private Map<String, Object> ioc = new HashMap<String, Object>(); // IOC容器
private List<String> classNames = new ArrayList<String>(); // 扫描出的.class文件
private String basePackage; // 指定扫描的包
public AnnotationBeanFactory(String configPath) {
loadConfig(configPath);
scanner(basePackage);
initIoC();
inject();
}
private void inject(){
// 装配Bean
}
private void initIoC(){
// 初始化IOC容器
}
private void scanner(String basePackage){
// 扫描指定包下的.class文件
}
private void loadConfig(String configPath){
// 加载配置文件
}
}
1.加载配置文件
按照Spring的模式,我们会将配置信息写在XML里,但是这里我们一切从简,将配置信息存放位置设置在properties文件中,且只读取ScannerPackage
这个熟属性。读取配置文件信息的方法如下:
private void loadConfig(String configPath) {
properties = new Properties();
InputStream is = getClass().getClassLoader().getResourceAsStream(configPath); // 根据路径寻找properties文件
try {
properties.load(is); // 加载properties
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close(); // 关闭输入流
} catch (IOException e) {
e.printStackTrace();
}
}
}
basePackage = properties.getProperty("ScanPackage"); // 读取配置文件中指定扫描的package
}
扫描包下的.Class文件
接下来就要根据之前读取的properties文件中的ScanPackage属性,定位到需要扫描的目录位置。主要是通过File
对象,使用递归
来遍历整个目录。
private void scanner(String basePackage) throws FileNotFoundException {
String path = "/" + basePackage.replaceAll("\\.", "/");
URL url = getClass().getResource(path);
// 用户配置的需要扫描的包有误,抛出异常
if (url == null) throw new FileNotFoundException("package " + path + " not exists");
File dir = new File(url.getFile());
for (File file : dir.listFiles()) {
if (file.isDirectory()) { // 如果当前File是文件夹,则递归遍历
scanner(basePackage + "." + file.getName());
}
if (!file.getName().endsWith(".class")) { // 当前File不是.class文件,则跳过不管
continue;
}
// 将读取到的.class文件的全限定名放入List中
classNames.add(basePackage + "." + file.getName().replace(".class", ""));
}
}
初始化IOC容器
IOC容器其实没有那么神秘,本质上是一个HashMap
,以BeanName为键,以Object为值。
首先遍历上一步扫描到的.class
文件,通过反射
检查当前遍历到的class
是否有@Repository
或者@Service
注解,没有的就跳过不管,有就进入下一步操作。
接下来要确定BeanName值,如果用户指定了具体的BeanName,则以用户为准,否则使用类的名称作为BeanName,首字母小写,并将BeanName作为键
,通过反射
创建的实例作为值
,put
进IOC容器。
另外,如果是@service
注解,且用户没有特别指定BeanName的值,那么我们就用反射
寻找这个类实现的接口
,并遍历,把所有接口的名称作为BeanName。
具体实现代码如下:
private void initIoC() throws Exception, ClassNotFoundException, IllegalAccessException, InstantiationException {
for (String className : classNames) { // 遍历扫描到的.class文件的全限定名
Class clazz = Class.forName(className); // 通过反射加载class
if (clazz.isAnnotationPresent(Repository.class)) { // 如果class被@Repository标记
Repository repository = (Repository) clazz.getAnnotation(Repository.class);// 加载注解的内容
String beanName = repository.name();
if ("".equals(beanName)) { // 没有特别指定BeanName
beanName = lowerFirst(clazz.getSimpleName().replace(".class", ""));
}
if (ioc.containsKey(beanName)) { // 已有重复的BeanName,抛出异常
throw new Exception("bean " + beanName + " already exists");
}
ioc.put(beanName, clazz.newInstance()); // 放进IOC容器
} else if (clazz.isAnnotationPresent(Service.class)) { // 如果class被@Service标记
Service service = (Service) clazz.getAnnotation(Service.class);
Class<?>[] interfaces = service.getClass().getInterfaces();// 获得该class实现的所有接口
for (Class<?> i : interfaces) {
String beanName = service.name();
if ("".equals(beanName)) { // 没有特别指定BeanName
beanName = lowerFirst(i.getSimpleName().replace(".class", ""));
}
if (ioc.containsKey(beanName)) { // 已有重复的BeanName,抛出异常
throw new Exception("bean " + beanName + " already exists");
}
ioc.put(beanName, clazz.newInstance()); // 放进IOC容器
}
}
}
}
/**
* 将类名首字符变为小写
*/
private String lowerFirst(String str) {
char[] chars = str.toCharArray();
chars[0] += 32;
return new String(chars);
}
装配bean
容器初始化完毕后,则需要将bean装配到用户用@Autowired
或者@Qualifier
标记的成员变量上。
我们通过遍历IOC容器中的所有实例,获得实例中的每一个成员变量,如果某变量被@Autowired
或者@Qualifier
标记,那么我们就通过BeanName作为键值
,去IOC容器get
相应的实例,并通过反射
注入给对应成员变量。
private void inject() throws IllegalAccessException, Exception {
for (Map.Entry<String, Object> entry : ioc.entrySet()) { // 遍历IOC容器
Class clazz = entry.getValue().getClass(); // 反射获得class
if (!clazz.isAnnotationPresent(Repository.class) && !clazz.isAnnotationPresent(Service.class)) {
continue; // 如果没有被`@Autowired`或者`@Qualifier`标记,跳过不管
}
Field[] fields = clazz.getDeclaredFields();// 通过反射获得所有成员变量
for (Field field : fields) { // 遍历成员变量
field.setAccessible(true); // 重要!如果没有这一段代码,则不能向private变量注入
if (field.isAnnotationPresent(Autowired.class)) { // 如果是`@Autowired`,则通过类名注入
String beanName = lowerFirst(field.getType().getSimpleName().replace(".class", ""));
if (!containsBean(beanName)) { // IOC容器中找不到相应的Bean,抛出异常
throw new Exception("Bean " + beanName + " not exist");
}
field.set(entry.getValue(), ioc.get(beanName)); // 注入实例
} else if (field.isAnnotationPresent(Qualifier.class)) {
Qualifier qualifier = field.getAnnotation(Qualifier.class);
String beanName = qualifier.value();
if ("".equals(beanName)) { // 用户没有特别指定BeanName,则通过类名注入
beanName = lowerFirst(field.getType().getSimpleName().replace(".class", ""));
}
if (!containsBean(beanName)) { // IOC容器中找不到相应的Bean,抛出异常
throw new Exception("Bean " + beanName + " not exist");
}
field.set(entry.getValue(), ioc.get(beanName));// 注入实例
}
}
}
}
// 判断IOC容器中是否有相应的Bean
public boolean containsBean(String beanName) {
return ioc.containsKey(beanName);
}
三、测试功能
将代码通过IDE打包成jar
之后,新建一个工程,引入该jar
,编写如下测试代码:
配置文件:
ScanPackage=org.dylan.application
测试类:
package org.dylan.application;
import org.dylan.application.serviceI.ServiceI;
import org.dylan.springframework.factory.AnnotationBeanFactory;
import org.dylan.springframework.factory.BeanFactory;
public class Main {
public static void main(String[] args) {
BeanFactory factory = new AnnotationBeanFactory("properties.properties");
ServiceI serviceI = (ServiceI) factory.getBean("service");
System.out.println(serviceI.query());
}
}
serviceI接口:
package org.dylan.application.serviceI;
public interface ServiceI {
String query();
}
service实现类:
package org.dylan.application.service;
import org.dylan.application.dao.Dao;
import org.dylan.application.serviceI.ServiceI;
import org.dylan.springframework.annotation.Autowired;
@org.dylan.springframework.annotation.Service
public class Service implements ServiceI {
@Autowired
private Dao dao; // 自动装配
public String query() {
return dao.query();
}
}
DAO类:
package org.dylan.application.dao;
import org.dylan.springframework.annotation.Repository;
@Repository
public class Dao {
public String query() {
return "注入成功";
}
}
运行结果:
注入成功
Process finished with exit code 0