文章目录
- @设计注解@
- $设计容器 $
- #完整代码#
在前文中 《手动开发-简单的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:文章来源:https://uudwc.com/A/12ALa
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容器机制》文章来源地址https://uudwc.com/A/12ALa