Spring IOC好神奇?我教你實現一個

目錄

前言

在閱讀本文之前,你必須:

  • 掌握Java語法
  • 掌握Java反射的用法
  • 掌握IOC的概念,並用過SpringIOC功能

預期達到的效果:

  • 能正確加載配置文件來確定掃描包的範圍
  • 能正確識別我們自定義的註解,初始化自定義的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功能,需要劃分爲幾個步驟:

  1. 加載配置文件
  2. 掃描指定包下的.class文件
  3. 初始化IOC容器
  4. 裝配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作爲,通過反射創建的實例作爲putIOC容器。

另外,如果是@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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章