手写Spring-第十三章-超进化!用注解完成Bean的注册

前言

我们的框架,到了今天,其实已经比较完备了。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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值