spring作为java家族最为辉煌的框架究竟做对了什么让它享有如此盛誉?下面通过剖析spring ioc这个spring最为核心的功能来把玩一番
一:spring ioc的原理:
小A刚到公司老大安排了一个活,公司前不久刚开发了一个社交网站,运行不太稳定,经常会出现莫名其妙的bug,需要在必要的地方加上日志,方便找到错误,小A很快就开发好了日志记录类,为了以后的扩展性,还添加了一个接口:
public interface ILogger {
void doLog();
}
public class ConsolLogger implements ILogger {
@Override
public void doLog() {
System.out.println("打印日志到控制台");
}
}
先在添加好友功能这里增加一个日志:
public class Friend {
private ILogger logger = new ConsoleLogger ();
public void addFriend(){
System.out.println("添加好友!");
logger.doLog();//添加日志
}
}
发现好多地方需要添加的,一个一个加上去,三天后终于全部加好了。
这天老大又找到了小A:小A啊,现在日志是在控制台打印的,你看能不能保存在文件里面啊。小A皱了皱眉,先估算了一下工作量,需要先开发一个保存日志到文件的类,然后逐个修改,工作量有点大哦,而且都是重复性的工作,太没挑战,万一以后还需要再改那岂不是又要浪费几天美好时光,有没有简单点的办法呢?
工厂模式,小A突然灵光一闪,对就是它了,先写一个记录日志到文件的类:
public class FileLogger implements ILogger{
public void doLog(){
System.out.println("记录日志到文件");
}
}
再写一个工厂类:
public class LoggerFactory {
public static ILogger createLogger(){
return new FileLogger();
}
}
现在可以通过工厂创建日志对象了:
public class Friend {
private FileLogger logger = LoggerFactory.createLogger();
public void addFriend(){
System.out.println("添加好友!");
logger.doLog();//添加日志
}
}
以后再有需求变动的时候只需要修改工厂类就可以了,完美,等等似乎还有一点瑕疵,如果能够实现连工厂类都不需要修改岂不是更完美,有点得寸进尺了哦,人类的智慧是无限的看步骤:
1. 将日志的实现类的全限定名放到配置文件中
<bean id = “myLogger” class=”cn.xh.logger.FileLogger”>
2. 通过xml解析根据id从配置文件中读出日志的实现类的全限定名
3. 通过反射动态创建日志实现类的对象
修改以后的工厂类伪代码如下:
public class LoggerFactory {
public static ILogger createLogger(String id){
//解析xml根据id获取要创建的日志类的全限定名
//使用反射动态创建日志实现类对象
//将该对象返回
return ...;
}
}
如果需要修改日志类,现在只需要修改xml配置文件就可以了,完美。
这就是spring ioc实现的基本原理,当然身为一个伟大的产品,怎么可以如此简单呢.
二.Spring容器
在Spring中有一个核心概念叫容器,顾名思意,容器是用来装东西的,装的什么东西呢?就是需要管理的对象,装在哪里呢?一个HashMap中, 大致的可以理解为以对象名为键,以对象本身为值的一个HashMap,当然远比这要复杂。
我们用容器来实例化对象,管理对象之间的依赖。
在spring中有两个重要的接口:BeanFactory和ApplicationContext,所谓的容器就是实现了BeanFactory接口或者BeanFactory接口的类的实例,BeanFactory是最顶层最基本的接口,它描述了容器需要实现的最基本的功能,比如对象的注册,获取。
public interface BeanFactory {
String FACTORY_BEAN_PREFIX = "&";
/*
* 四个不同形式的getBean方法,获取实例
*/
Object getBean(String name) throws BeansException;
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
<T> T getBean(Class<T> requiredType) throws BeansException;
Object getBean(String name, Object... args) throws BeansException;
// 是否存在
boolean containsBean(String name);
// 是否为单实例
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
//是否为多例
boolean isPrototype(String name) throws NoSuchBeanDefinitionException;//
// 名称、类型是否匹配
boolean isTypeMatch(String name, Class<?> targetType)
throws NoSuchBeanDefinitionException;
// 获取类型
Class<?> getType(String name) throws NoSuchBeanDefinitionException;
// 根据实例的名字获取实例的别名
String[] getAliases(String name);
}
ApplicationContext依赖BeanFactory接口,它描述的内容更加广泛,例如资源的获取等等。
当然除了这两个接口还有很多其它接口,这里不重点讨论,附上一张图以作了解。
通常我们使用的最多的容器是实现了ApplicationContext接口的类,ClassPathXmlApplicationContext和FileSystemXmlApplicationContext
ClassPathXmlApplicationContext在类路径下寻找配置文件来实例化容器,默认是读取 src 目录下的配置文件
FileSystemXmlApplicationContext在文件系统路径下寻找配置文件来实例化容器,默认是读取项目名下一级,与src同级的配置文件
这里的配置文件是xml文件,它描述了被管理的对象和对象之间的依赖(beans.xml)。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="userDao" class="cn.xh.dao.UserDaoImpl"></bean>
</beans>
在这个配置文件中有一行配置:
<bean id="userDao" class="cn.xh.dao.UserDaoImpl"></bean>
表示使用spring容器管理的对象UserDaoImpl
加载配置文件创建容器:
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
获取容器中的对象
UserDaoImpl userDao = (UserDaoImpl) applicationContext.getBean("userDao");
我们来比较一下beanFactory和applicationContext:
1.BeanFactory接口定义了容器的最基本功能,它可以读取类的配置文档,管理类的加载,实例化,维护类之间的依赖关系。实现它的容器实例化时并不会初始化配置文件中定义的类,初始化动作发生在第一次调用时。 第一次调用创建好对象后就放入缓存中以后使用直接从缓存中获取。
2. applicationContext接口除了提供容器的基本功能外还提供了很多的扩展功能,实现它的容器实例化时就会将配置文件中定义的类初始化。
3.最常用的的容器是实现了applicationContext接口的容器ClassPathXmlApplicationContext和FileSystemXmlApplicationContext
三.spring ioc的注入方式。
spring依赖注入(DI),是spring 控制反转(IOC)的具体实现,在一个类A中需要一个成员变量B,以前是直接给B赋值,现在通过Spring容器在需要的时候将B的值注入到A对象中,简单的说,就是通过spring在适当的时候给A的成员变量B赋值。
Spring的注入有三种方式:构造方法注入,setter方法注入,接口注入,使用最广泛的是setter方法注入,接口注入的方式现在已经很少用了。我们重点介绍前面两种注入方式:
构造方法注入:
就是使用类中的构造函数,给成员变量赋值,注意这里是使用spring框架来为我们赋值,上代码,创建一个Person类,通过构造方法为其成员变量赋值。
public class Person {
private int pid;
private String pname;
private int age;
public Person(int pid, String pname, int age) {
this.pid = pid;
this.pname = pname;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"pid=" + pid +
", pname='" + pname + '\'' +
", age=" + age +
'}';
}
}
spring的配置:
<bean id="person" class="cn.xh.dao.Person">
<constructor-arg name="pid" value="1"></constructor-arg>
<constructor-arg name="pname" value="张三"></constructor-arg>
<constructor-arg name="age" value="18"></constructor-arg>
</bean>
通过<constructor-arg name="pid" value="1"></constructor-arg>给Person的成员变量赋值。
构造方法注入有它的局限性,试想一下如果有20个成员变量,构造方法的参数岂不是要20个,很不优雅,使用setter方法注入可以解决这个问题。
Setter方法注入:
在一个类中我们往往会通过get和set方法来对成员变量进行赋值和取值的操作,可以通过set***方法来给成员变量赋值。
public class Person {
private int pid;
private String pname;
private int age;
public int getPid() {
return pid;
}
public void setPid(int pid) {
this.pid = pid;
}
public String getPname() {
return pname;
}
public void setPname(String pname) {
this.pname = pname;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"pid=" + pid +
", pname='" + pname + '\'' +
", age=" + age +
'}';
}
}
spring的配置:
<bean id="person" class="cn.xh.dao.Person">
<property name="pid" value="1"></property>
<property name="pname" value="张三"></property>
<property name="age" value="18"></property>
</bean>
配置<property name="pid" value="1"></property>以后可以通过类提供的set***方法将值赋给成员变量。
使用setter方法注入有一个需要特别注意的地方,假如我们给Person类加一个有参的构造函数:
public class Person {
private int pid;
private String pname;
private int age;
public Person(int pid, String pname, int age) {
this.pid = pid;
this.pname = pname;
this.age = age;
}
…
}
程序运行的时候会报错,原因就是当我们加上有参构造函数以后就不会默认生成无参构造函数,使用setter方法注入容器启动的时候会调用无参构造函数去实例化对象,所以我们需要手动加上无参构造函数。
总结一下:
1. spring注入方式有三种构造方法注入,setter方法注入,接口注入,最常用的是setter方法注入,接口注入基本已经没人用了。
2. 构造方法注入当需要注入的成员变量很多的时候构造方法也会很长,不够优雅,使用setter方法注入相对来说就优雅的多了。