Spring知识学习(一)认识Spring与Spring容器

    在我们学习某个框架时,首先应该明确三个问题:这个框架是为了解决什么问题而诞生的?这个框架的核心思想是什么?这个框架适合应用到哪些场景?其中思想是一个框架的灵魂,所以接下来我们将围绕Spring的设计思想从以下方面学习。

preview

认识 Spring 框架

Spring 框架是 Java 应用最广的框架,它的成功来源于理念,而不是技术本身。

  • Spring的核心是一个轻量级(Lightweight)的容器(Container)。
  • Spring是实现IoC(Inversion of Control)容器和非入侵性(No intrusive)的框架。
  • Spring提供AOP(Aspect-oriented programming)概念的实现方式。
  • Spring提供对持久层(Persistence)、事物(Transcation)的支持。
  • Spring供MVC Web框架的实现,并对一些常用的企业服务API(Application Interface)提供一致的模型封装。
  • Spring提供了对现存的各种框架(Structs、JSF、Hibernate、Ibatis、Webwork等)相整合的方案。

一句话概括就是,Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器(框架)

Spring框架结构和基本特征

  • Data Access/Integration层包含有JDBC、ORM、OXM、JMS和Transaction模块。
  • Web层包含了Web、Web-Servlet、WebSocket、Web-Porlet模块。
  • AOP模块提供了一个符合AOP联盟标准的面向切面编程的实现。
  • Core Container(核心容器):包含有Beans、Core、Context和SpEL模块。
  • Test模块支持使用JUnit和TestNG对Spring组件进行测试。

轻量:从大小与开销两方面而言Spring都是轻量的。完整的Spring框架可以在一个大小只有1MB多的JAR文件里发布。并且Spring所需的处理开销也是微不足道的。此外,Spring是非侵入式的:典型地,Spring应用中的对象不依赖于Spring的特定类。

控制反转IoC:Spring通过一种称作控制反转(IoC)的技术促进了低耦合。当应用了IoC,一个对象依赖的其它对象会通过被动的方式传递进来,而不是这个对象自己创建或者查找依赖对象。你可以认为IoC与JNDI相反——不是对象从容器中查找依赖,而是容器在对象初始化时不等对象请求就主动将依赖传递给它。

面向切面Aop:Spring提供了面向切面编程的丰富支持,允许通过分离应用的业务逻辑与系统级服务(例如审计(auditing)和事务(transaction)管理)进行内聚性的开发。应用对象只实现它们应该做的——完成业务逻辑——仅此而已。它们并不负责(甚至是意识)其它的系统级关注点,例如日志或事务支持。

容器:Spring包含并管理应用对象的配置和生命周期,在这个意义上它是一种容器,你可以配置你的每个bean如何被创建——基于一个可配置原型(prototype),你的bean可以创建一个单独的实例或者每次需要时都生成一个新的实例——以及它们是如何相互关联的。然而,Spring不应该被混同于传统的重量级的EJB容器,它们经常是庞大与笨重的,难以使用。

框架:Spring可以将简单的组件配置、组合成为复杂的应用。在Spring中,应用对象被声明式地组合,典型地是在一个XML文件里。Spring也提供了很多基础功能(事务管理、持久化框架集成等等),将应用逻辑的开发留给了你。

MVC:Spring的作用是整合,但不仅仅限于整合,Spring 框架可以被看做是一个企业解决方案级别的框架,Spring MVC是一个非常受欢迎的轻量级Web框架。

Spring 组成

Spring 框架是一个分层架构,由 7 个定义良好的模块组成。Spring 模块构建在核心容器之上,核心容器定义了创建、配置和管理 bean 的方式。

                            

组成 Spring 框架的每个模块(或组件)都可以单独存在,或者与其他一个或多个模块联合实现。每个模块的功能如下:

  • 核心容器:核心容器提供 Spring 框架的基本功能。核心容器的主要组件是 BeanFactory,它是工厂模式的实现。BeanFactory 使用控制反转(IOC) 模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。
  • Spring 上下文:Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息。Spring 上下文包括企业服务,例如 JNDI、EJB、电子邮件、国际化、校验和调度功能。
  • Spring AOP:通过配置管理特性,Spring AOP 模块直接将面向方面的编程功能集成到了 Spring 框架中。所以,可以很容易地使 Spring 框架管理的任何对象支持 AOP。Spring AOP 模块为基于 Spring 的应用程序中的对象提供了事务管理服务。通过使用 Spring AOP,不用依赖 EJB 组件,就可以将声明性事务管理集成到应用程序中。
  • Spring DAO:JDBC DAO 抽象层提供了有意义的异常层次结构,可用该结构来管理异常处理和不同数据库供应商抛出的错误消息。异常层次结构简化了错误处理,并且极大地降低了需要编写的异常代码数量(例如打开和关闭连接)。Spring DAO 的面向 JDBC 的异常遵从通用的 DAO 异常层次结构。
  • Spring ORM:Spring 框架插入了若干个 ORM 框架,从而提供了 ORM 的对象关系工具,其中包括 JDO、Hibernate 和 iBatis SQL Map。所有这些都遵从 Spring 的通用事务和 DAO 异常层次结构。
  • Spring Web 模块:Web 上下文模块建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了上下文。所以,Spring 框架支持与 Jakarta Struts 的集成。Web 模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。
  • Spring MVC 框架:MVC 框架是一个全功能的构建 Web 应用程序的 MVC 实现。通过策略接口,MVC 框架变成为高度可配置的,MVC 容纳了大量视图技术,其中包括 JSP、Velocity、Tiles、iText 和 POI。

Spring 容器

1、IoC 概述

1.Spring 容器

  • 在Spring中,任何的Java类和JavaBean(JavaBean是一种简单规范的Java对象)都被当作一种资源,即当作Bean处理,这些Bean通过容器管理和应用
  • Spring容器实现了IOC和AOP机制,这些机制可以简化Bean对象创建和Bean对象之间的解耦
  • Spring容器有BeanFactory和ApplicationContext两种类型
  • 何时使用Spring? 当需要管理JavaBean对象的时候就可以使用Spring,Spring是最简洁的对象管理方案之一

2.IoC 概念

IoC全称是Inversion of Control,被翻译为控制反转,IoC是指程序中对象的获取方式发生反转,由最初的new方式创建,转变为通过描述(在Java中可以是XML或者注解)然后由第三方框架创建和获取特定对象的方式

IoC按实现方法不同,可以分为依赖注入DI全称是Dependency Injection,被翻译为依赖注入,DI的基本原理后续会详细介绍)和依赖查找两种,Spring容器是采用DI方式实现了IoC控制,IoC是Spring框架的基础和核心。

IoC是一种思想,而DI是实现IOC的主要技术和途径。

3.Spring 容器的简单使用

Spring IoC容器的设计主要基于BeanFactoryApplicationContext这两个接口,其中ApplicationContext继承自BeanFactory接口,拥有更多的企业级的方法,下图为Spring相关的IoC容器的主要接口。

从图中可以看出BeanFactory位于设计最底层,所以它提供的方法和功能也较少,而ApplicationContext扩展了很多其他接口,功能十分强大,所以推荐使用ApplicationContext来实例化。

从本质上讲,BeanFactory和ApplicationContext仅仅只是一个维护Bean定义以及相互依赖关系的高级工厂接口而已,通过BeanFactory和ApplicationContext我们可以访问bean定义。

首先在容器配置文件applicationcontext.xml中添加Bean的定义

<?xml version='1.0' encoding='UTF-8' ?>
<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-4.0.xsd">

    //定义bean,这样Spring IoC容器就能在初始化的时候找到它们
    <bean id="source" class="com.ssm.chapter9.pojo.Source">
        <property name="fruit" value="橙汁"/>
        <property name="sugar" value="少糖"/>
        <property name="size" value="大杯"/>
    </bean>

    <bean id="juiceMaker2" class="com.ssm.chapter9.pojo.JuiceMaker2"
          destroy-method="destroy" init-method="init">
        <property name="beverageShop" value="贡茶"/>
        <property name="source" ref="source"/>
    </bean>

</beans>

 然后利用Application的实现类ClassPathXmlApplicationContext,创建BeanFactory和ApplicationContext容器对象

//加载文件系统的配置文件实例化
String config="applicationContext.xml所在的路径";
ApplicationContext ac = newFileSystemXmlApplicationContext(config);

//加载工程classpath下的配置文件实例化
String config="applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);

最后,调用BeanFactory容器对象的getBean()方法获取Bean的实例即可

ClassPathXmlApplicationContext ctx =
                new ClassPathXmlApplicationContext("spring-cfg.xml");
        JuiceMaker2 juiceMaker2 = (JuiceMaker2) ctx.getBean("juiceMaker2");

2、Bean的初始化

1.Bean 的初始化过程

上文简单的介绍了IoC 容器的简单使用,其实IoC 容器的初始化过程是远没有这么简单,但可以大致的理解一下其基本过程;Spring IoC容器的初始化有两大步骤:Bean的初始化依赖注入。

Bean的初始化分为3步:

  1. Resource定位,这步是是BeanDefinition的资源定位,通过XML注解都是常见的方式,ResourceLoader接口完成方式包括ClassPathContextResource,FileSystemContextResource,SerlvetContextResource
  2. BeanDefinition载入,将配置文件中的Bean,表示成IoC容器中的数据结构BeanDefinition,使IoC容器可以对其管理 
  3. 向IoC容器中注册BeanDefinition,将BeanDefinition注册到IoC容器中 ,这样开发人员就可以通过描述获取容器的Bean了

完成这3步后,Bean就在IoC容器上初始化了,接下来就得依赖注入其配置的资源,对于依赖注入,lazy-init配置选项表示是否初始化Bean,默认为false,即Spring会自动初始化不会懒加载;如果将其设置为true,那么 只有当我们使用IoC容器的 getBean()方法时才会进行初始化,完成依赖注入。

2.Bean 的实例化方法

将对象创建规则告诉Spring,Spring会帮你去创建对象:基于配置和默认规则,减少代码的书写。Spring容器创建Bean对象的方法有以下3种:

  1. 用构造器来实例化
  2. 使用静态工厂方法实例化
  3. 使用实例工厂方法实例化

使用构造器实例化:

<bean id="demo1" class="demo1路径" />
<bean name="demo2" class="demo2路径" />

id或者name属性指定Bean名称,用于从Spring中查找这个Bean对象,class用于指定Bean类型,会自动调用无参构造器创建对象。

使用静态工厂方法实例化:

<bean id="demo3" class="demo3路径" factoy-method="Static方法" />

id属性用于指定Bean名称,class属性用于指定工厂类型,factory-method属性用于指定工厂中创建Bean对象的方法,必须用Static修饰的方法。

使用实例工厂方法实例化:

<bean id="demo4" class="demo4路径" />
<bean id="demo5" factory-bean="demo4" factory-method="getTime" />

id用于指定Bean名称,factory-bean属性用于指定工厂Bean对象,factory-method属性用于指定工厂中创建Bean对象的方法

Bean 的命名:

在Spring容器中,每个Bean都需要有名字(即标识符),该名字可以用<bean>元素的id和name属性指定,id属性比name严格,要求具有唯一性,不允许用"/"等特殊字符。

为已定义好的Bean,再增加另外一个名字引用,可以使用<alias>指定,<alias name="demo6" alias="demo7" />这两个名字都可以使用。

3.Bean 的生命周期

     在IoC容器启动之后,并不会马上就实例化相应的bean,此时容器仅仅拥有所有对象的BeanDefinition(BeanDefinition是容器依赖某些工具加载的XML配置信息进行解析和分析,并将分析后的信息编组为相应的BeanDefinition)。只有当getBean()调用时才是有可能触发Bean实例化阶段的活动。

简单来说就是:

  1. Bean实例的创建(Resource定位、BeanDefinition载入和BeanDefinition注册) 
  2. 为Bean设置属性(依赖注入) 
  3. 调用Bean的初始化方法 
  4. 通过IoC容器使用Bean 
  5. 当容器销毁之前,调用Bean的销毁方法

基本生命周期流程如下:

                  
详细可分为以下几个步骤:

  1. 容器启动,实例化所有实现了BeanFactoyPostProcessor接口的类。他会在任何普通Bean实例化之前加载。
  2. 实例化剩下的Bean,对这些Bean进行依赖注入(setter等)
  3. 如果Bean实现了BeanNameAware接口,Spring将Bean的ID传递给setBeanName()方法(实现BeanNameAware主要是为了通过Bean的引用来获得Bean的ID,一般业务中是很少有用到Bean的ID的)
  4. 如果Bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory(BeanFactory bf)方法并把BeanFactory容器实例作为参数传入。(实现BeanFactoryAware 主要目的是为了获取Spring容器,如Bean通过Spring容器发布事件等)
  5. 如果Bean实现了ApplicationContextAwaer接口,Spring容器将调用setApplicationContext(ApplicationContext ctx)方法,把y应用上下文作为参数传入.(作用与BeanFactory类似都是为了获取Spring容器,不同的是Spring容器在调用setApplicationContext方法时会把它自己作为setApplicationContext 的参数传入,而Spring容器在调用setBeanDactory前需要程序员自己指定(注入)setBeanFactory里的参数BeanFactory )
  6. 如果Bean实现了BeanPostProcess接口,Spring将调用它们的postProcessBeforeInitialization(预初始化)方法(作用是在Bean实例创建成功后进行增强处理,如对Bean进行修改,增加某个功能)
  7. 如果Bean实现了InitializingBean接口,Spring将调用它们的afterPropertiesSet方法,作用与在配置文件中对Bean使用init-method声明初始化的作用一样,都是在Bean的全部属性设置成功后执行的初始化方法。
  8. 如果Bean配置有init属性,那么调用它属性中设置的方法
  9. 如果Bean实现了BeanPostProcess接口,Spring将调用它们的postProcessAfterInitialization(后初始化)方法(作用与6的一样,只不过6是在Bean初始化前执行的,而这个是在Bean初始化后执行的,时机不同 )
  10. Bean正常使用(经过以上的工作后,Bean将一直驻留在应用上下文中给应用使用,直到应用上下文被销毁)
  11. 如果Bean实现了DispostbleBean接口,Spring将调用它的destory方法,作用与在配置文件中对Bean使用destory-method属性的作用一样,都是在Bean实例销毁前执行的方法。
  12. 如果Bean配置有destory属性,那么调用它属性中设置的方法

需要注意的是,Spring IoC容器的最低要求仅仅是实现BeanFactory接口,其他都属于额外功能需求

上述8中的指定初始化回调方法init:

<bean id="demo8" class="demo8路径" init-method="init">
</bean>

上述12中的指定销毁回调方法destroy:

<bean id="demo9" class="demo9路径" destroy-method="销毁方法名">
</bean>
//提示:指定销毁回调方法,仅使用于singleton模式的Bean并且Spring会管理对象的创建过程

在顶级的<beans/>元素中的default-init-method属性和default-destory-method属性,可以为容器所有<bean>指定初始化回调方法和销毁回调方法。

<beans default-init-method="init">
<bean id="demo" class="demo路径"/>
</beans>

<beans default-destory-method="destory">
<bean id="demo" class="demo路径">
</beans>

下面以一个学生类Bean为例,来简单展示其生命周期的过程:

import org.springframework.beans.factory.BeanNameAware;

public class Student implements BeanNameAware {
	private String name;

	//无参构造方法
	public Student() {
		super();
	}

	/** 设置对象属性
	 * @param name the name to set
	 */
	public void setName(String name) {
		System.out.println("设置对象属性setName()..");
		this.name = name;
	}
	
	//Bean的初始化方法
	public void initStudent() {
		System.out.println("Student这个Bean:初始化");
	}
	
	//Bean的销毁方法
	public void destroyStudent() {
		System.out.println("Student这个Bean:销毁");
	}
	
	//Bean的使用
	public void play() {
		System.out.println("Student这个Bean:使用");
	}

	/* 重写toString
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return "Student [name = " + name + "]";
	}

	//调用BeanNameAware的setBeanName()
	//传递Bean的ID。
	@Override
	public void setBeanName(String name) {
		System.out.println("调用BeanNameAware的setBeanName()..." ); 
	}
	
}

Bean的配置(applicationContext.xml):

<?xml version="1.0" encoding="UTF-8"?>
<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">
 
   <!-- init-method:指定初始化的方法
        destroy-method:指定销毁的方法 -->
		<bean id="student" class="com.cycle.Student" init-method="initStudent" destroy-method="destroyStudent">
		<property name="name" value="LJ"></property>
		</bean>
</beans>

使用Bean:

import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class CycleTest {
    @Test
 	public void test() {
		ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
		Student student = (Student) context.getBean("student");
		//Bean的使用
		student.play();
		System.out.println(student);
		//关闭容器
		((AbstractApplicationContext) context).close();
	}
}

4.延迟实例化

在ApplicationContext实现的默认行为就是在启动时将所有singleton bean 提前实例化,如果不想让一个singleton bean在ApplicationContext 初始化时被提前实例化,可以使用<bean> 元素的lazy-init="true"属性改变,一个延迟初始化bean将在第一次被用到时实例化。

<bean id="demo10" lazy-init="true" class="demo10路径"/>

在顶级的<beans/>元素中的default-lazy-init属性,可以为容器所有<bean>指定延迟实例化特性

5.指定Bean依赖关系

当一个bean对另一个bean存在依赖关系时,可以利用<bean>元素的depends-on属性指定

<bean id="demo11" class="demo11路径" depends-on="manager" />
<bean id="manager" class="ManagerBean">

当一个bean对多个bean存在依赖关系时,depends-on属性可以指定多个bean名,用逗号隔开

<bean id="demo12" class="demo12路径" depends-on="manager1,manager2,manager3">

3、Bean的装配

在上述IoC概述中已经提到,IoC是一种设计思想,在实际环境中,实现IoC容器的方式主要有两大类:依赖查找依赖注入

而依赖注入DI是Spring IoC容器实现的主要技术,其可以分为3种方式:

  • 构造器注入
  • setter注入
  • 接口注入

构造器注入和setter注入是主要的方式,而接口注入是从别的地方注入的方式,比如在Web工程中,配置的数据源往往是通过服务器(tomcat)去配置,这个时候可以以JNDI的形式通过接口将他注入到Spring IoC容器来。

1.构造器注入

构造器注入依赖于构造方法实现,而构造方法可以是有参数的或者是无参数的。在大部分的情况下,我们都通过类的构造方法来创建类对象,Spring也可以采用反射的方式(反射的知识可见这篇文章),通过使用构造方法来完成注入,这就是构造器注入的原理。

为了让Spring完成对应的构造注入,我们有必要去描述具体的类、构造方法并设置对应的参数,这样Spring就会通过对应的信息用反射的形式创建对象。

以项目中常见的Dao层和Service层为例,Service层的对象往往需要使用Dao层的对象,那就需要Spring将Dao层的对象注入给它

public class UserServiceImp implements UserService {

	private UserDao userDao;
	private User user;

	public UserServiceImp(UserDao userDao, User user) {
		this.userDao = userDao;
		this.user = user;
	}	
	public void loginUser() {
		userDao.loginUser();
	}
}

在Spring的配置文件中注册UserService,将UserDaoJdbc通过constructor-arg标签注入到UserService的有参数的构造方法,constructor-arg标签用于定义类构造方法的参数,通过name属性指定要注入的值,与构造方法参数列表参数的顺序无关,ref属性指向其它<bean>标签的name属性;如果构造方法传入的两参数都是同类型的,为了分清哪个该赋对应值,则还可以使用index=“0”index=“1”等来指定参数的位置。

<!-- 注册userService -->
<bean id="userServiceImp" class="com.spring.service.impl.UserServiceImp">
	<constructor-arg name="userDao" ref="userDaoJdbc"></constructor-arg>
	<constructor-arg name="user" ref="user"></constructor-arg>
</bean>

<!-- 注册实体User类,用于测试 -->
<bean id="user" class="com.spring.entity.User"></bean>

<!-- 注册jdbc实现的dao -->
<bean id="userDaoJdbc" class="com.spring.dao.impl.UserDaoJdbc"></bean>

2.setter 注入

setter方式是最简单的注入方式,也是Spring主流的注入方式,它消除了使用构造器注入时多个参数造成的可读性差问题。它可以把构造方法声明为无参数的,然后再通过 setter 注入为其设置对应的值,其原理也是通过反射实现的。

如上例,Spring将Dao层的对象和实体层Entity对象注入给Service层,配置如下:

<!-- 注册userService -->
<bean id="userServiceImp" class="com.spring.service.impl.UserServiceImp">
	<!-- 写法一 User大写-->
	<!-- <property name="UserDao" ref="userDaoJdbc"></property> -->
	<!-- 写法二 user小写-->
	<property name="userDao" ref="userDaoJdbc"></property>
</bean>

<!-- 注册mybatis实现的dao -->
<bean id="userDaoJdbc" class="com.spring.dao.impl.userDaoJdbc"></bean>

需要注意的是,上面这两种写法都可以,Spring会将name值的每个单词首字母转换成大写,然后再在前面拼接上"set"构成一个方法名,然后去对应的类中查找该方法,通过反射调用,实现注入。

如果通过set方法注入属性,那么Spring会通过默认的空参构造方法来实例化对象,所以如果在类中写了一个带有参数的构造方法,一定要把空参数的构造方法写上,否则spring没有办法实例化对象,导致报错。

3.接口注入

public class ClassA {  
      private InterfaceB clzB;  
      public void doSomething() {  
        Ojbect obj = Class.forName(Config.BImplementation).newInstance();  
        clzB = (InterfaceB)obj;  
        clzB.doIt();   
      }  
}

在上述的代码中,ClassA依赖于InterfaceB的实现,我们如何获得InterfaceB的实现实例呢?传统的方法是在代码中创建 InterfaceB实现类的实例,并将赋予clzB.这样一来,ClassA在编译期即依赖于InterfaceB的实现。为了将调用者与实现者在编译期分离,于是有了上面的代码。我们根据预先在配置文件中设定的实现类的类名(Config.BImplementation),动态加载实现类,并通过InterfaceB强制转型后为ClassA所用,这就是接口注入的一个最原始的雏形。

接口注入模式因为具备侵入性,它要求组件必须与特定的接口相关联,因此并不被看好,实际使用有限。

4.装配Bean

创建应用对象之间协作关系的的行为通常称为装配(wiring),beans 本身是一个大工厂,beans中的每一个bean就等于定义了一个组件,每个组件中就是我们具体的某个功能, 通过配置Spring容器来告诉它需要加载哪些Bean和如何装配这些Bean,如此才能保证他们彼此的协作。

在 Spring 中提供了 3 种方法进行配置:

  1. 在 XML 文件中显式配置
  2. 在 Java 的接口和类中实现配置
  3. 隐式 Bean 的发现机制和自动装配原则

在现实的工作中,这 3 种方式都会被用到,并且在学习和工作之中常常混合使用,所以这里给出一些关于这 3 种优先级的建议:

  1. 通过隐式 Bean 的发现机制和自动装配的原则。基于约定由于配置的原则,这种方式应该是最优先的,减少程序开发者的决定权,简单又不失灵活。
  2. Java 接口和类中配置实现配置在没有办法使用自动装配原则的情况下应该优先考虑此类方法,可以避免 XML 配置的泛滥,也更为容易。比如一个父类有多个子类,比如学生类有两个子类,一个男学生类和女学生类,通过 IoC 容器初始化一个学生类,容器将无法知道使用哪个子类去初始化,这个时候可以使用 Java 的注解配置去指定。
  3. XML 方式配置在上述方法都无法使用的情况下,那么也只能选择 XML 配置的方式。简单易懂,当使用第三方类的时候,有些类并不是我们开发的,我们无法修改里面的代码,这个时候就通过 XML 的方式配置使用了。

(1)通过 XML 配置装配 Bean

上述依赖注入的例子都是采用XML配置的方式装配Bean的,因为XML配置是很简单明了的,XML是最常见的Spring应用系统配置源。使用 XML 装配 Bean 需要定义对应的 XML,这里需要引入对应的 XML 模式(XSD)文件,这些文件会定义配置 Spring Bean 的一些元素,一个简单的 XML 配置文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<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">
<!--Spring Bean配置代码-->
</beans>

这就只是一个格式文件,引入了一个 beans 的定义,引入了 xsd 文件,它是一个根元素,这样它所定义的元素将可以定义对应的 Spring Bean。

<!-- 配置 srouce 原料 -->
<bean name="source" class="com.pojo.Source">
    <property name="fruit" value="橙子"/>
    <property name="sugar" value="多糖"/>
    <property name="size" value="超大杯"/>
</bean>

<bean name="juickMaker" class="com.pojo.JuiceMaker">
    <!-- 注入上面配置的id为srouce的Srouce对象 -->
    <property name="source" ref="source"/>
</bean>

对于以上代码,class属性是一个类的全限定名,property 元素是定义类的属性,其中的 name 属性定义的是属性的名称,而 value 是它的值。

下半段表示注入自定义的类,这里先定义了一个 name source 的 Bean,然后再制造器中通过 ref 属性去引用对应的 Bean,而 source 正是之前定义的 Bean 的 name ,这样就可以相互引用了。

装配集合:

有些时候我们需要装配一些复杂的东西,比如 Set、Map、List、Array 和 Properties 等,假设有一个复杂类如下:

import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

public class ComplexAssembly {
 
    private Long id;
    private List<String> list;
    private Map<String, String> map;
    private Properties properties;
    private Set<String> set;
    private String[] array;

    /* setter and getter */ 
}

这个 Bean 没有任何的实际意义,只是为了介绍如何装配这些常用的集合类:

<bean id="complexAssembly" class="pojo.ComplexAssembly">
    <!-- 装配Long类型的id -->
    <property name="id" value="1"/>
 
    <!-- 装配List类型的list -->
    <property name="list">
        <list>
            <value>value-list-1</value>
            <value>value-list-2</value>
            <value>value-list-3</value>
        </list>
    </property>
 
    <!-- 装配Map类型的map -->
    <property name="map">
        <map>
            <entry key="key1" value="value-key-1"/>
            <entry key="key2" value="value-key-2"/>
            <entry key="key3" value="value-key-2"/>
        </map>
    </property>
 
    <!-- 装配Properties类型的properties -->
    <property name="properties">
        <props>
            <prop key="prop1">value-prop-1</prop>
            <prop key="prop2">value-prop-2</prop>
            <prop key="prop3">value-prop-3</prop>
        </props>
    </property>
 
    <!-- 装配Set类型的set -->
    <property name="set">
        <set>
            <value>value-set-1</value>
            <value>value-set-2</value>
            <value>value-set-3</value>
        </set>
    </property>
 
    <!-- 装配String[]类型的array -->
    <property name="array">
        <array>
            <value>value-array-1</value>
            <value>value-array-2</value>
            <value>value-array-3</value>
        </array>
    </property>
</bean>

(2)在Java中显示装配 Bean

上面,我们已经了解了如何使用 XML 的方式去装配 Bean,但是更多的时候已经不再推荐使用 XML 的方式去装配 Bean,更多的时候回考虑使用注解(annotation) 的方式去装配 Bean。

通过注解装配 Bean可以减少 XML 的配置项,采用了自动装配后,程序员所需要做的决断就少了,更加有利于对程序的开发,这就是“约定由于配置”的开发原则。

在 Spring 中,它提供了两种方式来让 Spring IoC 容器发现 Bean:

  • 组件扫描:通过定义资源的方式,让 Spring IoC 容器扫描对应的包,从而把 bean 装配进来。
  • 自动装配:通过注解定义,使得一些依赖关系可以通过注解完成。

使用@Component装配Bean

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component(value = "student1")
public class Student {

    @Value("1")
    int id;
    @Value("student_name_1")
    String name;

    // getter and setter
}

对于上述注解:

  • @Component注解:

表示 Spring IoC 会把这个类扫描成一个 Bean 实例,而其中的 value 属性代表这个类在 Spring 中的 id,这就相当于在 XML 中定义的 Bean 的 id<bean id="student1" class="pojo.Student" />,也可以简写成 @Component("student1"),甚至直接写成 @Component ,对于不写的,Spring IoC 容器就默认以类名来命名作为 id,只不过首字母小写,配置到容器中。

  • @Value注解:

表示值的注入,跟在 XML 中写 value 属性是一样的,等同于以下代码:

<bean name="student1" class="com.spring.pojo.Student">
    <property name="id" value="1" />
    <property name="name" value="student_name_1"/>
</bean>

但是现在我们声明了这个类,并不能进行任何的测试,因为 Spring IoC 并不知道这个 Bean 的存在,这个时候我们可以使用一个 StudentConfig 类去告诉 Spring IoC :

import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class StudentConfig { }

这个类十分简单,没有任何逻辑,但是需要说明两点:

  • 该类和 Student 类位于同一包名下
  • @ComponentScan注解:

代表进行扫描,默认是扫描当前包的路径,扫描所有同一包下且带有 @Component 注解的实体类。

这样一来,我们就可以通过 Spring 定义好的 Spring IoC 容器的实现类——AnnotationConfigApplicationContext 去生成 IoC 容器了:

ApplicationContext context = 
    new AnnotationConfigApplicationContext(StudentConfig.class);
Student student = (Student) context.getBean(Student.class);

但由于 @ComponentScan 注解只是扫描当前所在包的 Java 类,以及通过 @Value 注解并不能注入对象,所以上述方法局限性很大,需要进行改进, @ComponentScan 注解提供了2个配置项来解决这个问题:

  • basePackages:它是由 base 和 package 两个单词组成的,而 package 还是用了复数,意味着它可以配置一个 Java 包的数组,Spring 会根据它的配置扫描对应的包和子包,将配置好的 Bean 装配进来
  • basePackageClasses:它由 base、package 和 class 三个单词组成,采用复数,意味着它可以配置多个类, Spring 会根据配置的类所在的包,为包和子包进行扫描装配对应配置的 Bean

所以之前的辅助Spring IoC容器扫描的StudentConfig 类可以改为如下:

import org.springframework.context.annotation.ComponentScan;

@ComponentScan(basePackages = "com.spring.pojo需要扫描的包名")
public class StudentConfig { }


import org.springframework.context.annotation.ComponentScan;

@ComponentScan(basePackageClasses = com.spring.pojo.Student.class)
public class StudentConfig { }

对于 basePackages 和 basePackageClasses 的选择问题

basePackages 的可读性会更好一些,所以在项目中会优先选择使用它,但是在需要大量重构的工程中,尽量不要使用basePackages,因为很多时候重构修改包名需要反复地配置,而 IDE 不会给你任何的提示,而采用basePackageClasses会有错误提示。

(3)@Autowired自动装配

上面提到的两个弊端之一就是没有办法注入对象,通过自动装配我们将解决这个问题。

通过之前Spring IoC容器的学习,我们知道Spring是先完成Bean的定义和生成,然后再去寻找需要注入的资源,即在Spring生成了所有需要生成的Bean后,如果发现这个注解它就会在已有的Bean中查找相同的类型,找到后自动将其注入进来。

所谓自动装配技术是一种由 Spring 自己发现对应的 Bean,自动完成装配工作的方式它会应用到一个十分常用的注解 @Autowired ,这个时候 Spring 会根据类型去寻找定义的 Bean 然后将其注入。

1.先创建一个 StudentService 接口(使用接口是 Spring 推荐的方式,这样可以更为灵活,可以将定义和实现分离)

public interface StudentService {
    public void printStudentInfo();
}

2.为上面的接口创建一个 StudentServiceImp 实现类

import org.springframework.beans.factory.annotation.Autowired;
import com.spring.pojo.Student;

@Component("studentService")
public class StudentServiceImp implements StudentService {

    @Autowired
    private Student student = null;

     // getter and setter

    public void printStudentInfo() {
        System.out.println("学生的 id 为:" + student.getName());
        System.out.println("学生的 name 为:" + student.getName());
    }
}

该实现类实现了接口的 printStudentInfo() 方法,打印出成员对象 student 的相关信息,这里的 @Autowired 注解,表示在 Spring IoC 定位所有的 Bean 后,这个字段需要按类型注入,这样 IoC 容器就会寻找资源,然后将其注入。

// 第一步:修改 StudentConfig 类,告诉 Spring IoC 在哪里去扫描它:
import org.springframework.context.annotation.ComponentScan;

@ComponentScan(basePackages = {"pojo", "service"})//2个包位置
public class StudentConfig {
}

// 或者也可以在 XML 文件中声明去哪里做扫描
<context:component-scan base-package="pojo" />
<context:component-scan base-package="service" />

// 第二步:编写测试类:
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import pojo.StudentConfig;
import service.StudentService;
import service.StudentServiceImp;

public class TestSpring {

    public static void main(String[] args) {
        // 通过注解的方式初始化 Spring IoC 容器
        ApplicationContext context = 
new AnnotationConfigApplicationContext(StudentConfig.class);
        StudentService studentService = context.getBean("studentService", StudentServiceImp.class);
        studentService.printStudentInfo();
    }
}
  1. 理解:@Autowired 注解表示在 Spring IoC 定位所有的 Bean 后,再根据类型寻找资源,然后将其注入。
  2. 过程: 定义 Bean —> 初始化 Bean(扫描) —> 根据属性需要从 Spring IoC 容器中搜寻满足要求的 Bean —> 满足要求则注入
  3. 问题: IoC 容器可能会寻找失败,此时会抛出异常(默认情况下,Spring IoC 容器会认为一定要找到对应的 Bean 来注入到这个字段,但有些时候并不是一定需要,比如日志)
  4. 解决: 通过配置项 required 来改变,比如 @Autowired(required = false)

另外,@Autowired 注解不仅仅能配置在属性之上,还允许方法配置,常见的 Bean 的 setter 方法也可以使用它来完成注入,总之一切需要 Spring IoC 去寻找 Bean 资源的地方都可以用到,例如:

public class JuiceMaker {
    ......
    @Autowired
    public void setSource(Source source) {
        this.source = source;
    }
}

自动装配的歧义性(@Primary和@Qualifier)

在上面的例子中我们使用 @Autowired 注解来自动注入一个 Source 类型的 Bean 资源,但如果我们现在有两个 Srouce 类型的资源,Spring IoC 就会不知所措,不知道究竟该引入哪一个 Bean,如下

<bean name="source1" class="pojo.Source">
    <property name="fruit" value="橙子"/>
    <property name="sugar" value="多糖"/>
    <property name="size" value="超大杯"/>
</bean>
<bean name="source2" class="pojo.Source">
    <property name="fruit" value="橙子"/>
    <property name="sugar" value="少糖"/>
    <property name="size" value="小杯"/>
</bean>

我们可以会想到 Spring IoC 最底层的容器接口——BeanFactory 的定义,它存在一个按照类型获取 Bean 的方法,显然通过 Source.class 作为参数无法判断使用哪个类实例进行返回,这就是自动装配的歧义性

为了消除歧义性,Spring 提供了两个注解:

@Primary 注解:

代表首要的,当 Spring IoC 检测到有多个相同类型的 Bean 资源的时候,会优先注入使用该注解的类。该注解只是解决了首要的问题,但是并没有选择性的问题。

@Qualifier 注解:

上面所谈及的歧义性,一个重要的原因是 Spring 在寻找依赖注入的时候是按照类型注入引起的。除了按类型查找 Bean,Spring IoC 容器最底层的接口 BeanFactory 还提供了按名字查找的方法,如果按照名字来查找和注入就能消除歧义性。使用方法也很简单,直接指定名称即可:

public class JuiceMaker {
    ......
    @Autowired
    @Qualifier("source1")//表示注入的是此名称的类实例
    public void setSource(Source source) {
        this.source = source;
    }
}

(4)使用@Bean装配Bean

不论是在Java中显示的装配Bean,还是自动装配,都需要@Component注解来声明这个类会被Spring IoC扫描成Bean实例,而且只能注解在类上,当你需要引用第三方包的(jar 文件)时,往往并没有这些包的源码,这时候将无法为这些包的类加入@Component注解,让它们变成开发环境可会被Spring IoC扫描的 Bean 资源。

这个时候有一种方法是自己创建一个新的类来继承扩展包里的类,然后在新类上使用@Component注解,但这样也显得太蠢了。

所以为了不显得那么蠢,Spring提供 @Bean 注解,注解到方法之上,并将方法返回对象作为 Spring 的 Bean 资源

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

//@Configuration注解相当于XML文件的根元素,必须有,才能解析其中的 @Bean 注解
@Configuration
public class BeanTester {
//BeanTester类相当于封装文件,我们无法直接获得,但能调用其方法
    @Bean(name = "testBean")
    public String test() {
        String str = "测试@Bean注解";
        return str;
    }
}

//让测试类在Spring IoC容器中获取这个Bean
// 在 pojo 包下扫描
ApplicationContext context = new AnnotationConfigApplicationContext("pojo");
// 因为这里获取到的 Bean 就是 testBean 方法返回的对象,String类型,直接打印
System.out.println(context.getBean("testBean"));

@Bean的4个配置项:

  • name: 是一个字符串数组,允许配置多个 BeanName
  • autowire: 标志是否是一个引用的 Bean 对象,默认值是 Autowire.NO
  • initMethod: 自定义初始化方法
  • destroyMethod: 自定义销毁方法

使用@Bean注解的好处就是能够动态获取一个 Bean 对象,能够根据环境不同得到不同的 Bean 对象。或者说将 Spring 和其他组件分离(其他组件不依赖 Spring,但是又想 Spring 管理生成的 Bean)。

(5)Bean的作用域

在默认的情况下,Spring IoC 容器只会对一个 Bean 创建一个实例(单例模式,但有时候,我们希望能够通过 Spring IoC 容器获取多个实例,我们可以通过 @Scope 注解或者 <bean> 元素中的 scope 属性来设置,例如:

// XML 中设置作用域
<bean id="" class="" scope="prototype" />
// 使用注解设置作用域
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)

Spring Bean 中所说的作用域,在配置文件中即是“scope”,在面向对象程序设计中作用域一般指对象或变量之间的可见范围。而在Spring容器中是指其创建的Bean对象相对于其他Bean对象的请求可见范围

对于singleton作用域prototype作用域,Spring IoC容器的管理方式是不一样的:

对于作用域为prototype的bean,其destroy方法并没有被调用。如果bean的scope设为prototype时,当容器关闭时,destroy方法不会被调用。对于prototype作用域的bean,有一点非常重要,那就是Spring不能对一个prototype bean的整个生命周期负责:容器在初始化、配置、装饰或者是装配完一个prototype实例后,将它交给客户端,随后就对该prototype实例不闻不问了

不管何种作用域,容器都会调用所有对象的初始化生命周期回调方法。但对prototype而言,任何配置好的析构生命周期回调方法都将不会被调用。清除prototype作用域的对象并释放任何prototype bean所持有的昂贵资源,都是客户端代码的职责(让Spring容器释放被prototype作用域bean占用资源的一种可行方式是,通过使用bean的后置处理器,该处理器持有要被清除的bean的引用)。

Spring容器可以管理singleton作用域下bean的生命周期,在此作用域下,Spring能够精确地知道bean何时被创建,何时初始化完成,以及何时被销毁。而对于prototype作用域的bean,Spring只负责创建,当容器创建了bean的实例后,bean的实例就交给了客户端的代码管理,Spring容器将不再跟踪其生命周期,并且不会管理那些被配置成prototype作用域的bean的生命周期。

 

 

 

参考文章:

《Java EE 互联网轻量级框架整合开发》

  https://www.zhihu.com/question/21196869

  https://blog.csdn.net/fuzhongmin05/article/details/73389779

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值