依赖注入
依赖注入的目的是降低对象之间的耦合度。任何一个程序都是有很多对象协作完成任务的。不可能每一个对象都没有关联。我们希望对象之间的耦合方式越松越好(不是越少越好)。那么什么是紧耦合?比方说A依赖B,那么直接在A的构造器或者任何初始化方法里面构建B的方式就是紧耦合,因为A的依赖已经写死在A的代码里面,如果所有的依赖都按照这种方式来写,那么整个程序就不是松耦合的。这样的坏处在于,一旦我们要修改依赖关系,就得从各种代码里识别出依赖,然后修改替换,如果我们有成千上万个类,改起来是一项很大的工程,还容易出错。另外这样的模块很难测试,因为我们想测一个模块A,我们无法直接把A模块拿出来测试,我们需要把A的各种依赖构造好,这又是一个递归的过程。
那么依赖注入是如何做到松耦合的呢?
我们每一个类的依赖都不做直接的初始化,这样就消除了直接依赖关系,但是我们需要提供初始化依赖的方法或者接口,比如说构造器参数或者setter方法等等。这样每一个类仍然会依赖其他类,但是不再直接依赖。最后,我们再根据依赖关系手动把各个对象的依赖组装起来。这其实就分为了两个过程,一是定义二是组装。
在spring框架下,我们需要提供定义,然后spring帮我们组装依赖。
这样,对象与对象之间不再紧耦合,如果我们想修改依赖,只需要修改这个类的依赖和注入依赖的方法即可。如果我们想测试一个类,我们完全可以把这个类单独拿出来,然后mock它的直接依赖,注入即可。这就是依赖注入的优点。
配置概述
Spring配置bean的方式按照配置文件来说只有两种,分别是基于java文件和基于xml文件。这两种模式又分别支持自动扫描和装配,所以可以看成是三种方式。
如何选择?
自动配置是最方便的,能用就用。但是某一些场景无法使用,比如引入第三方的依赖。这是就只能走配置文件方式了。xml配置文件时spring早期的主要方式,但是现在已经不推荐使用了。相比较于java配置,有如下缺点:
1.配置复杂,需要有xml文件头部,需要有各种xml标签(集合);
2.基于string配置,无法享受到java config在编译器的检查。比如我们的类名可能填错,但是编译器无能为力。
下面就以一个简单的CD机的场景作为例子来示范下这三种方式。
Player与CD之间通过接口耦合。
public interface CD {
void sound();
}
1.xml
我们需要在一个或者多个xml文件中定义bean。
xml文件建议从spring指定位置下载(https://spring.io/tools/sts)或者借助ide模板生成,
<?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 = "cd3" class="spring.xml.config.CD3"></bean>
<bean id = "player3" class="spring.xml.config.Player3">
<constructor-arg ref="cd3"></constructor-arg>
</bean>
<bean id = "player4" class="spring.xml.config.Player3">
<property name="CD" ref="cd3"></property>
</bean>
</beans>
每一个bean通过<bean>定义。简单的只要指定下class全限定类名。如果有依赖的可以通过构造器和setter方法注入。由于xml用的不多,其他xml配置这里不做深入介绍了。
测试:
这里使用classpathApplication上下文,默认加载类路径下的xml配置文件。
public class Main {
public static void main(String args[]) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
Player3 player3 = (Player3) applicationContext.getBean("player3");
player3.play();
Player3 player4 = (Player3) applicationContext.getBean("player4");
player4.play();
}
}
cacaca
cacaca
Process finished with exit code 0
2.java config
其实和xml的原理一致。只是xml方式bean被定义在xml文件中,而java只是将bean定义安排在一个java类文件中,换汤不换药。
承载bean定义的java文件本质上与其他java文件没有任何区别,只是我们需要一些spring的标签来标识它,这样spring就知道从这个文件中加载bean。
首先定义player和cd类。
public class CD2 implements CD {
public void sound() {
System.out.println("bababa");
}
}
public class Player2 {
private CD cd;
public void play(){
cd.sound();
}
public void setCD(CD cd){
this.cd = cd;
}
}
然后写配置类,并且需要使用@Configuration标签标识。
@Configuration
public class JavaConfig {
@Bean
public CD2 getCD2(){
return new CD2();
}
@Bean(name = "player2")
public Player2 getPlayer2(CD cd){
Player2 player2 = new Player2();
player2.setCD(cd);
return player2;
}
}
先标识了一个简单的CD java bean,内部没有任何属性。然后是Player java bean,内部引用指向一个CD实例,我们通过方法的参数来让spring注入依赖。
最后测试下,上下文选择AnnotationConfigApplicationContext,传入配置类的类实例来告诉spring配置类是哪一个。
public class Main {
public static void main(String args[]) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(JavaConfig.class);
Player2 player2 = (Player2) applicationContext.getBean("player2");
player2.play();
}
}
输出:
bababa
Process finished with exit code 0
一般建议使用java config方式,原因是xml完全基于string,而java config则可以提供类型检查,更加安全。
3.自动化
可以基于xml也可以基于java config。
不论哪一种都包括两部分,定义bean和装配bean。
3.1 定义bean
需要使用@Component注解。
@Component
public class CD1 implements CD{
public void sound() {
System.out.println("lalal");
}
}
@Component
public class Player {
@Autowired
private CD cd;
public void play(){
cd.sound();
}
}
可以看到,这里如果要定义一个bean,需要在POJO类上使用@Component注解,spring就会自动识别出这个类的实例要被spring容器管理。
3.2 装配bean
需要使用@Autowired注解。表示某一个类的实例需要spring容器来注入。比方说上面的Player实例CD,就带有@Autowired注解,这个实例变量最终会被赋上值。
除此之外,我们还需要告诉spring我们使用的是自动装配的方式配置bean,因为默认情况下,是不开启自动装配的。
如果使用的是xml方式,我们需要在xml配置文件中使用如下标签启用扫描:
<context:component-scan base-package="xxx.xxx......" />
xxx部分为要被扫描的包名。
如果使用的是java config方式,我们需要在配置类即被@congfiguration标记的来上添加@ComponentScan注解来显式开始自动装配。
@Configuration
@ComponentScan
public class M {
public static void main(String args[]){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(M.class);
Player p = (Player) applicationContext.getBean("player");
p.play();
}
}
结果:
lalal
Process finished with exit code 0
@ComponentScan默认扫描被标记类所在包下的bean。比方说我们把CD1类移出M.class所在的包,再运行就会报错:
No qualifying bean of type [spring.test.CD] found for dependency: expected at least 1 bean which qualifies as autowire candidate for this dependency.
因为CD1这个bean没有被扫描到,所以无法注入依赖。
当然我们可以指定扫描包的位置,使用@Component(“xxx”)。
4. bean定义导入
大型项目,我们如果把所有bean定义放在一个配置文件中,这个文件会太大,难以维护,我们可以拆分配置文件。
通常的做法呢,是定义一个顶级的配置文件,然后由它来聚合所有的模块配置文件。
4.1 xml方式
还是上面的例子,我们把cd和player的定义拆到两个文件内,然后定义一个顶层confi.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">
<import resource="bean1.xml"></import>
<import resource="bean2.xml"></import>
</beans>
该文件通过import标签导入了其他xml文件。
bean1.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">
<bean id = "cd3" class="spring.xml.config.CD3"></bean>
</beans>
bean2.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 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<bean id = "player3" class="spring.xml.config.Player3">
<constructor-arg ref="cd3"></constructor-arg>
</bean>
<bean id = "player4" class="spring.xml.config.Player3">
<property name="CD" ref="cd3"></property>
</bean>
</beans>
4.2 java config方式
@Configuration
@Import({Bean1.class, Bean2.class})
public class JavaConfig {
}
public class Bean1 {
@Bean
public CD2 getCD2(){
return new CD2();
}
}
public class Bean2 {
@Bean(name = "player2")
public Player2 getPlayer2(CD cd){
Player2 player2 = new Player2();
player2.setCD(cd);
return player2;
}
}
5.条件化bean
使用的是@Conditional标签。该标签需要传入一个org.springframework.context.annotation.Condition接口的实现类,其中的match方法负责返回是否加载该bean。
我们可以在match方法内部根据环境变量等条件来决定是否加载。
举一个例子,根据环境变量是否加载。
public class Need implements Condition {
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
Environment environment = conditionContext.getEnvironment();
return environment.containsProperty("need");
}
}
@Component
@Conditional(Need.class)
public class Service {
}
@Configuration
@ComponentScan
public class JavaConfig {
public static void main(String args[]) {
ApplicationContext app = new AnnotationConfigApplicationContext(JavaConfig.class);
Service service = (Service) app.getBean("service");
System.out.println(service);
}
}
在idea中配置环境变量need。
最后测试下:
spring.condition.Service@543c6f6d
Process finished with exit code 0
说明加载到。
如果去掉,则报错:
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'service' is defined
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:638)
at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1159)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:282)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:195)
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:973)
at spring.condition.JavaConfig.main(JavaConfig.java:14)
Process finished with exit code 1
6.自动装配下bean的歧义性
我们使用@Autowired自动装配时,是按照类型装配的,如果上下文中有多个类型匹配的bean,那么spring将不知道用哪一个,会报错。
例子:
public interface Bean {
}
@Component
public class TypeA implements Bean {
}
@Component
public class TypeB implements Bean {
}
@Component
public class Service {
@Autowired
Bean bean;
}
@Configuration
@ComponentScan
public class JavaConfig {
public static void main(String args[]) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(JavaConfig.class);
System.out.println(applicationContext.getBean("service"));
}
}
运行会报错:
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [spring.qiyi.Bean] is defined: expected single matching bean but found 2: typeA,typeB
有两个实现,但是期望单一的。
这时有两种解决方案。
6.1 通过@Pimary
该标签表明 如果有歧义,首选被该标签标注的。
6.2 通过@Qualifier
指定要装配的bean的id
7.bean的作用域
Spring定义了多种作用域,可以基于这些作用域创建bean,包括:
单例(Singleton):在整个应用中,只创建bean的一个实例。默认是单例。
原型(Prototype):每次注入或者通过Spring应用上下文获取的
时候,都会创建一个新的bean实例。
会话(Session):在Web应用中,为每个会话创建一个bean实例。
请求(Rquest):在Web应用中,为每个请求创建一个bean实例。
前两种只需要通过@Scope标签指定。或两者需要额外的属性proxyMode。
因为在单例的bean引用session或者request的bean时,会有问题。
首先,单例的bean在最开始初始化时,被依赖的bean还没有生成,因为还没有请求,故无法注入;
其次,即便可以被注入,也需要注入多次来对应不同的session或者request。所以这里实际上会注入一个proxy实例,当处理具体session或者request的时候,再懒加载被依赖bean实例。proxyMode用于指定代理的类型,是基于class还是interface。
最后附上pom依赖:
<properties>
<junit.version>4.12</junit.version>
<spring.version>4.3.9.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<!-- 添加Spring依赖的jar包-->
<!--依赖注入包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<!--切片包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Beans包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- 容器包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- 容器依赖包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- 核心包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- 表达式包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>${spring.version}</version>
</dependency>
<!--spring-framework-bom包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>${spring.version}</version>
</dependency>
<!--spring-instrument包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-instrument</artifactId>
<version>${spring.version}</version>
</dependency>
<!--连接数据库包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring消息包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jms</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring信息包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring对象映射包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<!--spring-oxm包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-oxm</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring测试包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring事物管理包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<!--Spring文本项目包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<!--SpringMVC包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<!--spring-websocket包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>