前言
我们的框架,到了今天,其实已经比较完备了。Spring的两大特性:IOC和AOP,都已经被我们实现了。这让我想到了那句名言:【物理学的大厦已经落成,上面只有两朵乌云】(flag就这么立起来了)。那对于我们的框架来说,乌云是什么呢?易用性。我们虽然实现了这些基本功能,但是用起来总觉得很古老,和我们现在熟悉的Spring有所区别。我们现在根本不会去配置文件里面一个一个的配置bean,而是直接用注解来完成。只能说懒惰是第一生产力,人类为了偷懒,真的是煞费苦心。已经提供了配置文件这种方式,但还是不满足,还是要更进一步。那么这次,我们就来实现注解的自动化Bean注册。除此之外,我们还要实现一个配置文件的占位符替换。什么意思呢?我们经常会出现在xml配置中使用${xxx}的例子,这里的值,则是放在yml文件中。我们这里就是要实现这种将占位符替换为真正的值这样一个过程。
工程结构
├─src
│ ├─main
│ │ ├─java
│ │ │ └─com
│ │ │ └─akitsuki
│ │ │ └─springframework
│ │ │ ├─aop
│ │ │ │ │ AdvisedSupport.java
│ │ │ │ │ Advisor.java
│ │ │ │ │ BeforeAdvice.java
│ │ │ │ │ ClassFilter.java
│ │ │ │ │ MethodBeforeAdvice.java
│ │ │ │ │ MethodMatcher.java
│ │ │ │ │ Pointcut.java
│ │ │ │ │ PointcutAdvisor.java
│ │ │ │ │ TargetSource.java
│ │ │ │ │
│ │ │ │ ├─aspect
│ │ │ │ │ AspectJExpressionPointcut.java
│ │ │ │ │ AspectJExpressionPointcutAdvisor.java
│ │ │ │ │
│ │ │ │ └─framework
│ │ │ │ │ AopProxy.java
│ │ │ │ │ Cglib2AopProxy.java
│ │ │ │ │ JdkDynamicAopProxy.java
│ │ │ │ │ ProxyFactory.java
│ │ │ │ │ ReflectiveMethodInvocation.java
│ │ │ │ │
│ │ │ │ ├─adapter
│ │ │ │ │ MethodBeforeAdviceInterceptor.java
│ │ │ │ │
│ │ │ │ └─autoproxy
│ │ │ │ DefaultAdvisorAutoProxyCreator.java
│ │ │ │
│ │ │ ├─beans
│ │ │ │ ├─exception
│ │ │ │ │ BeanException.java
│ │ │ │ │
│ │ │ │ └─factory
│ │ │ │ │ Aware.java
│ │ │ │ │ BeanClassLoaderAware.java
│ │ │ │ │ BeanFactory.java
│ │ │ │ │ BeanFactoryAware.java
│ │ │ │ │ BeanNameAware.java
│ │ │ │ │ ConfigurableListableBeanFactory.java
│ │ │ │ │ DisposableBean.java
│ │ │ │ │ FactoryBean.java
│ │ │ │ │ HierarchicalBeanFactory.java
│ │ │ │ │ InitializingBean.java
│ │ │ │ │ ListableBeanFactory.java
│ │ │ │ │ PropertyPlaceholderConfigurer.java
│ │ │ │ │
│ │ │ │ ├─config
│ │ │ │ │ AutowireCapableBeanFactory.java
│ │ │ │ │ BeanDefinition.java
│ │ │ │ │ BeanDefinitionRegistryPostProcessor.java
│ │ │ │ │ BeanFactoryPostProcessor.java
│ │ │ │ │ BeanPostProcessor.java
│ │ │ │ │ BeanReference.java
│ │ │ │ │ ConfigurableBeanFactory.java
│ │ │ │ │ DefaultSingletonBeanRegistry.java
│ │ │ │ │ InstantiationAwareBeanPostProcessor.java
│ │ │ │ │ PropertyValue.java
│ │ │ │ │ PropertyValues.java
│ │ │ │ │ SingletonBeanRegistry.java
│ │ │ │ │
│ │ │ │ ├─support
│ │ │ │ │ AbstractAutowireCapableBeanFactory.java
│ │ │ │ │ AbstractBeanDefinitionReader.java
│ │ │ │ │ AbstractBeanFactory.java
│ │ │ │ │ BeanDefinitionReader.java
│ │ │ │ │ BeanDefinitionRegistry.java
│ │ │ │ │ CglibSubclassingInstantiationStrategy.java
│ │ │ │ │ DefaultListableBeanFactory.java
│ │ │ │ │ DisposableBeanAdapter.java
│ │ │ │ │ FactoryBeanRegistrySupport.java
│ │ │ │ │ InstantiationStrategy.java
│ │ │ │ │ SimpleInstantiationStrategy.java
│ │ │ │ │
│ │ │ │ └─xml
│ │ │ │ XmlBeanDefinitionReader.java
│ │ │ │
│ │ │ ├─context
│ │ │ │ │ ApplicationContext.java
│ │ │ │ │ ApplicationContextAware.java
│ │ │ │ │ ApplicationEvent.java
│ │ │ │ │ ApplicationEventPublisher.java
│ │ │ │ │ ApplicationListener.java
│ │ │ │ │ ConfigurableApplicationContext.java
│ │ │ │ │
│ │ │ │ ├─annotation
│ │ │ │ │ ClassPathBeanDefinitionScanner.java
│ │ │ │ │ ClassPathScanningCandidateComponentProvider.java
│ │ │ │ │ Scope.java
│ │ │ │ │
│ │ │ │ ├─event
│ │ │ │ │ AbstractApplicationEventMulticaster.java
│ │ │ │ │ ApplicationContextEvent.java
│ │ │ │ │ ApplicationEventMulticaster.java
│ │ │ │ │ ContextClosedEvent.java
│ │ │ │ │ ContextRefreshEvent.java
│ │ │ │ │ SimpleApplicationEventMulticaster.java
│ │ │ │ │
│ │ │ │ └─support
│ │ │ │ AbstractApplicationContext.java
│ │ │ │ AbstractRefreshableApplicationContext.java
│ │ │ │ AbstractXmlApplicationContext.java
│ │ │ │ ApplicationContextAwareProcessor.java
│ │ │ │ ClasspathXmlApplicationContext.java
│ │ │ │
│ │ │ ├─core
│ │ │ │ └─io
│ │ │ │ ClasspathResource.java
│ │ │ │ DefaultResourceLoader.java
│ │ │ │ FileSystemResource.java
│ │ │ │ Resource.java
│ │ │ │ ResourceLoader.java
│ │ │ │ UrlResource.java
│ │ │ │
│ │ │ ├─stereotype
│ │ │ │ Component.java
│ │ │ │
│ │ │ └─util
│ │ │ ClassUtils.java
│ │ │
│ │ └─resources
│ └─test
│ ├─java
│ │ └─com
│ │ └─akitsuki
│ │ └─springframework
│ │ └─test
│ │ │ ApiTest.java
│ │ │
│ │ └─bean
│ │ UserDao.java
│ │ UserService.java
│ │
│ └─resources
│ application.yml
│ spring.xml
占位符,先拿你开刀
我们的目的很明确,要替换掉配置中的占位符,并且将真正的值替换进去。那么我们首先要思考,这一步应该放在哪里。回顾一下Bean的生命周期,答案应该是Bean定义加载完成,准备实例化Bean的时候。因为我们的占位符,实际上是用来表示Bean中的属性的,也就是写在Bean定义中。那么我们就要在这个时候,修改Bean定义,将其替换成真正的值,然后再水到渠成的执行Bean的创建过程。我们之前刚好有一个后置处理器可以完成这件事情:BeanFactoryPostProcessor
。
package com.akitsuki.springframework.beans.factory;
import com.akitsuki.springframework.beans.exception.BeanException;
import com.akitsuki.springframework.beans.factory.config.BeanDefinition;
import com.akitsuki.springframework.beans.factory.config.BeanFactoryPostProcessor;
import com.akitsuki.springframework.beans.factory.config.PropertyValue;
import com.akitsuki.springframework.beans.factory.config.PropertyValues;
import com.akitsuki.springframework.core.io.DefaultResourceLoader;
import com.akitsuki.springframework.core.io.Resource;
import java.io.IOException;
import java.util.Properties;
/**
* @author ziling.wang@hand-china.com
* @date 2022/12/9 9:59
*/
public class PropertyPlaceholderConfigurer implements BeanFactoryPostProcessor {
public static final String DEFAULT_PLACEHOLDER_PREFIX = "${";
public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";
private String location;
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
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 pv : propertyValues.getPropertyValues()) {
Object value = pv.getValue();
if (!(value instanceof String)) {
continue;
}
String strValue = (String) value;
StringBuilder sb = new StringBuilder(strValue);
int startIndex = strValue.indexOf(DEFAULT_PLACEHOLDER_PREFIX);
int endIndex = strValue.indexOf(DEFAULT_PLACEHOLDER_SUFFIX);
if (-1 != startIndex && -1 != endIndex && startIndex < endIndex) {
String propKey = strValue.substring(startIndex + 2, endIndex);
String propValue = properties.getProperty(propKey);
sb.replace(startIndex, endIndex + 1, propValue);
propertyValues.addPropertyValue(new PropertyValue(pv.getName(), sb.toString()));
}
}
}
} catch (IOException e) {
throw new BeanException("加载配置时出错", e);
}
}
public void setLocation(String location) {
this.location = location;
}
}
乍一看起来有些长,但实际上内容并不复杂,我们一点点来分析。
首先是前后缀,这个很好理解,用它来定位占位符。然后是一个location,这个location是指我们的配置文件所在地。要注意的是,这里的配置文件并不是指我们之前的spring.xml,而是我们的键值对配置文件,用来配置变量的值的文件。比如我们常用的 application.yml
这样的文件。之后的内容都是后置处理器的内容,我们来详细分析。
第一段,通过我们之前实现的ResourceLoader,将配置文件中的内容,读取到Properties中。以便下面进行处理。之后,对Bean定义进行遍历,拿到Bean定义中的所有依赖属性,再对属性进行遍历,后面的内容看起来很长,其实是简单的字符串匹配、定位、替换的过程。将占位符替换成真正的值(存储在Properties中)后,再重新设置进Bean定义的PropertyValues中。这样,我们就完成了替换过程。
主角出场,自定义注解
接下来,我们要开始自定义注解的部分了。这次我们实现两个注解:@Scope注解和@Component注解。@Scope注解主要是用来表示Bean的作用域的,我们之前也介绍过关于单例Bean和非单例Bean的内容。而@Component注解,大家都非常熟悉了,我们用它来标识一个Bean。Spring中还有类似的@Controller、@Service等注解,但这些只是为了更加语义化,它们和@Component是等价的,我们这里就只实现@Component注解。
先来看@Scope注解
package com.akitsuki.springframework.context.annotation;
import java.lang.annotation.*;
/**
* @author ziling.wang@hand-china.com
* @date 2022/12/9 10:12
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {
String value() default "singleton";
}
然后是@Component注解
package com.akitsuki.springframework.stereotype;
import java.lang.annotation.*;
/**
* @author ziling.wang@hand-china.com
* @date 2022/12/9 10:14
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Component {
String value() default "";
}
注解本身没什么好说的,@Scope注解提供了一个属性,用于设置作用域。而@Component注解,设置的属性则是Bean的名称。
有了注解,我们要怎么使用呢?我们先来考虑@Component注解。hutool工具包给我们提供了一个方法,可以根据一个基本包路径和注解,扫描路径下所有包含这个注解的类。有了这个工具,我们就可以拿到这些类的信息,进而创建Bean定义了。
package com.akitsuki.springframework.context.annotation;
import cn.hutool.core.util.ClassUtil;
import com.akitsuki.springframework.beans.factory.config.BeanDefinition;
import com.akitsuki.springframework.stereotype.Component;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* 通过注解扫描指定路径下的bean定义
* @author ziling.wang@hand-china.com
* @date 2022/12/9 10:15
*/
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;
}
}
嗯,非常的好用,如果没有这个 scanPackageByAnnotation
方法,真不知道要怎么实现这个功能(笑)
但到这一步,我们只是实现了一个工具类而已。它只帮我们把Bean定义创建了出来,就没有下文了。所以我们要来继续完善这件事。
package com.akitsuki.springframework.context.annotation;
import cn.hutool.core.util.StrUtil;
import com.akitsuki.springframework.beans.factory.config.BeanDefinition;
import com.akitsuki.springframework.beans.factory.support.BeanDefinitionRegistry;
import com.akitsuki.springframework.stereotype.Component;
import lombok.AllArgsConstructor;
import java.util.Set;
/**
* @author ziling.wang@hand-china.com
* @date 2022/12/9 10:24
*/
@AllArgsConstructor
public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {
private BeanDefinitionRegistry registry;
public void doScan(String... basePackages) {
for (String basePackage : basePackages) {
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition beanDefinition : candidates) {
String scope = resolveBeanScope(beanDefinition);
if (StrUtil.isNotEmpty(scope)) {
beanDefinition.setScope(scope);
}
registry.registerBeanDefinition(determineBeanName(beanDefinition), beanDefinition);
}
}
}
/**
* 处理bean的作用域
* @param beanDefinition
* @return
*/
private String resolveBeanScope(BeanDefinition beanDefinition) {
Class<?> beanClass = beanDefinition.getBeanClass();
Scope scope = beanClass.getAnnotation(Scope.class);
if (null != scope) {
return scope.value();
}
return StrUtil.EMPTY;
}
/**
* 处理bean名称,以Component配置为准,如果没有配置,则取首字母小写的类名
* @param beanDefinition
* @return
*/
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;
}
}
又是个看起来挺长,没办法一眼就看懂的类。我们先从两个私有方法入手,这两个都是工具方法,分别用来处理作用域和Bean名称。我们先来看作用域,其实就是从Bean定义中拿到Class,再拿到@Scope注解,最后拿到注解里配置的值而已。如果没有注解或者是空,不要忘了我们的Bean定义会默认把作用域设置为单例,所以不用担心。接下来是处理Bean名称,和作用域类似,也是去拿注解的值。不同的是,这里如果没有拿到,那么就会用首字母小写的类名作为Bean名称。
然后我们看处理方法,其实也没什么好说的,只不过这里增加了多个包路径的循环操作,以及拿到了刚新建好的Bean定义,调用刚才的两个工具方法,设置作用域和Bean名称,最后注册到容器中。终于,在这一步,我们完成了注册的操作。
切入点:扫描配置
我们上面完成了这么多,但是,有这么一个问题:扫描的包地址,从哪里来。在Spring中,我们会在xml中配置component-scan,在其中配置我们要扫描的包路径。所以,久违的,我们要来扩充我们的xml解析功能了,加入对component-scan的解析操作。
package com.akitsuki.springframework.beans.factory.xml;
/**
* xml方式读取bean定义
*
* @author ziling.wang@hand-china.com
* @date 2022/11/9 14:18
*/
public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
/**
* 真正通过xml读取bean定义的方法实现
*
* @param inputStream xml配置文件输入流
* @throws BeanException e
* @throws ClassNotFoundException e
*/
private void doLoadBeanDefinitions(InputStream inputStream) throws BeanException, ClassNotFoundException, DocumentException, SAXException {
SAXReader reader = new SAXReader();
//安全措施,防止xxe攻击
reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
Document document = reader.read(inputStream);
Element root = document.getRootElement();
Element componentScan = root.element("component-scan");
if (null != componentScan) {
String scanPath = componentScan.attributeValue("base-package");
if (StrUtil.isEmpty(scanPath)) {
throw new BeanException("base package can not be null or empty");
}
scanPackage(scanPath);
}
//省略其他部分
}
private void scanPackage(String path) {
String[] basePackages = StrUtil.splitToArray(path, ",");
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(getRegistry());
scanner.doScan(basePackages);
}
}
这里我们只截取了部分操作,而且,对整体的xml解析功能进行了一个升级,改用dom4j来进行操作,替换掉了原来的w3c系列工具。具体详细的代码可以去看我上传到gitee的代码。
这里的主体思想,就是从component-scan元素中,找到base-package属性,这里的base-package可以配置多个,用逗号进行分割。然后再调用我们上面所实现的scanner,将解析出来的包路径传入进去,完成Bean定义的解析注册。
测试
这次的内容,相对来说没有那么闹心,一切的实现都很轻松写意,毕竟物理学的大厦已经落成…(再次立flag)
这次,我们得再次请出我们的老朋友:UserDao和UserService了。这俩好兄弟,陪着我们走过了漫长的旅程,这次依然要为我们的测试,做出伟大的贡献。
package com.akitsuki.springframework.test.bean;
import com.akitsuki.springframework.beans.factory.DisposableBean;
import com.akitsuki.springframework.beans.factory.InitializingBean;
import com.akitsuki.springframework.context.annotation.Scope;
import com.akitsuki.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* @author ziling.wang@hand-china.com
* @date 2022/11/8 14:42
*/
@Component
@Scope("prototype")
public class UserDao implements InitializingBean, DisposableBean {
private static final Map<Long, String> userMap = new HashMap<>();
public String queryUserName(Long id) {
return userMap.get(id);
}
@Override
public void destroy() throws Exception {
System.out.println("执行UserDao的destroyMethod");
userMap.clear();
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("执行UserDao的initMethod");
userMap.put(1L, "akitsuki");
userMap.put(2L, "toyosaki");
userMap.put(3L, "kugimiya");
userMap.put(4L, "hanazawa");
userMap.put(5L, "momonogi");
}
}
这次的UserDao,将为我们测试新的注解。相对的,我们的UserService则采用传统的方式
package com.akitsuki.springframework.test.bean;
import com.akitsuki.springframework.beans.factory.DisposableBean;
import com.akitsuki.springframework.beans.factory.InitializingBean;
import com.akitsuki.springframework.context.ApplicationContext;
import lombok.Getter;
import lombok.Setter;
/**
* @author ziling.wang@hand-china.com
* @date 2022/11/8 14:42
*/
@Getter
@Setter
public class UserService implements InitializingBean, DisposableBean {
private String dummyString;
private int dummyInt;
private UserDao userDao;
public void queryUserInfo(Long id) {
System.out.println("dummyString:" + dummyString);
System.out.println("dummyInt:" + dummyInt);
String userName = userDao.queryUserName(id);
if (null == userName) {
System.out.println("用户未找到>_<");
} else {
System.out.println("用户名:" + userDao.queryUserName(id));
}
}
@Override
public void destroy() throws Exception {
System.out.println("userService的destroy执行了");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("userService的afterPropertiesSet执行了");
}
}
嗯,基本内容都没啥变动,这里就不多介绍了
接下来,是我们的配置文件,这次我们不仅要有spring.xml,还要有application.yml
<?xml version="1.0" encoding="utf-8" ?>
<beans>
<component-scan base-package="com.akitsuki.springframework.test.bean" />
<bean id="userService" class="com.akitsuki.springframework.test.bean.UserService">
<property name="dummyString" value="${dummyString}"/>
<property name="dummyInt" value="${dummyInt}"/>
<property name="userDao" ref="userDao"/>
</bean>
<bean class="com.akitsuki.springframework.beans.factory.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:application.yml"/>
</bean>
</beans>
dummyString: kamisama
dummyInt: 114514
可以看到,我们在spring.xml中,配置了要扫描的包路径,userService也是用传统方法进行配置的,并且对于方法依赖的部分属性,也用了占位符来进行标记。然后在application.yml中,则是对占位符的变量进行了实际的配置。在我们对property解析的bean中,也将application.yml的路径,作为参数传了过去。
下面是主要测试类
package com.akitsuki.springframework.test;
import com.akitsuki.springframework.context.support.ClasspathXmlApplicationContext;
import com.akitsuki.springframework.test.bean.UserService;
import org.junit.Test;
/**
* @author ziling.wang@hand-china.com
* @date 2022/11/15 13:58
*/
public class ApiTest {
@Test
public void test() {
ClasspathXmlApplicationContext context = new ClasspathXmlApplicationContext("classpath:spring.xml");
context.registerShutdownHook();
UserService userService = context.getBean("userService", UserService.class);
userService.queryUserInfo(1L);
}
}
这个类,每次最轻松的就是它了,基本没啥大变化。
测试结果
执行UserDao的initMethod
userService的afterPropertiesSet执行了
dummyString:kamisama
dummyInt:114514
用户名:akitsuki
userService的destroy执行了
Process finished with exit code 0
我们来分析一下这个结果,首先,可以看到UserDao被成功的创建了,证明我们的@Component注解生效了。然后只有UserService的destroy方法执行了,证明我们用@Scope注解,将UserDao的作用域修改为prototype也成功了。然后我们配置在application.yml中的变量,也成功的打印出来了,证明我们的占位符替换功能也成功了。这一次的练习,也圆满完成了。
相关源码可以参考我的gitee:https://gitee.com/akitsuki-kouzou/mini-spring
,这里对应的代码是mini-spring-13