一、概念
IoC(Inverse of Control)控制反转,及某一接口具体实现类的选择控制权从调用类中移除,转交给第三方决定,即由Spring容器借由Bean配置来进行控制。后来软件界的Martin Fowler提出了DI(Dependency Injection)依赖注入的概念用来代替IoC,即让调用类对某一接口实现类的依赖关系由第三方(容器或协作类)注入,以移除调用类对某一接口实现类的依赖。
1、IoC的类型
(1)构造函数注入:通过调用类的构造函数,将接口实现类通过构造函数变量传入。
(2)属性注入:可以有选择地通过Setter方法完成调用类所需依赖的注入。
(3)接口注入:将调用类所有依赖注入的方法抽取到一个接口中,调用类通过实现该接口提供相应的注入方法。
注:由于通过接口注入需要额外声明一个接口,增加了类的数目,而且它的效果和属性注入并无本质区别,因此不提倡采用接口注入的方式。
2、通过容器完成依赖关系的注入
Spring通过配置文件或注解描述类和类之间的依赖关系, 自动完成类的初始化和依赖注入工作。
二、Java反射机制
Class反射对象描述类语义结构,可以从Class对象中获取构造函数、成员变量、方法类等类元素的反射对象,并以编程的方式通过这些反射对象对目标类对象进行操作。这些反射对象类在java.reflect包中定义,下面是三个主要的反射类:
1、Constructor:通过Class#getConstructors()方法可以获取类的所有构造函数反射对象数组,在Java5.0中通过getConstructor(Class...parameterTypes)获取拥有特定入参的构造函数反射对象。Constructor的方法newInstance(Object[] initargs)可以创建一个对象类的实例,相当于new关键字。
2、Method:类方法的反射类,通过Class#getDeclaredMethods()方法可以获取类的所有方法反射类对象数组Method[],在Java5.0中可以通过getDeclaredMethod(String name,Class... parameterTypes)获取特定签名的方法,其中name为方法名;Class...为方法入参类型列表。Method的方法invoke(Object obj,Object[] args),可以调用该对象的方法,其中obj表示操作的目标对象;args为方法的参数。Method中的getReturnType()方法可以获取方法的返回值类型,getParameterTypes()获取方法的参数类型数组,getExceptionTypes()获取方法的异常类型数组,getParameterAnnotations()获取方法的注解信息。
3、Field:类的成员变量的反射类,通过Class#getDelaredFields()方法可以获取类的成员变量反射对象数组,通过Class#getDeclaredField(String name)可以获取某个特定名称的成员变量反射对象。Field类的set(Object obj,Object value),其中obj表示操作的目标对象,通过value为目标对象的成员变量设置值。如果成员变量为基础类型,则用户可以使用Field类中提供的带类型名的值设置方法,如setBoolean(Object obj,boolean value)等。
在访问private或者protected成员变量和方法时,必须通过setAccessible(boolean access)方法取消Java语言检查,否则将抛出IllegalAccessException。如果JVM的安全管理器设置了相应的安全机制,那么调用该方法将抛出SecurityException。如下代码:
package com.smart reflect;
public class PrivateCar{
private String color;
protected void drive(){
System.out.println("drive private car!the color is:"+color);
}
}
public class PrivateCarReflect{
public static void main(String args[])throws Throwable{
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class clazz = loader.loadClass("com.smart.reflect.PrivateCar");
PrivateCar pcar = (PrivateCar)clazz.newInstance();
Field colorFld = clazz.getDeclaredField("color");
colorFld.setAccessible(true);
colorFld.set(pcar,"红色");
Method driveMtd = clazz.getDeclaredMethod("drive", (Class[])null);
driveMtd.setAccessible(true);
driveMtd.invoke(pcar, (Object[])null);
}
}
三、资源访问
1、资源抽象接口
Spring设计了一个Resource接口,该接口拥有对应不同资源类型的实现类。Resource接口的主要方法如下:
(1)boolean exists():资源是否存在
(2)boolean isOpen():资源是否打开
(3)URL getURL()throws IOException:如果底层资源可以表示成URL,返回对应的URL对象。
(4)File getFile()throws IOException:如果底层资源对应一个文件,返回对应的File对象。
(5)InputStream getInputStream()throws IOException:返回资源对应的输入流
下图是Resource的具体实现类:
WritableResource:可写资源接口,有两个实现类FileSystemResource和PathResource。
FileSystemResource:文件系统资源,资源以文件系统路径的方式表示,如:D:/conf/bean.xml
PathResource:Spring4.0提供的读取资源文件的新类。Path封装了java.net.URL、java.nio.file.Path、文件系统资源,它使用户能够访问任何可以通过URL、Path、系统文件路径表示的资源,如文件系统的资源、HTTP资源、FTP资源等。
ByteArrayResource:二进制数组表示的资源,二进制数组资源可以在内存中通过程序构造。
ClassPathResource:类路径下的资源,资源以相对于类路径的方式表示。
InputStreamResource:以输入流返回表示的资源
ServletContextResource:为访问Web容器上下文中的资源而设计的类,负责以相对于Web应用根目录的路径加载资源。它支持以流和URL的方式访问,在WAR解包的情况下,也可以通过File方式访问。该类还可以直接从JAR包中访问资源。
UrlResource:URL封装了java.net.URL,它使用户能够访问任何可以通过URL表示的资源,如文件系统的资源、HTTP资源、FTP资源等。
假设有一个文件位于Web应用的类路径下,可以通过以下方式对此文件资源进行访问:
(1)通过FileSystemResource以文件系统绝对路径的方式进行访问
(2)通过ClassPathResource以类路径的方式进行访问
(3)通过ServletContextResource以相对于Web应用根目录的方式进行访问。
下面代码分别通过FileSystemResource和ClassPathResource访问同一个文件资源:
public class FileSourceExample{
public static void main(String args[])throws Throwable{
try{
String filePath = "D:/masterSpring/code/chapter4/src/main/resources/conf/file1.txt";
//使用系统文件路径方式加载文件
WritableResource res1 = new PathResource(filePath);
//使用WritableResource接口写资源文件
OutputStream stream1 = res1.getOutputStream();
stream1.write("Hello!");
stream1.close();
//使用类路径方式加载文件
Resource res2 = new ClassPathResource("conf/file1.txt");
//使用Resource接口读资源文件
InputStream ins1 = res1.getInputStream();
InputStream ins2 = res2.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int i;
while((i = ins1.read()) != -1)
baos.write(i);
System.out.println(baos.toString());
System.out.println("res1:" + res1.getFilename());
System.out.println("res2:" + res2.getFilename());
}catch(IOException e){
e.printStackTrace();
}
}
}
2、资源加载
(1)资源地址表达
Spring提供了一个强大的加载资源的机制,能够通过“classpath:”、“file“等资源地址前缀识别不同的资源类型,而不需要显示使用Resource的实现类,还支持Ant风格带通配符的 资源地址。如下图是资源类型的地址前缀:
当有多个JAR包或文件系统类路径都拥有一个相同的包名(如com.smart)时,"classpath:"只会在第一个加载的com.smart包的类路径下查找,而"classpath*:"会扫描所有这些JAR包及类路径下出现的com.smart类路径。
Ant风格的资源地址支持3种匹配符:
?:匹配文件名中的一个字符
*:匹配文件名中的任意字符
**:匹配多层路径
(2)资源加载器
Spring定义了一套资源加载的接口,并提供了实现类,如下图所示:
ResourceLoader接口仅有一个getResource(String location)方法,可以根据一个资源地址加载文件资源,但仅支持带资源类型前缀的表达式,不支持Ant风格的资源路径表达式。
ResourcePatternResolver扩展ResourceLoader接口,定义了一个新的接口方法getResource(String locationPattern),该方法支持带资源类型前缀及Ant风格的资源路径表达式。
PathMatchingResourcePatternResolver是Spring提供的标准实现类,如下代码使用此类,将扫描所有类路径下及JAR包中对应com.smart类包下的路径,读取所有以.xml为后缀的文件资源。
public class PatternResolverTest{
@Test
public void getResources() throws Throwable{
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource resources[] = resolver.getResources("classpath*:com/smart/**/*.xml");
assertNotNull(resources);
for(Resource resource : resources){
System.out.println(resource.getDescription());
}
}
}
注:用Resource操作文件时,如果资源配置文件在项目发布时会被打包到JAR中,那么不能使用Resource#getFile()方法,否则会抛出FileNotFoundException,但可以使用Resource#getInputStream()方法读取。
四、BeanFactory和ApplicationContext
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">
<bean id="car1" class="com.smart.Car" init-method="myInit" destroy-method="myDestory"
p:brand="红旗CA72"
p:maxSpeed="200"/>
</beans>
然后通过BeanFactory装载配置文件,启动Spring Ioc容器。下面代码通过XmlBeanDefinitionReader、DefaultListableBeanFactory实现类启动Spring IoC容器
public class BeanFactoryTest{
@Test
public void getBean() throws Throwable{
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource res = resolver.getResource("classpath:com/smart/beanfactory/beans.xml");
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(res);
Car car = factory.getBean("Car",Car.class);
car.introduce();
}
}
通过BeanFactory启动IoC容器时,并不会初始化配置文件中定义的Bean,初始化动作发生在第一个调用时。对于单例的Bean,BeanFactory会缓存Bean实例,所以第二次使用getBean()获取Bean时,将直接从IoC容器的缓存中获取Bean实例。
ApplicationContext ctx = new ClassPathXmlApplicationContext("com/smart/context/beans.xml");
如果配置文件放在文件系统的路径下,可以使用FileSystemXmlApplicationContext实现类,如下:
ApplicationContext ctx = new FileSystemXmlApplicationContext("com/smart/context/beans.xml");
还可以指定一组配置文件,Spring会自动将多个配置文件在内存中“整合”成一个配置文件,如下:
ApplicationContext ctx = new ClassPathXmlApplicationContext(new String[]{"conf/beans1.xml","conf/beans2.xml"});
在获取ApplicationContext实例后,可以调用getBean(String beanName)返回Bean了。BeanFactory在初始化容器时,并未实例化Bean,直到第一次访问某个Bean时才实例化目标Bean;而ApplicationContext在初始化应用上下文时就实例化所有单实例的Bean。
@Configuration
public class Beans {
@Bean(name = "car")
public Car buildCar() {
Car car = new Car();
car.setBrand("红旗CA72");
car.setMaxSpeed(200);
return car;
}
}
使用AnnotationConfigApplicationContext启动Spring容器:
public class AnnotationApplicationContext {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(Beans.class);
Car car =ctx.getBean("car",Car.class);
}
}
3、WebApplicationContext
ConfigurableWebApplicationContext扩展了WebApplicationContext,允许通过配置的方式实例化WebApplicationContext,此类的setServletContext(ServletContext servletContext)可以为Spring设置Web应用上下文,setConfigLocations(String[] configLocations):设置Spring配合文件地址,一般配置文件的地址是相对于Web根目录的地址,如/WEB_INF/smart-dao.xml。
WebApplicationContext的初始化需要ServletContext实例,也就是说他必须在拥有Web容器的前提下才能完成启动工作。在web.xml中配置自启动的Servlet或定义Web容器监听器(ServletContextListener),借助二者中的任一个,可以完成启动Spring Web应用上下文的工作。
<!-- 指定配置文件-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB_INF/smart-dao.xml,/WEB_INF/smart-service.xml
</param-value>
</context-param>
<!-- 声明web容器监听器-->
<listener>
<listener-class> org.springframework.web.context.ContextLoaderListener </listener-class>
</listener>
<context-param> <param-name>contextConfigLocation</param-name> <param-value> /WEB_INF/smart-dao.xml,/WEB_INF/smart-service.xml </param-value> </context-param> <servlet> <servlet-name>contextLoaderServlet</servlet-name> <servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet>
如果使用标注@Configuration的Java类提供配置信息,则web.xml需要按以下方式配置:
<webapp>
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<!-- 指定标注了@Configuration的配置类,多个可以使用逗号或空格分割-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
com.smart.AppConfig1,com.smart.AppConfig2
</param-value>
</context-param>
<!-- ContextLoaderListener监听器将根据上面配置使用
AnnotationConfigWebApplicationContext根据contextConfigLocation
指定的配置类启动Spring容器-->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
</web-app>
4、父子容器
通过HierarchicalBeanFactory接口,Spring的IoC容器可以建立父子层级关联的容器体系,自容器可以访问父容器中的Bean,但父容器不能访问子容器中的Bean。在容器内,Bean的id必须是唯一的,但子容器可以拥有一个和父容器id相同的Bean。
Spring使用父子容器实现类很多功能,比如在SpringMVC中,展现层Bean位于一个自容器中,而业务层和持久层Bean位于父容器中。这样,展现层Bean就可以引用业务层和持久层Bean,而业务层和持久层Bean则看不到展现层Bean。
五、Bean的生命周期
1、BeanFactory中Bean的生命周期
上图描述了BeanFactory中Bean的生命周期的完整过程,可以将这些方法大致划分为4类:
(1)Bean自身方法:如调用Bean构造函数实例化Bean、调用Setter设置Bean的属性值及通过<bean>的init-method和destroy-method所指定的方法。
(2)Bean级生命周期接口方法:如BeanNameAware、BeanFactoryAware、InitializingBean和DisposableBean,这些接口的方法可以由Bean类直接实现。
(3)容器级生命周期接口方法:上图中带星号的步骤是由InstantiationAwareBeanPostProcessor和BeanPostProcessor这两个接口的方法实现的,一般称它们的实现类为“后处理器”。后处理器接口一般不由Bean本身实现,它们独立于Bean,实现类以容器附加装置的形式注册到Spring容器中,并通过接口反射为Spring容器扫描识别。当Spring容器创建任何Bean的时候, 这些后处理器都会发生作用,所以这些后处理器的影响是全局性的。
(4)工厂后处理器接口方法:包括AspectJWeavingEnabler、CustomAutowireConfigurer、ConfigurationClassPostProcessor等方法。工厂后处理器也是容器级的,在应用上下文装配配置文件后立即调用。
Spring容器可以注册多个后处理器,只要它们同时实现org.springframework.core.Ordered接口,容器将按特定的顺序依次调用这些后处理器。
2、ApplicationContext中的Bean的生命周期
与BeanFactory中Bean的生命周期不同的是:
(1)如果Bean实现了org.springframework.context.ApplicationContextAware接口,则会增加一个调用该接口方法setApplicationContext()的步骤
(2)ApplicationContext会利用Java反射机制自动识别出配置文件中定义的BeanPostProcessor、InstantiationAwareBeanPostProcessor和BeanFActoryPostProcessor,并自动将它们注册到应用上下文中;而BeanFactory需要在代码中通过手工调用addBeanPostProcessor()方法进行注册。