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

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 手动开发-简单的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();
}

@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
}

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

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

我们设置这个配置类的目的是:我们在初始化容器的时候,直接传递 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怎么帮我们创建的对象?通过反射创建的,反射所需要的类路径是我们在注解上读取过来的。

#完整代码#

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);
    }
}

Gitee:《实现Spring容器机制》

目录
相关文章
|
11天前
|
Java Spring
【Spring】方法注解@Bean,配置类扫描路径
@Bean方法注解,如何在同一个类下面定义多个Bean对象,配置扫描路径
137 73
|
6天前
|
Java Spring 容器
【SpringFramework】Spring IoC-基于注解的实现
本文主要记录基于Spring注解实现IoC容器和DI相关知识。
40 21
|
12天前
|
设计模式 XML Java
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
本文详细介绍了Spring框架的核心功能,并通过手写自定义Spring框架的方式,深入理解了Spring的IOC(控制反转)和DI(依赖注入)功能,并且学会实际运用设计模式到真实开发中。
【23种设计模式·全精解析 | 自定义Spring框架篇】Spring核心源码分析+自定义Spring的IOC功能,依赖注入功能
|
12天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
12天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
12天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
3天前
|
JSON 供应链 搜索推荐
淘宝APP分类API接口:开发、运用与收益全解析
淘宝APP作为国内领先的购物平台,拥有丰富的商品资源和庞大的用户群体。分类API接口是实现商品分类管理、查询及个性化推荐的关键工具。通过开发和使用该接口,商家可以构建分类树、进行商品查询与搜索、提供个性化推荐,从而提高销售额、增加商品曝光、提升用户体验并降低运营成本。此外,它还能帮助拓展业务范围,满足用户的多样化需求,推动电商业务的发展和创新。
18 5
|
11天前
|
存储 Java Spring
【Spring】获取Bean对象需要哪些注解
@Conntroller,@Service,@Repository,@Component,@Configuration,关于Bean对象的五个常用注解
|
11天前
|
Java Spring
【Spring配置】idea编码格式导致注解汉字无法保存
问题一:对于同一个项目,我们在使用idea的过程中,使用汉字注解完后,再打开该项目,汉字变成乱码问题二:本来a项目中,汉字注解调试好了,没有乱码了,但是创建出来的新的项目,写的注解又成乱码了。
|
8月前
|
消息中间件 SpringCloudAlibaba Java
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(八)Config服务配置+bus消息总线+stream消息驱动+Sleuth链路追踪
【Springcloud Alibaba微服务分布式架构 | Spring Cloud】之学习笔记(八)Config服务配置+bus消息总线+stream消息驱动+Sleuth链路追踪
1046 0