目錄
前言
在閱讀本文之前,你必須:
- 掌握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