手撸一个 Spring —— 2. 基于注解

上一篇中,读取 xml 解析为 BeanDefinition 的类 XmlBeanDefinitionReader 的代码在如下链接:https://github.com/CN-GuoZiyang/My-Spring-IOC/blob/82967670e5/src/main/java/top/guoziyang/springframework/reader/XmlBeanDefinitionReader.java,微信公众号似乎不允许内置链接。

上一节我们已经实现了一个包含基本功能的 IOC 容器,并且也解决了各种依赖问题。没看过上一篇的同学可以在我的讨论帖记录中查找。

这一节我们来对这个框架进行扩展,实现基于注解自动注入 bean 的方式,免去繁琐的配置文件。

本节最终的代码为:https://github.com/CN-GuoZiyang/My-Spring-IOC/tree/8a3a9c640e532c5d4aa8d62f18b42fa336c94f2e

定义注解

首先我们需要定义一些注解类,仿照 Spring,我们定义如下的五个注解:、@Component@Scope@Autowired@Qualifier@Value。他们分别的作用如下:

  • @Component:这个注解用于类,表示这个类是一个需要被注册进容器的 bean
  • @Scope:这个注解用于类,默认值是 "singleton",还可使用 "prototype",标明这个 bean 是单例还是多例的
  • @Autowired:这个注解用于属性,表示向属性自动注入容器中对应类型的 bean
  • @Qualifier:这个注解用于属性,需要传递一个字符串,表示给这个属性注入对应名称的 bean
  • @Value:这个注解用于属性,表示向这个属性注入某个值(基本类型)

我们来定义注解,如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
    String name() default "";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Scope {
    String value() default "singleton";
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Autowired{}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Qualifier {
    String value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Value {
    public String value();
}

定义注解同样需要一些注解(禁止套娃),@Retention 注解用来标示注解类型的生命周期,由于我们的这些注解都是需要在运行时通过反射获取的,所以这个注解的值都是 RetentionPolicy.RUNTIME@Target 注解用来定义注解能够被应用于源码的哪些位置,例如 ElementType.FIELD 为属性,ElementType.TYPE 是类。

由于不是 SpringBoot,我们仍然需要在配置文件中书写自动注入的扫描范围,来指导框架扫描。配置文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans>
    <component-scan base-package="top.guoziyang.main"></component-scan>
</beans>

启动后,框架会自动扫描该包及其子包下所有使用注解标明的 Bean,并注入容器。

扫描注解

由于配置文件发生了改变,自然我们需要改变 xml 文件的解析方式,在 XmlBeanDefinitionReader 类的 parseBeanDefinitions() 方法中,一旦我们发现了 component-scan 标签,说明我们是使用注解来注入 Bean 的:

protected void parseBeanDefinitions(Element root) {
  ...
  for(int i = 0; i < nodeList.getLength(); i ++) {
      if(nodeList.item(i) instanceof Element) {
          Element ele = (Element)nodeList.item(i);
          if(ele.getTagName().equals("component-scan")) {
              basePackage = ele.getAttribute("base-package");
              break;
          }
      }
  }
  if(basePackage != null) {
      parseAnnotation(basePackage);
      return;
  }
  ...
}

parseAnnotation() 方***获取到目标包下所有的类,并遍历解析:

protected void parseAnnotation(String basePackage) {
    Set<Class<?>> classes = getClasses(basePackage);
    for(Class clazz : classes) {
        processAnnotationBeanDefinition(clazz);
    }
}

getClass() 就是用来获取到包下的所有类,它的实现较为繁琐,具体代码在:https://github.com/CN-GuoZiyang/My-Spring-IOC/blob/8a3a9c640e/src/main/java/top/guoziyang/springframework/reader/XmlBeanDefinitionReader.java#L141,当然这个功能也可以用 guava 直接实现。

processAnnotationBeanDefinition() 这个方法利用了反射机制来获取类上的注解,以判断对应的类是否是需要注册的 bean,并寻找相关的注解,如 @Scope,形成对应的 BeanDefinition:

protected void processAnnotationBeanDefinition(Class<?> clazz) {
    if(clazz.isAnnotationPresent(Component.class)) {
        String name = clazz.getAnnotation(Component.class).name();
        if(name == null || name.length() == 0) {
            name = clazz.getName();
        }
        String className = clazz.getName();
        boolean singleton = true;
        if(clazz.isAnnotationPresent(Scope.class) && "prototype".equals(clazz.getAnnotation(Scope.class).value())) {
            singleton = false;
        }
        BeanDefinition beanDefinition = new BeanDefinition();
        processAnnotationProperty(clazz, beanDefinition);
        beanDefinition.setBeanClassName(className);
        beanDefinition.setSingleton(singleton);
        getRegistry().put(name, beanDefinition);
    }
}

processAnnotationProperty() 则是对类的每一个属性进行判断,来确定每个属性是否需要注入等:

protected void processAnnotationProperty(Class<?> clazz, BeanDefinition beanDefinition) {

    Field[] fields = clazz.getDeclaredFields();
    for(Field field : fields) {
        String name = field.getName();
        if(field.isAnnotationPresent(Value.class)) {
            Value valueAnnotation = field.getAnnotation(Value.class);
            String value = valueAnnotation.value();
            if(value != null && value.length() > 0) {
                // 优先进行值注入
                beanDefinition.getPropertyValues().addPropertyValue(new PropertyValue(name, value));
            }
        } else if(field.isAnnotationPresent(Autowired.class)) {
            if(field.isAnnotationPresent(Qualifier.class)) {
                Qualifier qualifier = field.getAnnotation(Qualifier.class);
                String ref = qualifier.value();
                if(ref == null || ref.length() == 0) {
                    throw new IllegalArgumentException("the value of Qualifier should not be null!");
                }
                BeanReference beanReference = new BeanReference(ref);
                beanDefinition.getPropertyValues().addPropertyValue(new PropertyValue(name, beanReference));
            } else {
                String ref = field.getType().getName();
                BeanReference beanReference = new BeanReference(ref);
                beanDefinition.getPropertyValues().addPropertyValue(new PropertyValue(name, beanReference));
            }
        }
    }
}

其实就是将原本从 xml 获取到的各种信息,通过反射的方式从注解中获取了出来。整体的逻辑并没有什么变化。

实际上,由于产生的结果一致(生成 beanDefinition 并存入 registry),可以仿照 Spring 的实现使用委托模式,这样耦合度就不会太高。但是由于使用注解同样还需要读取配置文件,较为繁琐,就没有解耦(实际上是我偷懒了)。

写了这么多,其实都仅仅是对 XmlBeanDefinitionReader 这个类的改动,因为这个类的职责就是,从文件中解析出 BeanDefinition。这个类的实现在 https://github.com/CN-GuoZiyang/My-Spring-IOC/blob/8a3a9c640e/src/main/java/top/guoziyang/springframework/reader/XmlBeanDefinitionReader.java 中。

看看效果

由于我们使用了注解,就需要修改相应的 bean,加上上面定义的注解即可:

@Component(name = "helloWorldService")
@Scope("prototype")
public class HelloWorldServiceImpl implements HelloWorldService {
    @Value("Hello, world")
    private String text;

    @Override
    public void saySomething() {
        System.out.println(text);
    }
}
@Component(name = "wrapService")
public class WrapService {
    @Autowired
    private HelloWorldService helloWorldService;

    public void say() {
        helloWorldService.saySomething();
    }
}

测试代码如下:

public class Main() {
  public static void annotationTest() throws Exception {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application-annotation.xml");
        WrapService wrapService = (WrapService) applicationContext.getBean("wrapService");
        wrapService.say();
        HelloWorldService helloWorldService = (HelloWorldService) applicationContext.getBean("helloWorldService");
        HelloWorldService helloWorldService2 = (HelloWorldService) applicationContext.getBean("helloWorldService");
        System.out.println("prototype验证:相等" + (helloWorldService == helloWorldService2));
        WrapService wrapService2 = (WrapService) applicationContext.getBean("wrapService");
        System.out.println("singleton验证:相等" + (wrapService == wrapService2));
    }
}

其实和第一节的测试一样,结果也是一致的。

结束语

到这里,自己手写的 IOC 容器的简单实现就完成了!还是挺有成就感的。使用体验和 Spring 基本没啥差别(误)。

下一篇文章,会基于已经实现的 IOC 容器,在其上层手撸一个 SpringMVC 的简单实现。

所以,不要停下来啊!

#Java##学习路径#
全部评论
注解这块还是蛮多优化空间的 1. 注解扫描可以考虑自定义@ComponentScan、@ComponentScans,并使用org.reflections增强包替代现有的扫描实现(尤其是xml配置的方式) 2. 可以考虑下实现Spring的注解别名@AliasFor,类似@Service、@Component这种只是语义不同的注解就不用重复实现了 3. 有了@Autowired,还可以尝试实现下@Bean、@Primary  以上功能我都实现过了,你还不够卷(狗头保命)
点赞 回复
分享
发布于 2021-02-28 12:43
大佬,这些能够写在简历上吗?我对Spring蛮熟悉得,也打算手写一个写简历上,除了写对三级缓存的优化以外,其它的不知道在简历上该怎么写了,想请教一下。
点赞 回复
分享
发布于 2022-05-01 19:09
联想
校招火热招聘中
官网直投

相关推荐

8 21 评论
分享
牛客网
牛客企业服务