跟我动手搭框架一之IOC容器实现

举报
西魏陶渊明 发表于 2022/09/25 01:22:49 2022/09/25
【摘要】 本篇文章面对的是有开发经验的Java developer 因为我们将要实现的Spring的IOC容器, 前些天由于工作中要开发公司的Callback系统,一直在研究Netty及IO模型,对于Netty这种非阻塞异步框架,非常崇拜,于是萌发一个想法,用Netty作为web容器,替换Tomcat研究性能.出于这种初衷,就开始为Sm...

本篇文章面对的是有开发经验的Java developer 因为我们将要实现的Spring的IOC容器,

前些天由于工作中要开发公司的Callback系统,一直在研究Netty及IO模型,对于Netty这种非阻塞异步框架,非常崇拜,于是萌发一个想法,用Netty作为web容器,替换Tomcat研究性能.出于这种初衷,就开始为SmileBoot项目开始慢慢积累开发知识.本篇属于小编SmileBoot中的一个模块,为什么要起名Smile呢?因为小编始终认为,我们要带着好的心态,才能学更多的东西,其实小编也是一个菜鸟,之所以要写下来,就是为了记忆和理解更深.因为如果把自己理解的东西,能清楚的讲给其他人,那么才算是真正的理解.

目录

  • 1.原理分析及设计
  • 2.实现方案
    • 2.1 拿到扫描范围
    • 2.2 更具扫描范围加载范围内所有字节码文件
    • 2.3 定义自己的上下文对象接口及实现类
  • 3.测试可用性
  • 4.扩展性
  • 5.下篇预告

1.原理分析及设计

Spring的源码,这里不跟着阅读,直接去实现,然后刚兴趣的童鞋,可以自己在看看,原理是一样的.

1.加载项目中所有的Class文件到Set集合

2.遍历Set将标记有IOC的组件的Class,获取到,注册到IOC容器,这个里面的重点是如何将Class里面的组件,注入进来.

4279695-41df675d5913607b.jpg
image

@SmileComponent

在这里@SmileComponent注解是用来标记,需要加入到IOC容器的类

@SmileBean

@SmileBean是用来标记方法中返回值作为Bean,是将要被注册到IOC容器的对象

@InsertBean

@InserBean是标记,该字段是一个Bean,需要从IOC容器中获取,然后注入到该对象中

2.实现方案

1.获取所有的Class字节码,在这中间我们有一个困难那就是如果知道,开发者的所有字节码呢?这个时候我们就可以用注解的形式,在启动类上做一个标记,那么我们就能获取到启动类的字节码,从而获取到将要扫描的跟目录.

我们看下Spring是如何实现的吧


  
  1. @SpringBootApplication
  2. public class OtoSaasApplication {
  3. public static void main(String[] args) {
  4. SpringApplication.run(OtoSaasApplication.class, args);
  5. }
  6. }

在这段代码中,有一个注解@SpringBootApplication ,了解Spring的开发同事,都是知道这个注解其实包括了多个注解的,其中一个就是@ComponentScan


  
  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target({ElementType.TYPE})
  3. @Documented
  4. @Repeatable(ComponentScans.class)
  5. public @interface ComponentScan {
  6. @AliasFor("basePackages")
  7. String[] value() default {}
  8. }

那么我们就可以知道,其实也main方法所包含的注解,拿到根目录的.这个有一个Spring的特性,那就是如果启动类在最外层的,那么默认就是扫描,其子目录中的Class,如果不是在根目录,那么要指定扫描的范围.

2.1拿到扫描范围

那么我们想,如果用户不指定,我们怎么拿到根目录呢?

好,如果有疑惑的话,那么久带着疑惑,看下面这段代码吧!

我们定一个注解@SmileBootApplication 目录就是获取到用户的根目录,这里关于注解不在解释,如果有不了解实现注解的可以看小编SpringBoot实践中的自定义注解


  
  1. @Target({ElementType.TYPE})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface SmileBootApplication {
  5. String[] basePackages() default {};
  6. }

  
  1. @SmileBootApplication
  2. public class SmileApplication {
  3. public static void main(String[] args) {
  4. SmileApplication.run(SmileApplication.class, args);
  5. }
  6. }

我们定义一个方法也就是在run方法中,根据class,文件,获取到注解的根目录


  
  1. public static String getBaseRootPackage(Class<?> cls) {
  2. SmileBootApplication declaredAnnotation = null;
  3. try {
  4. declaredAnnotation = cls.getDeclaredAnnotation(SmileBootApplication.class);
  5. } catch (Exception e) {
  6. throw new IllegalArgumentException("请添加@SmileBootApplication");
  7. }
  8. /**
  9. * 获取注解上的扫描目录
  10. * 如果没有指定,就从当前目录获取
  11. */
  12. String[] strings = declaredAnnotation.basePackages();
  13. String baseRootPackage = "";
  14. if (strings.length == 0) {
  15. baseRootPackage = cls.getPackage().getName();
  16. }
  17. return baseRootPackage;
  18. }

看到这里,我们已经拿到了项目的根目录,或者说是将要扫描的范围了

2.2 获取指定目录下的所有字节码文件

这个时候我们要知道一个基础的方法,那就是


  
  1. /**
  2. * @param className 完整类路径
  3. * @param isInitialized 是否初始化 第2个boolean参数表示类是否需要初始化Class.forName(className)默认是需要初始化。一旦初始化,就会触发目标对象的 static块代码执行,static参数也也会被再次初始化
  4. * @param classLoader 类加载器
  5. * @return
  6. */
  7. Class.forName(className, isInitialized, classLoader);

我们要用到一个工具类ClassUtils,该类中可以将根目录中所有字节码(.java,.jar文件)加载到Set<Class<?>>set中

这个工具也不是小编写的,是参考了很多博客大拿,发现都有用到,但是具体出自哪位,就不晓得了,那么也分享给大家

可以参考

GITHUB

2.3 定义自己的上下文对象接口及实现类


  
  1. /**
  2. * @Package: pig.boot.ioc.context
  3. * @Description: 上下文
  4. * @author: liuxin
  5. * @date: 2017/11/17 下午11:52
  6. */
  7. public interface ApplicationContext {
  8. Object getBean(String var1);
  9. <T> T getBean(String name, Class<T> requiredType);
  10. <T> T getBean(Class<T> name);
  11. boolean containsBean(String var1);
  12. void scan(String basePackRoot);
  13. }

  
  1. public class SmileApplicationContext implements ApplicationContext {
  2. /**
  3. * 扫描所有的类,并装载
  4. *
  5. * @param basePackRoot
  6. */
  7. @Override
  8. public void scan(String basePackRoot) {
  9. ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
  10. Set<Class<?>> classesByPackage = null;
  11. try {
  12. /**
  13. * recursively 是否从根目录,向下查找
  14. */
  15. classesByPackage = ClassUtils.getClassesByPackageName(classLoader, basePackRoot, true);
  16. } catch (IOException e) {
  17. e.printStackTrace();
  18. }
  19. /**
  20. * 加载到所有的bean
  21. */
  22. allBeans.addAll(classesByPackage);
  23. /**
  24. *扫描所有的标记,加入到容器
  25. */
  26. classesByPackage.forEach(this::scanComponent);
  27. /**
  28. * 将没有注册的bean检查,然后注入
  29. */
  30. processEarlyBeans();
  31. }
  32. }

这个类的重点方法就是scan扫描所有的标记,并从set中拿到每个字节码,传给scanComponent 方法去解析注册

这个时候我们可能遇到一种情况,就是BeanA中需要注册BeanB但是可能BeanB此时并没有解析到,那么这个时候,就要考虑,把暂时实例化不了的,放入到delayBeans,等所有能解析的解析之后,在回过头加载,我们来看这个方法,在看之前我们定义这样一个类 BeanDefinition 用处就是讲Bean的class和实例化对象都保存起来


  
  1. /**
  2. * @Package: pig.boot.ioc.context
  3. * @Description: bean描述
  4. * @author: liuxin
  5. * @date: 2017/11/17 下午11:53
  6. */
  7. public class BeanDefinition {
  8. Class<?> clazz;
  9. Object instance;
  10. public BeanDefinition(Class<?> clazz, Object instance) {
  11. this.clazz = clazz;
  12. this.instance = instance;
  13. }
  14. }

  
  1. /**
  2. * 扫描所有被标记的组件
  3. */
  4. public void scanComponent(Class<?> nextCls) {
  5. SmileComponent declaredAnnotation = nextCls.getDeclaredAnnotation(SmileComponent.class);
  6. Object beanInstance = null;
  7. if (declaredAnnotation != null) {
  8. try {
  9. beanInstance = nextCls.newInstance();
  10. String beanName = declaredAnnotation.vlaue();
  11. if (beanName.isEmpty()) {
  12. beanName = nextCls.getSimpleName();
  13. }
  14. /**
  15. * 保证bean名称的唯一性
  16. */
  17. Long beanId = beanIds.get();
  18. //将类名,首字母小写,并检查是否存在,如果存在就后面添加id,这个id是原子操作,保证唯一.
  19. beanName= getUniqueBeanNameByClassAndBeanId(nextCls,beanId);
  20. /**
  21. * 实例化里面的需要注入的字段都获取到
  22. * 如果返回true就可以直接添加到IOC容器
  23. * lastChance=true 如果注入失败就报错,false不报错,因为第一次,可能所有类没有初始化,所以等待延迟加载方法去,加载
  24. */
  25. if (autowireFields(beanInstance, nextCls, false)) {
  26. registeredBeans.put(beanName, new BeanDefinition(nextCls, beanInstance));
  27. } else {
  28. /**
  29. * 上面那种情况,可能会出现,当要注入,但是被注入的未加载到IOC容器中的情况,所以对于这种,就添加到earlyBeans中,后期注入
  30. */
  31. delayBeans.put(beanName, new BeanDefinition(nextCls, beanInstance));
  32. }
  33. /**
  34. * 获取方法上的bean
  35. * 因为方法肯定是有返回值,的返回值就是实例化对象,所以可以直接,加入到IOC容器
  36. */
  37. createBeansByMethodsOfClass(beanInstance, nextCls);
  38. } catch (Exception e) {
  39. }
  40. }
  41. }

3.测试可用性


  
  1. @SmileBootApplication
  2. public class SmileApplication {
  3. public static void main(String[] args) {
  4. SmileApplicationContext run = SmileApplication.run(SmileApplication.class, args);
  5. System.out.println(run.getBean(BeanB.class).toString());
  6. System.out.println(run.getBean(BeanA.class).beanB().toString());
  7. //BeanB{content='hi. iam is beanB'}
  8. //BeanB{content='hi. iam is beanB'}
  9. }
  10. }

  
  1. /**
  2. * @Package: pig.boot.ioc.context
  3. * @Description: 获取参数
  4. * @author: liuxin
  5. * @date: 2017/11/17 下午11:55
  6. */
  7. @SmileComponent
  8. public class BeanA {
  9. private String content;
  10. @InsertBean
  11. private BeanB beanb;
  12. public BeanA() {
  13. }
  14. public BeanA(String content) {
  15. this.content = content;
  16. }
  17. @SmileBean
  18. public BeanB beanB() {
  19. return new BeanB("hi. iam is beanB");
  20. }
  21. }

4.可扩展性

  • 定义上下文对象接口类,developer,可以定义自己的上下文类

    一个软件实体如类、模块和函数应该对扩展开放,对修改关闭

  • ApplicationContextInitialezer 初始化类执行,获取初始化条件和初始化方法,指定合适的上下文实现类

不要存在多于一个导致类变更的原因,通俗的说,即一个类只负责一项职责。

  • 小编最喜欢的方法是抽象,即具有共同特征的方法,用抽象类去实现,具体的方法有继承类去实现
  • 接口隔离,即依赖最小的接口,eg.A接口有五个方法 B此时要用3个,C要用2个,但是他们不得不全部实现.此时我们可以把A接口拆分为2个. 当D5个方法的时候,就继承2个接口,就可以

5.下篇预告

定义@SmileGetMapping,@SmilePostMapping,注解,绑定处理逻辑handler. 放入SmileNettyTaskHandler中,交给Netty处理异步处理

附录

好的代码就想一本书,读的书越多,思路就越广,想法就越多

每个开发人员要把自己当做一个工程师,而不是一个coding 的码农,工程师考虑问题要从顶层设计考虑,而不是为了单纯解决一个问题而code.

文章来源: springlearn.blog.csdn.net,作者:西魏陶渊明,版权归原作者所有,如需转载,请联系作者。

原文链接:springlearn.blog.csdn.net/article/details/102425340

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。