
作者:小傅哥
博客:https://bugstack.cn
沉淀、分享、成長,讓自己和他人都能有所識訓!😄
一、前言
忒復雜,沒等搞明白大促都過去了!
你經歷過618和雙11嗎?你加入過大促時候那么多復雜的營銷活動賺幾毛錢嗎?你開發過連讀明白玩法都需要一周但只使用3天的大促需求嗎?有時候對于有些產品的需求真的是太復雜了,復雜到開發、測驗都需要在整個程序中不斷的學習最后才可能讀懂產品為啥這樣的玩,要是一個長期的活動可能也就算了,培養用戶心智嗎!但這一整套拉新、助力、激活、下單、投保、領券、消費、開紅包等等一連串的騷操作下來,如果在線上只用3天呢,或者是只用1天,那TM連參與的用戶都沒弄明白呢,活動就結束了,最后能打來什么樣好的資料呢?對于這樣流程復雜,估計連羊毛當都看不上!!!
以上只是舉個例子,大部分時候并不會搞的這么惡心,評審也是過不去的!而同樣的道理用在程式設計開發和使用中也是一樣的,如果你把你的代碼邏輯實作的過于分散,讓外部呼叫方在使用的時候,需要呼叫你的介面多個和多次,還沒有訊息觸達,只能定時自己輪訓你的介面查看訂單狀態,每次還只能查10條,查多了你說不行,等等反人類的設計,都會給呼叫方帶來要干你的體會,
所以,如果我們能在完成目的的情況下,都是希望盡可能流程簡單、模式清晰、自動服務,那這在Spring的框架中也是有所體現的,這個框架的普及使用程度和它所能帶來的方便性是分不開的,而我們如果能做到如此的方便,那肯定是一種好的設計和實作,
二、目標
其實到本章節我們已經把關于 IOC 和 AOP 全部核心內容都已經實作完成了,只不過在使用上還有點像早期的 Spring 版本,需要一個一個在 spring.xml 中進行配置,這與實際的目前使用的 Spring 框架還是有蠻大的差別,而這種差別其實都是在核心功能邏輯之上建設的在更少的配置下,做到更簡化的使用,
這其中就包括:包的掃描注冊、注解配置的使用、占位符屬性的填充等等,而我們的目標就是在目前的核心邏輯上填充一些自動化的功能,讓大家可以學習到這部分的設計和實作,從中體會到一些關于代碼邏輯的實作程序,總結一些編碼經驗,
三、方案
首先我們要考慮🤔,為了可以簡化 Bean 物件的配置,讓整個 Bean 物件的注冊都是自動掃描的,那么基本需要的元素包括:掃描路徑入口、XML決議掃描資訊、給需要掃描的Bean物件做注解標記、掃描Class物件摘取Bean注冊的基本資訊,組裝注冊資訊、注冊成Bean物件,那么在這些條件元素的支撐下,就可以實作出通過自定義注解和配置掃描路徑的情況下,完成 Bean 物件的注冊,除此之外再順帶解決一個配置中占位符屬性的知識點,比如可以通過 ${token} 給 Bean 物件注入進去屬性資訊,那么這個操作需要用到 BeanFactoryPostProcessor,因為它可以處理 在所有的 BeanDefinition 加載完成后,實體化 Bean 物件之前,提供修改 BeanDefinition 屬性的機制 而實作這部分內容是為了后續把此類內容結合到自動化配置處理中,整體設計結構如下圖:

結合bean的生命周期,包掃描只不過是掃描特定注解的類,提取類的相關資訊組裝成BeanDefinition注冊到容器中,
在XmlBeanDefinitionReader中決議<context:component-scan />標簽,掃描類組裝BeanDefinition然后注冊到容器中的操作在ClassPathBeanDefinitionScanner#doScan中實作,
- 自動掃描注冊主要是掃描添加了自定義注解的類,在xml加載程序中提取類的資訊,組裝 BeanDefinition 注冊到 Spring 容器中,
- 所以我們會用到
<context:component-scan />配置包路徑并在 XmlBeanDefinitionReader 決議并做相應的處理,這里的處理會包括對類的掃描、獲取注解資訊等 - 最后還包括了一部分關于
BeanFactoryPostProcessor的使用,因為我們需要完成對占位符配置資訊的加載,所以需要使用到 BeanFactoryPostProcessor 在所有的 BeanDefinition 加載完成后,實體化 Bean 物件之前,修改 BeanDefinition 的屬性資訊,這一部分的實作也為后續處理關于占位符配置到注解上做準備
四、實作
1. 工程結構
small-spring-step-13
└── src
├── main
│ └── java
│ └── cn.bugstack.springframework
│ ├── aop
│ │ ├── aspectj
│ │ │ └── AspectJExpressionPointcut.java
│ │ │ └── AspectJExpressionPointcutAdvisor.java
│ │ ├── framework
│ │ │ ├── adapter
│ │ │ │ └── MethodBeforeAdviceInterceptor.java
│ │ │ ├── autoproxy
│ │ │ │ └── MethodBeforeAdviceInterceptor.java
│ │ │ ├── AopProxy.java
│ │ │ ├── Cglib2AopProxy.java
│ │ │ ├── JdkDynamicAopProxy.java
│ │ │ ├── ProxyFactory.java
│ │ │ └── ReflectiveMethodInvocation.java
│ │ ├── AdvisedSupport.java
│ │ ├── Advisor.java
│ │ ├── BeforeAdvice.java
│ │ ├── ClassFilter.java
│ │ ├── MethodBeforeAdvice.java
│ │ ├── MethodMatcher.java
│ │ ├── Pointcut.java
│ │ ├── PointcutAdvisor.java
│ │ └── TargetSource.java
│ ├── beans
│ │ ├── factory
│ │ │ ├── config
│ │ │ │ ├── AutowireCapableBeanFactory.java
│ │ │ │ ├── BeanDefinition.java
│ │ │ │ ├── BeanFactoryPostProcessor.java
│ │ │ │ ├── BeanPostProcessor.java
│ │ │ │ ├── BeanReference.java
│ │ │ │ ├── ConfigurableBeanFactory.java
│ │ │ │ ├── InstantiationAwareBeanPostProcessor.java
│ │ │ │ └── SingletonBeanRegistry.java
│ │ │ ├── support
│ │ │ │ ├── AbstractAutowireCapableBeanFactory.java
│ │ │ │ ├── AbstractBeanDefinitionReader.java
│ │ │ │ ├── AbstractBeanFactory.java
│ │ │ │ ├── BeanDefinitionReader.java
│ │ │ │ ├── BeanDefinitionRegistry.java
│ │ │ │ ├── CglibSubclassingInstantiationStrategy.java
│ │ │ │ ├── DefaultListableBeanFactory.java
│ │ │ │ ├── DefaultSingletonBeanRegistry.java
│ │ │ │ ├── DisposableBeanAdapter.java
│ │ │ │ ├── FactoryBeanRegistrySupport.java
│ │ │ │ ├── InstantiationStrategy.java
│ │ │ │ └── SimpleInstantiationStrategy.java
│ │ │ ├── support
│ │ │ │ └── XmlBeanDefinitionReader.java
│ │ │ ├── Aware.java
│ │ │ ├── BeanClassLoaderAware.java
│ │ │ ├── BeanFactory.java
│ │ │ ├── BeanFactoryAware.java
│ │ │ ├── BeanNameAware.java
│ │ │ ├── ConfigurableListableBeanFactory.java
│ │ │ ├── DisposableBean.java
│ │ │ ├── FactoryBean.java
│ │ │ ├── HierarchicalBeanFactory.java
│ │ │ ├── InitializingBean.java
│ │ │ ├── ListableBeanFactory.java
│ │ │ └── PropertyPlaceholderConfigurer.java
│ │ ├── BeansException.java
│ │ ├── PropertyValue.java
│ │ └── PropertyValues.java
│ ├── context
│ │ ├── annotation
│ │ │ ├── ClassPathBeanDefinitionScanner.java
│ │ │ ├── ClassPathScanningCandidateComponentProvider.java
│ │ │ └── Scope.java
│ │ ├── event
│ │ │ ├── AbstractApplicationEventMulticaster.java
│ │ │ ├── ApplicationContextEvent.java
│ │ │ ├── ApplicationEventMulticaster.java
│ │ │ ├── ContextClosedEvent.java
│ │ │ ├── ContextRefreshedEvent.java
│ │ │ └── SimpleApplicationEventMulticaster.java
│ │ ├── support
│ │ │ ├── AbstractApplicationContext.java
│ │ │ ├── AbstractRefreshableApplicationContext.java
│ │ │ ├── AbstractXmlApplicationContext.java
│ │ │ ├── ApplicationContextAwareProcessor.java
│ │ │ └── ClassPathXmlApplicationContext.java
│ │ ├── ApplicationContext.java
│ │ ├── ApplicationContextAware.java
│ │ ├── ApplicationEvent.java
│ │ ├── ApplicationEventPublisher.java
│ │ ├── ApplicationListener.java
│ │ └── ConfigurableApplicationContext.java
│ ├── core.io
│ │ ├── ClassPathResource.java
│ │ ├── DefaultResourceLoader.java
│ │ ├── FileSystemResource.java
│ │ ├── Resource.java
│ │ ├── ResourceLoader.java
│ │ └── UrlResource.java
│ ├── stereotype
│ │ └── Component.java
│ └── utils
│ └── ClassUtils.java
└── test
└── java
└── cn.bugstack.springframework.test
├── bean
│ ├── IUserService.java
│ └── UserService.java
└── ApiTest.java
工程原始碼:公眾號「bugstack蟲洞堆疊」,回復:Spring 專欄,獲取完整原始碼
在Bean的生命周期中自動加載包掃描注冊Bean物件和設定占位符屬性的類關系,如圖 14-2

- 整個類的關系結構來看,其實涉及的內容并不多,主要包括的就是 xml 決議類 XmlBeanDefinitionReader 對 ClassPathBeanDefinitionScanner#doScan 的使用,
- 在 doScan 方法中處理所有指定路徑下添加了注解的類,拆解出類的資訊:名稱、作用范圍等,進行創建 BeanDefinition 好用于 Bean 物件的注冊操作,
- PropertyPlaceholderConfigurer 目前看上去像一塊單獨的內容,后續會把這塊的內容與自動加載 Bean 物件進行整合,也就是可以在注解上使用占位符配置一些在組態檔里的屬性資訊,
2. 處理占位符配置
cn.bugstack.springframework.beans.factory.PropertyPlaceholderConfigurer
public class PropertyPlaceholderConfigurer implements BeanFactoryPostProcessor {
/**
* Default placeholder prefix: {@value}
*/
public static final String DEFAULT_PLACEHOLDER_PREFIX = "${";
/**
* Default placeholder suffix: {@value}
*/
public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";
private String location;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 加載屬性檔案
try {
DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
Resource resource = resourceLoader.getResource(location);
Properties properties = new Properties();
properties.load(resource.getInputStream());
String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames();
for (String beanName : beanDefinitionNames) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
PropertyValues propertyValues = beanDefinition.getPropertyValues();
for (PropertyValue propertyValue : propertyValues.getPropertyValues()) {
Object value = propertyValue.getValue();
if (!(value instanceof String)) continue;
String strVal = (String) value;
StringBuilder buffer = new StringBuilder(strVal);
int startIdx = strVal.indexOf(DEFAULT_PLACEHOLDER_PREFIX);
int stopIdx = strVal.indexOf(DEFAULT_PLACEHOLDER_SUFFIX);
if (startIdx != -1 && stopIdx != -1 && startIdx < stopIdx) {
String propKey = strVal.substring(startIdx + 2, stopIdx);
String propVal = properties.getProperty(propKey);
buffer.replace(startIdx, stopIdx + 1, propVal);
propertyValues.addPropertyValue(new PropertyValue(propertyValue.getName(), buffer.toString()));
}
}
}
} catch (IOException e) {
throw new BeansException("Could not load properties", e);
}
}
public void setLocation(String location) {
this.location = location;
}
}
- 依賴于 BeanFactoryPostProcessor 在 Bean 生命周期的屬性,可以在 Bean 物件實體化之前,改變屬性資訊,所以這里通過實作 BeanFactoryPostProcessor 介面,完成對組態檔的加載以及摘取占位符中的在屬性檔案里的配置,
- 這樣就可以把提取到的配置資訊放置到屬性配置中了,
buffer.replace(startIdx, stopIdx + 1, propVal); propertyValues.addPropertyValue
3. 定義攔截注解
cn.bugstack.springframework.context.annotation.Scope
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {
String value() default "singleton";
}
- 用于配置作用域的自定義注解,方便通過配置Bean物件注解的時候,拿到Bean物件的作用域,不過一般都使用默認的 singleton
cn.bugstack.springframework.stereotype.Component
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Component {
String value() default "";
}
- Component 自定義注解大家都非常熟悉了,用于配置到 Class 類上的,除此之外還有 Service、Controller,不過所有的處理方式基本一致,這里就只展示一個 Component 即可,
4. 處理物件掃描裝配
cn.bugstack.springframework.context.annotation.ClassPathScanningCandidateComponentProvider
public class ClassPathScanningCandidateComponentProvider {
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<>();
Set<Class<?>> classes = ClassUtil.scanPackageByAnnotation(basePackage, Component.class);
for (Class<?> clazz : classes) {
candidates.add(new BeanDefinition(clazz));
}
return candidates;
}
}
- 這里先要提供一個可以通過配置路徑
basePackage=cn.bugstack.springframework.test.bean,決議出 classes 資訊的工具方法 findCandidateComponents,通過這個方法就可以掃描到所有 @Component 注解的 Bean 物件了,
cn.bugstack.springframework.context.annotation.ClassPathBeanDefinitionScanner
public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {
private BeanDefinitionRegistry registry;
public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {
this.registry = registry;
}
public void doScan(String... basePackages) {
for (String basePackage : basePackages) {
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition beanDefinition : candidates) {
// 決議 Bean 的作用域 singleton、prototype
String beanScope = resolveBeanScope(beanDefinition);
if (StrUtil.isNotEmpty(beanScope)) {
beanDefinition.setScope(beanScope);
}
registry.registerBeanDefinition(determineBeanName(beanDefinition), beanDefinition);
}
}
}
private String resolveBeanScope(BeanDefinition beanDefinition) {
Class<?> beanClass = beanDefinition.getBeanClass();
Scope scope = beanClass.getAnnotation(Scope.class);
if (null != scope) return scope.value();
return StrUtil.EMPTY;
}
private String determineBeanName(BeanDefinition beanDefinition) {
Class<?> beanClass = beanDefinition.getBeanClass();
Component component = beanClass.getAnnotation(Component.class);
String value = component.value();
if (StrUtil.isEmpty(value)) {
value = StrUtil.lowerFirst(beanClass.getSimpleName());
}
return value;
}
}
- ClassPathBeanDefinitionScanner 是繼承自 ClassPathScanningCandidateComponentProvider 的具體掃描包處理的類,在 doScan 中除了獲取到掃描的類資訊以后,還需要獲取 Bean 的作用域和類名,如果不配置類名基本都是把首字母縮寫,
5. 決議xml中呼叫掃描
cn.bugstack.springframework.beans.factory.xml.XmlBeanDefinitionReader
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
protected void doLoadBeanDefinitions(InputStream inputStream) throws ClassNotFoundException, DocumentException {
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
Element root = document.getRootElement();
// 決議 context:component-scan 標簽,掃描包中的類并提取相關資訊,用于組裝 BeanDefinition
Element componentScan = root.element("component-scan");
if (null != componentScan) {
String scanPath = componentScan.attributeValue("base-package");
if (StrUtil.isEmpty(scanPath)) {
throw new BeansException("The value of base-package attribute can not be empty or null");
}
scanPackage(scanPath);
}
// ... 省略其他
// 注冊 BeanDefinition
getRegistry().registerBeanDefinition(beanName, beanDefinition);
}
private void scanPackage(String scanPath) {
String[] basePackages = StrUtil.splitToArray(scanPath, ',');
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(getRegistry());
scanner.doScan(basePackages);
}
}
- 關于 XmlBeanDefinitionReader 中主要是在加載組態檔后,處理新增的自定義配置屬性
component-scan,決議后呼叫 scanPackage 方法,其實也就是我們在 ClassPathBeanDefinitionScanner#doScan 功能, - 另外這里需要注意,為了可以方便的加載和決議xml,XmlBeanDefinitionReader 已經全部替換為 dom4j 的方式進行決議處理,
五、測驗
1. 事先準備
@Component("userService")
public class UserService implements IUserService {
private String token;
public String queryUserInfo() {
try {
Thread.sleep(new Random(1).nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
return "小傅哥,100001,深圳";
}
public String register(String userName) {
try {
Thread.sleep(new Random(1).nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
return "注冊用戶:" + userName + " success!";
}
@Override
public String toString() {
return "UserService#token = { " + token + " }";
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
}
- 給 UserService 類添加一個自定義注解
@Component("userService")和一個屬性資訊String token,這是為了分別測驗包掃描和占位符屬性,
2. 屬性組態檔
token=RejDlI78hu223Opo983Ds
- 這里配置一個 token 的屬性資訊,用于通過占位符的方式進行獲取
3. spring.xml 配置物件
spring-property.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context">
<bean class="cn.bugstack.springframework.beans.factory.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:token.properties"/>
</bean>
<bean id="userService" class="cn.bugstack.springframework.test.bean.UserService">
<property name="token" value="${token}"/>
</bean>
</beans>
- 加載
classpath:token.properties設定占位符屬性值${token}
spring-scan.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context">
<context:component-scan base-package="cn.bugstack.springframework.test.bean"/>
</beans>
- 添加
component-scan屬性,設定包掃描根路徑
4. 單元測驗(占位符)
@Test
public void test_property() {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring-property.xml");
IUserService userService = applicationContext.getBean("userService", IUserService.class);
System.out.println("測驗結果:" + userService);
}
測驗結果
測驗結果:UserService#token = { RejDlI78hu223Opo983Ds }
Process finished with exit code 0
- 通過測驗結果可以看到 UserService 中的 token 屬性已經通過占位符的方式設定進去組態檔里的
token.properties的屬性值了,
5. 單元測驗(包掃描)
@Test
public void test_scan() {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:spring-scan.xml");
IUserService userService = applicationContext.getBean("userService", IUserService.class);
System.out.println("測驗結果:" + userService.queryUserInfo());
}
測驗結果
測驗結果:小傅哥,100001,深圳
Process finished with exit code 0
- 通過這個測驗結果可以看出來,現在使用注解的方式就可以讓 Class 注冊完成 Bean 物件了,
六、總結
- 通過整篇的內容實作可以看出來,目前的功能添加其實已經不復雜了,都是在 IOC 和 AOP 核心的基礎上來補全功能,這些補全的功能也是在完善 Bean 的生命周期,讓整個功能使用也越來越容易,
- 在你不斷的實作著 Spring 的各項功能時,也可以把自己在平常使用 Spring 的一些功能想法融入進來,比如像 Spring 是如何動態切換資料源的,執行緒池是怎么提供配置的,這些內容雖然不是最基礎的核心范圍,但也非常重要,
- 可能有些時候這些類實作的內容對新人來說比較多,可以一點點動手實作逐步理解,在把一些稍微較有難度的內容實作后,其實后面也就沒有那么難理解了,
七、系列推薦
- 13年畢業,用兩年時間從外包走進互聯網大廠
- 作業兩三年了,整不明白架構圖都畫啥?
- 面試現場:小伙伴美團一面的分享和分析(含解答)
- LinkedList插入速度比ArrayList快?你確定嗎?
- Netty+JavaFx實戰:仿桌面版微信聊天
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/291029.html
標籤:java
上一篇:Windows命令列簡易入門
下一篇:容器型資料型別——元組
