目錄 一、前言
二、目標
三、設計
四、實現(xiàn)
1. 工程結構
2. 資源加載接口定義和實現(xiàn)
3. 包裝資源加載器
4. Bean定義讀取接口
5. Bean定義抽象類實現(xiàn)
6. 解析XML處理Bean注冊
五、測試
1. 事先準備
2. 配置文件
3. 單元測試(資源加載)
4. 單元測試(配置文件注冊Bean)
六、總結
七、系列推薦
一、前言 你寫的代碼,能接的住產品加需求嗎?
接,是能接的,接幾次也行,哪怕就一個類一片的 if...else 也可以!但接完成什么樣可就不一定了,會不會出事故也不是能控制住的。
那出事故時,你說因為我寫 if...else 多了導致代碼爛了,但可是你先動的手?。?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(30, 107, 184);background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;">你說的需求還得加、你說的老板讓上線、你說的合同都簽了,搬磚碼農的我沒辦法,才以堆代碼平需求,需求太多不好搞,我才以搬磚平需求!諸侯不服,我才以兵服諸侯,你不服,我就打到你服!
但代碼爛了有時候并不是因為需求加的快、也不是著急上線。因為往往在承接產品需求的前幾次,一個功能邏輯的設計并不會太復雜,也不會有多急迫,甚至會留出讓你做設計、做評審、做開發(fā)的時間,如果這個時候仍不能把以后可能會發(fā)生的事情評估到需求里,那么導致代碼的混亂從一開始就已經埋下了,以后只能越來越亂!
承接需求并能把它做好,這來自于對需求的理解,產品場景開發(fā)的經驗以及對代碼實踐落地的把控能力等綜合多方面因素的結果。就像你現(xiàn)在做的開發(fā)中,你的代碼有哪些是經常變化的,有哪些是固定通用的,有哪些是負責邏輯拼裝的、有哪些是來做核心實現(xiàn)的。那么現(xiàn)在如果你的核心共用層做了頻繁變化的業(yè)務層包裝,那么肯定的說,你的代碼即將越來越亂,甚至可能埋下事故的風險!
在我們實現(xiàn)的 Spring 框架中,每一個章節(jié)都會結合上一章節(jié)繼續(xù)擴展功能,就像每一次產品都在加需求一樣,那么在學習的過程中可以承上啟下的對照和參考,看看每一個模塊的添加都是用什么邏輯和技術細節(jié)實現(xiàn)的。這些內容的學習,會非常有利于你以后在設計和實現(xiàn),自己承接產品需求時做的具體開發(fā),代碼的質量也會越來越高,越來越有擴展性和可維護性。
二、目標 在完成 Spring 的框架雛形后,現(xiàn)在我們可以通過單元測試進行手動操作 Bean 對象的定義、注冊和屬性填充,以及最終獲取對象調用方法。但這里會有一個問題,就是如果實際使用這個 Spring 框架,是不太可能讓用戶通過手動方式創(chuàng)建的,而是最好能通過配置文件的方式簡化創(chuàng)建過程。需要完成如下操作:
如圖中我們需要把步驟:2、3、4整合到Spring框架中,通過 Spring 配置文件的方式將 Bean 對象實例化。 接下來我們就需要在現(xiàn)有的 Spring 框架中,添加能解決 Spring 配置的讀取、解析、注冊Bean的操作。 三、設計 依照本章節(jié)的需求背景,我們需要在現(xiàn)有的 Spring 框架雛形中添加一個資源解析器,也就是能讀取classpath、本地文件和云文件的配置內容。這些配置內容就是像使用 Spring 時配置的 Spring.xml 一樣,里面會包括 Bean 對象的描述和屬性信息。在讀取配置文件信息后,接下來就是對配置文件中的 Bean 描述信息解析后進行注冊操作,把 Bean 對象注冊到 Spring 容器中。整體設計結構如下圖:
資源加載器屬于相對獨立的部分,它位于 Spring 框架核心包下的IO實現(xiàn)內容,主要用于處理Class、本地和云環(huán)境中的文件信息。 當資源可以加載后,接下來就是解析和注冊 Bean 到 Spring 中的操作,這部分實現(xiàn)需要和 DefaultListableBeanFactory 核心類結合起來,因為你所有的解析后的注冊動作,都會把 Bean 定義信息放入到這個類中。 那么在實現(xiàn)的時候就設計好接口的實現(xiàn)層級關系,包括我們需要定義出 Bean 定義的讀取接口 BeanDefinitionReader 以及做好對應的實現(xiàn)類,在實現(xiàn)類中完成對 Bean 對象的解析和注冊。 四、實現(xiàn) 1. 工程結構small-spring-step-05 └── src ├── main │ └── java │ └── cn.bugstack.springframework │ ├── beans │ │ ├── factory │ │ │ ├── factory │ │ │ │ ├── AutowireCapableBeanFactory.java │ │ │ │ ├── BeanDefinition.java │ │ │ │ ├── BeanReference.java │ │ │ │ ├── ConfigurableBeanFactory.java │ │ │ │ └── SingletonBeanRegistry.java │ │ │ ├── support │ │ │ │ ├── AbstractAutowireCapableBeanFactory.java │ │ │ │ ├── AbstractBeanDefinitionReader.java │ │ │ │ ├── AbstractBeanFactory.java │ │ │ │ ├── BeanDefinitionReader.java │ │ │ │ ├── BeanDefinitionRegistry.java │ │ │ │ ├── CglibSubclassingInstantiationStrategy.java │ │ │ │ ├── DefaultListableBeanFactory.java │ │ │ │ ├── DefaultSingletonBeanRegistry.java │ │ │ │ ├── InstantiationStrategy.java │ │ │ │ └── SimpleInstantiationStrategy.java │ │ │ ├── support │ │ │ │ └── XmlBeanDefinitionReader.java │ │ │ ├── BeanFactory.java │ │ │ ├── ConfigurableListableBeanFactory.java │ │ │ ├── HierarchicalBeanFactory.java │ │ │ └── ListableBeanFactory.java │ │ ├── BeansException.java │ │ ├── PropertyValue.java │ │ └── PropertyValues.java │ ├── core.io │ │ ├── ClassPathResource.java │ │ ├── DefaultResourceLoader.java │ │ ├── FileSystemResource.java │ │ ├── Resource.java │ │ ├── ResourceLoader.java │ │ └── UrlResource.java │ └── utils │ └── ClassUtils.java └── test └── java └── cn.bugstack.springframework.test ├── bean │ ├── UserDao.java │ └── UserService.java └── ApiTest.java工程源碼 :公眾號「bugstack蟲洞棧」,回復:Spring 專欄,獲取完整源碼
Spring Bean 容器資源加載和使用類關系,如圖 6-3
圖 6-3 本章節(jié)為了能把 Bean 的定義、注冊和初始化交給 Spring.xml 配置化處理,那么就需要實現(xiàn)兩大塊內容,分別是:資源加載器、xml資源處理類,實現(xiàn)過程主要以對接口 Resource、ResourceLoader 的實現(xiàn),而另外 BeanDefinitionReader 接口則是對資源的具體使用,將配置信息注冊到 Spring 容器中去。 在 Resource 的資源加載器的實現(xiàn)中包括了,ClassPath、系統(tǒng)文件、云配置文件,這三部分與 Spring 源碼中的設計和實現(xiàn)保持一致,最終在 DefaultResourceLoader 中做具體的調用。 接口:BeanDefinitionReader、抽象類:AbstractBeanDefinitionReader、實現(xiàn)類:XmlBeanDefinitionReader,這三部分內容主要是合理清晰的處理了資源讀取后的注冊 Bean 容器操作。接口管定義,抽象類處理非接口功能外的注冊Bean組件填充,最終實現(xiàn)類即可只關心具體的業(yè)務實現(xiàn) 另外本章節(jié)還參考 Spring 源碼,做了相應接口的集成和實現(xiàn)的關系,雖然這些接口目前還并沒有太大的作用,但隨著框架的逐步完善,它們也會發(fā)揮作用。如圖 6-4
圖 6-4 BeanFactory,已經存在的 Bean 工廠接口用于獲取 Bean 對象,這次新增加了按照類型獲取 Bean 的方法:<T> T getBean(String name, Class<T> requiredType) ListableBeanFactory,是一個擴展 Bean 工廠接口的接口,新增加了 getBeansOfType、getBeanDefinitionNames() 方法,在 Spring 源碼中還有其他擴展方法。 HierarchicalBeanFactory,在 Spring 源碼中它提供了可以獲取父類 BeanFactory 方法,屬于是一種擴展工廠的層次子接口。Sub-interface implemented by bean factories that can be part of a hierarchy. AutowireCapableBeanFactory,是一個自動化處理Bean工廠配置的接口,目前案例工程中還沒有做相應的實現(xiàn),后續(xù)逐步完善。 ConfigurableBeanFactory,可獲取 BeanPostProcessor、BeanClassLoader等的一個配置化接口。 ConfigurableListableBeanFactory,提供分析和修改Bean以及預先實例化的操作接口,不過目前只有一個 getBeanDefinition 方法。 2. 資源加載接口定義和實現(xiàn)cn.bugstack.springframework.core.io.Resource
public interface Resource { InputStream getInputStream () throws IOException ; }在 Spring 框架下創(chuàng)建 core.io 核心包,在這個包中主要用于處理資源加載流。 定義 Resource 接口,提供獲取 InputStream 流的方法,接下來再分別實現(xiàn)三種不同的流文件操作:classPath、FileSystem、URL ClassPath :cn.bugstack.springframework.core.io.ClassPathResource
public class ClassPathResource implements Resource { private final String path; private ClassLoader classLoader; public ClassPathResource (String path) { this (path, (ClassLoader) null ); } public ClassPathResource (String path, ClassLoader classLoader) { Assert.notNull(path, "Path must not be null" ); this .path = path; this .classLoader = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); } @Override public InputStream getInputStream () throws IOException { InputStream is = classLoader.getResourceAsStream(path); if (is == null ) { throw new FileNotFoundException( this .path + " cannot be opened because it does not exist" ); } return is; } }這一部分的實現(xiàn)是用于通過 ClassLoader 讀取ClassPath 下的文件信息,具體的讀取過程主要是:classLoader.getResourceAsStream(path) FileSystem :cn.bugstack.springframework.core.io.FileSystemResource
public class FileSystemResource implements Resource { private final File file; private final String path; public FileSystemResource (File file) { this .file = file; this .path = file.getPath(); } public FileSystemResource (String path) { this .file = new File(path); this .path = path; } @Override public InputStream getInputStream () throws IOException { return new FileInputStream(this .file); } public final String getPath () { return this .path; } }通過指定文件路徑的方式讀取文件信息,這部分大家肯定還是非常熟悉的,經常會讀取一些txt、excel文件輸出到控制臺。 Url :cn.bugstack.springframework.core.io.UrlResource
public class UrlResource implements Resource { private final URL url; public UrlResource (URL url) { Assert.notNull(url,"URL must not be null" ); this .url = url; } @Override public InputStream getInputStream () throws IOException { URLConnection con = this .url.openConnection(); try { return con.getInputStream(); } catch (IOException ex){ if (con instanceof HttpURLConnection){ ((HttpURLConnection) con).disconnect(); } throw ex; } } }通過 HTTP 的方式讀取云服務的文件,我們也可以把配置文件放到 GitHub 或者 Gitee 上。 3. 包裝資源加載器按照資源加載的不同方式,資源加載器可以把這些方式集中到統(tǒng)一的類服務下進行處理,外部用戶只需要傳遞資源地址即可,簡化使用。
定義接口 :cn.bugstack.springframework.core.io.ResourceLoader
public interface ResourceLoader { /** * Pseudo URL prefix for loading from the class path: "classpath:" */ String CLASSPATH_URL_PREFIX = "classpath:" ; Resource getResource (String location) ; }定義獲取資源接口,里面?zhèn)鬟f location 地址即可。 實現(xiàn)接口 :cn.bugstack.springframework.core.io.DefaultResourceLoader
public class DefaultResourceLoader implements ResourceLoader { @Override public Resource getResource (String location) { Assert.notNull(location, "Location must not be null" ); if (location.startsWith(CLASSPATH_URL_PREFIX)) { return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length())); } else { try { URL url = new URL(location); return new UrlResource(url); } catch (MalformedURLException e) { return new FileSystemResource(location); } } } }在獲取資源的實現(xiàn)中,主要是把三種不同類型的資源處理方式進行了包裝,分為:判斷是否為ClassPath、URL以及文件。 雖然 DefaultResourceLoader 類實現(xiàn)的過程簡單,但這也是設計模式約定的具體結果,像是這里不會讓外部調用放知道過多的細節(jié),而是僅關心具體調用結果即可。 4. Bean定義讀取接口cn.bugstack.springframework.beans.factory.support.BeanDefinitionReader
public interface BeanDefinitionReader { BeanDefinitionRegistry getRegistry () ; ResourceLoader getResourceLoader () ; void loadBeanDefinitions (Resource resource) throws BeansException ; void loadBeanDefinitions (Resource... resources) throws BeansException ; void loadBeanDefinitions (String location) throws BeansException ; }這是一個 Simple interface for bean definition readers. 其實里面無非定義了幾個方法,包括:getRegistry()、getResourceLoader(),以及三個加載Bean定義的方法。 這里需要注意 getRegistry()、getResourceLoader(),都是用于提供給后面三個方法的工具,加載和注冊,這兩個方法的實現(xiàn)會包裝到抽象類中,以免污染具體的接口實現(xiàn)方法。 5. Bean定義抽象類實現(xiàn)cn.bugstack.springframework.beans.factory.support.AbstractBeanDefinitionReader
public abstract class AbstractBeanDefinitionReader implements BeanDefinitionReader { private final BeanDefinitionRegistry registry; private ResourceLoader resourceLoader; protected AbstractBeanDefinitionReader (BeanDefinitionRegistry registry) { this (registry, new DefaultResourceLoader()); } public AbstractBeanDefinitionReader (BeanDefinitionRegistry registry, ResourceLoader resourceLoader) { this .registry = registry; this .resourceLoader = resourceLoader; } @Override public BeanDefinitionRegistry getRegistry () { return registry; } @Override public ResourceLoader getResourceLoader () { return resourceLoader; } }抽象類把 BeanDefinitionReader 接口的前兩個方法全部實現(xiàn)完了,并提供了構造函數(shù),讓外部的調用使用方,把Bean定義注入類,傳遞進來。 這樣在接口 BeanDefinitionReader 的具體實現(xiàn)類中,就可以把解析后的 XML 文件中的 Bean 信息,注冊到 Spring 容器去了。以前我們是通過單元測試使用,調用 BeanDefinitionRegistry 完成Bean的注冊,現(xiàn)在可以放到 XMl 中操作了 6. 解析XML處理Bean注冊cn.bugstack.springframework.beans.factory.xml.XmlBeanDefinitionReader
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader { public XmlBeanDefinitionReader (BeanDefinitionRegistry registry) { super (registry); } public XmlBeanDefinitionReader (BeanDefinitionRegistry registry, ResourceLoader resourceLoader) { super (registry, resourceLoader); } @Override public void loadBeanDefinitions (Resource resource) throws BeansException { try { try (InputStream inputStream = resource.getInputStream()) { doLoadBeanDefinitions(inputStream); } } catch (IOException | ClassNotFoundException e) { throw new BeansException("IOException parsing XML document from " + resource, e); } } @Override public void loadBeanDefinitions (Resource... resources) throws BeansException { for (Resource resource : resources) { loadBeanDefinitions(resource); } } @Override public void loadBeanDefinitions (String location) throws BeansException { ResourceLoader resourceLoader = getResourceLoader(); Resource resource = resourceLoader.getResource(location); loadBeanDefinitions(resource); } protected void doLoadBeanDefinitions (InputStream inputStream) throws ClassNotFoundException { Document doc = XmlUtil.readXML(inputStream); Element root = doc.getDocumentElement(); NodeList childNodes = root.getChildNodes(); for (int i = 0 ; i < childNodes.getLength(); i++) { // 判斷元素 if (!(childNodes.item(i) instanceof Element)) continue ; // 判斷對象 if (!"bean" .equals(childNodes.item(i).getNodeName())) continue ; // 解析標簽 Element bean = (Element) childNodes.item(i); String id = bean.getAttribute("id" ); String name = bean.getAttribute("name" ); String className = bean.getAttribute("class" ); // 獲取 Class,方便獲取類中的名稱 Class<?> clazz = Class.forName(className); // 優(yōu)先級 id > name String beanName = StrUtil.isNotEmpty(id) ? id : name; if (StrUtil.isEmpty(beanName)) { beanName = StrUtil.lowerFirst(clazz.getSimpleName()); } // 定義Bean BeanDefinition beanDefinition = new BeanDefinition(clazz); // 讀取屬性并填充 for (int j = 0 ; j < bean.getChildNodes().getLength(); j++) { if (!(bean.getChildNodes().item(j) instanceof Element)) continue ; if (!"property" .equals(bean.getChildNodes().item(j).getNodeName())) continue ; // 解析標簽:property Element property = (Element) bean.getChildNodes().item(j); String attrName = property.getAttribute("name" ); String attrValue = property.getAttribute("value" ); String attrRef = property.getAttribute("ref" ); // 獲取屬性值:引入對象、值對象 Object value = StrUtil.isNotEmpty(attrRef) ? new BeanReference(attrRef) : attrValue; // 創(chuàng)建屬性信息 PropertyValue propertyValue = new PropertyValue(attrName, value); beanDefinition.getPropertyValues().addPropertyValue(propertyValue); } if (getRegistry().containsBeanDefinition(beanName)) { throw new BeansException("Duplicate beanName[" + beanName + "] is not allowed" ); } // 注冊 BeanDefinition getRegistry().registerBeanDefinition(beanName, beanDefinition); } } }XmlBeanDefinitionReader 類最核心的內容就是對 XML 文件的解析,把我們本來在代碼中的操作放到了通過解析 XML 自動注冊的方式。
loadBeanDefinitions 方法,處理資源加載,這里新增加了一個內部方法:doLoadBeanDefinitions,它主要負責解析 xml 在 doLoadBeanDefinitions 方法中,主要是對xml的讀取 XmlUtil.readXML(inputStream) 和元素 Element 解析。在解析的過程中通過循環(huán)操作,以此獲取 Bean 配置以及配置中的 id、name、class、value、ref 信息。 最終把讀取出來的配置信息,創(chuàng)建成 BeanDefinition 以及 PropertyValue,最終把完整的 Bean 定義內容注冊到 Bean 容器:getRegistry().registerBeanDefinition(beanName, beanDefinition) 五、測試 1. 事先準備cn.bugstack.springframework.test.bean.UserDao
public class UserDao { private static Map<String, String> hashMap = new HashMap<>(); static { hashMap.put("10001" , "小傅哥" ); hashMap.put("10002" , "八杯水" ); hashMap.put("10003" , "阿毛" ); } public String queryUserName (String uId) { return hashMap.get(uId); } }cn.bugstack.springframework.test.bean.UserService
public class UserService { private String uId; private UserDao userDao; public void queryUserInfo () { return userDao.queryUserName(uId); } // ...get/set }Dao、Service,是我們平常開發(fā)經常使用的場景。在 UserService 中注入 UserDao,這樣就能體現(xiàn)出Bean屬性的依賴了。 2. 配置文件important.properties
# Config File system.key=OLpj9823dZspring.xml
<?xml version="1.0" encoding="UTF-8"?> <beans > <bean id ="userDao" class ="cn.bugstack.springframework.test.bean.UserDao" /> <bean id ="userService" class ="cn.bugstack.springframework.test.bean.UserService" > <property name ="uId" value ="10001" /> <property name ="userDao" ref ="userDao" /> </bean > </beans > 這里有兩份配置文件,一份用于測試資源加載器,另外 spring.xml 用于測試整體的 Bean 注冊功能。 3. 單元測試(資源加載)案例
private DefaultResourceLoader resourceLoader; @Before public void init () { resourceLoader = new DefaultResourceLoader(); } @Test public void test_classpath () throws IOException { Resource resource = resourceLoader.getResource("classpath:important.properties" ); InputStream inputStream = resource.getInputStream(); String content = IoUtil.readUtf8(inputStream); System.out.println(content); } @Test public void test_file () throws IOException { Resource resource = resourceLoader.getResource("src/test/resources/important.properties" ); InputStream inputStream = resource.getInputStream(); String content = IoUtil.readUtf8(inputStream); System.out.println(content); } @Test public void test_url () throws IOException { Resource resource = resourceLoader.getResource("https://github.com/fuzhengwei/small-spring/important.properties" InputStream inputStream = resource.getInputStream(); String content = IoUtil.readUtf8(inputStream); System.out.println(content); }測試結果
# Config File system.key=OLpj9823dZ Process finished with exit code 0 這三個方法:test_classpath、test_file、test_url,分別用于測試加載 ClassPath、FileSystem、Url 文件,URL文件在Github,可能加載時會慢 4. 單元測試(配置文件注冊Bean)案例
@Test public void test_xml () { // 1.初始化 BeanFactory DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); // 2. 讀取配置文件&注冊Bean XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); reader.loadBeanDefinitions("classpath:spring.xml" ); // 3. 獲取Bean對象調用方法 UserService userService = beanFactory.getBean("userService" , UserService.class ) ; String result = userService.queryUserInfo(); System.out.println("測試結果:" + result); }測試結果
測試結果:小傅哥 Process finished with exit code 0 在上面的測試案例中可以看到,我們把以前通過手動注冊 Bean 以及配置屬性信息的內容,交給了 new XmlBeanDefinitionReader(beanFactory) 類讀取 Spring.xml 的方式來處理,并通過了測試驗證。 六、總結 此時的工程結構已經越來越有 Spring 框架的味道了,以配置文件為入口解析和注冊 Bean 信息,最終再通過 Bean 工廠獲取 Bean 以及做相應的調用操作。 關于案例中每一個步驟的實現(xiàn)小傅哥這里都會盡可能參照 Spring 源碼的接口定義、抽象類實現(xiàn)、名稱規(guī)范、代碼結構等,做相應的簡化處理。這樣大家在學習的過程中也可以通過類名或者接口和整個結構體學習 Spring 源碼,這樣學習起來就容易多了。 看完絕對不等于會,你只有動起手來從一個小小的工程框架結構,敲到現(xiàn)在以及以后不斷的變大、變多、變強時,才能真的掌握這里面的知識。另外每一個章節(jié)的功能實現(xiàn)都會涉及到很多的代碼設計思路,要認真去領悟。當然實踐起來是最好的領悟方式! 七、系列推薦