f10@t's blog

Spring基础学习 - IOC机制

字数统计: 9.3k阅读时长: 40 min
2020/05/06

Spring 全家桶

先来搞清楚Spring全家桶的关系吧,见过了Spring Boot、Spring Cloud等,还有什么SpringMVC,Spring Data啥的,一直没有搞清楚它们的关系。MVC设计模式的原理不再赘述,可以移步到参考链接中学习。

Spring框架自2002年开始发展,包括SpringMVC、SpringBoot、Spring Cloud、Spring Cloud Dataflow等解决方案,俗称Spring 全家桶。

准确来说的话SpringMVC是SpringFramework的一个组件,其他什么Spring Data、Spring Cloud都是建立在SpringFramework的基础上的:

其中对于Spring Framework本身:

底层中核心有AOP(Aspect-Oriented Programming, 面向切面编程)和IOC(Inverse of Control,控制反转),这两者是Spring的两大特性,知乎前辈指导先把这俩学清楚。

Spring的所有包模块

Spring模块本质就是多个jar文件,其中打包了该模块所需的代码,从Spring5版本开始Spring提供了21个模块,使用的时候如Maven,只需要引入依赖即可。下面的总结表格来自《Spring5高级编程(第五版)》。

模块 描述
aop 该模块包含在应用程序中使用 Spring的AOP 功能时所需的所有类。如果打算在 Spring 中使用其他使用了 AOP 的功能, 例如声明式事务管理, 则需要在应用程序中包含此JAR 文件。 此外,支持与 AspectJ 集成的类也封装在此模块中
aspects 该模块包含与 AspecU AOP 库进行高级集成的所有类。 例如, 如果为完成 Spring 配置而使用 Java 类,并且需要 Asp创 风格的注解驱动的事务管理,则需要使用此模块
beans 该模块包含所有支持 Spring 对 Spring bean进行操作的类。 该模块中的大多数类都支持 Spring 的阳bean工厂实现。例如, 处理 Spring XML 配置文件和 Java 注解所需的类被封装到此模块中
beans-groovy 此模块包含用于支持 Spring 对 Spring bean 进行操作的 Groovy 类
context 该模块包含为 Spring Core 提供许多扩展的类。所有类都需要使用 Spring 的 ApplicationContext功能以及 Enterprise JavaBeans(EJB)、Java Naming and Directory lnterface(JNDI)和 Java Management Extensions(JMX) 集成的类。此模块中还包含 Spring 远程处理类,与动态脚本语言(例如 JRuby 、Groovy 和 BeanShe ll)、 JSR-303(Bean Validation)、调度和任务执行等集成的类
context-indexer 该模块包含一个索引器实现,它提供对META-INF/spring.components 中定义的候选项的访问功能。但核心类 CandidateComponentslndex并不能在外部使用
context-support 该模块包含对 spring-context模块的进一步扩展。 在用户界面方面,有一些用于支持邮件并与模板引擎(例如Velocity、FreeMarker和JasperReports)集成的类。 此外,还包括与各种任务执行和调度库(包括 CommonJ和Quatz的集成
core 这是每个 Spring 应用程序都需要的主要模块。在该 JAR 文件中,可以找到所有其他 Spring 模块{例如,用于访问配置文件的类)所共享的所有类。另外,在该 JAR 文件中,会发现在整个 Spring 代码库中都使用的非常有用的实用程 序类,可以在自己的应用程序中使用它们
expression 该模块包含 Spring Expression Language(SpEL)的所有支持类
instrument 该模块包含用于JVM启动的Spring工具代理。 如果在 Spring 应用程序中使用 AspectJ 实现加载时植入,那么该模块是必需的
jdbc 该模块包含所有的 JDBC 支持类。对于需要数据库访问的所有应用程序,都需要此模块。支持数据源、 JDBC 数据 类型、 JDBC 模板、本地 JDBC 连接等的类都被打包在此模块中
jms 该模块包含川S 支持的所有类
orm 该模块扩展了 Spring 的标准 JDBC 功能集,支持流行的 ORM 工具,包括 Hibemate、JDO、JPA 和数据映射器 iBATIS。该 JAR 文件中的许多类都依赖于 spring-jdbc JAR 文件中所包含的类, 因此也需要把它包含在应用程序中。
oxm 该模块为Object/XML映射(OXM)提供支持。 用于抽象 XML 编组和解组以及支持Castor、JAXB、XMLBeans和XStream等常用工具的类都包含在此模块中
test Spring 提供一组模拟类来帮助测试应用程序,并且许多模拟类都在 Spring 测试套件中使用,所以它们都经过了很好 的测试,从而使测试应用程序变得更简单。 在对 Web 应用程序进行单元测试时会发现模拟HttpServletRequest和 HttpServletResponse类所带来的好处。 另一方面,Spring提供了与 JUnit 单元测试框架的紧密集成, 并且在该模块中 提供了许多支持 JUnit 测试用例开发的类:例如:SpringJUnit4ClassRunner提供了一种在单元测试环境中引导Spring ApplicationContext的简单方法
tx 该模块提供支持Spring事务基础架构的所有类。 可以从事务抽象层找到相应的类来支持Java Transaction APl(JT A) 以及与主要供应商的应用程序服务器的集成
web 此模块包含在 Web 应用程序中使用Spring所需的核心类,包括用于自动加载 ApplicationContext 功能的类、文件上传支持类以及一些用于执行重复任务(比如从查询字符中解忻整数值)的有用类
web-reactive 该模块包含SpringWeb Reactive模型的核心接口和类
wev-mvc 该模块包含Spring自己的 MVC 框架的所有类。 如果想要为应用程使用单独的 MVC框架,则不需要此 JAR 文件中的任何类。
webfsocket 该模块提供对 J SR-356仰ebSocket 的 Java API)的支持

IOC

控制反转,本质就是将创建对象的控制权交给SpringFramework。简单来说我们一般新建一个对象:Person person = new Person()使用了new关键字来进行操作,现在将这个工作交给SpringFramework的意思。

IoC Implementation Strategies

IoC is a broad concept that can be implemented in different ways. There are two main types:

Dependency Lookup: The container provides callbacks to components, and a lookup context.This is the EJB and Apache Avalon approach. It leaves the onus on each component to use container APIs to look up resources and collaborators. The Inversion of Control is limited to the container invoking callback methods that application code can use to obtain resources.

Dependency Injection: Components do no look up; they provide plain Java methods enabling the container to resolve dependencies. The container is wholly responsible for wiring up components, passing resolved objects in to JavaBean properties or constructors. Use of JavaBean properties is called Setter Injection; use of constructor arguments is called Constructor Injection.

The second IoC strategy-Dependency Injection-is usually preferable. ——《expert ONE-ON-ONE J2EE Development without EJB》

​ 即在广义上,IOC机制有两种实现机制:

  • 依赖查找。即容器提供了组件调用组件时的路径以及查询组件的上下文,将资源等的查找责任交给了组件本身,使用容器的API即可实现。EJB(Enterprise Java Beans)和Apache Avalon使用了这种方法。
  • 依赖注入(DI)。组件不需要取查找依赖,而是将java方法提供给容器来解决依赖。容器完全负责连接组件,将解析的对象传递给JavaBean属性或构造函数。其中使用JavaBean属性称为Setter注入,使用构造函数参数称为构造器注入。

这里的容器 - container指的是IOC容器。

下面是更具体的对于IOC机制的解释:

Spring框架的核心是基于控制反转(Inversion of Control, IoC)的原理。 IoC 是一种将组件依赖项的创建和管理外部化的技术。请参考一个示例,比如类 Foo 依赖于 Bar 类的一个实例来执行某种处理。 传统上, Foo 通过使用 new 操作符来创建 Bar 实例或从某个工厂类中获取 Bar 实例。 而如果使用 IoC 方法, Bar 的一个实例(或一个子类)在运行时由某个外部进程提供给 Foo。这种在运行时注入依赖项的行为促使 Martin Fowler将 IoC 重命名为更具描述性的依赖注入ρependency Injection, DI) 。

——《Spring5高级编程(第五版)》

DI其实是Martin Fowler对IOC的另一种更具体的解释。即Spring通过DI来实现在程序运行的时候,动态的向某个对象提供他所需要的其他对象。

Spring的DI实现基于两个核心的Java概念:JavaBeans和接口,JavaBean也叫做Plain Ordinary Java Object(POJO,简单普通Java对象),它概念这里不做解释,下面的代码会说到它的特点。当使用Spring作为DI提供程序时,可以通过不同的方式来在应用程序中灵活定义依赖项配置,如:

  • XML文件
  • Java配置类
  • Java注解

JavaBean提供了一种创建Java资源的标准机制,这些资源可以通过多种方式进行配置,这个配置的过程叫做注入,这也就是依赖注入名字的意思了,下面我们把配置这个动词换为注入。那么使用DI的好处是什么呢?

  • 减少粘合代码,这是DI的最大优点之一。
  • 简化应用程序配置
  • 能够在单个存储库中管理常见依赖项
  • 可测试性
  • 良好的程序设计

Spring依赖注入的方式有四种:setter注入、构造器注入、基于注解、静态工厂注入。下面一个个的进行进行解释。首先我们先来了解什么是JavaBean,并通过一个例子来理解为啥需要这种IOC机制。

JavaBean

其实但是我看到这个名字就很好奇,javabean,bean?豆子?啥意思?想比喻啥?专门查了以下才知道...好吧javabean是咖啡豆哈哈哈,煮咖啡不得搞咖啡豆(滑稽。

这里我不想抛出太多的专业解释,只放出我的理解。JavaBean又叫POJO(Plain Ordinary Java Object),是简单Java对象的意思,命名原因据说是为了和EJB做区分。

简单对象嘛,那就是很简单的对象喽....

1
2
3
4
5
6
7
8
9
10
11
12
public class Person {

private String name;

public getName(){
return this.name;
}

public setName(String name){
this.name = name;
}
}

上面这个类就是javabean,好了我说完了。

哈哈哈你是不有点懵,其实JavaBean就是一个提供了getter、setter方法的对象,可以就如同一个黑盒子,你甭管我咋写的,你就知道可以get和set就完事了。对于Spring,这就是DI的一个最小管理单元,用来由Spring在合适的时候动态的向其中提供类或取出类,那我们下面一个例子就知道JavaBean的作用了。

一个实例

我们现在考虑这样的经典场景,消费者和生产者,生产者生产消息、消费者输出消息:

1
2
3
4
5
6
7
8
9
10
package demo1;

/**
* 消息提供者
*/
public interface MessageProvider {

String getMessage();

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package demo1;

/**
* 消息渲染者(消费者)
*/
public interface MessageRender {

/*
输出消息
*/
void render();

/*
设置生产者
*/
void setMessageProvider(MessageProvider messageProvider);

/*
获取生产者
*/
MessageProvider getMessageProvider();

}

对应的实现:

1
2
3
4
5
6
7
package demo1;

public class HelloWorldMessageProvider implements MessageProvider{
public String getMessage() {
return "Hello, World";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package demo1;

public class StandardOutMessageRender implements MessageRender {

private MessageProvider messageProvider;

public void render() {
if (messageProvider == null) {
throw new RuntimeException(
"your must set the property messageProvider of class:"
+ StandardOutMessageRender.class.getName());
}
System.out.println(this.messageProvider.getMessage());
}

public void setMessageProvider(MessageProvider messageProvider) {
this.messageProvider = messageProvider;
}

public MessageProvider getMessageProvider() {
return this.messageProvider;
}

}

现在想一想,如果我们要使用的话,main函数是不是这么写?

1
2
3
4
5
6
7
8
9
10
11
12
package demo1;

public class HelloWorldcoupled {

public static void main(String[] args) {
MessageProvider msgP = new HelloWorldMessageProvider();
MessageRender msgR = new StandardOutMessageRender();
msgR.setMessageProvider(msgP);
msgR.render();
}

}

即我们需要手动new对象,再进行对应的操作。

这是因为我们的应用程序比较简单,你这么搞没人说你错,但是想一想,如果我要增加一个生产者呢?我是不是还得修改对应的类?修改main方法?假如你这是个编写好的程序,你不还得重新打包、重新发布?

换句话说:代码的耦合性很高,难于维护。

那好了有人说用工厂类,再加个配置文件,用反射机制动态获取类,可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package demo1;

import java.util.Properties;

public class MessageSupportedFactory {

private static MessageSupportedFactory instance;

private Properties props;
private MessageRender renderer;
private MessageProvider provider;

static {
instance = new MessageSupportedFactory();
}

private MessageSupportedFactory() {
props = new Properties();
try {
props.load(this.getClass().getResourceAsStream("/msf.properties"));
String rendererClassName = props.getProperty("renderer.class");
String providerClassName = props.getProperty("provider.class");

renderer = (MessageRender)Class.forName(rendererClassName).newInstance();
provider = (MessageProvider)Class.forName(providerClassName).newInstance();
} catch (Exception ex) {
ex.printStackTrace();
}
}

public static MessageSupportedFactory getInstance() {
return instance;
}

public MessageRender getRenderer() {
return renderer;
}

public MessageProvider getProvider() {
return provider;
}

}

配置文件:

1
2
renderer.class = demo1.StandardOutMessageRender
provider.class = demo1.HelloWorldMessageProvider

再修改以下main函数:

1
2
3
4
5
6
7
8
9
10
package demo1;

public class HelloWorldDecoupled {
public static void main(String[] args) {
MessageProvider msgP = MessageSupportedFactory.getInstance().getProvider();
MessageRender msgR = MessageSupportedFactory.getInstance().getRenderer();
msgR.setMessageProvider(msgP);
msgR.render();
}
}

OK,不需要new对象了,直接在工厂类里用反射机制动态获取配置文件中的类,再操作就行了。那其实你还是需要用set方法来进行配置,如果你这个类有一堆setter呢?还是需要修改main函数的。那么下来我们使用Spring进行重构。

使用Spring重构实例

明确问题:解决手动调用set方法进行对象的依赖管理。

我们首先写一个spring.xml,名字无所谓

1
2
3
4
5
6
7
8
9
<?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 class="demo1.HelloWorldMessageProvider" id="provider" />
<bean class="demo1.StandardOutMessageRender" id="render">
<property name="messageProvider" ref="provider"/>
</bean>
</beans>

我来解释以下这个文件的内容,那几行xmlns是自动生成的,作用是充当命名空间,你下面使用的标签都是上面这些命名空间里定义的,也有其他的如http://www.springframework.org/schema/p等可以实现如简写的功能,这里不用管他,后面我会说到常用命名空间的使用,下面的<bean>标签是我们的重点。

1
<bean class="demo1.HelloWorldMessageProvider" id="provider" />

对于这一个bean对象,它对应的是刚才HelloWorldMessageProvider这个类,id属性是我们对这个bean的标识。这就是一个bean了,其实就如同一个单元,交给了spring的上下文。另一个是一样的,但是多了个property属性,什么意思呢?

在StandardOutMessageRender这个类中我们有一个Setter方法以及一个私有属性messageProvider:

1
2
3
4
5
private MessageProvider messageProvider;

public void setMessageProvider(MessageProvider messageProvider) {
this.messageProvider = messageProvider;
}

ok,messageprovider是属性吧?行,那这个标签叫property你就理解了,ref是什么鬼?ref对应的就是这个setter方法,它的属性值就是这个setter方法的参数类型(MessageProvider)。

即这个bean指向HelloWorldMessageProvider对象,这个对象有一个messageProvider属性,他的setter方法里参数填充为providerprovider是谁?-- > HelloWorldMessageProvider,这就是id的作用了。

我们再重新写一下main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package demo1;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class HelloWorldSpringDI {

public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
MessageRender mr = context.getBean("render", MessageRender.class);
mr.render();
}

}

ApplicationContext是一个上下文接口类,ClassPathXmlApplicationContext类可以用来读取xml文档,这里读取的就是我们的bean定义文档。通过这个函数我们可以获取到Spring的上下文,这个上下文里就包含了我们定义的beans(那两个类),再通过getBean方法来获取指定的对象,这里我们获取id为render的类,即StandardOutMessageRender类对象,最后调用render()方法进行输出。

没有new吧?(上下文那个不算...),我没调用setter方法吧?那运行一下。

可以这就是IOC的魅力,Spring通过IOC机制在合适的时候创建需要的对象(StandardOutMessageRender和HelloWorldMessageProvider),并自动调用其中的setter方法,将HelloWorldMessageProvider对象自动注入到了StandardOutMessageRender类对象中,这个过程过程我们叫依赖注入

基于注解的依赖注入

从Spring3.0开始开发Spring应用程序已经不需要XML配置文件了,可以把这些配置都换为如下两类:

  • 注解:使用@Bean进行注解,标识这是一个bean。
  • 配置类:使用@Configuration注解的java类,可以在内部使用@Bean来定义bean,也可以通过@Component注解来扫描指定包下的bean定义

我们再看一眼上面的那个XML配置文件:

1
2
3
4
5
6
7
8
9
<?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 class="demo1.HelloWorldMessageProvider" id="provider" />
<bean class="demo1.StandardOutMessageRender" id="render">
<property name="messageProvider" ref="provider"/>
</bean>
</beans>

我们定义了两个bean交由Spring上下文进行管理,现在我们使用注解来完成这一功能,xml文档就可以扔了,取而代之的是一个配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package annotated;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class HelloWorldConfig {

@Bean
public MessageProvider provider() {
return new HelloWorldMessageProvider();
}

@Bean
public MessageRender renderer() {
MessageRender messageRender = new StandardOutMessageRender();
messageRender.setMessageProvider(provider());
return messageRender;
}
}

首先用@Configuration标注这是一个配置类,我们要从这个配置类中获取上下文。定义两个函数分别获取生产者和消费者,并使用@Bean进行注解,使用该注解就相当于再XML里定义了两个bean。接口和实现无需修改,我们重新写下main函数:

1
2
3
4
5
6
7
8
9
10
11
12
package annotated;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class HelloWorldSpringAnnotated {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(HelloWorldConfig.class);
MessageRender msgR = context.getBean("renderer", MessageRender.class);
msgR.render();
}
}

使用AnnotationConfigApplicationContext函数从我们的配置类中获取上下文即可,该类作用如下:

1
2
Create a new AnnotationConfigApplicationContext, deriving bean definitions from the given annotated classes and automatically refreshing the context.
@param annotatedClasses one or more annotated classes,

那么运行结果也是一样的:

为什么在getBean方法中指定id为renderer呢?因为对应的bean的函数名叫renderer。

DI的类型

这里再澄清一下IoC和DI的关系和含义,IoC(控制反转)的核心是DI(依赖注入),旨在提供一种更简单的机制来设置组件依赖项,前面我们也提到IoC有两种类型:依赖查找和依赖注入,所以DI是IoC的一种实现而已。

对于依赖查找这篇不做解释,常用于配合JNDI API来进行依赖管理,包含 依赖拉取(DL, dependency pull)和上下文依赖查找(CDL, contextualized dependency lookup)。

对于依赖注入DI,共四种方法:常用风格有两种:构造函数注入和setter依赖注入,另外不太常用的有方法注入和字段注入(field injection)。

构造函数注入

使用构造函数注入常见的方法是使用XML配置文件,另外也有更简洁的注解配置方法。

XML配置方法

HelloWorldMessageProvider的返回消息是硬编码的"Hello, Word",我们重新写一个可以通过配置改变的MessageProvider。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package annotated;

public class ConfigurableMessageProvider implements MessageProvider {

private final String msg;

public ConfigurableMessageProvider(String msg) {
if (msg == null) {
throw new IllegalArgumentException(
"You need to provide the message.");
}
this.msg = msg;
}

@Override
public String getMessage() {
return msg;
}

}

通过构造方法来定义消息,并拥有get方法返回消息。我们继续配置xml文件。

1
2
3
4
5
6
7
8
<?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 class="annotated.ConfigurableMessageProvider" id="configurableMessageProvider">
<constructor-arg value="msg from xml" type="java.lang.String"/>
</bean>
</beans>

通过construtor-arg的标签就可以实现,value指定它的值,type指定类型。main函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package annotated;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class HelloWorldSpringAnnotated {

public static void main(String[] args) {
//ApplicationContext context = new AnnotationConfigApplicationContext(HelloWorldConfig.class);
ApplicationContext context = new ClassPathXmlApplicationContext("spring/construct-DI.xml");
MessageProvider msgP = context.getBean("configurableMessageProvider", MessageProvider.class);
System.out.println(msgP.getMessage());
}
}

此外你也可以通过其他命名空间来实现简写,效果相同:

1
2
3
4
5
6
7
8
<?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:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="annotated.ConfigurableMessageProvider" id="configurableMessageProvider"
c:msg="msg from xml" /> # 使用c标签
</beans>

如果构造方法有多个参数怎么办?使用下标就可以了,如下:

1
2
<bean class="annotated.ConfigurableMessageProvider" id="configurableMessageProvider"
c:_0="msg from xml" />

第一个形参就是_0,第二个是_1,以此类推。

注解配置方法

用注解配置的方法则更为简洁,我们修改一下StandardOutMessageRender,使用构造器注入的方式注入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package annotated;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service("renderer")
public class StandardOutMessageRender implements MessageRender {

private MessageProvider messageProvider;

// 注意此处使用了@Autowired注解
@Autowired
public StandardOutMessageRender(MessageProvider messageProvider) {
this.messageProvider = messageProvider;
}

public void render() {
if (messageProvider == null) {
throw new RuntimeException(
"your must set the property messageProvider of class:"
+ StandardOutMessageRender.class.getName());
}
System.out.println(this.messageProvider.getMessage());
}

@Autowired
public void setMessageProvider(MessageProvider messageProvider) {
this.messageProvider = messageProvider;
}

public MessageProvider getMessageProvider() {
return this.messageProvider;
}

}

方法很简单,我们只需要在构造方法上标注@Autowired注解即可。我们重新写一个xml文件以扫描包下所有组件:

1
2
3
4
5
6
7
<?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: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="annotated"/>
</beans>

需要注意的是,注入时bean的查找必须是唯一的。我们前面还写了一个ConfigurableMessageProvider,同样实现了MessageProvider,且带有@Service注解。这里需要暂时去掉该注解,并注释掉HelloWorldConfig中提供的bean即可。不然就会出现注入时,可用的bean超过一个,spring不知道注入哪一个的问题。

setter注入

构造器注入有一个缺点,即构造参数过多的话,那XML文件和构造方法的代码就会很冗长。这样的情况下,使用setter注入是一个比较好的选择。依旧是两种方式。

XML配置方法

其实之前我们已经用过这个方法了,即:

1
2
3
4
<bean class="decoupled.HelloWorldMessageProvider" id="provider" />
<bean class="decoupled.StandardOutMessageRender" id="render">
<property name="messageProvider" ref="provider"/>
</bean>

这就是XML中配置setter注入的办法,其中StandardOutMessageRender类依赖于HelloWorldMessageProvider类对象,并提供了setMessageProvider方法来设置,通过这样设置XML文档,初始化StandardOutMessageRender对象时就会自动实例化一个HelloWorldMessageProvider对象填充到setter方法中。

前面也说过了,使用其他命名空间也可以实现简写:

1
2
3
4
5
6
7
8
9
<?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: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 class="decoupled.HelloWorldMessageProvider" id="provider" />
<bean class="decoupled.StandardOutMessageRender" id="render"
p:messageProvider-ref="provider" />
</beans>

即property的缩写p,并设置它的属性的依赖bean。这样可以简化配置

注解配置方法

使用XML的方式我们就需要使用ref标签来定义依赖,如果使用注解的方式会更加简单,我们只需要@Autowired就可以了:

1
2
3
4
@Autowired
public void setMessageProvider(MessageProvider messageProvider) {
this.messageProvider = messageProvider;
}

另外在StandardOutMessageRender类上加@Service注解即可。

1
2
@Service("renderer")
public class StandardOutMessageRender implements MessageRender {}

这时,@Autowired注解就会自动取寻找类型为MessageProvider的bean,最终会在配置类中查找到这个bean如下,并自动返回这个对象到形参中,完成setter方法。

1
2
3
4
@Bean
public MessageProvider provider() {
return new HelloWorldMessageProvider();
}

使用注解的方式是要明显简单于XML文档配置的。

字段注入

其实个人觉得field翻译为变量更准确,即变量注入,但是书里是这么写的哈哈。顾名思义就是将bean直接注入到变量里,举一个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package annotated2;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service("Singer")
public class Singer {

@Autowired
private Person person; // 字段注入

public void sing() {
System.out.println("I am a " + person.getName() + "~");
}

}

这里我们没有针对这个变量的setter方法,或者构造器方法,即我们不使用上述两种方法了,直接标记@Autowired。而Person类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package annotated2;

import org.springframework.stereotype.Component;

@Component
public class Person {

private String name = "Singer";

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

main函数:

1
2
3
4
5
6
7
8
9
10
11
12
package annotated2;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Application {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring/field-DI.xml");
Singer singer = context.getBean(Singer.class);
singer.sing();
}
}

XML文件使用compnent-scan来扫描base-package包下的所有组件。

1
2
3
4
5
6
7
<?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: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="annotated2"/>
</beans>

运行结果:

可以看到也是可以实现的,但是并不建议使用字段注入的方法,包括从2019年的Idea开始也将这个方法标记为"不建议":

它的问题主要有三个:

  • 使用该注解方式后,我们就没办法单独使用这个类了。比如我就不能直接new Singer(),原因在于spring的注解是运行时注入的,我们直接new会导致空指针的问题。即该类脱离了容器环境无法使用,因此在编写测试时,字段注入会带来困难。
  • 可能会导致循环依赖的问题,即A类中需要自动注入B,而B类中需要自动注入A。
  • 字段注入不能用于final字段。这种类型的字段只能使用构造函数注入来初始化。

方法注入

最后一个不常用的是DI手段是方法注入,他又有两种子方法:查找注入(Lookup Method Injection)和方法替换(Method Replacement)。前者可以获取一个依赖项,而后者可以实现在不修改代码的情况下任意替换bean上任何方法的实现。

查找注入

该方法于spring1.1时加入,用来解决当前bean依赖于另一个具有不同作用范围的bean的问题。注意,我所说的作用范围指的是单例模式or多例模式,或者就是书中说的不同生命周期,理解就可以。为了讲清楚这个东西存在的意义,我们思考如下场景。

提前说一下,在XML文档配置中,默认的当前bean就是单例模式,通过关键字scope来定义,有两种值:

即单例模式和多例模式。默认的就是单例模式

单例模式和多例模式这里不做解释。代码也就不写了,这里简单说明一下scope即可。

学下出题的口气,咳咳...

假设现有两个bean:Singer和Person,其中bean:Singer依赖于Person,问:Singer中获取两次Person,这两个Person指向的对象一样吗?已知依赖配置如下:

1
2
3
4
<bean class="annotated2.Person" id="person" scope="singleton"/>
<bean class="annotated2.Singer" id="singer" scope="singleton">
<property name="person" ref="person" />
</bean>

Singer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package annotated2;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class Singer {

private Person person;

public Person getPerson() {
return person;
}

@Autowired
public void setPerson(Person person) {
this.person = person;
}

@Override
public String toString() {
return "Singer{" +
"person=" + person +
'}';
}
}

Person.java

1
2
3
4
5
6
package annotated2;

import org.springframework.stereotype.Component;

@Component
public class Person {}

main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package annotated2;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Application {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring/field-DI.xml");
Singer singer_1 = context.getBean("singer", Singer.class);
Singer singer_2 = context.getBean("singer", Singer.class);
System.out.println("singer_1=" + singer_1);
System.out.println("singer_2=" + singer_2);
System.out.println("singer_1.Person == singer_2.Person = " + (singer_1 == singer_2));
}
}

结果是一样的:

理由很简单,在全局都只存在一个Singer和一个Person,所以你这两个对象包括属性都是一样的。

问2:现将Singer换为多例,结果?

那你都多例了,肯定两个Singer对象是不一样的,但是由于Person是单例的,所以你可以看到Person属性是同一个对象。

问3:现在将二者都换为多例子,结果?

那更简单了,两两都是不一样的,不做解释。

问4(压轴题哈哈):现将Singer换为单例,Person换为多例,结果?

这个是由迷惑性的,我第一次就以为两个Person属性是不一样的,实际上:

结果和二者都为单例是一样的结果。

抛出问题为啥呢?

要理解也很简单,因为Singer是单例的,所以他只被初始化了一次,即Person也只被初始化了一次,所以两个Singer的Person属性是一样的。

那么回归主题,需求是什么?

在问题4中我们看到,单例搭配多例的依赖会导致单例的每一个对象都是同一个,问题是我想要单例模式下的每一个对象它的依赖都不一样呢?好了,我们有了查找注入。我们对Singer的代码进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package annotated2;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public abstract class Singer {

@Autowired
public abstract Person getPerson();

public void pringPerson() {
System.out.println(getPerson());
}

}

将getPerson改为抽象方法,返回Person对象,并使用getPerson来输出这个对象的信息,去掉setter,即不使用setter注入,当然Singer也就成了抽象类。配置文件:

1
2
3
4
5
6
7
8
9
<?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 class="annotated2.Person" id="person" scope="prototype"/>
<bean class="annotated2.Singer" id="singer" scope="singleton">
<lookup-method name="getPerson" bean="person"/>
</bean>
</beans>

使用lookup-method标签指定方法,Spring会对这个抽象方法进行覆写,并指定name - 方法名称;bean返回的bean类型:

我们再次运行:

至此我们就是用了方法注入的手段实现了单例模式下的对象多次获取后可以拥有不同的依赖。那么注意事项就来了:

  • 当想要使用两个具有不同生命周期的bean时,可以使用查找方法注入。当bean 共享相同的生命周期时, 应该避免使用查找方法注入,尤其是当这些 bean 都是单例时。原因很好理解,都是单例时使用方法注入会导致运行时间增长,书中的例子中达到了1:400以上的差异。
  • 只有在必要的时候才使用查找方法注 入,即使当拥有不同生命周期的bean时。
  • 这只适用于 XML 配置,基于注解的配置强制方法的空白实现,否则,就不会创建bean。

方法替换

该手段的场景如下:例如, 有一个在 Spring 应用程序中使用了第三方库, 并且需要更改某个方法的逻辑。 此时,无法更改源代码,因为它是由第三方提供的,所以一种解决方案是使用方法替换,使用自己的实现来替换该方法的逻辑。

我们还是用代码来说明,假设这里有一个库函数,我们不能修改:

1
2
3
4
5
6
7
8
9
10
11
12
package annotated3;

public class ReplacementTarget {

public String formatMessage(String msg) {
return "<h1>" + msg + "</h1>";
}

public String formatMessage(Object msg) {
return "<h1>" + msg + "</h1>";
}
}

该库函数有两个方法,即重载,且渲染标签为<h1>,那我不想要了,我想要<h2>怎么在不能修改库函数的代码来实现?

首先我们定义业务逻辑,需要实现MethodReplacer接口,并覆写reimplement方法参数说明见下方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package annotated3;

import org.springframework.beans.factory.support.MethodReplacer;

import java.lang.reflect.Method;

public class FormatMessageReplacer implements MethodReplacer {

/**
*
* @param args0 被调用的原始方法
* @param method 被重写的方法的Method实例
* @param args 该方法的参数数组
* @return 重写方法后执行的结果
* @throws Throwable IllegalArgumentException
*/
@Override
public Object reimplement(Object args0, Method method, Object[] args) throws Throwable {
if (isFormatMessageMethod(method)) {
String msg = (String) args[0];
return "<h2>" + msg + "</h2>";
} else {
throw new IllegalArgumentException("Unable to reimplement method "
+ method.getName());
}
}

public boolean isFormatMessageMethod(Method method) {
if (method.getParameterTypes().length != 1) {
return false;
}
if (!("formatMessage".equals(method.getName()))) {
return false;
}
if (method.getReturnType() != String.class) {
return false;
}
if (method.getParameterTypes()[0] != String.class) {
return false;
}

return true;
}
}

我们使用了一个过滤器来使传入的方法一定是这样的:Sring formatMessage(String args)。即我们要重新实现库函数的第一个方法。

定义XML:

1
2
3
4
5
6
7
8
9
10
11
12
<?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="methodReplacer" class="annotated3.FormatMessageReplacer"/>
<bean id="replacementTarget" class="annotated3.ReplacementTarget">
<replaced-method name="formatMessage" replacer="methodReplacer">
<arg-type>String</arg-type>
</replaced-method>
</bean>
<bean id="standardTarget" class="annotated3.ReplacementTarget"/>
</beans>

我们定义了两种bean,methodReplacer就是我们的重实现器,另外有两个ReplacementTarget的bean,并把其中一个使用replaced-method标签指定重实现方法名为formatMessage、重实现器为methodReplacer、有两个重载你要换哪一个?使用arg-type指定我们要换的是参数类型为String的哪一个,这里的值Spring会自己器正则匹配,所以不用写成java.lang.String。另一个就是原库函数bean,不做修改。下来是主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package annotated3;


import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.util.StopWatch;

public class MethodReplacementDemo {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("spring/method-DI.xml");
ReplacementTarget replacementTarget = context.getBean("replacementTarget", ReplacementTarget.class);
ReplacementTarget standardTarget = context.getBean("standardTarget", ReplacementTarget.class);

displayInfo(replacementTarget);
displayInfo(standardTarget);
}

private static void displayInfo(ReplacementTarget target) {
System.out.println(target.formatMessage("Thanks for reading"));

StopWatch stopWatch = new StopWatch();
stopWatch.start("perTest");

for (int x = 0; x < 1000000; x++) {
String out = target.formatMessage("No filter in my head.");
}

stopWatch.stop();

System.out.println("100000 invocations took: " + stopWatch.getTotalTimeMillis() + " ms");
}
}

我们让他们输出消息Thanks for reading,看看标签有没有被换好。下面displayInfo函数是用来计算原函数和重新实现后的函数的效率,注意,包括上面的查找注入,它们都是Spring使用了CGLIB类的结果。那看一下运行结果:

OK,我们看到<h1>标签被换为了<h2>标签,说明重新实现成功,且被换了后的函数的性能是未换的性能的1/5,啧啧,真垃圾。

那么依然是注意事项:

  • 如果想要使用方法替换作为应用程序的一部分,建议为每个方法或一组重载的方法使用一个 MethodReplacer。
  • 很多人也仍然倾向于使用标准的Java机制来重写方法,而不是依赖于运行时字节码的增强。

小结

上述常见的注入方式实际上就三个,即:构造器注入、Setter注入、字段注入。分别常见用处如下:

  • 构造器注入常见于强制对象注入
  • Setter注入常见于可选对象注入
  • 字段注入应当避免

总结

上面写的这些东西其实是蛮多的,一两天可能才能消化吸收,也有一些我是没有写出来的,比较费时间(其实是我懒了),如如何在依赖注入中注入特定的数据类型?如简单类型?Map、Set、List、Properties类型?这些都是通过bean标签来控制的;除了单例和多例还有哪些作用域?(请求作用域、会话作用域);怎么在XML里或java配置里给bean起别名?这些我后面会补充,你也可以参考这个视频进行学习:https://www.imooc.com/learn/1108。

到此IoC机制就基本学完了,依赖注入的概念、四种手段方法、配置方法、注解方法都了解了。下一篇进行AoP的学习,完后开始SpringMVC及SpringBoot、SpringCloud的正式学习,另会写写华为servicecomb的使用。冲冲冲。

参考

  • 浅谈MVC
  • https://www.cnblogs.com/best/p/5653916.html
  • https://zhuanlan.zhihu.com/p/64001753
  • https://blog.csdn.net/dkbnull/article/details/87219562
  • 《Spring5高级编程(第五版)》
  • https://www.imooc.com/learn/1108

CATALOG
  1. 1. Spring 全家桶
  2. 2. Spring的所有包模块
  3. 3. IOC
    1. 3.1. JavaBean
      1. 3.1.1. 一个实例
      2. 3.1.2. 使用Spring重构实例
      3. 3.1.3. 基于注解的依赖注入
    2. 3.2. DI的类型
      1. 3.2.1. 构造函数注入
        1. 3.2.1.1. XML配置方法
        2. 3.2.1.2. 注解配置方法
      2. 3.2.2. setter注入
        1. 3.2.2.1. XML配置方法
        2. 3.2.2.2. 注解配置方法
      3. 3.2.3. 字段注入
      4. 3.2.4. 方法注入
        1. 3.2.4.1. 查找注入
        2. 3.2.4.2. 方法替换
      5. 3.2.5. 小结
    3. 3.3. 总结
    4. 3.4. 参考