依赖注入的3种方式
1.在xml中显式配置
2.在java中基于注解配置
3.隐式Bean的发现机制和自动装配原则
在现实工作中,这三种方式都会用到,并且经常混合使用。建议优先级如下
(1)基于约定优于配置的原则,最优先的应该是隐式Bean的发现机制和自动装配原则。这样的好处是减少程序开发者的决定权,简单又不失灵活。
(2)在没有办法使用自动装配原则的情况下应该优先考虑Java接口和类中实现配置。这样的好处是避免xml配置的泛滥,也更为容易
(3)上述方法都无法使用的情况下,那么只能选择xml去配置spring IoC容器,比如第三方的类库。
1.通过xml装配
1.1 通过setter方法配置
<bean id="personService" class="com.wise.tiger.service.impl.PersonServiceImpl">
<property name="personName" value="张三丰"/>
<property name="hobbys">
<array>
<value>看美女</value>
<value>看小说</value>
<value>看电影</value>
</array>
</property>
<property name="hobbyList">
<list>
<value>hobby-list-1</value>
<value>hobby-list-2</value>
<value>hobby-list-3</value>
<value>hobby-list-4</value>
</list>
</property>
<property name="hobbySet">
<set>
<value>hobby-set-1</value>
<value>hobby-set-2</value>
<value>hobby-set-3</value>
</set>
</property>
<property name="intro">
<map>
<entry key="name" value="peppa"/>
<entry key="age" value="5"/>
<entry key="hobby" value="我喜欢乒,我喜欢乓,我喜欢suzy和我一起跳"/>
</map>
</property>
<property name="prop">
<props>
<prop key="prop1">prop-value-1</prop>
<prop key="prop2">prop-value-2</prop>
</props>
</property>
</bean>
- id:spring找到这个Bean的编号,不是一个必须的属性,如果没有指定,spring采用"权限定名“(类名首字母小写)的格式生成编号,id不支持特殊字符,且必须唯一。
- name:spring配置Bean的名称,支持重复命名和特殊字符
- class:一个类的全限定名
- property:定义类中的属性
1.2 通过构造器注入
<bean id="departmentService" class="com.wise.tiger.service.impl.DepartmentServiceImpl" init-method="init" destroy-method="destory">
<!--使用类构造器实例化Bean -->
<constructor-arg name="departmentName" value="生产部"/>
</bean>
constructor-arg用于定义类构造方法的参数,其中index用于定义参数的位置,而value则是设置值,也可以通过参数名name进行注入。
1.3 使用命名空间注入
<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.xsd">
<bean name="user" class="com.wise.tiger.User" p:name="jack" p:age="20" p:car-ref="car"></bean>
<beans/>
p:name代表构造方法参数名为name的参数,也可以采用p:_0表示构造方法的第一个参数
p:car-ref代表引用属性
1.4 其他命名空间注入(如xmlns:c xmlns:util)
<?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:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
<util:list name="emails">
<value>pechorin@hero.org</value>
<value>raskolnikov@slums.org</value>
<value>stavrogin@gov.org</value>
<value>porfiry@gov.org</value>
</util:list>
<util:map id="emails">
<entry key="pechorin" value="pechorin@hero.org"/>
<entry key="raskolnikov" value="raskolnikov@slums.org"/>
<entry key="stavrogin" value="stavrogin@gov.org"/>
<entry key="porfiry" value="porfiry@gov.org"/>
</util:map>
<util:set name="emails">
<value>pechorin@hero.org</value>
<value>raskolnikov@slums.org</value>
<value>stavrogin@gov.org</value>
<value>porfiry@gov.org</value>
</util:set>
<util:properties id="jdbcConfiguration" location="classpath:jdbc-production.properties"/>
</beans>
2.通过注解装配Bean
在Spring中,提供了两种方式来让 Spring IoC容器发现Bean。
- 组件扫描:通过定义资源的方式,让Spring IoC容器扫描对应的包,从而把Bean装配进来。
- 自动装配:通过注解定义,使得一些依赖关系可以通过注解完成
2.1 使用@Component装配Bean
首先打开注解扫描开关
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.2.xsd ">
<context:component-scan base-package="cn.itcast.bean"></context:component-scan>
</beans>
再在需要设置为Bean的类上面添加@Component注解
@Component
public class EmilSender {
}
注解@Component代表Spring IoC容器会把这个类扫描成Bean实例。而其中的value属性代表这个实例在Spring中的id,相当于xml方式定义的Bean的id,也可以简写成@Component(""),也可以直接写成@Component,id即为类的简单名称首字母小写。
spring主要有四种注解可以注册bean,每种注解可以任意使用,只是语义上有所差异:
- @Component:可以用于注册所有bean
- @Repository:主要用于注册dao层的bean
- @Controller:主要用于注册控制层的bean
- @Service:主要用于注册服务层的bean
2.2 自动装配@Autowired
为了应对这种明确的装配场景,Spring提供了自动装配。
当涉及到自动装配Bean的依赖关系时,Spring有多种处理方式。因此,Spring提供了4种自动装配策略。
<bean id="" class="" autowire="no|byName|byType|constructor|autodetect"></bean>
- no:不进行自动装配,手动设置Bean的依赖关系
- byName:根据Bean的名字进行自动装配
- byType:根据Bean的类型进行自动装配
- constructor:类似于byType,不过是应用于构造器的参数,如果正好有一个Bean与构造器的参数类型相同则可以自动装配,否则会导致错误
- autodetect:如果有默认的构造器,则通过constructor的方式进行自动装配,否则使用byType的方式进行自动装配
@Autowired默认按类装配。当 Spring 容器启动时,AutowiredAnnotationBeanPostProcessor 将扫描 Spring 容器中所有 Bean,当发现 Bean 中拥有 @Autowired 注释时就找到和其匹配的 Bean,并注入到对应的地方中去。IoC容器有时候会寻找失败,在默认情况下失败就会抛出异常,可以通过配置项required来改变它,比如:@Autowired(required=false)
@Service
public class PersonServiceImpl implements PersonSerivce {
@Autowired(required=false)
private PersonDao dao;
}
2.3 自动装配@Autowired的歧义性
@Autowired仅有一个bean匹配所需的结果时,自动装配才是有效的。如果符合条件的bean不只一个,这时就会阻碍Spring自动装配属性、构造器参数或方法参数。 为了消除歧义性,Spring提供了两个注解@Primary和@Qualifier。
@Primary标识首选的bean,某个接口有多个实现类,可在某个实现类上标注@Primary,出现歧义时,Spring会首选该bean,忽略其他的,但@Primary只能标注在一个接口的一个实现类上
以下三种方式:类、xml、方法使用primary
@Component
@Primary
public class PersonServiceImpl1 implements PersonService{
}
--------------------------------------------------------
<bean id="personService1" class="com.wise.serivce.impl.PersonServiceImpl1 " primary="true" />
--------------------------------------------------------
@Bean
@Primary
public PersonService getPersonService(){
return new PersonServiceImpl1();
}
其次,可以使用@Qualifier("beanName")明确指定要注入的是哪个bean
public class BookServiceImplTest {
@Autowired
@Qualifier("bookService")
private BookServiceImpl service;
}
2.4 @Resource注解解决歧义(推荐使用)
@Resource(name="car")
private Car car;
**2.5 装配带有参数的构造方法类**
@Service
public class PersonServiceImpl {
private PersonDao dao;
public PersonServiceImpl(@Autowired PersonDao dao) {
this.dao = dao;
}
}
@Autowired和@Qualifier这两个注解可以支持到参数。
3. 使用@Bean装配Bean
以上大部分都是通过@Component装配Bean,但是@Component只能注解在类上,不能注解到方法上,@Bean可以注解到方法上,将方法返回的对象作为Spring的Bean存放在IoC容器中,如没指定name,bean的id默认为方法名。
@Bean
public DataSource devDataSource(@Value("${driverClassName}") String driverClassName,
@Value("${url}") String url, @Value("${username}") String username,
@Value("${password}") String password, @Value("${connectionProperties}") String connectionProperties) throws Exception {
var prop = new Properties();
prop.put("driverClassName", driverClassName);
prop.put("url", url);
prop.put("username", username);
prop.put("password", password);
prop.put("connectionProperties", connectionProperties);
return BasicDataSourceFactory.createDataSource(prop);
}
3.1 注解自定义Bean的初始化和销毁方法
@Bean(name="", initMethod="init",destroyMethod="destroy")
@Bean的配置项中包含4个配置项。
- name:字符串数组,允许配置多个BeanName,没有配置默认为方法名。
- autowire:标志是否是一个引用的Bean对象,默认值是Autowire.NO
- initMethod:自定义初始化方法
- destroyMethod:自定义销毁方法
**3.2 装配的混合使用**
spring-data.xml->使用xml配置数据源
context:property-placeholder location="classpath:jdbc.properties"/
<!--定义数据源:Druid数据源默认是自动提交事务,需要关闭自动提交 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
<property name="driverClassName" value="${driverClassName}"/>
<property name="url" value="${url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${password}"/>
<property name="connectionProperties" value="${connectionProperties}"/>
<property name="defaultAutoCommit" value="${defaultAutoCommit}"/>
</bean>
这种方式我们不需要去了解第三方的更多细节,也不需要过多的java代码,尤其是不用try...catch...finally...语句去处理它们,相对与@Bean的注入会更好一些,也更为简单,所以对于第三方的包或者其它外部的接口,建议使用xml的方式。
spring同时支持这两种形式的装配,可以自由选择,无论是xml还是注解都是将bean装配到Spring IoC容器中,这样就可以通过spring IoC容器去管理各类资源了,首先使用@ImportResource,引入spring-data.xml所定义的类容。
@Configuration
@ComponentScan(basePackages = "com.wise.tiger")
@ImportResource(locations = {"classpath:spring-data.xml"})
public class ApplicationConfig {
}
当需要使用到数据源DataSource时,就可以使用@Autowired或者@Resource进行注入
4.使用Profile
在软件开发过程中,敏捷开发模式很常见,一种时间控制的迭代式实现和发布软件的方法。那么可能是开发人员使用一套环境,而测试人员使用另外一套环境,而这两天系统的数据库是不一样的,毕竟测试人员也需要花费很多时间去构建测试数据,可不想老是被开发人员修改那些测试数据,这样就有了在不同环境中进行切换的需求了。spring也会对这样的场景进行支持。
4.1使用@Profile配置
@Bean(name = "devDataSource")
@Profile("dev")
public DataSource getDataSource() {
var dataSource = new BasicDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setDriverClassName(driverClassName);
dataSource.setPassword(password);
dataSource.setMaxTotal(maxTotal);
dataSource.setMaxIdle(maxIdle);
dataSource.setMaxWaitMillis(maxWaitMillis);
dataSource.setConnectionProperties(connectionProperties);
dataSource.setAutoCommitOnReturn(defaultAutoCommit);
return dataSource;
}
@Bean(name = "testDataSource")
@Profile("test")
public DataSource getDataSource1(
@Value("${driverClassName}") String driverClassName,@Value("jdbc:mysql:///test") String url,
@Value("${username}") String username,@Value("${password}") String password) {
var dataSource = new BasicDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setDriverClassName(driverClassName);
dataSource.setPassword(password);
return dataSource;
}
4.2.使用xml定义Profile
<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="...">
<!-- other bean definitions -->
<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>
4.3 启动Profile
当启动java配置或者xml配置profile时,Bean并不会被加载到IoC容器中。需要自行激活Profile。激活方法有5种
- 在使用SpringMVC的情况下可以配置Web上下文参数,或者DispatchServlet参数
- 作为JNDI条目
- 配置环境变量
- 配置JVM启动参数
- 在集成测试环境中使用@ActiveProfiles
常用激活:
在测试代码中激活Profile,如果是开发人员进行测试,那么可以使用注解@ActiveProfiles进行定义
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="classpath:spring-profile.xml")
@ActiveProfiles("dev")
public class TestActiveProfile {
}
在测试代码中可以加入@ActiveProfiles来指定加载哪个Profile,这样程序就会自己去加载对应的profile了。但是毕竟不是什么时候都是在测试代码中运行,有些时候要在服务器上运行,那么这个时候可以配置java虚拟机的启动项,关于指定profile的参数存在两个。
spring.profiles.active 启动的
spring.profiles.default 默认的
可以配置JVM的参数来启动对应的Profile,比如需要启动test:
JAVA_OPTS="-Dspring.profiles.active=test"
在大部分情况下需要启动web服务器,通常可以在 web.xml 中定义全局 servlet 上下文参数 spring.profiles.default 实现,代码如下
<!-- 配置spring的默认profile -->
<context-param>
<param-name>spring.profiles.default</param-name>
<param-value>test</param-value>
</context-param>
**5 加载属性(properties)文件**
在开发过程中,配置文件往往就是那些属性(properties)文件,比如:
driverClassName = com.mysql.cj.jdbc.Driver
url = jdbc:mysql://localhost:3306/db_book
username = peppa
password = pedro
########## dbcp连接池基本属性 #############
# 初始化连接
initialSize=20
#最大连接数量,设 0 为没有限制
maxTotal = 0
#最大空闲连接
maxIdle = 10
#最小空闲连接
minIdle = 3
#超时等待时间以毫秒为单位
maxWaitMillis = 1000
#JDBC驱动建立连接时附带的连接属性属性的格式必须为这样:[属性名=property;]
#注意:"user" 与 "password" 两个属性会被明确地传递,因此这里不需要包含他们。
connectionProperties = serverTimezone=UTC;useSSL=false;useUnicode=true;characterEncoding=utf-8
#指定由连接池所创建的连接的自动提交(auto-commit)状态。如果指定为false表示关闭自动提交
defaultAutoCommit = false
#driver default 指定由连接池所创建的连接的只读(read-only)状态。默认false
#如果没有设置该值,则“setReadOnly”方法将不被调用。(某些驱动并不支持只读模式,如:Informix)
#defaultReadOnly=
#driver default 指定由连接池所创建的连接的事务级别(TransactionIsolation)。
#可用值为下列之一:(详情可见javadoc。)NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
#defaultTransactionIsolation = REPEATABLE_READ
使用properties是十分常见的情景,可以有效减少硬编码,有效提高运维人员的操作便利性。
** 5.1.使用注解方式加载属性文件**
Spring提供了@PropertySource来加载属性文件
- name:字符串,属性配置的名称
- value:字符串数组,可以配置多个属性文件
- ignoreResourceNotFound:boolean值,默认值为false:如果属性文件没有找到是否忽略处理。
- encoding:编码
@Configuration
@ComponentScan(basePackages = "com.wise.tiger")
@PropertySource(value = "classpath:dbcp-config.properties",ignoreResourceNotFound = true,encoding = "UTF-8")
public class ApplicationConfig {
}
使用注解@Value和占位符去解析属性占位符
@Configuration
@ComponentScan(basePackages = "com.wise.tiger")
@PropertySource(value = "classpath:dbcp-config.properties",ignoreResourceNotFound = true,encoding = "UTF-8")
public class ApplicationConfig {
@Value("${url}")
private String url;
@Value("${username}")
private String username;
@Value("${password}")
private String password;
@Value("${driverClassName}")
private String driverClassName;
@Value("${maxTotal}")
private int maxTotal;
@Value("${maxWaitMillis}")
private long maxWaitMillis;
@Value("${maxIdle}")
private int maxIdle;
@Value("${defaultAutoCommit}")
private boolean defaultAutoCommit;
@Value("${connectionProperties}")
private String connectionProperties;
@Bean(name = "dataSource")
public DataSource getDataSource() {
var dataSource = new BasicDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setDriverClassName(driverClassName);
dataSource.setPassword(password);
dataSource.setMaxTotal(maxTotal);
dataSource.setMaxIdle(maxIdle);
dataSource.setMaxWaitMillis(maxWaitMillis);
dataSource.setConnectionProperties(connectionProperties);
return dataSource;
}
}
** 5.2 使用xml方式加载属性文件**
<context:property-placeholder location="classpath:dbcp-config.properties" ignore-resource-not-fount="true"/>
ignore-resource-not-fount属性代表是否允许文件不存在,当默认值为false时,不允许文件不存在,如果不存在,spring会抛出异常
location是一个配置文件路径的选项,可以配置多个或者单个文件,多个文件之间用,分割。如果系统中存在很多文件,那么属性location就要配置长长的字符串了,不过还有其它的xml方式可以进行配置:
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<!-- 字符串数组,可以配置多个属性文件-->
<property name="locations">
<array>
<value>classpath:jdbc.properties</value>
<value>classpath:message.properties</value>
</array>
</property>
<property name="ignoreResourceNotFount" value="true"/>
</bean>
6 条件化装配Bean
在某些条件下不需要去装配Bean,比如当属性文件中没有数据源的基础配置的时候就不要去创建数据源。这时就需要通过条件化去判断。Spring提供了@Conditional去配置,通过配置一个或多个类,只是这些类需要实现接口Condition。
@Bean(name = "dataSource")
@Conditional(DataSourceCondition.class)
public DataSource getDataSource(
@Value("${driverClassName}") String driverClassName,
@Value("${url}") String url,
@Value("${username}") String username,
@Value("${password}") String password) {
var dataSource = new BasicDataSource();
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setDriverClassName(driverClassName);
dataSource.setPassword(password);
return dataSource;
}
通过@Value往参数里注入了对应属性文件的配置,但是我们没有办法确定这些数据源连接池的属性是否在属性文件中已经配置完整,如果是不充足的属性配置,则会引起创建失败,为此要判断属性文件的配置是否满足才能继续创建Bean。通过@Conditional去引入了一个条件判断类----DataSourceCondition,由它来进行判断。
package com.wise.tiger;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
/**
* 条件判断,需要实现Condition接口
*/
public class DataSourceCondition implements Condition {
/**
* 判断属性文件中是否配置了数据源的相关参数
* @param context:通过它可以获取spring的运行环境
* @param metadata:通过它可以获得关于该Bean的注解信息
* @return true:创建对应的Bean,false:不会创建
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//获取Spring的运行环境
var env = context.getEnvironment();
//判断是否存在关于数据源的基础配置
return env.containsProperty("driverClassName")
&& env.containsProperty("url")
&& env.containsProperty("username")
&& env.containsProperty("password");
}
}
6.1 Bean的作用域
在默认情况下,Spring IoC容器只会对一个Bean创建一个实例。bean可以定义为部署在多个作用域中的一个作用域中。可以采用@Scope注解声明Bean的作用域
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
7 使用SPring表达式(Spring EL)
Spring还提供了更灵活的注入方式,那就是Spring表达式,Spring表达式语言(简称spel)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于统一的EL,但提供了其他特性,最显著的是方法调用和基本的字符串模板功能:
- 使用Bean的id来引用Bean
- 调用指定对象的方法和访问对象的属性
- 进行运算
- 提供正则表达式进行匹配
- 集合配置
7.1 Spring EL相关类
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'");
String message = (String) exp.getValue();//The value of the message variable is 'Hello World'.
exp = parser.parseExpression("'Hello World'.concat('!')");
message = (String) exp.getValue(); //The value of message is now 'Hello World!'.
// Create and set a calendar
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);
// The constructor arguments are name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name");
String name = (String) exp.getValue(tesla);
// name == "Nikola Tesla"
exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true
7.2 Bean的属性和方法
使用注解的方式需要用到@Value,在属性文件的读取中使用的是$,而在spring el中则使用#。
@Component
public class Person {
@Value("#{100}")
private Long id;
@Value("#{'jorge'}")
private String name;
//setters and getters
}
@Component
public class ELBean {
@Value("#{person.id}")
private Long id;
@Value("#{'peppa'}")
private String name;
@Value("#{person}")
private Person person;
//setters and getters
}
7.3 使用类的静态常量和方法,运算
@Value("#{T(Math).PI}")
private double pi;
@Value("#{T(Math).random() * 100}")
private int random;
@Value("#{person.getName()?.toString()}")
private String note;
@Value("#{person.getName()?:'peppa'}")
private String defaultnote;