pom.xml
配置如下:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<groupId>com.ajngroupId>
<artifactId>springartifactId>
<version>1.0.0-SNAPSHOTversion>
<properties>
<spring.version>5.1.3.RELEASEspring.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-coreartifactId>
<version>${spring.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-beansartifactId>
<version>${spring.version}version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>${spring.version}version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-pluginartifactId>
<version>3.7.0version>
<configuration>
<source>1.8source>
<target>1.8target>
configuration>
plugin>
plugins>
build>
project>
项目的GAV(groupId, artifactId, version)配置不一定要和示例一样,可以自定义
Spring框架是一个Java平台,它为Java应用程序提供全面的基础框架支持。它也是现在非常流行的开源框架,只要是做Java开发,肯定接触过Spring的使用,不管我们对它的了解如何,多或者少,我们还是要尽力挖掘出Spring的功能价值。Spring框架的功能被分开为多个模块,如下图所示:
spring-core
:Spring核心模块spring-beans
:Bean容器支持spring-context
:建立在Bean模块基础上,扩展了功能spring-context-support
:支持整合第三方库到Spring上下文spring-expression
:提供表达式语言spring-aop
:提供AOP(页面切面编程)支持spring-aspects
:提供AspectJ的集成spring-instrumentation
:提供类植入支持和类加载器的实现spring-messaging
:消息传递模块spring-jdbc
:提供JDBC抽象层spring-tx
:支持编程和声明式事务管理spring-orm
:为流行的对象关系映射API提供集成层spring-oxm
:提供支持对象/XML映射实现的抽象层spring-jms
:用于生产和消费消息的功能spring-web
:提供基本的面向Web的集成功能spring-webmvc
:用于Web应用程序的模型-视图-控制器(MVC)和REST Web Services实现spring-websocket
:提供Web Socket的支持spring-test
:支持使用JUnit或TestNG对Spring组件进行单元测试和集成测试。在使用Maven或者其他构建工具管理项目时,可以选择性地添加Spring的依赖,使用什么或能就添加什么依赖。
这是Spring的完整使用案例,基于Tomcat Servlet容器,所有定制的业务逻辑都可以使用简单的POJO1实现,并由Spring的IoC容器进行管理,一般使用的结构是从上到下在项目中分层:
假设我们要使用Spring框架来重构一个项目,情况不允许你完全切换到另一个不同的框架。Spring框架不强制你使用它所有的功能,这不是一个全有或全无的解决方案,我们从它由多个模块组件的构成就可以看出。我们完全可以使用第三方Web框架,比如Struts、Tapestry、JSF等UI框架,它们都可以与基于Spring的中间层集成,从而使用Spring的IoC和事务等功能。而你只需要做的就是使用ApplicationContext连接你的业务逻辑,并使用WebApplicationContext来集成你的Web层。此时分层结构是:
你现在需要通过Web服务访问现有代码时,可以使用Spring的Hessian-,Rmi-或HttpInvokerProxyFactoryBean类,启用对现有应用程序的远程访问并不困难。
Spring还为EJBs2提供了一个访问和抽象层,使你能够重用现有的POJO,并将其包装在无状态会话bean中。
通过上面的使用场景了解到Spring在企业项目中的应用和定位,我们完全可以按照自己的意愿选择性地使用Spring,比如搭建一个基于SSM框架的项目:
不管使用什么框架,Java类都可以交由Spring的IoC容器来管理。
IoC(Inversion of Control,控制反转)也被称为DI(Dependency Injection,依赖注入)。在实际操作中,假如A调用了B,我们可以称A依赖于B,此时A是调用者,B是被调用者,在传统的程序设计过程中,通常由调用者来创建被调用者的实例。在Spring依赖注入的模式下,创建被调用者的工作不再由调用者来完成,因此称为控制反转,创建被调用者实例的工作由Spring容器来完成,然后注入给调用者,因为也称为依赖注入3。在Spring中完成这个功能的两个重要的接口:
BeanFactory
:提供一种高级的配置机制能够管理任何类型的对象。ApplicationContext
:是BeanFactory接口的子接口,它更容易集成Spring的AOP功能、消息资源处理、事件发布和特定的上下文应用层。比如:WebApplicationContext
。在Spring中,由Spring的IoC容器管理的对象叫做beans,bean就是由Spring IoC容器实例化、组装和以其他方式管理的对象。此外bean只是你应用中许多对象中的一个,Beans以及他们之间的依赖关系是通过容器配置元数据反映出来的。下图是Spring如何工作的展示,你应用中所有的类都由元数据组装到一起,所以当ApplicationContext创建和实例化后,你就有一个完全可配置和中执行的系统或应用。
在使用Spring框架时,配置Bean的方式有三种:
在后面我们会讲到基于三种方式的Bean配置,为了由浅入深地讲解, 使用BeanFactory
的实现类来写基于XML配置的Bean管理,使用ApplicationContext
的实现类来实现基于注解和基于Java的配置。
使用前面创建好的Maven项目开始实验,使用简单的XmlBeanFactory
来实现Spring的IoC功能。
创建两个实体类并且存在依赖关系
// 这个类是被调用方
public class BeanProvider {
public void sayHello(String name, Integer age) {
System.out.println("name = [" + name + "], age = [" + age + "]");
}
}
// 这个类是调用方
public class BeanExample {
private String name;
private Integer age;
private BeanProvider beanProvider;
// Getter and Setter
public void run() {
this.beanProvider.sayHello(this.name, this.age);
}
}
Get和Set方法省略
配置元数据使用Spring来管理类
在项目的src/main/resources
目录下创建bean.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="beanExample" class="com.ajn.spring.beanfactory.bean.BeanExample"/>
<bean id="beanProvider" class="com.ajn.spring.beanfactory.bean.BeanProvider"/>
beans>
编写主方法类开始实验
public class Main {
public static void main(String[] args) {
BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("bean.xml"));
BeanExample bean = (BeanExample) beanFactory.getBean("beanExample");
bean.run();
}
}
此时运行代码是会报空指针的,因为
BeanExample
类中依赖的beanProvider
还未注入
<bean id="beanExample" class="com.ajn.spring.beanfactory.bean.BeanExample">
<property name="name" value="ajn"/>
<property name="age" value="24"/>
<property name="beanProvider" ref="beanProvider"/>
bean>
将前面配置好的bean添加property
子标签,这些属性会通过set方法注入到创建的实例中。此时运行主方法成功输出:
name = [ajn], age = [24]
我们发现BeanExample
类不仅注入了基本数据类型,还有引用的beanProvider
。配置实例属性的值有两种方式:
value
:等值注入,将具体的值注入到实例属性中ref
:引用注入,将被依赖类的实例引用注入到属性中当有依赖注入的情况时,所有参与的类都必须交由Spring容器管理。
给BeanExample
类,添加一个全参构造方法:
public BeanExample() {
}
public BeanExample(String name, Integer age, BeanProvider beanProvider) {
this.name = name;
this.age = age;
this.beanProvider = beanProvider;
}
注上面配置的bean子标签替换成constructor-arg
配置,使用index
指定参数位置:
<bean id="beanExample" class="com.ajn.spring.beanfactory.bean.BeanExample">
<constructor-arg index="0" value="ajn"/>
<constructor-arg index="1" value="24"/>
<constructor-arg index="2" ref="beanProvider"/>
bean>
此时运行主方法输出结果是一样的,不过在这里我们要注意,Spring创建实例也是通过类的构造器来创建的,如果我们配置了一个全参的构造器,那么就无法使用无参构造器,这种情况我们在编码过程中要避免,养成一个好的习惯,创建有参构造器时,也要创建一个无参构造器。
创建一个静态工厂类如下:
public class BeanProivderFactory {
public static BeanProvider getBeanProvider() {
return new BeanProvider();
}
}
配置通过静态工厂方式注入:
<bean id="beanExample" class="com.ajn.spring.beanfactory.bean.BeanExample">
<constructor-arg index="0" value="ajn"/>
<constructor-arg index="1" value="24"/>
<constructor-arg index="2" ref="beanProviderFactory"/>
bean>
<bean id="beanProviderFactory" class="com.ajn.spring.beanfactory.bean.BeanProivderFactory" factory-method="getBeanProvider"/>
首先把工厂类的bean,然后配置factory-method
属性指定工厂方法,再把beanProvider的引用换成工厂类的引用。运行主方法会输出同样的内容。
<bean id="beanExample" class="com.ajn.spring.beanfactory.bean.BeanExample" autowire="byName">
<property name="name" value="ajn"/>
<property name="age" value="24"/>
bean>
将Bean添加属性autowire
值为byName
配置自动注入,几种常用的自动注入方式:
no
:默认值,不使用自动装配,使用
注入byName
:通过属性名称和bean的名称自动注入byType
:通过属性类型和bean的类型自动注入constructor
:通过构造器参数以byType
的方式注入可以在
标签添加 default-autowire
属性来指定默认自动注入方式。此时Spring中所有的bean都是自动注入的候选者,有时候我们不希望其中某一个bean通过自动注入机制注入到其他bean,可以在这个bean上添加 autowire-candidate="false"
属性,这样就从自动注入中排除这个bean。还有一种情况,当有多个候选者bean都符合注入的要求时,比如一个接口有多个实现类,这时Spring不知道会注入哪一个,这时可以在一个bean上的
标签上添加 primary="true"
属性来确定主要候选者。
在
根节点上可以配置很多默认属性,自己慢慢去挖掘。
这里我们最简单地展示注入,还有一些特殊情况配置,比如:注入内部类、注入集合、注入空值等。
有时候Bean之间的依赖关系并不是特别明确,比如初始化一个静态类,可以使用depends-on
属性来明确指定在初始化该bean之前,强制初始化一个或多个bean。例如:
<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
<property name="manager" ref="manager" />
bean>
<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />
depends-on
配置多个bean分隔符可以用逗号、空格或分号
除了指定依赖关系之外,
标签还可以配置抽象abstract="true"
属性和配置继承parent="accountDao"
指定继承关系。
默认Spring容器管理的bean都会以单例模式创建一个实例,当这个bean被别的bean引用时,都只会引用这一个实例,Spring的单例模式不同于我们平时使用的单例模式,四人帮定义的单例是指在JVM进程中仅有一个实例,而Spring的单例是指在一个Spring容器中仅有一个实例。
XML模式配置单例:
<bean id="accountService" class="com.something.DefaultAccountService"/>
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>
每次请求以原型作用域模式创建的bean时都会创建一个该bean新的实例,也就是在该bean被注入其他bean或者使用getBean()
方法来请求它的时候。一般情况下,我们会对有状态bean使用原型作用域,对无状态bean使用单例作用域。比如:DAO4层不会配置成原型作用域,因为典型的DAO不会保持任何会话状态。与其他作用域不同的是,Spring不管理一个原型作用域bean的完整的生命周期。
XML模式配置原型:
<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>
除了单例模式和原型模式的作用域之外,还有Request, Session, Application, 和WebSocket作用域,我们还可以自定义作用域,具体怎么操作自己拓展。
默认情况下,IoC容器会以单例模式初始化创建好所有的bean实例,通常这种预先实例化的方案是可取的,因为配置或环境的错误在容器启动的时候就会被立即发现。如果这种方案不能满足你的需求,可以配置在第一次请求bean的时候创建实例,在XML配置中,在
标签使用 lazy-init
属性:
<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
但是,如果一个懒加载的bean被别的非懒加载单例bean依赖时,它也会在容器启动的时候创建,因为必须要保持一个安全的依赖关系。
我们可以将一些配置属性,比如:数据库连接地址、用户名和密码等相关配置,这些配置通常是与环境相关并且修改的几率很大,所以为了降低修改主XML文件或容器文件的复杂性和风险,我们会把它们抽象到一个.properties
文件中,并通过点位符来注入到bean属性中。
修改bean.xml
配置,使用
来导入资源文件:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:property-placeholder location="classpath:spring.properties"/>
<bean id="beanExample" class="com.ajn.spring.beanfactory.bean.BeanExample">
<property name="name" value="${name}"/>
<property name="age" value="${age}"/>
<property name="beanProvider" ref="beanProvider"/>
bean>
<bean id="beanProvider" class="com.ajn.spring.beanfactory.bean.BeanProvider"/>
beans>
在src/main/resources
目录下新建资源文件spring.properties
如下:
name=ajn
age=24
classpath
表示类所在的路径,${key}
为占位符,key对应.properties
文件中的key。
前面我们使用简单的BeanFactory
接口完成了IoC容器管理Bean的实验,后面将使用功能更多的ApplicationContext
来实现其他功能。该接口下有很多开箱即用的实现类,比如ClassPathXmlApplicationContext
和FileSystemXmlApplicationContext
,因为我们在实际应用中,用的最多的也是该接口,它是BeanFactory
的子接口,所有功能更强大。修改主方法类:
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");
// applicationContext.registerShutdownHook();
BeanExample bean = (BeanExample) applicationContext.getBean("beanExample");
bean.run();
applicationContext.close();
}
}
applicationContext.registerShutdownHook();
和applicationContext.close();
这两个方法都是为了演示销毁的生命周期而用,至于它们有什么区别自己拓展。
InitializingBean
接口只含有一个方法afterPropertiesSet()
,在容器初始化bean之前执行DisposableBean
接口也只有一个方法destroy()
,在容器销毁bean之后执行BeanPostProcessor
接口提供两个接口postProcessBeforeInitialization()
和postProcessAfterInitialization
分别在初始化前后执行,如果你需要自定义一些Spring默认不提供的特性或生命周期行为,你可以使用它InitializingBean
接口和DisposableBean
接口,因为它们会加大代码与Spring的耦合,推荐使用bean的自定义初始化方法init-method
和销毁方法destroy-method
,配置方法如下:添加初始化和销毁方法:
public class BeanProvider {
// ...
public void init() {
System.out.println("BeanProvider.init");
}
public void destroy() {
System.out.println("BeanProvider.destroy");
}
}
然后在对应
标签添加两个属性:
<bean id="beanProvider" class="com.ajn.spring.beanfactory.bean.BeanProvider" init-method="init" destroy-method="destroy"/>
在基于注解配置情况时,可以使用
@PostConstruct
和@PreDestroy
来代替init-method
和destroy-method
Aware接口
容器管理的bean一般不需要了解容器的状态和直接使用容器,但是在某些情况下,是需要在bean中直接对IoC容器进行操作的,这时候,就需要在Bean中设定对容器的感知。该类接口就会提供对应操作的感知,例如:ApplicationContextAware
和 BeanNameAware
,可以获取相应的ApplicationContext和BeanName的信息。需要注意的是,这些接口会将代码绑定到Spring API而且不遵循IoC风格,所以我们只建议将他们使用于需要以编程方式访问容器的基础框架bean。
BeanPostProcessor接口
BeanPostProcessor
接口可以用来自定义扩展bean,实现该接口来提供你自己的实例逻辑、依赖解析逻辑等等。如果要在Spring容器完成实例化、配置和初始化bean之后实现一些自定义逻辑,你也可以定义一个或多个BeanValidationPostProcessor
接口的实现类来完成。
新建类BeanLifeCycle.java
并实现相关接口如下:
public class BeanLifeCycle implements BeanNameAware, BeanFactoryAware, ApplicationContextAware, BeanPostProcessor, InitializingBean, DisposableBean {
@Override
public void setBeanName(String name) {
System.out.println("BeanLifeCycle.setBeanName");
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("BeanLifeCycle.setBeanFactory");
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
System.out.println("BeanLifeCycle.setApplicationContext");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("BeanLifeCycle.afterPropertiesSet");
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("BeanLifeCycle.postProcessBeforeInitialization beanName: " + beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("BeanLifeCycle.postProcessAfterInitialization beanName: " + beanName);
return bean;
}
@Override
public void destroy() throws Exception {
System.out.println("BeanLifeCycle.destroy");
}
private void init() {
System.out.println("BeanLifeCycle.init");
}
private void destroyByClass() {
System.out.println("BeanLifeCycle.destroyByClass");
}
}
添加XML配置如下:
<bean id="beanLifeCycle" class="com.ajn.spring.applicationcontext.BeanLifeCycle" init-method="init" destroy-method="destroyByClass"/>
运行输出结果:
BeanLifeCycle.setBeanName
BeanLifeCycle.setBeanFactory
BeanLifeCycle.setApplicationContext
BeanLifeCycle.afterPropertiesSet
BeanLifeCycle.init
BeanLifeCycle.postProcessBeforeInitialization beanName: beanProvider
BeanProvider.init
BeanLifeCycle.postProcessAfterInitialization beanName: beanProvider
BeanLifeCycle.postProcessBeforeInitialization beanName: beanExample
BeanExample.init
BeanLifeCycle.postProcessAfterInitialization beanName: beanExample
name = [ajn], age = [24]
BeanExample.destroy
BeanProvider.destroy
BeanLifeCycle.destroy
BeanLifeCycle.destroyByClass
在bean.xml
中配置:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<bean id="beanExample" class="com.ajn.spring.beanfactory.bean.BeanExample"/>
<bean id="beanProvider" class="com.ajn.spring.beanfactory.bean.BeanProvider"/>
beans>
配置只能在该配置所在容器的bean中寻找注解,如果你在DispatcherServlet
中一个WebApplicationContext
中配置,它只能寻找到Controller中的注解,不会找到Service中的注解。
@Autowired
注解提供的功能和前面讲的自动注入(autowire)功能一样。重新修改两个类:
// 被注入的bean
public class BeanProvider {
public void sayHello(String name, Integer age) {
System.out.println("name = [" + name + "], age = [" + age + "]");
}
}
// 通过字段注入
public class BeanExample {
@Autowired
private BeanProvider beanProvider;
public void run() {
String name = "ajn";
Integer age = 24;
this.beanProvider.sayHello(name, age);
}
}
// 通过构造器方法注入
public class BeanExample {
private BeanProvider beanProvider;
@Autowired
public BeanExample(BeanProvider beanProvider) {
this.beanProvider = beanProvider;
}
public void run() {
String name = "ajn";
Integer age = 24;
this.beanProvider.sayHello(name, age);
}
}
运行主方法类输出结果可知,beanProvider
已经被注入了。@Autowired
注解不仅能在字段上注入bean,还可以用其他方式:
所以,总的来讲就是使用该注解可以把bean注入到另一个的bean的字段中,或方法的参数中,这些方法不局限于构造器方法或setter方法。
默认情况下自动注入的bean没有候选者,程序就会因为自动注入失败而报异常。当一个bean是否成功注入对程序没有什么影响,你可以在该注解上添加属性:@Autowired(required = false)
来避免报异常。
前面说过通过类型自动注入时,当有多个候选者bean都符合注入的要求时,比如一个接口有多个实现类,这时Spring不知道会注入哪一个,这时可以在一个bean上的
标签上添加 primary="true"
属性来确定主要候选者(参考:2.1.3)。这是一种解决方案,还有另一种方案就是在@Autowired
注解后使用@Qualifier
注解来指定候选者,也就是指定某个需要注入的bean。
可以指定bean的名称:
public class BeanExample {
@Autowired
@Qualifier("beanProvider")
private BeanProvider beanProvider;
// ...
}
可以自定义Qualifier名称:
<bean class="example.SimpleMovieCatalog">
<qualifier value="main"/>
bean>
public class MovieRecommender {
@Autowired
@Qualifier("main")
private MovieCatalog movieCatalog;
// ...
}
使用@Qualifier
可以更细粒度地选择bean的注入候选者,上面演示的是在XML中
标签下配置子标签
来配置名称,当基于注解配置bean时,可以在类上面使用该注解来配置qualifier名称,如下:
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
@Resource
注解作用和@Autowired
作用类似,它是JDK自带的注解,只不过它默认是通过bean名称注入,也就是我们前面讲过的byName
自动注入方式。
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Resource(name="myMovieFinder")
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
如果不指定name
属性,注解在字段上则会取字段名,注解在setter方法上则会取bean的属性名称:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Resource
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
@Resource
默认会通过名称注入bean,如果找不到对应名称的bean,才会通过类型注入,但一但指定name
属性,则只会通过名称注入。@Autowired
注解只能通过类型注入,要想实现通过名称注入,则要结合@Qualifier
注解使用。
在1.7节我们讲了使用外部属性值,使用@Value
在基于注解的情况下注入属性值:
@Component
public class BeanExample {
@Value("${name}")
private String name;
@Value("${age}")
private Integer age;
@Resource
private BeanProvider beanProvider;
public void run() {
this.beanProvider.sayHello(this.name, this.age);
}
}
在前面讲生命周期的时候说到了配置自定义初始化方法init-method
和销毁方法destroy-method
,在基于注解配置的情况下,我们使用@PostConstruct
和@PreDestroy
来指定自定义初始化方法和销毁方法:
public class BeanExample {
// ...
@PostConstruct
public void init() {
System.out.println("BeanExample.init");
}
@PreDestroy
public void destroy() {
System.out.println("BeanExample.destroy");
}
}
前面我们都是使用XML方式来配置元数据,定义一个bean的,即便前面讲了好多使用注解方式配置,但bean的定义依然是在XML文件中配置的,本节介绍通过注解指定并通过扫描类路径来检测候选组件。只需将bean.xml
配置改写成:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="com.ajn.spring"/>
beans>
包含了
的功能,所以我们只需配置一个。
修改类为:
@Component
public class BeanProvider {
public void sayHello(String name, Integer age) {
System.out.println("name = [" + name + "], age = [" + age + "]");
}
}
@Component
public class BeanExample {
@Resource
private BeanProvider beanProvider;
public void run() {
String name = "ajn";
Integer age = 24;
this.beanProvider.sayHello(name, age);
}
}
Spring还提供了同样功能的注解@Repository
、@Service
和 @Controller
,它们用于区分分层开发模型中bean的类型,比如:@Repository
用于数据持久层,它可以自动转换异常,@Service
用于业务层,@Controller
用于控制层。
使用这些注解创建的bean的名称默认是类名转化成小驼峰的形式,例如:BeanProvider
的bean的名称是beanProvider
,也可以自己指定bean的名称:
@Component("beanProvider")
public class BeanProvider {
public void sayHello(String name, Integer age) {
System.out.println("name = [" + name + "], age = [" + age + "]");
}
}
基于注解配置bean时也可以使用注解@Scope
来指定作用域,例如使用原型模式:
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class BeanProvider {
public void sayHello(String name, Integer age) {
System.out.println("name = [" + name + "], age = [" + age + "]");
}
}
这两个注解用于基于Java方式注册bean,下面是基于XML配置的bean:
<beans>
<bean id="beanExample" class="com.ajn.spring.beanfactory.bean.BeanExample"/>
<bean id="beanProvider" class="com.ajn.spring.beanfactory.bean.BeanProvider"/>
beans>
替换成基于Java配置的形式如下:
@Configuration
public class AppConfig {
@Bean
public BeanExample beanExample() {
return new BeanExample();
}
@Bean
public BeanProvider beanProvider() {
return new BeanProvider();
}
}
修改主方法类:
通过AnnotationConfigApplicationContext
简单启动
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
BeanExample beanExample = applicationContext.getBean(BeanExample.class);
beanExample.run();
applicationContext.close();
}
}
通过register(Class>…)
方法构建容器
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.register(AppConfig.class);
applicationContext.refresh();
BeanExample beanExample = applicationContext.getBean(BeanExample.class);
beanExample.run();
applicationContext.close();
}
}
前面说过@Bean
注解可以用于基于Java定义bean,下面是该注解的详细用法:
参数自动注入,使用注解的方法可以有任意个参数,依赖的bean都会通过参数注入,比如:BeanExample
依赖BeanProvider
,可以通过如下方式使用参数自动依赖
@Configuration
public class AppConfig {
@Bean
public BeanExample beanExample(BeanProvider beanProvider) {
BeanExample beanExample = new BeanExample();
beanExample.setBeanProvider(beanProvider);
return beanExample;
}
@Bean
public BeanProvider beanProvider() {
return new BeanProvider();
}
}
添加属性initMethod
和destroyMethod
使用生命周期方法
@Configuration
public class AppConfig {
@Bean(initMethod = "init", = "destroy")
public BeanExample beanExample() {
return new BeanExample();
}
}
// 在类中添加方法
public class BeanExample {
// ...
private void init() {
System.out.println("BeanExample.init");
}
private void destroy() {
System.out.println("BeanExample.destroy");
}
}
默认情况下使用该注解所在的方法名作为bean的名称,添加属性name
可以另行配置
@Configuration
public class AppConfig {
@Bean(name = "myThing")
public Thing thing() {
return new Thing();
}
}
可以结合@Profile
、@Scope
、@Lazy
、@DependsOn
和 @Primary
等注解使用。
@Profile
用来区分环境,我们后面会讲到,表示只有在某个环境下才实例化当前bean@Scope
可以用来配置单例模式还是原型模式(见1.5)@Lazy
表示懒加载(见1.6)@DependsOn
指定依赖关系(见1.4)@Primary
定义该bean为依赖的主要候选者(见1.3)@Configuration
public class AppConfig {
@Bean
@DependsOn("beanProvider")
@Scope(BeanDefinition.SCOPE_SINGLETON)
@Primary
@Profile("prod")
@Lazy
public BeanExample beanExample() {
return new BeanExample();
}
@Bean
public BeanProvider beanProvider() {
return new BeanProvider();
}
}
基于XML方式配置使用注解@Component
、@Repository
、@Service
和 @Controller
创建bean的配置如下:
<beans>
<context:component-scan base-package="com.ajn.spring"/>
beans>
修改为使用@ComponentScan
注解配置:
@Configuration
@ComponentScan("com.ajn.spring")
public class AppConfig {
}
也可以在主方法中使用scan(String…)
方法来实现这个功能:
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.scan("com.ajn.spring");
applicationContext.refresh();
BeanExample beanExample = applicationContext.getBean(BeanExample.class);
beanExample.run();
applicationContext.close();
}
}
在基于XML配置时,我们使用
标签来导入另一个bean.xml
文件,在程序设计的时候,可以根据模块来定义不同的bean.xml
文件。在使用基于Java配置时,可以使用@Import
注解来导入另一个bean配置类:
@Configuration
public class ConfigA {
@Bean
public A a() {
return new A();
}
}
@Configuration
@Import(ConfigA.class)
public class ConfigB {
@Bean
public B b() {
return new B();
}
}
这样处理的话,就不需要在主方法里面实例化容器的时候配置两个类了,上面的例子只需要配置ConfigB
就可以。
有时我们需要会有条件的启用或禁用@Configuration
注解的类和@Bean
注解的方法,基于一些系统状态而对bean是否注册的控制。此时可以使用@Conditional
来完成这个功能,新建一个类实现Spring提供的Condition接口:
public class TestCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return false;
}
}
在@Configuration
注解下或@Bean
注解下使用@Conditional
注解并使用前面的类:
@Configuration
@Conditional(TestCondition.class)
public class AppConfig {
@Bean
public BeanExample beanExample() {
return new BeanExample();
}
@Bean
public BeanProvider beanProvider() {
return new BeanProvider();
}
}
此时,matches()
方法返回false
时AppConfig
类就不会生效,两个bean也不会被注册进容器,返回true
时才能正常注册。
在Spring项目中可以组合使用基于Java和基于XML的配置。
基于XML配置的Spring容器要使用@Configuration
注解,只需把声明该注解的类当作普通的bean即可,例如存在如下类:
@Configuration
public class AppConfig {
@Autowired
private DataSource dataSource;
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
@Bean
public TransferService transferService() {
return new TransferService(accountRepository());
}
}
要想使上面的配置类生效,只要system-test-config.xml
在配置文件中声明bean:
<beans>
<context:annotation-config/>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
<bean class="com.acme.AppConfig"/>
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
bean>
beans>
此时主方法加载XML配置:
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
查看@Configuration
源码,可以看出它使用了元注解@Component
,所以我们可以使用
,通过扫描配置类所在包来创建bean。将XML文件修改如下:
<beans>
<context:component-scan base-package="com.acme"/>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
<bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
bean>
beans>
基于Java配置的Spring容器要使用XML文件配置,很简单,只需使用@ImportResource
注解来导入XML文件,例如:
@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
return new DriverManagerDataSource(url, username, password);
}
}
properties-config.xml
<beans>
<context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
beans>
此时主方法加载注解启动:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
Environment
接口是集成在容器中抽象,它包含了应用程序环境两个关键的点:profiles和properties。
profiles是在bean定义时的命名、逻辑组,只有在profile激活状态下才能将bean注册到容器中。beans被分配到profile中,不管是基于XML定义还是基于注解定义的。Environment
与profiles有关的作用是决定哪些profile(如果有)被激活,或者哪些profile(如果有)应该要默认激活。
properties几乎在所有应用程序中扮演着重要的角色,它有多个来源:properties文件、JVM系统属性、系统环境变量、JNDI、Servlet上下文参数、特殊设置的Properties
对象和Map
对象等等。Environment
与properties有关的作用是给用户提供一个方便的服务接口来配置属性和解析属性。
@Profile
注解是基于@Conditional
注解实现的,源码如下:
// 注解源码
@Target({
ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
/**
* The set of profiles for which the annotated component should be registered.
*/
String[] value();
}
// 接口实现源码
class ProfileCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
return true;
}
}
return false;
}
return true;
}
}
Bean定义profile在核心容器中提供了一种在不同环境中注册不同bean的机制。environment这个词对于不同的用户意味着不同的东西,这个特性有很多使用场景,包括:
拿第一个场景举例,在实际应用中需要配置一个数据源DataSource
,在开发环境配置如下:
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}
假设在QA或者生产环境,数据源配置如下:
@Bean
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
问题是怎样基于当前环境来选择两个配置。现在,Spring用户有很多种解决方案,比如使用一组环境变量和XML配置
与${placeholder}
占位符来实现,这样当前配置就可以根据环境变量来区分。bean定义profile这个核心容器的特性也提供了一种解决方案。使用这个特性,我们可以定义一个环境专有的bean。
使用@Profile
注解表示当一个或多个profile激活的时候注册某个组件。使用前面的例子:
类级配置
// 开发环境
@Configuration
@Profile("development")
public class StandaloneDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
}
// 生产环境
@Configuration
@Profile("production")
public class JndiDataConfig {
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
指定profile时可以使用一些简单的逻辑运算符:
!
:非,例如:!prod
表示在prod没激活的时候注册&
:与,例如:local & dev
表示在local和dev同时激活的时候注册|
:或,例如:local | dev
表示在local或dev激活的时候注册
建议将环境的profile定义使用自定义注解封装起来:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
方法级配置
@Configuration
public class AppConfig {
@Bean("dataSource")
@Profile("development")
public DataSource standaloneDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}
@Bean("dataSource")
@Profile("production")
public DataSource jndiDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
为什么这里
@Bean
注解要指定bean的名称?基于前面讲解的这个注解,自己想想为什么。
配置文件级别配置
<beans profile="development"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xsi:schemaLocation="...">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
jdbc:embedded-database>
beans>
<beans profile="production"
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
beans>
标签级别配置
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="...">
<beans profile="development">
<jdbc:embedded-database id="dataSource">
<jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
<jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
jdbc:embedded-database>
beans>
<beans profile="production">
<jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
beans>
beans>
如果我们配置好上面的示例后启动项目,肯定会报NoSuchBeanDefinitionException
异常,因为容器找不到命名为dataSource
的bean。需要激活profile才能注册相应环境配置的bean,在主方法类里面激活:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
另外也可以通过声明spring.profiles.active
属性来激活,可以在系统环境变量、JVM系统属性、在web.xml
中的Servlet上下文参数等等(见4.8),在测试模块中也可以使用@ActiveProfiles
注解来配置激活。也可以同时激活多个profile:
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
也可以配置默认就激活的Profile:
@Configuration
@Profile("default")
public class DefaultDataConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.build();
}
}
如果没有profile被激活,则会使用默认值,只要有一个profile被激活,默认值就会失效。你也可以用Environment
的setDefaultProfiles()
方法来配置,或者声明spring.profiles.default
属性。
在了解这个注解之前讲讲Spring的环境属性机制,前面说到Environment
包含profile和properties,现在就讲讲properties。它提供了层级属性源的搜索功能,如下:
ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);
在上面的例子中,我们使用一种高级的方式来判断当前环境是否定义了my-property
属性。Environment
对象会从一组PropertySource
对象中搜索,一个PropertySource
是一些简单的键值对属性,Spring中StandardEnvironment
包含两个PropertySource对象,JVM系统属性(System.getProperties()
)和系统环境变量(System.getenv()
)。上面例子中,当Java系统属性或环境变量中配置了my-property
属性,env.containsProperty("my-property")
就会返回true
。
属性的搜索存在优先级,比如在多个地方存在同样的属性时,Spring会优先选择某个属性。在一个普通的StandardServletEnvironment
中,优先级从高到低如下:
DispatcherServlet
上下文)web.xml
上下文入口)java:comp/env/
入口)-D
指定的命令行参数)最重要的是,这个机制是完全可配置的。你可能需要集成自己的属性源,为此,只要实现PropertySource
并实例化,然后添加到Environment
中的PropertySources
中,例如:
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
在上面的示例中,MyPropertySource
拥有最高的优先级,MutablePropertySources
提供了一些操作数据源有用的方法。
@PropertySource
注解给Spring的Environment
添加PropertySource
提供了一个简单的的声明机制。创建一个文件app.properties
,添加键值对`testbean.name=myTestBean,然后创建下面示例:
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}
除了使用Environment
来获取属性,也可以使用@Value
和${}
占位符来获取。
在本节前面讲了org.springframework.beans.factory
包的BeanFactory
接口,后来又说了org.springframework.context
包的ApplicationContext
接口,它继承了BeanFactory
接口,提供了更强大的功能,为了使BeanFactory
功能更具面向框架风格,context
包还提供了下面的功能:
MessageSource
接口以国际化风格访问消息ResourceLoader
接口访问像URL和文件资源ApplicationListener
和ApplicationEventPublisher
接口来监听和发布事件HierarchicalBeanFactory
接口来加载多个分级context
,让每个容器都专注特定的层,例如:应用程序的Web层这些功能就不全讲,需要使用的时候完全可以去找资料文档来使用这些功能。
ApplicationContext
通过ApplicationEvent
类和ApplicationListener
接口提供了事件处理机制,每次ApplicationEvent
事件发布到ApplicationContext
时,实现了ApplicationListener
接口的bean就会被通知,这运用了标准的观察者模式。
Spring提供了一些标准事件如下:
它们的功能就不介绍了,自己去查询资料,我们拿ContextClosedEvent
举例,新建监听类:
@Service
public class ApplicationNotifier implements ApplicationListener<ContextClosedEvent> {
@Override
public void onApplicationEvent(ContextClosedEvent event) {
System.out.println("ApplicationNotifier.onApplicationEvent");
}
}
运行程序,当ApplicationContext
调用close()
方法时就会通知该类。在Spring4.2以后,还提供了基于注解创建监听器,把上面代码修改如下:
@Service
public class ApplicationNotifier {
@EventListener
public void onApplicationEvent(ContextClosedEvent event) {
System.out.println("ApplicationNotifier.onApplicationEvent");
}
}
也能达到同样的效果,使用注解配置不需要实现ApplicationListener
接口,默认@EventListener
注解是根据方法参数指定监听的事件,也可以使用该注解的属性来添加被监听的事件,可以配置多个,例如:
@EventListener({
ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
// ...
}
我们除了使用Spring提供的事件外,还可以自己创建和发布自定义事件,创建事件类如下:
public class BlackListEvent extends ApplicationEvent {
private String address;
private String content;
public BlackListEvent(Object source, String address, String content) {
super(source);
this.address = address;
this.content = content;
}
// Getter, Setter and toString
}
通过调用ApplicationEventPublisher
的publishEvent()
方法来发布一个自定义ApplicationEvent
。通常会创建一个bean类来实现ApplicationEventPublisherAware
接口并注册,例如:
@Service
public class EmailService implements ApplicationEventPublisherAware {
private List<String> blackList;
private ApplicationEventPublisher publisher;
public void setBlackList(List<String> blackList) {
this.blackList = blackList;
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void sendEmail(String address, String content) {
if (blackList.contains(address)) {
publisher.publishEvent(new BlackListEvent(this, address, content));
return;
}
System.out.println("EmailService.sendEmail");
}
}
创建监听类:
@Service
public class BlackListNotifier {
@EventListener
public void onApplicationEvent(BlackListEvent event) {
System.out.println("BlackListNotifier.onApplicationEvent:" + event.toString());
}
}
使用@Async
配置监听器异常执行
@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
// BlackListEvent is processed in a separate thread
}
使用异步监听要注意两点:
AsyncUncaughtExceptionHandler
ApplicationEventPublisher
手动发送如果一个事件被多个监听器监听,我们可以使用@Order
注解来指定监听器执行的顺序
@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {
// notify appropriate parties via notificationAddress...
}
你可以使用ContextLoader
以声明方式创建ApplicationContext
实例。当然,你也可以使用ApplicationContext
的任何一个实现来以编程方式创建ApplicationContext
实例。
你可以使用ContextLoaderListener
来注册一个ApplicationContext
,如下:
<context-param>
<param-name>contextConfigLocationparam-name>
<param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xmlparam-value>
context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListenerlistener-class>
listener>
监听器会检查contextConfigLocation
参数,如果该参数不存在,则会默认使用/WEB-INF/applicationContext.xml
。当存在参数并且有多个参数时,可能使用逗号、分号或空格来隔开。也可以使用模糊路径,比如/WEB-INF/*Context.xml
(在WEB-INF
目录下的所有以Context.xml
结尾的文件)和/WEB-INF/**/*Context.xml
(在WEB-INF
目录下和所有子目录下以Context.xml
结尾的文件)。
敬请期待下一章【第4章-架构篇之Spring-卷2】
参考文献
Spring Framework 5.0.0.RC1官方文档:https://docs.spring.io/spring/docs/5.0.0.RC1/spring-framework-reference/overview.html
Spring Framework 5.1.3 官方文档:https://docs.spring.io/spring/docs/5.1.3.RELEASE/spring-framework-reference/
POJO: (Plain Ordinary Java Object),普通的Java类 ↩︎
EJB: (Enterprise Java Bean),企业级JavaBean ↩︎
参考文献:Spring的核心机制:依赖注入(控制反转)https://blog.csdn.net/a906998248/article/details/7514085 ↩︎
DAO: (Data Access Object),数据访问对象 ↩︎