文章目录
- @设计注解@
- $设计容器 $
- #完整代码#
在前文中 《手动开发-简单的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)路径下的文件进行遍历=》ioFile 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.classSystem.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,分配idif (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();//反射完成//放入到容器中,将类的首字母变成小写,这里用了Stringutilsioc.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容器机制》