前两篇文章介绍了关于手写Spring IOC控制反转,由Spring工厂在运行过程中动态地创建对象的两种方式。如感兴趣,可移步:
今天将详细介绍如何手写Spring DI依赖注入,在运行过程中如何动态地为对象的属性赋值。
首先还是创建项目,用于本次测试需要使用到junit,因此创建的是Maven项目,方便添加依赖jar包,JDK环境还是1.7:
接下来在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包出现:
进行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<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;
}
工厂类创建完毕后,开始写测试类进行测试:
测试类内容如下:
@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属性所赋的值已经是不同的对象了,无参构造方法执行了两次。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。