[spring学习]7、手写spring注解扫描机制

目录

前言

总体设计思路

具体功能实现

实现自定义注解

实现配置类

实现MySpring容器

提供getBean方法

提供一个getAllObjectName的方法

测试

 获取所有bean对象名称

测试getBean方法

总结


前言

        在这一篇文章中,我们将会手动实现一个和spring包扫描类似的功能,这里先说明一下这篇文章中要使用到的一些核心技术,主要用到了注解、I/O、反射,不说废话了,直接开始吧


总体设计思路

        我们会参照spring的实现思路,实现一个类似的,总体设计图如下

[spring学习]7、手写spring注解扫描机制_第1张图片


具体功能实现

实现自定义注解

        首先新建一个项目,然后创建一个包,存放自定义的一些注解

[spring学习]7、手写spring注解扫描机制_第2张图片

         自定义注解都会放到myAnnotation下面,下面开始写5个自定义注解,由于它们功能基本都一样,所以仅对@MyRepository注解进行详细的说明

        首先先创建该注解类 

public @interface MyRepository {
}

         设置@Target注解,表示修饰到哪些元素上。设置@Retention注解,表示注解保留时间

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)

         创建一个value属性,用于保存用户自定义bean的名称

    String value() default "";

         最终该注解全部内容如下

@MyRepository 

package com.ttpfx.mySpring.myAnnotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRepository {
    String value() default "";
}

         另外四个注解类似的,换个名称即可,如下

 @MyService

package com.ttpfx.mySpring.myAnnotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyService {
    String value() default "";
}

@MyController

package com.ttpfx.mySpring.myAnnotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyController {
    String value() default "";
}

@MyComponent

package com.ttpfx.mySpring.myAnnotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyComponent {
    String value() default "";
}

@MyComponentScan

package com.ttpfx.mySpring.myAnnotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyComponentScan {
    String path() default "";
}

实现配置类

        创建一个config包,然后在里面创建MySpringConfig类

[spring学习]7、手写spring注解扫描机制_第3张图片

         加上@MyComponentScan注解,最终代码如下

package com.ttpfx.mySpring.config;

import com.ttpfx.mySpring.myAnnotation.MyComponentScan;

@MyComponentScan
public class MySpringConfig {
}

实现MySpring容器

        第一步先创建MySpringApplicationContext类

[spring学习]7、手写spring注解扫描机制_第4张图片

         设置3个成员属性

    //要传入的配置类
    private Class configClass;
    //单例对象池
    private final ConcurrentHashMap singletonObjects;
    //存放要实例化的一些自定义注解
    private final Class[] classes;

         然后在代码块中进行初始化

    {
        //初始化
        singletonObjects = new ConcurrentHashMap<>();
        classes = new Class[]{MyRepository.class, MyService.class,
                MyController.class, MyComponent.class};
    }

         下一步就是创建构造器,在创建容器时要传入一个配置类

    public MySpringApplicationContext(Class configClass) {
        this.configClass = configClass;
    }

         创建一个方法,在这个方法中会根据配置类来完成初始化工作,也就是扫描包,我就创建了一个init方法

    public void init(){
        
    }

         在这个方法中,首先通过配置类拿到@MyConponentScan注解,然后通过该注解获取到要进行包扫描的路径

//得到配置类上的MyComponentScan注解
MyComponentScan annotation = configClass.getAnnotation(MyComponentScan.class);
//获取包扫描路径
String path = annotation.path();

         由于配置路径的形式是以.进行分割的,我们要将其变为/,还要注意路径中的中文问题

//处理中文
path = URLDecoder.decode(path,"UTF-8");
//用/替换.
String newPath = path.replace(".", "/");

         这个路径只是相对于该项目的,我们通过配置类获取到ClassLoader,然后获取到实际的路径,并进行拼接,获取到最终路径

//得到配置类的ClassLoader
ClassLoader classLoader = configClass.getClassLoader();
//得到最终的绝对扫描路径
String fullPath = classLoader.getResource(newPath).getPath();
//处理中文
fullPath = URLDecoder.decode(fullPath,"UTF-8");

         然后创建文件,遍历该目录下的所有文件,对于子目录,这里不进行处理

//创建文件
File rootFile = new File(fullPath);
//判断是不是一个目录
if (rootFile.isDirectory()) {
    //得到该目录下的所以文件
    File[] files = rootFile.listFiles();
    if (files == null)return;
    //遍历所以文件
    for (File file : files) {

    }
}

         判断该文件是不是一个java文件

//得到文件名
String fileName = file.getName();
//判断该文件是不是java文件
if (fileName.endsWith(".class")) {
                    
}

         得到类的全路径

//得到类名
String className = fileName.substring(0, fileName.lastIndexOf(".class"));
//得到类的全路径
String fullClassName = path + "." + className;

         得到类的加载器

//得到类的加载器
Class loadClass = classLoader.loadClass(fullClassName);

        判断该类是否存在我们自定义的那几个注解

Class legalAnnotation = null;
//对所有我们自己定义的合法注解进行遍历
for (Class aClass : classes) {
//判断该类是否存在我们自定义的合法注解
    if (loadClass.isAnnotationPresent(aClass)){
        legalAnnotation = aClass;
        break;
    }
}
if (legalAnnotation == null)return;

         下一步就是设置一个id变量,表示用户给bean设置的名字

//id表示用户给bean设置的名称
String id = "";

         然后判断该注解是那一个注解,然后获取注解的value

//判断该注解是哪一个注解
if (legalAnnotation == MyRepository.class) {
    //获取该注解
    MyRepository loadClassAnnotation = loadClass.getAnnotation(MyRepository.class);
    //获取注解的值
    id = loadClassAnnotation.value();
} else if (legalAnnotation == MyService.class) {
    MyService loadClassAnnotation = loadClass.getAnnotation(MyService.class);
    id = loadClassAnnotation.value();
} else if (legalAnnotation == MyController.class) {
    MyController loadClassAnnotation = loadClass.getAnnotation(MyController.class);
    id = loadClassAnnotation.value();
} else if (legalAnnotation == MyComponent.class) {
    MyComponent loadClassAnnotation = loadClass.getAnnotation(MyComponent.class);
    id = loadClassAnnotation.value();
}

         如果用户没有设置bean的名称,那么就默认该类目首字母小写作为名称

//如果id为空,那么就默认该类目首字母小写作为名称
if ("".equals(id)){
    id = className.substring(0,1).toLowerCase()+className.substring(1);
}

        下一步就是实例化该对象

//得到该类的实例化对象
Object o = Class.forName(fullClassName).newInstance();

         然后将该对象放入单例对象池

//将对象放入单例对象池
singletonObjects.put(id, o);

         最后,我们在构造器方法中调用init方法,然后我们的初始化容器就结束了,init方法的完整代码如下

    public void init() throws UnsupportedEncodingException, ClassNotFoundException, InstantiationException, IllegalAccessException {
        //得到配置类上的MyComponentScan注解
        MyComponentScan annotation = configClass.getAnnotation(MyComponentScan.class);
        //获取包扫描路径
        String path = annotation.path();
        //处理中文
        path = URLDecoder.decode(path, "UTF-8");
        //用/替换.
        String newPath = path.replace(".", "/");
        //得到配置类的ClassLoader
        ClassLoader classLoader = configClass.getClassLoader();
        //得到最终的绝对扫描路径
        String fullPath = classLoader.getResource(newPath).getPath();
        //处理中文
        fullPath = URLDecoder.decode(fullPath, "UTF-8");
        //创建文件
        File rootFile = new File(fullPath);
        //判断是不是一个目录
        if (rootFile.isDirectory()) {
            //得到该目录下的所以文件
            File[] files = rootFile.listFiles();
            if (files == null) return;
            //遍历所以文件
            for (File file : files) {
                //得到文件名
                String fileName = file.getName();
                //判断该文件是不是java文件
                if (fileName.endsWith(".class")) {
                    //得到类名
                    String className = fileName.substring(0, fileName.lastIndexOf(".class"));
                    //得到类的全路径
                    String fullClassName = path + "." + className;
                    //得到类的加载器
                    Class loadClass = classLoader.loadClass(fullClassName);
                    Class legalAnnotation = null;
                    //对所有我们自己定义的合法注解进行遍历
                    for (Class aClass : classes) {
                        //判断该类是否存在我们自定义的合法注解
                        if (loadClass.isAnnotationPresent(aClass)) {
                            legalAnnotation = aClass;
                            break;
                        }
                    }
                    if (legalAnnotation == null) return;
                    //id表示用户给bean设置的名称
                    String id = "";
                    //判断该注解是哪一个注解
                    if (legalAnnotation == MyRepository.class) {
                        //获取该注解
                        MyRepository loadClassAnnotation = loadClass.getAnnotation(MyRepository.class);
                        //获取注解的值
                        id = loadClassAnnotation.value();
                    } else if (legalAnnotation == MyService.class) {
                        MyService loadClassAnnotation = loadClass.getAnnotation(MyService.class);
                        id = loadClassAnnotation.value();
                    } else if (legalAnnotation == MyController.class) {
                        MyController loadClassAnnotation = loadClass.getAnnotation(MyController.class);
                        id = loadClassAnnotation.value();
                    } else if (legalAnnotation == MyComponent.class) {
                        MyComponent loadClassAnnotation = loadClass.getAnnotation(MyComponent.class);
                        id = loadClassAnnotation.value();
                    }
                    //如果id为空,那么就默认该类目首字母小写作为名称
                    if ("".equals(id)) {
                        id = className.substring(0, 1).toLowerCase() + className.substring(1);
                    }
                    //得到该类的实例化对象
                    Object o = Class.forName(fullClassName).newInstance();
                    //将对象放入单例对象池
                    singletonObjects.put(id, o);
                }
            }
        }
    }

提供getBean方法

        我们像spring那样提供getBean方法,很简单,就是根据key返回value,下面直接上代码

    public Object getBean(String name){
        return singletonObjects.get(name);
    }
    public T getBean(String name,Class aclass){
        return (T)singletonObjects.get(name);
    }

提供一个getAllObjectName的方法

        为了方便测试,我们提供一个显示所有bean名称的方法

    public String[] getAllObjectName(){
        //存放所有对象的名称
        String[] names = new String[singletonObjects.size()];
        //设置当前索引
        int index = 0;
        //获取所有的名称
        ConcurrentHashMap.KeySetView keySet = singletonObjects.keySet();
        for (String s : keySet) {
            //将名称加入到数组中
            names[index++] = s;
        }
        //返回所有对象名称
        return names;
    }

测试

        创建一个use包,里面创建4个类,分别是UserDao,UserService,UserController,UserComponent,每个类加上相应的注解。如下

UserDao

package com.ttpfx.mySpring.use;

import com.ttpfx.mySpring.myAnnotation.MyRepository;

@MyRepository
public class UserDao {
}

 UserService

package com.ttpfx.mySpring.use;

import com.ttpfx.mySpring.myAnnotation.MyService;

@MyService
public class UserService {
}

UserController

package com.ttpfx.mySpring.use;

import com.ttpfx.mySpring.myAnnotation.MyController;

@MyController
public class UserController {
}

UserComponent

package com.ttpfx.mySpring.use;

import com.ttpfx.mySpring.myAnnotation.MyController;

@MyController
public class UserController {
}

        这时我们的项目结构如下

         在MySpringConfig中配置包扫描路径,如下

package com.ttpfx.mySpring.config;

import com.ttpfx.mySpring.myAnnotation.MyComponentScan;

@MyComponentScan(path = "com.ttpfx.mySpring.use")
public class MySpringConfig {
}

         创建一个测试包,然后里面创建一个测试类,然后创建我们自己的spring容器

package com.ttpfx.mySpring.test;

import com.ttpfx.mySpring.config.MySpringConfig;
import com.ttpfx.mySpring.spring.MySpringApplicationContext;

public class MySpringTest {

    public static void main(String[] args) throws Exception {
        MySpringApplicationContext ioc = new MySpringApplicationContext(MySpringConfig.class);
    }
}

 获取所有bean对象名称

        通过前面我们编写的方法获取全部bean的名称,看是否成功将我们的对象装入到容器中

    public static void main(String[] args) throws Exception {
        MySpringApplicationContext ioc = new MySpringApplicationContext(MySpringConfig.class);
        String[] names = ioc.getAllObjectName();
        for (String name : names) {
            System.out.println(name);
        }
    }

         输入如下

[spring学习]7、手写spring注解扫描机制_第5张图片

         由于我们没有设置bean的名称,所以默认首字母小写作为bean的名称,下面我们将每个bean的名称都设置为my+类名,输出如下

[spring学习]7、手写spring注解扫描机制_第6张图片

         可以发现,我们编写的MySpring容器没有问题


测试getBean方法

        我们上面测试了获取所有bean名称的方法,现在测试获取bean的方法,由于我们的bean名称都设置为了my+类名的形式,所以我们就按照设置的来获取名称,代码如下 

    public static void main(String[] args) throws Exception {
        MySpringApplicationContext ioc = new MySpringApplicationContext(MySpringConfig.class);
        UserDao userDao = ioc.getBean("myUserDao", UserDao.class);
        UserService myUserService = ioc.getBean("myUserService", UserService.class);
        System.out.println("userDao:"+userDao);
        System.out.println("userService:"+myUserService);

    }

         控制台输出如下

[spring学习]7、手写spring注解扫描机制_第7张图片

         可以发现,我们的getBean也没有问题,到此,我们的MySpring就测试完毕了


总结

        在这篇文章中,我们自己手动实现了spring基于包扫描创建bean的机制,虽然不是很完善,但是核心功能都已经实现了,相信在自己手动实现之后,对spring又有了更深的认识。这里只是简单实现了下,在后面介绍完AOP之后,将会手动实现一个相对完善的spring,解开spring的神秘面纱,深入理解spring 

你可能感兴趣的:(spring,spring,学习,java)