手写Spring之DI依赖注入

前两篇文章介绍了关于手写Spring IOC控制反转,由Spring工厂在运行过程当中动态地建立对象的两种方式。如感兴趣,可移步:java

手写Spring之IOC基于xml动态建立对象spring

手写Spring之IOC基于注解动态建立对象segmentfault

今天将详细介绍如何手写Spring DI依赖注入,在运行过程当中如何动态地为对象的属性赋值。服务器

首先仍是建立项目,用于本次测试须要使用到junit,所以建立的是Maven项目,方便添加依赖jar包,JDK环境仍是1.7:ide

clipboard.png

接下来在pom.xml文件中添加junit的依赖坐标:测试

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.10</version>
  <scope>test</scope>
</dependency>

第一次添加时,若本地仓库中没有此版本的jar包,Maven会根据配置的镜像联网下载,默认是去中心仓库下载,中心仓库的服务器在国外,下载速度较慢,建议修改配置文件链接阿里云的Maven镜像仓库下载,速度较快,如何配置在此很少赘述。你也能够根据本身本地仓库已有的junit版本 对依赖坐标的版本进行修改,这样就能够直接使用本地仓库的jar包,不用耗时连外网去下载了。阿里云

完成后在Maven Dependencies中会有相关的jar包出现:url

clipboard.png

进行DI注入前须要建立工厂,在运行时从工厂中取出对象为属性赋值。所以先作一些准备工做,建立几个要用到的注解:spa

clipboard.png

MyComponent注解内容以下:prototype

package annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**@Target 属性用于注明此注解用在什么位置,
 * ElementType.TYPE表示可用在类、接口、枚举上等*/
@Target(ElementType.TYPE)
/**@Retention 属性表示所定义的注解什么时候有效,
 * RetentionPolicy.RUNTIME表示在运行时有效*/
@Retention(RetentionPolicy.RUNTIME)
/**@interface 表示注解类型*/
public @interface MyComponent {
    /**为此注解定义scope属性*/
    public String scope() default "";
}

MyAutowired注解内容以下:

package annotation;

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

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAutowired {

}

MyValue注解内容以下:

package annotation;

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

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyValue {
    /**定义value属性*/
    public String value();
}

接下来建立实体类:

clipboard.png

User实体类内容以下,实体类中的属性值暂用注解方式写死做为测试(实际中并不会这么用),此实体类暂时为单例类(不注明scope属性默认为单例模式):

@MyComponent
public class User {
    @MyValue("1")
    private Integer id;
    @MyValue("zhangsan")
    private String name;
    @MyValue("zhangsan")
    private String password;
    
    public User() {
        System.out.println("无参构造方法执行");
    }
    
    public void login(){
        System.out.println("用户登陆:id=" + id + ", name=" + name + ", password=" + password);
    }
    
    //setters和getters...
}

而后建立UserService类,在Service类中使用依赖注入User:

clipboard.png

UserService内容以下:

package service;

import annotation.MyAutowired;
import annotation.MyComponent;
import entity.User;
@MyComponent
public class UserService {

    @MyAutowired
    User user1;
    
    @MyAutowired
    User user2;
    
    public void userLogin(){
        System.out.println("用户1:"+user1);
        user1.login();
        System.out.println("用户2:"+user2);
        user2.login();
    }
}

建立注解工厂类:

clipboard.png

工厂类的内容以下:

public class AnnotationConfigApplicationContext {
    /**此Map容器用于存储类定义对象*/
    private Map<String, Class<?>> beanDefinationFacotry=new ConcurrentHashMap<>();
    /**此Map容器用于存储单例对象*/
    private Map<String,Object> singletonbeanFactory=new ConcurrentHashMap<>();
    /**有参构造方法,参数类型为指定要扫描加载的包名,此工厂可接收多个包路径*/
    public AnnotationConfigApplicationContext(String... packageNames) {
        //遍历扫描指定的全部包路径
        for (String packageName : packageNames) {
            System.out.println("开始扫描包:"+packageName);
            /**扫描指定的包路径*/
            scanPkg(packageName);
        }
        /**进行DI依赖注入*/
        dependencyInjection();
    }
}

在工厂类的构造方法中,能够接收多个包路径,而且遍历循环扫描每个包路径,扫描包的scanPkg方法以下:

/**
 * 扫描指定包,找到包中的类文件。
 * 对于标准(类上有定义注解的)类文件反射加载建立类定义对象并放入容器中
 */
private void scanPkg(final String pkg){
    //替换包名中的".",将包结构转换为目录结构
    String pkgDir=pkg.replaceAll("\\.", "/");
    //获取目录结构在类路径中的位置(其中url中封装了具体资源的路径)
    URL url=getClass().getClassLoader().getResource(pkgDir);
    //基于这个路径资源(url),构建一个文件对象
    File file=new File(url.getFile());
    //获取此目录中指定标准(以".class"结尾)的文件
    File[] fs=file.listFiles(new FileFilter() {
        @Override
        public boolean accept(File file) {
            //获取文件名
            String fName=file.getName();
            //判断该文件是否为目录,如为目录,递归进一步扫描其内部全部文件
            if(file.isDirectory()){
                scanPkg(pkg+"."+fName);
            }else{
                //断定文件的后缀是否为.class
                if(fName.endsWith(".class")){
                    return true;
                }
            }
            return false;
        }
    });    
    //遍历全部符合标准的File文件
    for(File f:fs){
        //获取文件名
        String fName=f.getName();
        //获取去除.class以后的文件名
        fName=fName.substring(0,fName.lastIndexOf("."));
        //将名字(类名,一般为大写开头)的第一个字母转换小写(用它做为key存储工厂中)
        String beanId=String.valueOf(fName.charAt(0)).toLowerCase()+fName.substring(1);
        //构建一个类全名(包名.类名)
        String pkgCls=pkg+"."+fName;
        try{
            //经过反射构建类对象
            Class<?> c=Class.forName(pkgCls);
            //断定这个类上是否有MyComponent注解
            if(c.isAnnotationPresent(MyComponent.class)){
                //将类对象存储到map容器中
                beanDefinationFacotry.put(beanId, c);
            }
        }catch(Exception e){
            throw new RuntimeException(e); 
        }
    }
}

扫描全部的包完成以后,对须要的属性进行注入,dependencyInjection方法以下:

/**
 * 此方法用于对属性进行依赖注入。
 * 从工厂中获取全部的类对象,若是类中的属性上有MyAutowired注解,
 * 那么首先从根据属性名从工厂中获取对象,或者根据对象类型获取对象。
 * 最后用该对象对属性进行注入。
 */
private void dependencyInjection(){
    //获取容器中全部的类定义对象
    Collection<Class<?>> classes = beanDefinationFacotry.values();
    //遍历每个类对象
    for (Class<?> cls : classes) {
        //获取类对象的名字全称(包名+类名)
        String clsName = cls.getName();
        //获取类名
        clsName = clsName.substring(clsName.lastIndexOf(".")+1);
        //将类名(一般为大写开头)的第一个字母转换小写
        String beanId=String.valueOf(clsName.charAt(0)).toLowerCase()+clsName.substring(1);
        //获取类中全部的属性
        Field[] fields = cls.getDeclaredFields();
        //遍历每个属性
        for (Field field : fields) {
            //若是这个属性上有MyAutowired注解,进行注入操做
            if(field.isAnnotationPresent(MyAutowired.class)){
                try {
                    //获取属性名
                    String fieldName = field.getName();
                    System.out.println("属性名:"+fieldName);
                    //定义为属性注入的bean对象(此对象从容器中获取)
                    Object fieldBean = null;
                    //首先根据属性名从容器中取出对象,若是不为null,则赋值给fieldBean对象
                    if(beanDefinationFacotry.get(fieldName) != null){
                        fieldBean = getBean(fieldName,field.getType());
                    }else{    //不然按照属性的类型从容器中取出对象进行注入
                        //获取属性的类型(包名+类名)
                        String type = field.getType().getName();
                        //截取最后的类名
                        type = type.substring(type.lastIndexOf(".")+1);
                        //将类名(一般为大写开头)的第一个字母转换小写
                        String fieldBeanId=String.valueOf(type.charAt(0)).toLowerCase()+type.substring(1);
                        System.out.println("属性类型ID:"+fieldBeanId);
                        //根据转换后的类型beanId,从容器中获取对象并赋值给fieldBean对象
                        fieldBean = getBean(fieldBeanId,field.getType());
                    }
                    System.out.println("要为属性注入的值:"+fieldBean);
                    //若是fieldBean对象不为空,则为该属性进行注入
                    if(fieldBean != null){
                        //获取此类定义的对象的实例对象
                        Object clsBean = getBean(beanId, cls);
                        //设置此属性可访问
                        field.setAccessible(true);
                        //为该属性注入值
                        field.set(clsBean, fieldBean);
                        System.out.println("注入成功!");
                    }else{
                        System.out.println("注入失败!");
                    }
                } catch (IllegalArgumentException | IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

在dependencyInjection方法中调用了getBean方法,内容以下:

/**
 * 根据传入的bean的id值获取容器中的对象,类型为Object
 */
public Object getBean(String beanId){
    //根据传入beanId获取类对象
    Class<?> cls = beanDefinationFacotry.get(beanId);
    //根据类对象获取其定义的注解
    MyComponent annotation = cls.getAnnotation(MyComponent.class);
    //获取注解的scope属性值
    String scope = annotation.scope();
    try {
        //若是scope等于singleton,建立单例对象
        if("singleton".equals(scope) || "".equals(scope)){
            //判断容器中是否已有该对象的实例,若是没有,建立一个实例对象放到容器中
            if(singletonbeanFactory.get(beanId)==null){
                Object instance = cls.newInstance();
                setFieldValues(cls,instance);
                singletonbeanFactory.put(beanId,instance);
            }
            //根据beanId获取对象并返回
            return singletonbeanFactory.get(beanId);
        }
        //若是scope等于prototype,则建立并返回多例对象
        if("prototype".equals(scope)){
            Object instance = cls.newInstance();
            setFieldValues(cls,instance);
            return instance;
        }
        //目前仅支持单例和多例两种建立对象的方式
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    //若是遭遇异常,返回null
    return null;
}
/**
 * 此为重载方法,根据传入的class对象在内部进行强转,
 * 返回传入的class对象的类型
 */
public <T>T getBean(String beanId, Class<T> c){
    return (T)getBean(beanId);
}

在getBean方法中从工厂容器中获取对象,而且须要调用setFieldValues方法为对象的属性赋值,该方法内容以下:

/**
 * 此方法用于为对象的属性赋值
 * 内部是经过获取成员属性上注解的值,在转换为类型以后,经过反射为对象赋值
 * @param cls 类定义对象
 * @param obj 要为其赋值的实例对象
 */
private void setFieldValues(Class<?> cls,Object obj){
    //获取类中全部的成员属性
    Field[] fields = cls.getDeclaredFields();
    //遍历全部属性
    for (Field field : fields) {
        //若是此属性有MyValue注解修饰,对其进行操做
        if(field.isAnnotationPresent(MyValue.class)){
            //获取属性名
            String fieldName = field.getName();
            //获取注解中的值
            String value = field.getAnnotation(MyValue.class).value();
            //获取属性所定义的类型
            String type = field.getType().getName();
            //将属性名改成以大写字母开头,如:id改成ID,name改成Name
            fieldName = String.valueOf(fieldName.charAt(0)).toUpperCase()+fieldName.substring(1);
            //set方法名称,如:setId,setName...
            String setterName = "set" + fieldName;
            try {
                //根据方法名称和参数类型获取对应的set方法对象
                Method method = cls.getDeclaredMethod(setterName, field.getType());
                //判断属性类型,如类型不一致,则转换类型后调用set方法为属性赋值
                if("java.lang.Integer".equals(type) || "int".equals(type)){
                    int intValue = Integer.valueOf(value);
                    method.invoke(obj, intValue);
                } else if("java.lang.String".equals(type)){
                    method.invoke(obj, value);
                }
                //做为测试,仅判断Integer和String类型,其它类型同理
            } catch (NoSuchMethodException | SecurityException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }
}

最后是释放工厂资源的close方法,内容以下:

/**
 * 销毁方法,用于释放资源
 */
public void close(){
    beanDefinationFacotry.clear();
    beanDefinationFacotry=null;
    singletonbeanFactory.clear();
    singletonbeanFactory=null;
}

工厂类建立完毕后,开始写测试类进行测试:

clipboard.png

测试类内容以下:

@MyComponent
public class TestSpringDi {
    /**建立AnnotationConfigApplicationContext对象*/
    AnnotationConfigApplicationContext ctx;
    /**建立UserService对象*/
    UserService userService;
    /**
     * 初始化方法
     */
    @Before
    public void init(){
        //实例化工厂类,传入entity/service/springTest三个包路径进行扫描
        ctx = new AnnotationConfigApplicationContext("entity","service","springTest");
        //调用工厂的getBean方法动态获取对象
        userService = ctx.getBean("userService",UserService.class);
    }
    /**
     * 测试方法
     */
    @Test
    public void testSpringDi(){
        userService.userLogin();
    }
    /**
     * 销毁方法
     */
    @After
    public void close(){
        ctx.close();
    }
}

以上是全部的代码,写完以后就能够运行程序进行测试了。运行结果以下:

clipboard.png

从控制台打印输出的结果能够看出,UserService类中的两个User属性都已经成功注入,并调用了模拟用户登陆的login方法,输出的结果正是为User对象所设置的值。因为User类是单例的,所以UserService中的两个User属性所注入的值都是同一个对象(根据对象所映射的地址hashcode值相同能够证实这一点),并且无参的构造方法也只执行了一次。

那么如何为多例模式的对象进行注入呢?咱们在User类的注解中加上scope属性,指定为prototype:

@MyComponent(scope="prototype")
public class User {
    ... ...
}

而后再次运行程序进行测试,结果以下:

clipboard.png

如今能够看到,为两个User属性所赋的值已是不一样的对象了,无参构造方法执行了两次。

相关文章
相关标签/搜索