模拟Spring框架 有关IOC,DI的介绍和应用(上篇)

1)简述

我从百度百科中查到说,Spring框架是由于软件开发的复杂性而创建的,Spring使用的是基本的JavaBean来完成以前只可能由EJB完成的事情,可以应用于绝大部分Java应用。简言之,Spring是一个轻量级控制反转(IoC)和面向切面(AOP)的容器框架。(本篇先重点介绍IOC)

2)介绍IOC和DI

首先,我们给出一个简单例子来引出今天的问题,看下面代码;

先定义一个很简洁的类TwoClass,基本上只有toString()方法:

public class TwoClass {
	
	public TwoClass() {
	}
	
	@Override
	public String toString() {
		return "这是一个TwoClass的对象!";
	}
}

接着再继续定义一个类OneClass,里面定义了一个TwoClass类型的成员,并且进行了输出:

public class OneClass {
	
	private TwoClass two;
	
	public OneClass() {
	}
	
	public void doOnething() {
		System.out.println(two);
	}
}

来看看测试结果:

public class Test {

	public static void main(String[] args) {
		OneClass one = new OneClass();
		one.doOnething();
	}
}

执行结果如下:

null

那么,上面的运行结果肯定是错的,它错在哪?会发现在OneClass 这个类中并没有对TwoClass类型的成员two进行初始化,这两个类之间存在了依赖关系,它为null也是必然的结果。当然一般情况下,为了方便我们可以直接在OneClass这个类的无参构造中完成对two成员的初始化。但是现在因为要解释有关Spring中的IOC和DI,也就是通过一种工具,自动完成对two成员的初始化(也可以说是对two成员的“注入”),就需要把问题复杂化(也不能说复杂化,就是现在为了解释这个Spring框架,不得不抛弃了最常规的做法,用另一种方法实现,它实际上的用处是非常多,而且很方便的)。

接着来说一下上面提到的两个名词——IOC(Inversion of control)DI(Dependency Injection):IOC的意思是“反转控制”(或控制反转),拿上述例子中的两个类来说,OneClass 中存在一个TwoClass类这个类型的成员,按理说是OneClass控制了TwoClass。但我们想,要是TwoClass这个类(或者说它的对象的值)不确定的话,根本没办法进行OneClass 的完整操作,可以说实际上TwoClass牵制了有关OneClass的相关操作,所以在这里两个类之间有了一种“翻转控制”的关系。而DI的意思“依赖注入”,还是以上面两个类为例,要完整的进行OneClass类相关的操作,需要保证它里面的two成员有值才行,也就是两个类之间存在着依赖关系,就需要在用到这个TwoClass对象的时候确定他已经被赋值(或者说“注入”值)了。我个人认为,这两个词实际上是从两种不同的角度,对于同一件事进行了分析。

3)有关Spring的简单应用

(依旧以上面两个类为基础来进行)先给出一种最简单的实现;

public class Test {

	public static void main(String[] args) {
		OneClass one = new OneClass();
		Class<?>klass = one.getClass();
		
		try {
			Field field = klass.getDeclaredField("two");
			 field.setAccessible(true);
			
			 Object value = new TwoClass();
			 field.set(one, value);
			
		} catch (NoSuchFieldException e) {
			e.printStackTrace();
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}
		one.doOnething();
	}
}

最终结果变成了

这是一个TwoClass的对象!

结合代码来看,在OneClass 类中通过反射机制根据成员的具体名字得到成员,将成员设置为可更改,new出一个新的对象完成赋值。这样做OneClass 的成员two就不再是null。这种方式就是在相关的类外实现了对成员“注入”了确定的值,是Spring的初步体现,当然这是一种很简陋的实现,只是为了方便理解Spring起到的作用,下面会给出更加完整的应用实现。

4)Spring的深入理解

在具体编程之前,先明确要做些什么事:
第一步要实现的是,通过“包扫描”的方法先得到某个包里面所有的类,然后筛选找出所有存在依赖关系的类,把这些类都集合起来放到一个容器中,那在这里有个问题,怎么才能知道某一个类是否满足有依赖关系的条件,有两种具体的实现方法(本篇主要用注解的方式来解释):

1.在xml文件中事先配置好所有类和对象的映射关系,然后在使用时进行解析就可以;
2.利用“注解”的方式,对类进行标记,筛选类的时候,只要发现有特定的注解标志的,符合条件的类,就加入到容器中。

上面的两种方法都可以实现对类的选择,大体上功能是差不多的,但这两种方法各有优点,XML文件配置优点是,不侵害源代码,保证了“开闭原则”;“注解”配置的优点呢就是,程序的可读性强不需要额外代码(开发效率高)

今天我们主要通过“注解”配置的方法来实现相关操作,结合代码来看:
先给出与类标志相关的注解

@Retention(RUNTIME)
@Target(TYPE)
public @interface Component {
	boolean singleton() default true;
}
@Retention(RUNTIME)
@Target({ FIELD, METHOD })
public @interface AutoWired {
}

将用于标记类的Component 注解的保留期设置为将注解保存到JVM,以便程序运行时可以使用,要进行注解的目标是类,也就是该注解会被用到类身上,里面还有一个boolean类型的singleton属性,是不是“单例”模式,默认为单例(注意,单例还是非单例是由我们自己通过赋true或false值来决定的)。除此之外还需要标记被注入的成员,给出另一个AutoWired 注解,可以用在成员或者方法身上。有了这两个注解之后就可以开始标记类和需要注入的成员了了,上面的TwoClass和OneClass类都要加到池子里,所以都需要被注解,而OneClass类中存在需要注入的成员two也要被注解,具体如下:

@Component
//OneClass默认是单例模式
public class OneClass {
	@AutoWired
	//表明需要被注入
	private TwoClass two;
	
	@AutoWired
	public void setTwo(TwoClass two) {
		this.two = two;
	}
	
	public OneClass() {
	}
	
	public void doOnething() {
		System.out.println(two);
	}
}
@Component(singleton = false)
//将TwoClass设置为非单例模式
public class TwoClass {
	public TwoClass() {
	}
	
	@Override
	public String toString() {
		return "这是一个TwoClass的对象!";
	}
}

在把类加到池子里之前,先给一个专门的类将类和对象的映射封装起来,并且定义了singleton和inject两个成员,初值赋为单例和未注入,之后可以用来表示判断是否单例和是否已经被注入,代码如下:

public class BeanDefinition {
	
	private Class<?> klass;
	private Object object;
	private boolean singleton;
	private boolean ingect;
	
	public BeanDefinition() {
		this.singleton = true;
		this.ingect = false;
	}
	
	boolean isIngect() {
		return ingect;
	}

	void setIngect(boolean ingect) {
		this.ingect = ingect;
	}

	Class<?> getKlass() {
		return klass;
	}

	void setKlass(Class<?> klass) {
		this.klass = klass;
	}

	Object getObject() {
		return object;
	}

	void setObject(Object object) {
		this.object = object;
	}

	boolean isSingleton() {
		return singleton;
	}

	void setSingleton(boolean singleton) {
		this.singleton = singleton;
	}
}

做完这些工作之后就可以开始正式的操作了,首先通过“包扫描”得到所有的类再进一步判断筛选,看代码(给出的只是部分重点代码,并不能粘贴直接运行):
先定义一个static的map容器,顺便完成其初始化,因为是static修饰的,所以这个map容器在类加载时就完成了加载;

public static final Map<String, BeanDefinition> beanPool
		 = new HashMap<String, BeanDefinition>();

通过“包扫描”(是因为之前写过了包扫描的具体实现,并做成了架包,所以现在可以直接拿来用,这里对包扫描不做过多解释)得到所有类再进行筛选得到符合条件(例如不包括接口,数组,八大基本类型,String类型或者没有被Component注解等等)的类,之后要做的就是讲类实例化放入容器中,意味着包扫描操作完成之后,容器中就有了内容。

public static void scanPackage(String packageName) {
		
		new PackageScanner() {
			
			@Override
			public void dealClass(Class<?> klass) {
				if(klass.isPrimitive()
						|| klass == String.class
						|| klass.isAnnotation()
						|| klass.isInterface()
						|| klass.isArray()
						|| !klass.isAnnotationPresent(Component.class)) {
					
					return;
				}
								
				Object object = null;
				Component component = klass.getAnnotation(Component.class);
				Boolean singleton = component.singleton();
				
				try {
					object = klass.newInstance();
					
					BeanDefinition bdf = new BeanDefinition();
					bdf.setKlass(klass);
					bdf.setObject(object);
					bdf.setSingleton(singleton);
					
					beanPool.put(klass.getName(), bdf);
				
				} catch (InstantiationException e) {
					e.printStackTrace();
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				}
			}
		}.scanPackage(packageName);;
	}

还有一个点需要注意,我们在得到类之后需要将其实例化,是通过klass.newInstance()这种方式,但是要提醒大家的是,现在有些jdk新出来的版本取消了类.newInstance()方法的使用,在这种情况下采用下面的方法同样可以达到产生一个对象的效果

Constructor<?> constructor = klass.getConstructor(new Class<?>[0]);
constructor.newInstance(new Object[0]);

接下来要进行的是在得到某一个对象之后(可以通过类或者类名得到),要判断需不需要进行“注入”工作;

public <T> T getBean(Class<?> klass) {
		return getBean(klass.getName());
	}
public <T> T getBean(String klassName) {
	BeanDefinition bdf = beanPool.get(klassName);
		
		if(bdf == null) {
			System.out.println(klassName + "不存在");
			return null;
		}
		
		Class<?> klass = bdf.getKlass();
		
		Object object = null;
		if(bdf.isSingleton()) {
			object = bdf.getObject();
		} else {
			try {
				object = klass.newInstance();
				bdf.setObject(object);
				
			} catch (InstantiationException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			}
		}
		
		if( !bdf.isIngect() || !bdf.isSingleton()) {
			bdf.setIngect(true);
			inject(bdf);
		}
		return (T)object;
	}

如果满足没有被注入或者是非单例的,就通过以下操作完成“注入”,(可以直接针对成员本身“注入”,也可以通过成员相关的set方法完成注入);

private void inject(BeanDefinition beanDefinition) {
	Class<?> klass = beanDefinition.getKlass();
	Object object = beanDefinition.getObject();
		
		injectField(klass, object);
		injectMethod(klass, object);
	}
private void injectField(Class<?> klass,Object object) {
		
		Field[] fields = klass.getDeclaredFields();
		for(Field field : fields) {
			if(!field.isAnnotationPresent(AutoWired.class)) {
				continue;
			}
			Class<?> returnType = field.getType();
			Object value = getBean(returnType);
			
			field.setAccessible(true);
			try {
				field.set(object, value);
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			}
		}
	}
@Component
public class OneClass {
	private TwoClass two;
	
	@AutoWired
	public void setTwo(TwoClass two) {
		this.two = two;
	}
	
	public OneClass() {
	}
	
	public void doOnething() {
		System.out.println(two);
	}
}

给OneClass中增加two成员的set方法,加上AutoWired注解,下面给出通过set方法对成员进行“注入”的代码:

private <T> T injectMethod(Class<?> klass,Object object) {
		Method[] methods = klass.getDeclaredMethods();
		
		for(Method method : methods) {
			if(method.getName().startsWith("set")
					||method.getParameterCount() == 1
					||!Modifier.isPublic(method.getModifiers())
					||!method.isAnnotationPresent(AutoWired.class)) {
				continue;
			}
			Class<?>[] parametertype = method.getParameterTypes();
			Object value = getBean(parametertype[0]);
			
			try {
				method.invoke(object, new Object[] {value});
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			}
		}
		return (T)object;
	}

拿上面的两个类来说,首先调用getBean()方法得到OneClass的一个对象之后,判断它没有被注入,所以通过反射机制得到类中的所有成员,遍历成员看有没有被AutoWired注解,有的话得到成员的类型,并在容器中找到这个类的对象,完成初始化操作;或者是通过反射机制得到类中的所有方法,遍历方法进行筛选,然后通过方法 的参数类型,在容器中找到该类的对象,通过调用Invoke()方法完成初始化。

但有一点需要注意一下,在从容器中获得TwoClass的对象的时候,需要再次调用getBean()方法,继续进行判断是否单例然后产生对象,接着还需要判断是否需要注入,继续注入,由于TwoClass类中已经没有需要注入的成员了,所以操作结束。上述的一系列过程有一点“递归”的意思,相当于从一开始得到一个对象之后,就一层一层进行剖析,判断是否需要注入,需要的话在得到新的对象,以此类推,一直执行到不需要再进行注入的时候才停止。

上面说到的这种方式,可以完整地得到我们所要的结果,

public class Test {

	public static void main(String[] args) {
		BeanFactory.scanPackage("com.yyqx.mpring.demo");
		BeanFactory bdf = new BeanFactory();
		OneClass one = bdf.getBean(OneClass.class);
		one.doOnething();
	}
}

首先进行包扫描,从容器中获得OneClass 的对象,用来调用类里面的一个方法,输出一定的字符串,我们来看执行的结果:

这是一个TwoClass的对象!

结果是正确的,以上所说的方法相比于第一种提出来的操作就更复杂一点,但是代码的完整度也更高。但我要说的是,这远远不够,这种操作依旧达不到更好的要求,所以下面我们继续来改进,希望可以达到更好的实用性,能够处理多种不同的情况。

5)有关Spring的改进(Bean注解)

上面说到的通过给Autowired注解的方法对某个成员完成“注入”,这种方法存在一定的缺陷,他只能针对研究存放到容器中的类的对象,进一步完成“注入”工作。那么考虑到对于一些不能存放到容器中的类,或者其他的特殊情况,该怎么处理呢?下面就几种特殊情况进一步说明:

1.架包中的类无法操作

对于架包中的类,我们知道是无法对一个类进行修改的,它是一010二进制形式存放的,我们只能使用,无法更改源代码。这样的话,我们就没办法给这个类加上Component注解,但它又是某个类中需要注入的成员所在的类,那怎么才能把它加到容器中以供我们使用呢?可以给出另外一个类加上Component注解,把这个类先加到容器中(它跟容器中其他的类都没有依赖关系,可以说是多出来的一个类,但在这里,我们主要确保容器中的类不会缺少就好了,多一个类影响不大),在这个类中给出相关的get方法,在方法里面完成对架包中某个类的实例化,得到一个对象,将该类的对象加到容器中。如此一来,容器中就包含了两部分,有Component注解的类和通过有注解的类得到的一些不能加注解的类;
先给出一个Bean注解,适用于方法身上,

@Retention(RUNTIME)
@Target(METHOD)
public @interface Bean {
}

这里我以自己架包中有的一个类Complex为例来说明(Complex类的具体代码不需要给出,只要知道这个类是架包中的一个类,以010形式存在,我们无法更改源代码),通过另外在容器中的类来完成Complex类的初始化,如下:

@Component
public class Config {
	
	public Config() {
	}
	
	@Bean
	public Complex getComplex() {
		Complex c = new Complex(1.1, 2.8);
		return c;
	}
}

使用并解析Bean注解的具体过程来看,

private static void dealBean(Class<?> klass,Object object) {
		Method[] methods = klass.getDeclaredMethods();
		for(Method method : methods) {
			if(method.getParameterCount() > 0
					||!method.isAnnotationPresent(Bean.class)) {
				continue;
			}
			
			Class<?> returnType = method.getReturnType();
			try {
				Object value = method.invoke(object);
				BeanDefinition bdf = new BeanDefinition();
				bdf.setKlass(returnType);
				bdf.setObject(value);
				bdf.setSingleton(true);
				bdf.setIngect(false);
				
				beanPool.put(returnType.getName(), bdf);
				
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			}
		}	
	}
2.相关类没有提供可用的无参构造

前面说到的通过给类中某个成员增加set()方法来完成队成员的初始化,一般情况下都是默认通过类的无参构造来产生一个对象,即通过调用类.newInstance()来完成。但我们要知道并不是所有的类都可以newInstance生成一个对象,此时需要考虑如果相关类没有提供可以用的构造方法,意思是:某个类的相关构造方法是private修饰的,不能被外部使用,或者构造方法不能直接调用,再或者调用构造方法不能直接生成一个对象,此时就可以通过Bean注解的方法,调用合适的方法来获取该类的对象,并加入容器中;
举例:Calendar类本身就没有newInstance()方法,它是通过类直接调用静态的getInstance()方法来产生一个对象的:

@Component
public class Config {
	
	public Config() {
	}
	
	@Bean
	public Calendar getCanlendar( ) {
		Calendar calendar = Calendar.getInstance();
		return calendar;
	}
}

有关Bean注解的使用和解析已经在上面第一种情况就解释清楚了,这里不做重复说明。下面说第三种情况

3.有些类直接调用无参构造不能产生一个可以正常使用的对象

拿以前学过的数据库相关操作为例进行说明,使用数据库第一步就是连接数据库,连接过程需要生成一个对象,调用的不是类的无参构造,来看;

@Component
public class Config {
	
	public Config() {
	}

	@Bean
	public Connection getConnection() {
		Connection connection = null;
		try {
			Class.forName("com.mysql.jdbc.Driver");
			connection = DriverManager.getConnection(
					"jdbc:mysql://localhost:3306/yyqx_javase_20191118?useSSL=false", 
					"root",
					"20001128");
			
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		
		return connection;
	}
}

上面的这部分代码是连接数据库的必要操作,里面涉及到的connection对象是一个很典型的,不能通过直接调用无参构造生成一个对象就可以正常使用的例子,我们就可以给一个方法给出Bean注解,在这个方法中完成有关对象初始化的一系列相关操作。

上面所介绍的三种情况是比较特殊的,相比之下可以看出Bean注解比Autowired注解好用很多,他考虑了很多Autowired注解不能解决的情况,但是不代表Bean注解不能实现Autowired注解可以实现的东西,对于正常情况的处理,Bean注解依旧可以达到同样的效果,比如前面所说的用Autowired注解某个成员的set方法,来完成队成员的初始化,用Bean注解来实现也是可以的,如下;

@Component
public class Config {
	
	public Config() {
	}
	
	@Bean
	public TwoClass getTwpClass() {
		TwoClass two = new TwoClass();
		return two;
	}
}

有关Spring框架的多种应用就介绍完了,但这只是简单的依赖关系的处理,后续会介绍Spring框架中的“循环依赖”,总之Spring是一个应用很强悍的工具,需要反复琢磨。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值