本部分讲述如何将 Bean 注入 Spring IoC容器中。
全部章节传送门:
Spring学习笔记(一):Spring IoC 容器
Spring学习笔记(二):Spring Bean 装配
Spring学习笔记(三): Spring 面向切面
Spring学习笔记(四): Spring 数据库编程
Spring学习笔记(五): Spring 事务管理
依赖注入的方式
依赖注入可以分为3种方式:
- 构造器注入。
- setter注入。
- 接口注入。
其中构造器注入和 setter 注入是主要的方式。
构造器注入
构造器注入依赖于构造方法的实现,构造方法可以是有参或者无参的。
创建一个实体类 Role 。
package com.wyk.springdemo.pojo;
public class Role {
private Long id;
private String roleName;
private String note;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
public Role(String roleName, String note) {
this.roleName = roleName;
this.note = note;
}
}
在配置文件中添加 Bean 。其中 index 指定参数的位置,而 value 设置值。
构造器注入还是比较简单的,缺点是当参数较多时,构造方法比较复杂。
使用 setter 注入
setter 注入是 Spring 中最主流的注入方式,它会首先调用无参的构造方法,然后使用 setter 注入为其设置对应的值。
为前面的实体类添加一个无参构造方法。
public Role() {}
然后在配置文件中添加 Bean 。
接口注入
当资源来自外界的时候,可以通过采用接口注入的方式获取它,比如数据库连接资源。
装配 Bean 概述
下面讲述如何将 Bean 装配到 Spring IoC 容器中,常用的方法有3种:
- 在XML中显示配置。
- 在 Java 的接口和类中实现配置。
- 隐式 Bean 的发现机制和自动装配原则。
在实际开发中,建议使用的方式是:
- 基于约定由于配置的原则,最优先的应该是通过隐式 Bean 的发现机制和自动装配原则。
- 在无法使用自动装配原则的情况下优先考虑 Java 接口和类中实现配置。
- 最后考虑XML配置。
通过 XML 配置装配 Bean
使用 XML 装配 Bean 需要引入对应的 XML 模式(XSD)。
装配简易值
看一个简单的 Bean 。
简单说明一下。
- id 属性是 Spring 找到这个 Bean 的编号,不过 id 不是一个必需的属性,如果没声明它,那么 Spring 将会采用 “全限定名#{number}”的格式生成编号,例如上面的例子,如果没有声明 id , 那么它的编号就是“com.wyk.springdemo.pojo.Drinks#0”。
- class 是一个类全限定名。
- property定义类的属性。
如果需要注入一些自定义的类,则可以通过 ref 属性。
装配集合
本部分讲述如何装配复杂元素,比如Set、Map、List等。
创建一个 Bean 。
package com.wyk.springdemo.pojo;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
public class ComplexAssembly {
private Long id;
private List list;
private Map map;
private Properties props;
private Set set;
private String[] array;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public List getList() {
return list;
}
public void setList(List list) {
this.list = list;
}
public Map getMap() {
return map;
}
public void setMap(Map map) {
this.map = map;
}
public Set getSet() {
return set;
}
public void setSet(Set set) {
this.set = set;
}
public String[] getArray() {
return array;
}
public void setArray(String[] array) {
this.array = array;
}
public Properties getProps() {
return props;
}
public void setProps(Properties props) {
this.props = props;
}
}
在 XML 中装配这个实体。
value-list-1
value-list-2
value-list-3
value-prop-1
value-prop-2
value-prop-3
value-set-1
value-set-2
value-set-3
value-array-1
value-array-2
value-array-3
简单介绍一下集合类型的属性如何装配:
- List 属性通过
- 元素进行装配,然后通过
元素设置值。 - Map 属性通过
- Properties 属性通过
元素进行装配,然后通过多个 元素设置值,每个prop包含一个key。 - Set 属性通过
元素进行装配,然后通过 元素设置值。 - 数组通过
元素进行装配,然后通过 元素设置值。
上面讨论了各个集合的装载,当集合的元素是实体对象的时候,情况会更复杂。
首先创建2个实体类,一个是前面使用过的 Role 类,另一个是下面的 User 类。
package com.wyk.springdemo.pojo;
public class User {
private Long id;
private String userName;
private String note;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
然后创建一个复杂一些的实体类。
package com.wyk.springdemo.pojo;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class UserRoleAssembly {
private Long id;
private List list;
private Map map;
private Set set;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public List getList() {
return list;
}
public void setList(List list) {
this.list = list;
}
public Map getMap() {
return map;
}
public void setMap(Map map) {
this.map = map;
}
public Set getSet() {
return set;
}
public void setSet(Set set) {
this.set = set;
}
}
然后在 XML 文件中装配实体。
从上例可以看出:
- List 属性通过
- 元素定义注入,然后通过多个元素的 Bean 属性去引用定义好的 Bean。
- Map 属性通过
- Set 属性通过
元素定义注入,然后通过多个元素的 Bean 属性去引用定义好的 Bean。
命名空间装配
Spring 还提供了对应的命名空间的定义,只是在使用命名空间的时候需要先引入对应的命名空间和 XSD 文件。
前面讲到可以通过构造器和 setter 进行依赖注入,在有了p命名空间和c命名空间时我们可以简单的把它们当做 Bean 的一个属性来进行定义。先看一个例子。
使用p命名空间和c命名空间时需要先声明使用对应的命名空间,即在beans元素上加入xmlns:p="http://www.springframework.org/schema/p" 和xmlns:c="http://www.springframework.org/schema/c"。
其中,使用c进行构造器注入,c:_0代表第一个参数,以此类推,如果想引用其它 Bean,添加-ref即可,比如 c:_1-ref 。
使用p进行 setter 注入,比如p:id="2",以2为值,使用的setId方法设置,同样,如果引用其它 Bean , 使用-ref,比如 p:id-ref。
补充一下,如果在IDEA中报错URI is not registered,需要在File →Settings→Schemas and DTDs中添加命名空间的引用。
还可以使用util命名空间xmlns:util="http://www.springframework.org/schema/util" (对应java.util包)引入集合类型。
张三
李四
王五
通过注解装配 Bean
通过注解的方式可以减少 XML 的配置,注解功能更为强大,它既能实现 XML 的功能,也提供了自动装配的功能。符合“约定优于配置”的开发原则。
Spring 中,有两种发现 Bean 的方式:
- 组件扫描。通过定义资源的方式,让 Spring IoC 容器扫描对应的包,从而把 Bean 装配进来。
- 自动装配。通过注解定义,使得一些依赖关系可以通过注解完成。
使用@Component装配 Bean
首先定义一个实体类。
package com.wyk.beanDemo.pojo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component(value="role")
public class Role {
@Value("1")
private Long id;
@Value("xiaoming")
private String roleName;
@Value("haha")
private String note;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getRoleName() {
return roleName;
}
public void setRoleName(String roleName) {
this.roleName = roleName;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
注意其中的注解。
- 注解 @Component 代表 Spring IoC 会把这个类扫描成 Bean ,其中的 value 属性代表这个类在 Spring 中的 id 。也可以简写成 @Component("role") ,甚至直接写成 @Component ,这时id会默认成首字母小写的类名。
- 注解 @Value 代表值的注入。
这时候还需要一个 Java Config告诉 Spring 去哪里扫描 Bean 。
package com.wyk.beanDemo.pojo;
import com.wyk.beanDemo.service.impl.RoleServiceImpl;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
public class PojoScan {
}
需要注意的是, @ComponentScan 代表进行扫描,默认扫描的当前的包,因此实体和配置所在的包必须一致。
最后,需要通过 Spring IoC 容器的实现类 AnnotationConfigApplicationContext 去生成容器。
package com.wyk.beanDemo;
import com.wyk.beanDemo.pojo.PojoScan;
import com.wyk.beanDemo.pojo.Role;
import com.wyk.beanDemo.service.RoleService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class mainApp {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(PojoScan.class);
Role role = context.getBean(Role.class);
System.err.println(role.getId());
}
}
上面的 @ComponentScan 只扫描当前包路径,有很大的局限。这里讲一下它的2个配置项:
- basePackages 。它的值是 Java 包的数组,Spring 会根据它的配置扫描对应的包和子包。
- basePackageClasses 。它可以配置多个类,Spring 会根据配置的所在的包,为包和子包扫描装配对应配置的 Bean 。
创建一个接口。
package com.wyk.beanDemo.service;
import com.wyk.beanDemo.pojo.Role;
public interface RoleService {
public void printRoleInfo(Role role);
}
再创建一个接口的实现类, @Component 代表它是一个 Spring 的 Bean 。
package com.wyk.beanDemo.service.impl;
import com.wyk.beanDemo.pojo.Role;
import com.wyk.beanDemo.service.RoleService;
import org.springframework.stereotype.Component;
@Component
public class RoleServiceImpl implements RoleService {
public void printRoleInfo(Role role) {
System.out.println(role.getId());
System.out.println(role.getRoleName());
System.out.println(role.getNote());
}
}
修改配置类。
package com.wyk.beanDemo.pojo;
import com.wyk.beanDemo.service.impl.RoleServiceImpl;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan(basePackageClasses = {Role.class, RoleServiceImpl.class})
//@ComponentScan(basePackages = {"com.wyk.beanDemo.pojo","com.wyk.beanDemo.service"})
public class PojoScan {
}
这里使用 basePackages 或者 basePackageClasses 均可,一般来说,basePackages 可读性较好,优先使用它;但在大量需要重构的工程中,尽量不要使用 basePackages ,因为很多时候修改包名需要反复配置,而IDE不会进行提示,而使用 basePackageClasses 的话,IDE会报错提示,并且可以轻松处理这些错误。
另外,建议只使用一个 @ComponentScan 注解,因为如果采用多个,Spring 会为每一个 @ComponentScan 注解生成一个新的对象,这往往不是我们需要的。如果同一个 @ComponentScan 注解定义重复的包或者存在其子包的定义,也不会配置生成多个对象。
进行测试。
package com.wyk.beanDemo;
import com.wyk.beanDemo.pojo.PojoScan;
import com.wyk.beanDemo.pojo.Role;
import com.wyk.beanDemo.service.RoleService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class mainApp {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(PojoScan.class);
Role role = context.getBean(Role.class);
RoleService roleService = context.getBean(RoleService.class);
roleService.printRoleInfo(role);
}
}
自动装配 @Autowired
所谓自动装配技术,是由 Spring 自己发现对应的 Bean ,自动完成装配工作的方式,它会应用到注解 @Autowired ,这时候 Spring 会根据类型去寻找定义的 Bean 然后将其注入。
创建一个接口 RoleService2 。
package com.wyk.beanDemo.service;
public interface RoleService2 {
void printRoleInfo();
}
这个接口采用了 Spring 推荐的接口方式,这样更为灵活,因为定义和实现进行了分离,接下来创建实现类。
package com.wyk.beanDemo.service.impl;
import com.wyk.beanDemo.pojo.Role;
import com.wyk.beanDemo.service.RoleService2;
import org.springframework.beans.factory.annotation.Autowired;
public class RoleServiceImpl2 implements RoleService2 {
@Autowired
private Role role;
public Role getRole() {
return role;
}
public void setRole(Role role) {
this.role = role;
}
public void printRoleInfo() {
System.out.println(role.getId());
System.out.println(role.getRoleName());
System.out.println(role.getNote());
}
}
在这里使用了 @Autowired 注解, Spring 会按照类型找到定义的实例,将其注入。
IoC 容器有时候会寻找失败,在默认情况下寻找失败会抛出异常,如果不希望抛出异常,可以通过配置项 required 来改变它,设置为 @Autowired(required=false)。
@Autoeired 除可以配置在属性上,还可以配置到方法上,常见的 Bean 的 setter 方法也可以使用它来完成注入。
@Autowired
public void setRole(Role role) {
this.role = role;
}
自动装配的歧义性
在一些情况下,一个接口可能会有多个实现类,这样在注入的时候,会由于无法判断使用哪个实现类,导致注入失败。
为了消除歧义性,Spring 提供了2个注解 @Primary 和 @Qualifier 。
注解 @Primary 代表首要,当 Spring 通过一个接口或抽象类注入对象的时候,该注解会告诉 Spring 首先使用哪个类。
@Component("roleSerivce3")
@Primary
public class RoleServiceImpl3 implements RoleService {
}
如果有多个实现类都标注了 @Primary , Spring 会抛出异常,@Primary只能解决首要性问题,无法解决选择性问题。
@Qualifier 注解改为使用按照名称查找的方法,而不是按照类型,这样就可以消除歧义性。
public class RoleController {
@Autowired
@Qualifier("roleService3")
private RoleService roleService;
...
}
装载带有参数的构造方法类
对于带有参数的构造方法,也允许我们直接通过注解进行注入。
public RoleController(@Autowired RoleService roleService) {
this.roleService = roleService;
}
使用 @Bean 装配 Bean
前面通过 @Component 装配 Bean 只能注解到类上,注解 @Bean 可以注解到方法上,适用于第三方包引入不方便添加 @Component 的类。
@Bean(name="dataSource")
public DataSource getDataSource() {
...
}
@Bean 有4个配置项:
- name: 是一个字符串数组,允许配置多个 BeanName 。
- autowire: 标志是否是一个引用的 Bean 对象,默认值是 autowire.NO 。
- initMethod: 自定义初始化方法。
- destroyMethod: 自定义销毁方法。
装配的混合使用
在现实中,一般的使用方法是对自己工程中所开发的类尽量使用注解的方法,对于引入的第三方包或者服务,尽量使用 XML 方式, 这样的好处是可以尽量减少对第三方包或者服务细节的理解,也更加清晰和明朗。
在程序中,可以通过注解 @ImportResource引入 XML 文件,其中配置的内容是一个数组可以引入多个 XML 文件。
@ComponentScan
@ImportResource({"classpath:spring-dataSource.xml"})
public class ApplicationConfig {
}
然后我们就可以使用 @Autowired 注入在 spring-dataSource.xml 中定义的 Bean 了。
如果有多个类似 ApplicationConfig 的配置类需要注入,可以使用 @Import 注解。
@ComponentScan
@Import({ApplicationConfig2.class, ApplicationConfig3.class})
public class ApplicationConfig {
}
同样,可以通过 import 元素在一个 XML 文件中引入其他 XML 文件。
也许你希望使用 XML 加载 Java 配置类,不过目前 Spring 不支持。但是 Spring 支持通过 XML 的配置扫描主机的包,只要通过
使用 Profile
在软件开发中,有很多情况下需要准备多套环境,Spring 也会对这样的场景进行支持,在 Spring 中我们可以定义 Bean 的 Profile 。
使用注解 @Profile 配置
首先看注解 @Profile 如何配置,下面举个例子,配置两个环境,一个用于开发(dev),一个用于测试(test)。
@Component
public class ProfileDataSource {
@Bean(name="devDataSource")
@Profile("dev")
public DataSource getDevDataSource() {
...
}
@Bean(name="testDataSource")
@Profile("test")
public DataSource getTestDataSource() {
...
}
}
使用 XML 定义 Profile
在 XML 中 beans 标签中添加 Profile 属性即可,但直接添加会导致整个文件都属于同一个 Profile , 所以一般放在内部,这样可以在一个 XML 文件中配置多个 Profile 。
...
...
启动 Profile
如果直接启动程序, Profile 相关的 Bean 并不会加载到 Spring 之中,这是因为需要自行激活 Profile 。激活 Profile 的方法有以下5种:
- 在使用 Spring MVC 的情况下可以配置 Web 上下文参数,或者DispatchServlet参数。
- 作为 JNDI 条目。
- 配置环境变量。
- 配置 JVM 启动参数
- 在集成测试环境中使用 @ActiveProfiles 。
下面对几种常用的方法进行介绍。
在测试代码中激活 Profile , 需要使用注解 @ActiveProfiles 。
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=ProfileConfig.class)
@ActiveProfiles("dev")
public class ProfileTest {
@Autowired
private DataSource dataSource;
@Test
public void test() {
...
}
}
如果需要在服务器上运行,最好还是配置 Java 虚拟机的启动项,关于定制 Profile 的参数存在两个:
- spring.profiles.default: 默认启动的 Profile ,如果系统没有配置有关 Profile 参数的时候,那么它将启动。
- spring.profiles.active: 启动的 Profile ,如果配置了它,那么 spring.profiles.default将失效。
比如需要启动 test ,可以在 JVM 中配置:
JAVA_OPTS="-Dspring.profiles.active=test"
在大部分情况下需要启动 Web 服务器,如果使用的是 Spring MVC,那么可以设置 Web 环境参数或者 DispatcherServlet参数来选择对应的Profile,比如可以在 web.xml中进行配置。
......
spring.profiles.active
test
......
dispatcher
org.springframework.web.servlet.DispatcherServlet
2
spring.profiles.active
test
......
加载属性文件
在 Spring 项目中,属性(properties)文件经常用做配置文件,比如使用 properties 文件配置数据库文件。
jdbc.database.driver=com.mysql.jdbc.Driver
jdbc.database.url=jdbc:mysql://localhost:3306/wyk
jdbc.database.username=wyk
jdbc.database.password=123123
在 Spring 中可以通过注解或者 XML 的方式加载属性文件。
使用注解方式加载属性文件
Spring 提供注解 @PropertySource 来加载属性文件,它有以下配置项:
- name: 字符串,配置这次属性配置的名称。
- value: 字符串数组,可以配置多个属性文件。
- ignoreResourceNotFound: boolean值,默认为 false ,其含义为如果找不到对应的属性文件是否进行忽略处理,由于默认值为 false ,所以在默认情况下找不到配置文件会抛出异常。
- encoding: 编码,默认为空。
@Configuraton
@PropertySource(value={"classpath:database-config.properties"},ignoreResourceNotFound=true)
public class ApplicationConfig {
}
然后可以通过环境来获取对应的配置属性。
ApplicationContext context = new AnnotationConfigApplicaitonContext(ApplicationConfig.class);
String url = context.getEnvironment().getProperty("jdbc.database.url");
但是这种方式没有解析属性占位符(即${})的能力,因此更加推荐的方式是使用一个属性文件解析类(PropertySourcesPlcaholderConfigurer)进行处理。
首先修改 Java 配置类。
@Configuraton
@ComponentScan
@PropertySource(value={"classpath:database-config.properties"},ignoreResourceNotFound=true)
public class ApplicationConfig {
@Bean
public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
这样就可以通过 @Value 注解使用属性占位符。
@Component
public class DataSourceBean {
@Value("${jdbc.database.driver}")
private String driver;
@Value("${jdbc.database.url}")
private String url;
@Value("${jdbc.database.username}")
private String username;
@Value("${jdbc.database.password}")
private String password;
...
}
使用 XML 方式加载属性文件
使用 XML 方式加载属性文件,只需要使用
上面的方式在属性文件较多时会导致 location 属性较长。还可以在 bean 标签中加载属性文件,这样可以分别加载多个属性文件,这时需引入 PropertyPlaceholderConfigurer 类。
classpath:quartz.properties
classpath:db.properties
条件化装配 Bean
在某些情况下不需要去装配 Bean ,比如当某些配置文件不存在的时候。这个时候需要条件化判断,Spring 提供了注解 @Conditonal 去配置类,但是这些类需要实现 Condition (org.springframework.context.annotation.Condition)接口。
@Bean
@Conditional({DataSourceCondition.class})
public DataSource getDataSource() {
@Value("${jdbc.database.driver}") String driver;
@Value("${jdbc.database.url}") String url;
@Value("${jdbc.database.username}") String username;
@Value("${jdbc.database.password}") String password;
...
}
这里通过 @Value 引入了属性文件中的配置,但是我们无法确定属性是否存在,因此通过 @Conditional 注解引入一个类 DataSourceCondition 去进行判断。
public class DataSourceCondition implements Condition {
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
return env.containsProperty("jdbc.database.driver")
&& env.containsProperty("jdbc.database.url")
&& env.containsProperty("jdbc.database.username")
&& env.containsProperty("jdbc.database.password")
}
}
Spring Bean 作用域
Spring 框架支持以下五个作用域,分别为singleton、prototype、request、session和global session,5种作用域说明如下所示。
作用域 | 描述 |
---|---|
singleton | 在spring IoC容器仅存在一个Bean实例,Bean以单例方式存在,默认值 |
prototype | 每次从容器中调用Bean时,都返回一个新的实例,即每次调用getBean()时,相当于执行newXxxBean() |
request | 每次HTTP请求都会创建一个新的Bean,该作用域仅适用于WebApplicationContext环境 |
session | 同一个HTTP Session共享一个Bean,不同Session使用不同的Bean,仅适用于WebApplicationContext环境 |
global-session | 一般用于Portlet应用环境,该运用域仅适用于WebApplicationContext环境 |
在 Bean 中的添加方式如下。
Spring 表达式
待补充