文章目录
基本问题
class A{}
class B{
B(){this.a = new A(...)}//紧耦合
}
class C{
C(A a){this.a = a}//松耦合
}
依赖
:如果B类需要使用到A类,那么类B就依赖于A。上面例子中,B依赖于A,C依赖于A。耦合
:存在依赖,就存在耦合。
– 上面例子中,A与B存在耦合,A与C存在耦合。
– 但耦合是有松紧
的,当依赖的类发生改变时,当前类需要做的改动越少,那么耦合程度就越低。
– 上面例子中,如果A的构造方法发生变化,那么B需要进行修改,C不需要进行修改,前者耦合程度低于后者的耦合程度。如果在实际中,大量的类都需要依赖A类,那么这些类全部都要修改,者带来很大的工作量。
那么我们应该采取怎样的设计理念来降低耦合度?
控制反转与依赖注入思想
- 上面例子中,紧耦合来自B类需要
控制
A类的初始化,我们可以将A类的初始化交给第三方对象来处理,然后保持对第三方对象的引用即可,比如一个简单的思路如下:
class B{
B(){
this.a = AFactory.newInstance();
}
}
使用工厂模式后,B减少响应A的修改,B和A是松耦合。
- 控制反转(Inversion Of Control,
IoC
)就要把B类对A类的控制权抽离出来,交给一个第三方去做,把控制权反转给第三方
。 - 第三方可以设计为一个
容器
,我们只需要将设计好的对象放入容器中,使用时从容器中获取即可。这样Ioc容器将控制所有放进去的对象
,这种思想就是依赖注入。 - 依赖注入(Dependency Injection,
DI
)是IoC最典型的实现方法。由第三方(我们称作IoC容器)来控制依赖,把IoC中的对象通过构造函数、属性或者工厂模式
等方法,注入到需要改对象的类中。
Spring的IoC容器
1. IoC容器和Bean概述
- IoC容器相关的类位于
org.springframework.beans
,org.springframework.context
这两个包下; BeanFactory
是所有IoC容器实现类的根接口,提供了配置框架和基本功能;ApplicationContext
接口继承于BeanFactory
,增加了更多关于企业的功能;Bean
的定义:在Spring应用中,构成应用程序主干
并且被IoC容器管理
的对象称为Bean。- 运行流程
首先生成IoC容器,为容器提供原始对象和配置对象的元数据(即如何构造对象),然后使用IoC容器获取装配好的对象。
2. 装配Bean
- 有3种装配机制:
在XML中显示配置
、在Java中显示配置
、自动装配
,使用优先级为3>2>1。
2.1 自动装配
@Component
- 在类上使用
@Component
注解,表明该类是组件类,Spring会将该类放入IoC容器管理,该类成为了容器中的一个Bean。 @Component
代表通用的组件类,还可以使用@Service
、@Controller
、@Repository
来表示服务组件、控制器组件、持久化组件,这3个注释只是添加了语义化,没有添加其他功能
。- 可以带参数为Bean指定id,
@Component("beanId")
- IoC容器默认调用的是Bean的无参构造方法,如果有参数,需要使用Value注解。
@Component
class Student{
String name;
Student(@Value("${name}")String name){
this.name = name;
}
}
@ComponentScan
- 该注解放在配置类的类注解上,表示启用
组件扫描
,默认扫描与配置类相同的包及子包,如果查找到某个类带有@Component
注解,那么会将该类放入IoC容器管理。 - 可以带参数,指定需要扫描的基础包
@Component(basePackages={})
@Autowired
- 使用这个注释在使用的地方自动装配Bean,可以在
成员变量、构造方法、构造方法参数、一般方法、setter方法等
位置
例:在B类注入A实例
//成员变量
@Autowired
A a;
//构造方法
@Autowired
public B(A a){
//use a do something
}
//构造方法参数
public B(@Autowired A a){
//use a do something
}
//一般方法,该方法由容器调用,一般指定为setter方法
@Autowired
public void f(A a){
//use a do something
}
//Autowired注释方法参数时,只能注释构造方法才有效,下面这个注入失败
public void g(@Autowired A a){
//这里a为null
}
- 可以带一个参数required,默认为true。当
@Autowired(required=true)
时,如果IoC容器没找到需要的Bean,就会抛出NoSuchBeanDefinitionException
;当@Autowired(required=false)
时,IoC容器没找到不会抛出异常,但注入的值为null,因此需要自己手动判断是否为空,否则容易造成NullPointerException
- 注意:如果一个类位于
容器内
注入另一个Bean,那么可以使用Autowired;如果一个类位于容器外,那么需要使用到容器的相关引用(如ApplicationContext
等)才能获取容器内的Bean。
- ApplicationContext
- 使用这个类创建IoC容器,两种创建方式
- 第一种,使用类生成,可以传
配置类
,组件类
,一般类
,配置类将会解析其中的注解配置,组件类就直接生成Bean定义,一般类(没有加Component注解)也会纳入容器作为Bean管理。
new AnnotationConfigApplicationContext(A.class,B.class,Config.class...);
- 第二种,传递基础包,自动扫描
配置类和组件类
,一般类会被忽略
new AnnotationConfigApplicationContext("test");
- 例子
//在B类,声明为一个组件/Bean
@Component
class B{}
//在Config类可以注入B类实例
@Configuration//表明这是一个配置类
@ComponentScan
class Config{
//使用上面几种autowired都可以
@Autowired B b;
}
//在Main方法
public static void main(String[] args){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Config.class);
}//声明容器,Config自动成为一个Bean
Config config = applicationContext.getBean(Config.class);//获取Bean
assertNotNull(config.b);//这里b已经成功注入了
2.2 通过Java代码装配
- 如果想注入
第三方库
中的组件,没法在其类上添加@Component注解,因此只能显式装配
,显式装配包括Java代码装配和XML装配。
@Bean
- 即声明一个Bean,Bean的id默认为方法名, 也可以使用name属性指定
//C类被声明为Bean;id为cName,默认为方法名c
@Bean(name="cName")
public C c() {
//这里可以做很多自定义操作来初始化C
return new C();
}
2.3 通过XML装配
- 这是比较传统的装配方式,与使用Java注释装配达到的最终效果是一致的。
- 在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.xsd">
<bean id="c" class="test.C"/>
</beans>
- 在代码中使用
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:config.xml");
C c = applicationContext.getBean(C.class);
System.out.println(c);
3. Bean限定符
3.1 @primary
- 在同一个类有多个Bean的候选时,可以指定其中一个为主要的Bean
@Bean
//@Primary
public D d1(){
return new D();
}
@Bean
public D d2(){
return new D();
}
//如果不加primary,那么下面两种情况会报错
//1.autowired
@Autowired D d;
//2.appContext.getBean
appContext.getBean(D.class);
//但这样不会报错
@Autowired D d1;
3.2 @Qualifier
- 限定符,消除Bean的歧义
- 在Bean的定义上添加这个注解
@Component
@Qualifier("d")
class D{}
@Bean
@Qualifier("d")
publlic D d(){
return new D();
}
- 使用
@Autowired
@Qualifier("d")
D d;
4. Bean的作用域(Scope)
4.1 6种作用域
单例(singleton)
整个应用只创建bean的1个实例原型(Prototype)
每次获取时,都创建一个新的实例- 会话(Session)
在Web应用中,为每个会话创建一个实例 - 请求(Request)
在Web应用中,为每个请求创建一个实例 - 应用(Application)
在Web应用中,为每个Servlet创建一个实例 - Socket
在Web应用中,为每个Socket创建一个实例
4.2 使用@Scope
在Bean定义的时候标记
@Component
@Scope("prototype")
public class A{}
@Bean
@Scope("prototype")
public A a(){
}
5. 条件化地创建Bean
5.1 @Conditional
以生产和开发环境为例,例如数据库连接对象等,在生产开发环境中需要不同的对象
//生产环境需要创建这个Bean
@Bean
@Conditional(ProdCondition.class)
C c1() {
return new C();
}
//开发环境创建这个Bean
@Bean
@Conditional(DevCondition.class)
C c2() {
return new C();
}
5.2 Condition接口
class ProdCondition implements Condition{
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return "prod".equals(context.getEnvironment().getProperty("env"));
}
}
class DevCondition implements Condition{
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return "dev".equals(context.getEnvironment().getProperty("env"));
}
}
matches方法返回是否需要创建这个Bean
6. 运行时注入外部值
6.1 @PropertySource
- 可以在配置类声明外部值来源的文件
@PropertySource("classpath:application.properties")
文件内容为key=value的形式
name=myName
key=value
6.2 Environment接口
- 使用Environment接口获取值
- 可以直接注入Environment对象
@Autowired Environment env;
- 也可以通过context获取
appContext.getEnvironment();
- 然后获取值
env.getProperty("key");
6.3 @Value
- 可以注入Spring表达式(SpEL)的值,包括字面量和外部的值。具体SpEL后面会写到。
- 主要注释字段和构造方法,为对象赋予初始值
- 直接赋予字面量值,内容可以为Spring表达式
@Value("myName")
private String name;
- 也可以赋予外部值
@Value("${name}")
private String name;