• 手动开发-简单的Spring基于注解配置的程序--源码解析



    在前文中 《手动开发-简单的Spring基于XML配置的程序–源码解析》,我们是从XML配置文件中去读取bean对象信息,再在自己设计的容器中进行初始化,属性注入,最后通过getBean()方法进行返回。这篇文章,我们将基于注解的视角,实现简单的Spring容器。在这里我们还将做一些改动,前文我们是通过xml文件名进行传值容器初始化,这里,我们通过传值接口类型进行初始化容器。所以本文有下面两个特色:

    • 基于注解实现Spring容器模拟
    • 通过接口类型初始化ioc容器

    @设计注解@

    Spring中有很多注解,在这里我们将自己设计一个注解进行使用。那么怎么设计注解呢?Spring的注解设计是基于 元注解实现的。元注解是Java基础,元注解如下:

    • @Target

      用于指定注解的使用范围

      • ElementType.TYPE:类、接口、注解、枚举
      • ElementType.FIELD:字段、枚举常量
      • ElementType.METHOD:方法
      • ElementType.PARAMETER:形式参数
      • ElementType.CONSTRUCTOR:构造方法
      • ElementType.LOCAL_VARIABLE:局部变量
      • ElementType.ANNOTATION_TYPE:注解
      • ElementType.PACKAGE:包
      • ElementType.TYPE_PARAMETER:类型参数
      • ElementType.TYPE_USE:类型使用
    • @Retention

      用于指定注解的保留策略

      • RetentionPolicy.SOURCE:注解只保留在源码中,在编译时会被编译器丢弃
      • RetentionPolicy.CLASS:(默认的保留策略) 注解会被保留在Class文件中,但不会被加载到虚拟机中,运行时无法获得
      • RetentionPolicy.RUNTIME:注解会被保留在Class文件中,且会被加载到虚拟机中,可以在运行时获得
    • @Documented

      • 用于将注解包含在javadoc中
      • 默认情况下,javadoc是不包括注解的,但如果使用了@Documented注解,则相关注解类型信息会被包含在生成的文档中
    • @Inherited

      用于指明父类注解会被子类继承得到

    • @Repeatable

      用于声明标记的注解为可重复类型注解,可以在同一个地方多次使用

    这些元注解就是最基本的部件,我们设计注解需要用到它们。现在我们就设计一个自己的ComponentScan注解:

    /**
     * @author linghu
     * @date 2023/8/30 13:56
     */
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ComponentScan {
        String value();
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    @Retention(RetentionPolicy.RUNTIME)表明这个注解在运行时会生效;@Target(ElementType.TYPE)表明注解可以修饰的类型,我们进入这个Type的源码,我们通过注释得知,TYPE包含了 Class, interface (including annotation type), or enum declaration,也就是可以是类,接口…:

    public enum ElementType {
        /** Class, interface (including annotation type), or enum declaration */
        TYPE,
    
        /** Field declaration (includes enum constants) */
        FIELD,
    
        /** Method declaration */
        METHOD,
    
        /** Formal parameter declaration */
        PARAMETER,
    
        /** Constructor declaration */
        CONSTRUCTOR,
    
        /** Local variable declaration */
        LOCAL_VARIABLE,
    
        /** Annotation type declaration */
        ANNOTATION_TYPE,
    
        /** Package declaration */
        PACKAGE,
    
        /**
         * Type parameter declaration
         *
         * @since 1.8
         */
        TYPE_PARAMETER,
    
        /**
         * Use of a type
         *
         * @since 1.8
         */
        TYPE_USE
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    这个时候我们为了验证我们设计的新注解ComponentScan,我们新建一个LingHuSpringConfig配置类,其实这个配置类不会具体实现什么,就是在类名上放一个注解ComponentScan,然后设置一个value值,如下:

    /**
     * @author linghu
     * @date 2023/8/30 14:09
     * 这个配置文件作用类似于beans.xml文件,用于对spring容器指定配置信息
     */
    @ComponentScan(value = "com.linghu.spring.component")
    public class LingHuSpringConfig {
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我们设置这个配置类的目的是:我们在初始化容器的时候,直接传递 LingHuSpringConfig.class接口就行了,通过接口类型初始化ioc容器,容器根据我们设计的注解去扫描这个全类路径com.linghu.spring.component

    $设计容器 $

    其实这个容器的设计和《手动开发-简单的Spring基于XML配置的程序–源码解析》讲的差不多,都需要:

    • 一个 ConcurrentHashMap作为容器
    • 一个构造器,对容器进行初始化。
    • 提供一个 getBean方法,返回我们 的ioc容器。

    这里面大部分工作是在构造器里完成的,完成的工作如下:

    • 找到 @ComponentScan配置类,并读取value值,得到类路径。
    • 通过上一步的类路径,我们需要到对应的 target目录的路径下去索引所有文件,其实就是那些.class文件,我们对这些文件进行过滤,过滤的过程中判断它们有没有加注解,如果加了就把这些文件的类路径放到ioc容器中保存下来。
    • 在对文件进行检索过滤的时候,我们需要把保存在 component文件下的.class文件的名字提取出来,然后保存这些名字到容器中。
    • 获取完整的类路径,判断这些类有没有注解:@compoment,@controller,@Service…。是不是需要注入容器
    • 获取Component注解的value值,这个值作为bean对象的id名,存到ioc容器中

    最后我们实现了,我们通过自己定义的注解,将被注解的类的类路径扫描并加入到了我们自己创建的容器ioc中,最后我们通过我们自己设计的ioc容器得到了我们需要的对象。ioc怎么帮我们创建的对象?通过反射创建的,反射所需要的类路径是我们在注解上读取过来的。

    img

    #完整代码#

    LingSpringApplicationContext.java:

    package com.linghu.spring.annotation;
    
    import org.springframework.stereotype.Component;
    import org.springframework.stereotype.Controller;
    import org.springframework.stereotype.Repository;
    import org.springframework.stereotype.Service;
    import org.springframework.util.StringUtils;
    
    import java.io.File;
    import java.lang.annotation.Annotation;
    import java.net.URL;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * @author linghu
     * @date 2023/8/30 14:13
     * 这个类充当spring原生的容器ApplicationContext
     */
    public class LingSpringApplicationContext {
        private Class configClass;
    
        //ioc里存放的是通过反射创建的对象(基于注解形式)
        private final ConcurrentHashMap<String,Object> ioc=
                new ConcurrentHashMap<>();
    
    
        public LingSpringApplicationContext(Class configClass) {
            this.configClass = configClass;
    //        System.out.println("this.configClass="+this.configClass);
    
            //获取到配置类的@ComponentScan(value = "com.linghu.spring.component")
            ComponentScan componentScan = 
                    (ComponentScan) this.configClass.getDeclaredAnnotation(ComponentScan.class);
            //取出注解的value值:com.linghu.spring.component。得到类路径,要扫描的包
            String path = componentScan.value();
    //        System.out.println("value="+value);
    
            //得到要扫描包下的资源(.class文件)
            //1、得到类的加载器
            ClassLoader classLoader =
                    LingSpringApplicationContext.class.getClassLoader();
            path = path.replace(".", "/");
    
            URL resource = classLoader.getResource(path);
    //        System.out.println("resource="+resource);
    
            //将要加载的资源(.class)路径下的文件进行遍历=》io
            File file = new File(resource.getFile());
            if (file.isDirectory()){
                File[] files = file.listFiles();
                for (File f :files) {
                    //获取"com.linghu.spring.component"下的所有class文件
                    System.out.println("===========");
                    //D:\Java\JavaProjects\spring\target\classes\com\linghu\spring\component\UserDAO.class
                    System.out.println(f.getAbsolutePath());
                    String fileAbsolutePath = f.getAbsolutePath();
    
                    //只处理.class文件
                    if (fileAbsolutePath.endsWith(".class")){
    
                    //1、获取到类名=》字符串截取
                    String className =
                            fileAbsolutePath.substring(fileAbsolutePath.lastIndexOf("\\") + 1, fileAbsolutePath.indexOf(".class"));
    //                System.out.println("className="+className);
    
                    //2、获取类的完整的路径
                    String classFullName = path.replace("/", ".") + "." + className;
                    System.out.println("classFullName="+classFullName);
    
                    //3、判断该类是不是需要注入容器,就看该类是不是有注解@compoment,@controller,@Service...
                        try {
                            //得到指定类的类对象,相当于Class.forName("com.xxx")
                            Class<?> aClass = classLoader.loadClass(classFullName);
                            if (aClass.isAnnotationPresent(Component.class)||
                                    aClass.isAnnotationPresent(Service.class)||
                                    aClass.isAnnotationPresent(Repository.class)||
                                            aClass.isAnnotationPresent(Controller.class)){
    
                                //演示一个component注解指定value,分配id
                                if (aClass.isAnnotationPresent(Component.class)){
                                    Component component = aClass.getDeclaredAnnotation(Component.class);
                                    String id = component.value();
                                    if (!"".endsWith(id)){
                                        className=id;//用户自定义的bean id 替换掉类名
                                    }
                                }
    
    
                                //这时就可以反射对象,放入到ioc容器中了
                                Class<?> clazz = Class.forName(classFullName);
                                Object instance = clazz.newInstance();//反射完成
                                //放入到容器中,将类的首字母变成小写,这里用了Stringutils
                                ioc.put(StringUtils.uncapitalize(className),instance);
    
                            }
    
                        } catch (ClassNotFoundException e) {
                            e.printStackTrace();
                        } catch (InstantiationException e) {
                            throw new RuntimeException(e);
                        } catch (IllegalAccessException e) {
                            throw new RuntimeException(e);
                        }
                    }
    
    
            }
        }
        }
    
        //返回容器中的对象
        public Object getBean(String name){
            return ioc.get(name);
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116

    Gitee:《实现Spring容器机制》

  • 相关阅读:
    使用python获取浏览器收藏夹和历史浏览记录,然后可以...
    shell脚本学习笔记02(小滴课堂)
    2.22每日一题(含绝对值的定积分+极值+凹凸区间+单调区间)
    加密货币出现反弹的迹象,牛市即将回归?
    【无标题】
    综合数据分析及可视化实战
    持续性输出,继续推荐5款好用的软件
    Vue3 快速入门及巩固基础
    Mybatis练习(多条件查询)
    点云从入门到精通技术详解100篇-基于点云数据的钢板表面三维缺陷检测(续)
  • 原文地址:https://blog.csdn.net/weixin_43891901/article/details/132789804