在前文中 《手动开发-简单的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容器机制》