简述
本文将对Spring IoC容器的仿写进行剖析,梳理仿写过程中需要解决的问题,并附全部仿写代码进行说明教学,本文的仿写中不引用任何外部类,纯手动实现简要IoC控制反转执行流程。
对IoC的作用不熟悉或不了解什么是控制反转的读者,可以先参考笔者先前的文章或查阅其他资料:IoC控制反转的原理思想
仿写Spring IoC需要解决哪些问题
想要顺利并且理解的仿写一个IoC框架,在动手前应该先理清要解决的问题,或者说IoC框架的执行流程。
笔者将IoC执行流程大致归纳为了以下几个步骤:
- 对指定包路径进行扫描,找出所有添加了IoC注解的目标类。
- 获取目标类的信息,Class对象以及类名beanName,并将其封装以便后续使用。
- 根据封装好的信息类动态创建bean对象。
- bean的自动装载。
详细流程图如下:
以上四步是IoC容器仿写的大概流程,也基本是Spring IoC运作的大致流程,下文将对具体的工作进行介绍和分析。
仿写
本部分代码解释说明性质的文字都包含在注释中,重点关注代码注释部分。
前期准备
准备测试类
首先准备测试类,非必须,本部分可以跳过,读者仿写时可自行设计,笔者仿写完整测试类代码如下(为便于理解,给出的是未添加任何注解版本):
User类:
public class User{
public User() {
}
private Long id;
private String username;
private String password;
private Relationship relationship;
private Role role;
}
Role类:
public class Role extends Relationship{
private Long id;
private String roleName;
public Role(Long id, String roleName) {
this.id = id;
this.roleName = roleName;
}
public Role() {
}
}
Relationship类:
public class Relationship {
private Long userId;
private Long RoleId;
public Relationship(Long userId, Long roleId) {
this.userId = userId;
RoleId = roleId;
}
public Relationship() {
}
}
准备IoC容器入口类
准备一个AnnotationApplicationContext,在本类中完成上文中阐述的四个步骤,实际分步骤测试时,直接准备一个主类在其main方法下创建AnnotationApplicationContext对象测试即可。
AnnotationApplicationContext类结构如下(省略了方法内的内容,下文中会给出):
public class AnnotationApplicationContext {
public static final Map<String, Object> beans = new HashMap<>();
public AnnotationApplicationContext(String packageName) {
Set<BeanDefinition> beanDefinitions = getBeanDefinitions(packageName);
createObject(beanDefinitions);
autowireObject(beanDefinitions);
}
public void autowireObject(Set<BeanDefinition> beanDefinitions){
}
public Object getBean(String beanName){
return beans.get(beanName);
}
public void createObject(Set<BeanDefinition> beanDefinitions){
}
public Set<BeanDefinition> getBeanDefinitions(String packageName){
}
}
扫描目标包下目标类
首先要进行扫描包,此步骤只负责将目标路径下的所有类,即所有.class文件保存,以便后续使用,判断类是否有IoC注解的步骤整合到下一步。
笔者仿写时单独准备了一个工具类专门用于包扫描,工具类PackageScanner代码如下:
public class PackageScanner {
public final static Set<Class<?>> clazz = new LinkedHashSet();
public static Set<Class<?>> getClasses(String packageName){
String packagePath = packageName.replace('.','/');
URL resource = Thread.currentThread().getContextClassLoader().getResource(packagePath);
if("file".equals(resource.getProtocol())){
findLocalClasses(packageName);
return clazz;
}
return null;
}
private static void findLocalClasses(String packageName){
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
URI uri = null;
try {
uri = classLoader.getResource(packageName.replace('.', '/')).toURI();
} catch (URISyntaxException e) {
throw new RuntimeException();
}
File file = new File(uri);
file.listFiles(new FileFilter() {
@Override
public boolean accept(File chiFile) {
if(chiFile.isDirectory()){
findLocalClasses(packageName + "." + chiFile.getName());
}
if(chiFile.getName().endsWith(".class")){
Class<?> c = null;
try {
c = classLoader.loadClass(packageName + "." + chiFile.getName().replace(".class", ""));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
if(!c.isInterface()){
clazz.add(c);
}
return true;
}
return false;
}
});
}
}
扫包工具类总结
在工具类PackageScanner中
- getClasses(String)方法起预处理作用。负责将类名转换为包名,并在判断传入包名是本地文件后调用findLocalClasses(String)。
- findLocalClasses(String)方法实际扫描包并保存.class文件。本方法递归调用,遇到文件夹则继续调用查找,将所有.class文件保存到一个Set集合中。
关于File部分的说明
非核心部分,只是为了更好理解代码,可忽略。
文中使用了File类中包含的方法listFiles,该方法需要传入一个文件拦截器FileFilter,并且返回一个File[]数组,本文中没有接受该数组,因为在判断过程中就已经可以将.class文件加入Set集合了。
FileFilter是一个接口,接口中只有一个accept方法需要实现,该方法返回值类型为boolean,重写accept方法时需要写出判断逻辑,因为File.listFiles方法就是依靠accept方法的返回值来判断文件是否要添加到File[]数组中。本文直接在accept中判断文件类型,如果为文件夹则递归调用findLocalClasses(String)方法向下查找,如果是.class文件则添加到Set集合。
File中的listFiles方法源码如下:
public File[] listFiles(FileFilter filter) {
String ss[] = list();
if (ss == null) return null;
ArrayList<File> files = new ArrayList<>();
for (String s : ss) {
File f = new File(s, this);
if ((filter == null) || filter.accept(f))
files.add(f);
}
return files.toArray(new File[files.size()]);
}
目标类信息封装
本部分操作均在AnnotationApplicationContext的getBeanDefinitions(String)方法下完成。
@Component注解
此处模仿Spring IoC,在目标路径下,打上了@Component注解注解的类表示该类交由IoC容器管理,在SpringBoot中经常使用的@Service、@Repository等注解与本文实现的原理大致相同,只是针对不同功能的类有不同的处理,本文只实现@Component。
笔者仿写@Component注解中value值设置了默认值,为的就是在使用@Component注解时,可以使用@Component(“xxxx”)指定beanName,也可以直接使用@Component使用默认值,直接使用时会将类名首字母小写作为默认beanName,处理详见下文。
@Component注解代码如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
String value() default "";
}
使用示例如下:
@Component
public class User{
}
@Component("USER")
public class User{
}
BeanDefinition类
BeanDefinition类用于保存每个受管对象的Class对象和类名beanName。
代码如下:
public class BeanDefinition {
private String beanName;
private Class beanClass;
public BeanDefinition(String beanName, Class beanClass) {
this.beanName = beanName;
this.beanClass = beanClass;
}
public BeanDefinition(){}
}
将目标类封装为BeanDefinition
getBeanDefinitions(String)方法中会先调用扫包工具类获取目标路径下的所有.class文件的Class对象,并使用反射技术获取每个Class类对象的注解,如果发现包含@Component注解,说明该对象应交给IoC容器管理,将Class和beanName封装成BeanDefinition添加到Set集合,最终方法返回Set集合。
getBeanDefinitions(String)方法代码如下:
public Set<BeanDefinition> getBeanDefinitions(String packageName){
Set<Class<?>> clazz = getClasses(packageName);
Set<BeanDefinition> beanDefinitions = new HashSet<>();
for(Class c : clazz){
Component component = (Component) c.getAnnotation(Component.class);
if(component != null){
String beanName = component.value();
if(beanName.equals("")){
String className = c.getSimpleName();
beanName = className.substring(0,1).toLowerCase() + className.substring(1,className.length());
}
beanDefinitions.add(new BeanDefinition(beanName,c));
}
}
return beanDefinitions;
}
动态创建bean对象
我们现在获取了目标路径下的所有受管对象的必要信息,现在可以开始根据这些信息创建bean对象了,本部分操作全部在AnnotationApplicationContext的createObject(Set<BeanDefinition>)方法下完成。
@Value注解
在创建bean对象时,开发人员期待可以对对象进行初始化操作,仍然通过注解+反射实现这种操作, 详细见下文。
@Value注解代码如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Value {
String value();
}
@Value注解代码中可以窥见一个问题,就是我们的value值是被写死为String的,因此处理时需要判断目标字段的类型并进行正确的类型转换。
使用示例如下:
@Value("1")
private Long id;
@Value("admin")
private String username;
创建bean对象
实现过程中,由于只探讨/仿写IoC核心部分,笔者对两个细枝末节的地方进行了简化。
- 默认bean对象均使用单例模式。
- 对于@Value注解,只处理了Long的类型转换,并且没有添加任何关于传入值的判断,期望使用人员每次传入的值都是合理正确的。Long类型除外的类型转换与本文的Long类型处理完全一致。
createObject(Set<BeanDefinition>)方法代码如下:
public void createObject(Set<BeanDefinition> beanDefinitions){
for(BeanDefinition b : beanDefinitions){
Class c = b.getBeanClass();
if(!beans.containsKey(b.getBeanName())) {
try {
Object obj = c.getConstructor().newInstance();
Field[] declaredFields = c.getDeclaredFields();
for (Field f: declaredFields) {
Value value = f.getAnnotation(Value.class);
if(value != null){
String fieldName = f.getName();
Method method = c.getMethod("set" + fieldName.substring(0,1).toUpperCase()
+ fieldName.substring(1,fieldName.length())
,f.getType());
if(f.getType().equals(Long.class)){
method.invoke(obj,Long.parseLong(value.value()));
}else if(f.getType().equals(String.class)){
method.invoke(obj,value.value());
}
}
}
beans.put(b.getBeanName(), obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
bean的自动装载
到了这一步,我们成功创建了目标类的对象,并且为部分字段做了初始化处理,bean对象已经接近可以使用了,此部分需要解决最后一个问题,即将bean注入到其他bean中,bean的自动装载。翻译成人话就是,我们的bean对象中可能会引用其他的bean对象,我们需要对这部分对象进行初始化处理(注入)。
Spring中提供了两种方法进行bean注入
- byName:顾名思义就是直接通过指定beanName注入。
- byType:顾名思义就是查找匹配的类型进行bean注入。
本部分操作全部在AnnotationApplicationContext的autowireObject(Set<BeanDefinition>)方法下完成。
@Qualifier注解
先从较为简单的byName入手,下文方法代码内读者也可以先挑读Qualifier部分。
@Qualifier注解代码如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Qualifier {
String value();
}
使用示例如下:
@Component
public class User{
@Qualifier("relationship")
private Relationship relationship;
}
@Autowired注解
@Autowired注解模仿Spring IoC添加一个判断是否必要注入的字段required,默认false不需要。
@Autowired注解代码如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
boolean required() default false;
}
使用示例如下:
public class User{
@Autowired(required = true)
private Role role;
}
自动装载
autowireObject(Set<BeanDefinition>)方法代码如下:
public void autowireObject(Set<BeanDefinition> beanDefinitions){
for(BeanDefinition b : beanDefinitions){
Class c = b.getBeanClass();
Field[] declaredFields = c.getDeclaredFields();
for(Field f : declaredFields){
Autowired autowired = f.getAnnotation(Autowired.class);
Qualifier qualifier = f.getAnnotation(Qualifier.class);
Object targetObj = beans.get(b.getBeanName());
Object obj = null;
try {
String fieldName = f.getName();
Method method = c.getMethod("set" + fieldName.substring(0, 1).toUpperCase()
+ fieldName.substring(1, fieldName.length())
, f.getType());
if (qualifier != null) {
obj = beans.get(qualifier.value());
if (obj == null) {
return;
}
method.invoke(targetObj,obj);
} else if (autowired != null) {
String fieldType = f.getType().getTypeName();
Collection<Object> values = beans.values();
for(Object o : values){
if(o.getClass().isAssignableFrom(f.getType())){
if(obj == null){
obj = o;
}else{
String fieldTypeName= f.getType().getSimpleName();
String beanName = fieldTypeName.substring(0,1).toLowerCase()
+ fieldTypeName.substring(1,fieldTypeName.length());
if(beans.containsKey(beanName)){
obj = beans.get(beanName);
}else{
obj = null;
}
break;
}
}
}
if(obj == null){
if(autowired.required()){
}
}else{
method.invoke(targetObj,obj);
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
}
public Object getBean(String beanName){
return beans.get(beanName);
}
至此我们的IoC框架的简单仿写已经彻底完成,从零手动实现了控制反转,下面演示代码的测试。
测试
代码结构
目录结构如下:
测试类
最总测试时entity类下三个用于测试的模拟类代码如下:
User类:
@Component
public class User{
public User() {
}
@Value("1")
private Long id;
@Value("admin")
private String username;
private String password;
@Qualifier("relationship")
private Relationship relationship;
@Autowired(required = true)
private Role role;
}
Role类:
@Component("role")
public class Role extends Relationship{
}
Relationship类:
@Component
public class Relationship {
}
测试结果
Main类代码准备如下:
public class Main {
public static void main(String[] args) {
AnnotationApplicationContext annotationApplicationContext = new AnnotationApplicationContext("Spring.SpringIoc.entity");
User user = (User) annotationApplicationContext.getBean("user");
System.out.println(user.getId());
System.out.println(user.getUsername());
System.out.println(user.getPassword());
System.out.println(user.getRole());
System.out.println(user.getRelationship());
}
}
打印结果如下:
1
admin
null
Spring.SpringIoc.entity.Role@4b1210ee
Spring.SpringIoc.entity.Relationship@4d7e1886
由打印结果可见我们从零仿写的IoC成功完成了对象的管理、注入等工作,除展示的测试部分除外,剩余各部分代码笔者均已测试无误。
|