资讯专栏INFORMATION COLUMN

手写Spring之DI依赖注入

Cruise_Chan / 1623人阅读

摘要:如感兴趣,可移步手写之基于动态创建对象手写之基于注解动态创建对象今天将详细介绍如何手写依赖注入,在运行过程中如何动态地为对象的属性赋值。完成后在中会有相关的包出现进行注入前需要创建工厂,在运行时从工厂中取出对象为属性赋值。

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

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

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

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

首先还是创建项目,用于本次测试需要使用到junit,因此创建的是Maven项目,方便添加依赖jar包,JDK环境还是1.7:

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


  junit
  junit
  4.10
  test

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

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

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

MyComponent注解内容如下:

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();
}

接下来创建实体类:

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:

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();
    }
}

创建注解工厂类:

工厂类的内容如下:

public class AnnotationConfigApplicationContext {
    /**此Map容器用于存储类定义对象*/
    private Map> beanDefinationFacotry=new ConcurrentHashMap<>();
    /**此Map容器用于存储单例对象*/
    private Map 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> 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 getBean(String beanId, Class 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;
}

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

测试类内容如下:

@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();
    }
}

以上是所有的代码,写完之后就可以运行程序进行测试了。运行结果如下:

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

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

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

然后再次运行程序进行测试,结果如下:

现在可以看到,为两个User属性所赋的值已经是不同的对象了,无参构造方法执行了两次。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68497.html

相关文章

  • Spring IOC知识点一网打尽!

    摘要:使用的好处知乎的回答不用自己组装,拿来就用。统一配置,便于修改。 前言 只有光头才能变强 回顾前面: 给女朋友讲解什么是代理模式 包装模式就是这么简单啦 单例模式你会几种写法? 工厂模式理解了没有? 在刷Spring书籍的时候花了点时间去学习了单例模式和工厂模式,总的来说还是非常值得的! 本来想的是刷完《Spring 实战 (第4版)》和《精通Spring4.x 企业应用开发实战》...

    djfml 评论0 收藏0
  • 搞懂依赖注入, 用 PHP 手写简易 IOC 容器

    摘要:依赖注入控制反转的一种具体实现方法。接下来,我们使用依赖注入实现控制反转,使依赖关系倒置依赖被动传入。从单元测试的角度看,依赖注入更方便和操作,方便了测试人员写出质量更高的测试代码。 前言 好的设计会提高程序的可复用性和可维护性,也间接的提高了开发人员的生产力。今天,我们就来说一下在很多框架中都使用的依赖注入。 一些概念 要搞清楚什么是依赖注入如何依赖注入,首先我们要明确一些概念。 D...

    antz 评论0 收藏0
  • Spring框架我见(三)——IOC、AOP

    摘要:模块负责的所有面向切面的功能。总结的统一管理,降低了对象之间的耦合对主流的框架提供了很好的集成支持提供众多组件,事务管理,等具有高度可开放性,开发者可以自由选择部分或全部主要使用工厂模式和代理模式。 聊完了Spring框架中最重要的两种设计模式,我们来看一下Spring框架的模块和结构图。 Spring框架的结构 下图是Spring官方给出的Spring框架的结构图。 showImg(...

    khs1994 评论0 收藏0
  • 深入理解依赖注入

    摘要:上面这部分代码不变,还是通过在构造器中传入依赖的方式初始化依赖调用这里,调用方无需了解内部对的依赖。而配置一般用于上自动扫描并注入的代码如下这里只给出直接在依赖对象上添加注解的形式,还可以通过构造器和注入依赖,这里就不多说了。 前言 相信所有面试java开发的童鞋一定都被问到过是否使用过Spring,是否了解其IOC容器,为什么不直接使用工厂模式,以及究竟IOC和DI区别在于哪里这种问...

    e10101 评论0 收藏0
  • Spring旅第一站(不得不佩服老外...)

    摘要:表示少女与紧耦合在它的构造函数中自行创建了。面向切面编程往往被定义为促使软件系统实现关注点的分离一项技术系统由许多不同的组件组成,每个组件各负责一特定的功能。我们可以把切面想象为覆盖在很多组件之上的一个外壳。 第1章 Spring之旅 说明 1、本文抄写了《Spring 实战》重点内容,参考了GitHub上的代码 2、每个人的学习方式不一样,但目的是一样的,活学活用。最近一直在听《我...

    thekingisalwaysluc 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<