Spring 源碼學習 1:ApplicationContext
Bean 定義和 Bean 實例
AnnotationConfigApplicationContext
首先,創建一個最簡單的 Spring Boot 應用。
在入口類中接收SpringApplication.run
的返回值:
@SpringBootApplication
public class DemoApplication {public static void main(String[] args) {ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args);System.out.println(context);}}
ConfigurableApplicationContext
是一個接口,查看其繼承關系:
比較重要的父接口有ApplicationContext
和BeanFactory
。
打上斷點,啟動調試模式,可以看到實際運行時使用的真實類型:
查看AnnotationConfigApplicationContext
的繼承關系比較復雜,最值得注意的:
GenericApplicationContext
實現了抽象類AbstractApplicationContext
,可以看做是ApplicationContext
接口的一個通用實現。其包含一個beanFactory
屬性:
private final DefaultListableBeanFactory beanFactory;
DefaultListableBeanFactory
老版本的 Spring 會直接使用DefaultListableBeanFactory
作為容器實現,新版本的 Spring 在外邊又包裝了一層。SpringBean 的注冊、獲取等操作都由DefaultListableBeanFactory
實現,它包含一個屬性beanDefinitionMap
:
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
這個 Map 保存了 Spring Bean 的定義,key 則是 Spring Bean 的名稱。
可以用代碼的方式打印當前項目中已經注冊的 Bean 定義:
ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args);
System.out.println(context);
AnnotationConfigApplicationContext acaContext = (AnnotationConfigApplicationContext) context;
ConfigurableListableBeanFactory beanFactory = acaContext.getBeanFactory();
String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {System.out.println(beanFactory.getBean(beanDefinitionName));
}
可以看到,即使沒有任何自定義的 Bean,默認情況下 Spring 框架也會注冊很多必要的 Bean,甚至包含了AnnotationConfigApplicationContext
容器自己。
如果添加了自定義 Bean,在這里也會看到。
DefaultSingletonBeanRegistry
查看DefaultListableBeanFactory
的繼承關系:
它有一個基類DefaultSingletonBeanRegistry
,這個類使用一個 Map 結構保存所有的單例 Bean:
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
同樣,可以用代碼的方式打印 Bean 對象:
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory)acaContext.getBeanFactory();
DefaultSingletonBeanRegistry registry = beanFactory;
String[] singletonNames = registry.getSingletonNames();
for (String name : singletonNames) {System.out.println(registry.getSingleton(name));
}
Bean 對象的打印結果要多于 Bean 定義,這是顯而易見的,因為同一個 Bean 定義可以生成多個 Bean 對象:
小結
從上面的分析不難看出,AnnotationConfigApplicationContext
的設計采用了代理(委托)模式,它包含一個 DefaultListableBeanFactory
類型的 BeanFactory
,具體的 Bean 定義和 Bean 實例都保存在其中,而AnnotationConfigApplicationContext
本身所有對 Bean 的注冊、獲取等操作都代理給DefaultListableBeanFactory
的對應方法實現。而DefaultListableBeanFactory
和AnnotationConfigApplicationContext
都實現了基本的BeanFactory
接口,這也正是代理模式的基礎。
其它功能
上面介紹了 Bean 工廠的核心功能——維護 Bean 定義和 Bean 實例。事實上 ApplicationContext
除了繼承 BeanFactory 的相關接口,還繼承了其它的幾個接口:
EnvironmentCapable
可以利用這個接口獲取環境信息,比如系統環境變量和項目的配置信息:
private static void printEnvironment(EnvironmentCapable environmentCapable) {Environment environment = environmentCapable.getEnvironment();String property = environment.getProperty("spring.application.name");System.out.println(property);String javaHome = environment.getProperty("java_home");System.out.println(javaHome);
}
注意,在獲取系統環境變量時,是大小寫不敏感的,比如這里的 getProperty("java_home")
,實際上系統中配置的環境變量是大寫的JAVA_HOME
,但這里依然可以獲取到。
ApplicationEventPublisher
是 Spring 事件框架的一部分,可以用它來發布事件:
private static void testApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {applicationEventPublisher.publishEvent(new UserUpdatedEvent(applicationEventPublisher, 1L));
}
UserUpdatedEvent
是用戶自定義事件,代表用戶數據被更新,提示事件處理程序進行相應處理(比如更新用戶緩存):
public class UserUpdatedEvent extends ApplicationEvent {@Getterprivate final Long userId;public UserUpdatedEvent(Object source, Long userId) {super(source);this.userId = userId;}
}
要處理這個事件,需要添加事件監聽:
@Component
public class UserListener {@EventListenerpublic void userUpdatedEventHandler(UserUpdatedEvent userUpdatedEvent){Long userId = userUpdatedEvent.getUserId();System.out.println("User updated: " + userId);}
}
resourcePatternResolver
可以用這個接口獲取項目的資源文件:
private static void printResource(ResourcePatternResolver resourcePatternResolver) throws IOException {Resource resource = resourcePatternResolver.getResource("classpath:application.properties");BufferedReader reader = FileUtil.getReader(resource.getFile(), StandardCharsets.UTF_8);do{String line = reader.readLine();System.out.println(line);}while(reader.ready());
}
這里使用了
hutool
依賴。
classpath:xxx
只會返回在 classpath 下檢索到的第一個匹配的資源文件。如果要檢索所有的 classpath 路徑中匹配到的資源文件(比如包含其它 jar 包中的資源文件),需要使用classpath*:xxx
:
private static void printResources(ResourcePatternResolver resourcePatternResolver) throws IOException {Resource[] resources = resourcePatternResolver.getResources("classpath*:META-INF/spring.factories");for(Resource resource : resources){System.out.println(resource.getFilename());}
}
打印當前項目使用的 classpath:
private static void printClassPaths(){String classpath = System.getProperty("java.class.path");String[] classpathArr = classpath.split(";");for (String classpathStr : classpathArr) {System.out.println(classpathStr);}
}
可以看到結果中包含了通過 Maven 導入的 jar 包。
MessageSource
MessageSource 可以實現國際化。
添加國際化相關資源文件:
messages_en_US.properties
:
login.title=User Login
login.username=Username
messages_zh_CN.properties
:
login.title=用戶登錄
login.username=用戶名
在配置文件application.properties
中添加配置信息:
spring.messages.basename=messages/messages
spring.messages.encoding=UTF-8
使用 MessageSource 按照語言地域輸出信息:
private static void testMessageResource(MessageSource messageSource) {String enTitle = messageSource.getMessage("login.title", null, Locale.US);String enUserName = messageSource.getMessage("login.username", null, Locale.US);String cnTitle = messageSource.getMessage("login.title", null, Locale.CHINA);String cnUserName = messageSource.getMessage("login.username", null, Locale.CHINA);System.out.println(String.format("%s %s", enTitle, enUserName));System.out.println(String.format("%s %s", cnTitle, cnUserName));
}
本文的完整示例代碼可以從這里獲取。
The End.
參考資料
- 黑馬程序員Spring視頻教程,深度講解spring5底層原理