Spring 核心技术 IoC & DI 详解

Posted by 暮夏有五 on 2021-01-13
Estimated Reading Time 26 Minutes
Words 5.9k In Total
Viewed Times

IoC 概念

IoC(Inversion of Control),中文叫做控制反转。这是一个概念,也是一种思想。控制反转,实际上就是指对一个对象的控制权的反转。如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Book {
private Integer id;
private String name;
private Double price;
//省略getter/setter
}

public class User {
private Integer id;
private String name;
private Integer age;

public void doSth() {
Book book = new Book();
book.setId(1);
book.setName("故事新编");
book.setPrice((double) 20);
}
}

在这种情况下,Book 对象的控制权在 User 对象里边,这样,Book 和 User 高度耦合,如果在其他对象中需要使用 Book 对象,得重新创建,也就是说,对象的创建、初始化、销毁等操作,统统都要开发者自己来完成。如果能够将这些操作交给容器来管理,开发者就可以极大的从对象的创建中解脱出来。

使用 Spring 之后,我们可以将对象的创建、初始化、销毁等操作交给 Spring 容器来管理。就是说,在项目启动时,所有的 Bean 都将自己注册到 Spring 容器中去(如果有必要的话),然后如果其他 Bean 需要使用到这个 Bean ,则不需要自己去 new,而是直接去 Spring 容器去要。

通过一个简单的例子看下这个过程。

IoC 初体验

首先创建一个普通的 Maven 项目,然后引入 spring-context 依赖,如下:

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
</dependencies>

XML 配置

接下来,在 resources 目录下创建一个 applicationContext.xml 配置文件(注意,一定要先添加依赖,后创建配置文件,否则创建配置文件时,没有模板选项),在这个文件中,我们可以配置所有需要注册到 Spring 容器的 Bean,此处配置一个 Book 对象:

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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean class="com.antoniopeng.hello.spring.Book" id="book"/>
</beans>

class 属性表示需要注册的 bean 的全路径,id 则表示 bean 的唯一标记,也开可以 name 属性作为 bean 的标记,在超过 99% 的情况下,id 和 name 其实是一样的,特殊情况下不一样。

接下来,加载这个配置文件,执行 main 方法,配置文件就会被自动加载,进而在 Spring 中初始化一个 Book 实例。此时,我们显式的指定 Book 类的无参构造方法,并在无参构造方法中打印日志,可以看到无参构造方法执行了,进而证明对象已经在 Spring 容器中初始化了,然后通过其 getBean 方法,可以从容器中去获取对象。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Book book = (Book) ctx.getBean("book");
System.out.println(book);
}
}

加载方式,除了ClassPathXmlApplicationContext 之外(去 classpath 下查找配置文件),另外也可以使用 FileSystemXmlApplicationContext ,FileSystemXmlApplicationContext 会从操作系统路径下去寻找配置文件。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
FileSystemXmlApplicationContext ctx = new FileSystemXmlApplicationContext("F:\\workspace5\\workspace\\spring\\spring-ioc\\src\\main\\resources\\applicationContext.xml");
Book book = (Book) ctx.getBean("book");
System.out.println(book);
}
}

Bean 的获取

在上一小节中,我们通过 ctx.getBean 方法来从 Spring 容器中获取 Bean,传入的参数是 Bean 的 name 或者 id 属性。除了这种方式之外,也可以直接通过 Class 去获取一个 Bean。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
Book book = ctx.getBean(Book.class);
System.out.println(book);
}
}

这种方式有一个很大的弊端,如果存在多个实例,这种方式就不可用,例如,xml 文件中存在两个 Bean:

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="com.antoniopeng.hello.spring.Book" id="book"/>
<bean class="com.antoniopeng.hello.spring.Book" id="book2"/>
</beans>

此时,如果通过 Class 去查找 Bean,会报如下错误,所以,一般建议使用 name 或者 id 去获取 Bean 的实例。

1
Exception in thread "main" org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.antoniopeng.hello.spring.Book' available: expected single matching bean but found 2: book,book2

属性的注入

在 XML 配置中,属性的注入(DI)存在多种方式。

构造方法注入

通过 Bean 的构造方法给 Bean 的属性注入值。

1. 第一步首先给 Bean 添加对应的构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Book {
private Integer id;
private String name;
private Double price;

public Book() {
System.out.println("-------book init----------");
}

public Book(Integer id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
}

2. 在 xml 文件中注入 Bean

1
2
3
4
5
6
7
8
9
10
11
<?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="com.antoniopeng.hello.spring.Book" id="book">
<constructor-arg index="0" value="1"/>
<constructor-arg index="1" value="三国演义"/>
<constructor-arg index="2" value="30"/>
</bean>
</beans>

这里需要注意的是,constructor-arg 中的 index 属性和 Book 中的构造方法参数一一对应。写的顺序可以颠倒,但是 index 的值和 value 要一一对应。

另一种构造方法中的属性注入,则是通过直接指定参数名来注入:

1
2
3
4
5
<bean class="com.antoniopeng.hello.spring.Book" id="book2">
<constructor-arg name="id" value="2"/>
<constructor-arg name="name" value="红楼梦"/>
<constructor-arg name="price" value="40"/>
</bean>

如果有多个构造方法,则会根据给出参数个数以及参数类型,自动匹配到对应的构造方法上,进而初始化一个对象。

Set 方法注入

除了构造方法之外,我们也可以通过 set 方法注入值。

1
2
3
4
5
<bean class="com.antoniopeng.hello.spring.Book" id="book3">
<property name="id" value="3"/>
<property name="name" value="水浒传"/>
<property name="price" value="30"/>
</bean>

Set 方法注入,有一个很重要的问题,就是属性名。很多人会有一种错觉,觉得属性名就是你定义的属性名,这个是不对的。在所有的框架中,凡是涉及到反射注入值的,属性名统统都不是 Bean 中定义的属性名,而是通过 Java 中的内省机制分析出来的属性名,简单说,就是根据 get/set 方法分析出来的属性名。

P 名称空间注入

P 名称空间注入,使用的比较少,它本质上也是调用了 Set 方法。

1
<bean class="com.antoniopeng.hello.spring.Book" id="book4" p:id="4" p:bookName="西游记" p:price="33"></bean>

外部 Bean 的注入

有时候,我们使用一些外部 Bean,这些 Bean 可能没有构造方法,而是通过 Builder 来构造的,这个时候,就无法使用上面的方式来给它注入值了。

例如在 OkHttp 的网络请求中,原生的写法如下:

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
public class OkHttpMain {
public static void main(String[] args) {
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.build();
Request request = new Request.Builder()
.get()
.url("http://b.hiphotos.baidu.com/image/h%3D300/sign=ad628627aacc7cd9e52d32d909032104/32fa828ba61ea8d3fcd2e9ce9e0a304e241f5803.jpg")
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
System.out.println(e.getMessage());
}

@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
FileOutputStream out = new FileOutputStream(new File("E:\\123.jpg"));
int len;
byte[] buf = new byte[1024];
InputStream is = response.body().byteStream();
while ((len = is.read(buf)) != -1) {
out.write(buf, 0, len);
}
out.close();
is.close();
}
});
}
}

这个 Bean 有一个特点,OkHttpClient 和 Request 两个实例都不是直接 new 出来的,在调用 Builder 方法的过程中,都会给它配置一些默认的参数。这种情况,我们可以使用 静态工厂注入或者实例工厂注入来给 OkHttpClient 提供一个实例。

1. 静态工厂注入

首先提供一个 OkHttpClient 的静态工厂:

1
2
3
4
5
6
7
8
9
public class OkHttpUtils {
private static OkHttpClient OkHttpClient;
public static OkHttpClient getInstance() {
if (OkHttpClient == null) {
OkHttpClient = new OkHttpClient.Builder().build();
}
return OkHttpClient;
}
}

在 xml 文件中,配置该静态工厂:

1
<bean class="com.antoniopeng.hello.spring.OkHttpUtils" factory-method="getInstance" id="okHttpClient"></bean>

这个配置表示 OkHttpUtils 类中的 getInstance 是我们需要的实例,实例的名字就叫 okHttpClient。然后,在 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
27
28
29
30
public class OkHttpMain {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
OkHttpClient okHttpClient = ctx.getBean("okHttpClient", OkHttpClient.class);
Request request = new Request.Builder()
.get()
.url("http://b.hiphotos.baidu.com/image/h%3D300/sign=ad628627aacc7cd9e52d32d909032104/32fa828ba61ea8d3fcd2e9ce9e0a304e241f5803.jpg")
.build();
Call call = okHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
System.out.println(e.getMessage());
}

@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
FileOutputStream out = new FileOutputStream(new File("E:\\123.jpg"));
int len;
byte[] buf = new byte[1024];
InputStream is = response.body().byteStream();
while ((len = is.read(buf)) != -1) {
out.write(buf, 0, len);
}
out.close();
is.close();
}
});
}
}

2. 实例工厂注入

实例工厂就是工厂方法是一个实例方法,这样,工厂类必须实例化之后才可以调用工厂方法。

这次的工厂类如下:

1
2
3
4
5
6
7
8
9
public class OkHttpUtils {
private OkHttpClient OkHttpClient;
public OkHttpClient getInstance() {
if (OkHttpClient == null) {
OkHttpClient = new OkHttpClient.Builder().build();
}
return OkHttpClient;
}
}

此时,在 xml 文件中,需要首先提供工厂方法的实例,然后才可以调用工厂方法:

1
2
<bean class="com.antoniopeng.hello.spring.OkHttpUtils" id="okHttpUtils"/>
<bean class="okhttp3.OkHttpClient" factory-bean="okHttpUtils" factory-method="getInstance" id="okHttpClient"></bean>

自己写的 Bean 一般不会使用这两种方式注入,但是,如果需要引入外部 jar,外部 jar 的类的初始化,有可能需要使用这两种方式。

复杂属性的注入

对象注入

1
2
3
4
5
6
7
8
<bean class="com.antoniopeng.hello.spring.User" id="user">
<property name="cat" ref="cat"/>
</bean>

<bean class="com.antoniopeng.hello.spring.Cat" id="cat">
<property name="name" value="小白"/>
<property name="color" value="白色"/>
</bean>

可以通过 xml 注入对象,通过 ref 来引用一个对象。

数组注入

数组注入和集合注入在 xml 中的配置是一样的。如下:

1
2
3
4
5
6
7
8
9
<bean class="com.antoniopeng.hello.spring.User" id="user">
<property name="favorites">
<array>
<value>足球</value>
<value>篮球</value>
<value>乒乓球</value>
</array>
</property>
</bean>

注意,array 节点,也可以被 list 节点代替。当然,array 或者 list 节点中也可以是对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<bean class="com.antoniopeng.hello.spring.User" id="user">
<property name="cat" ref="cat"/>
<property name="cats">
<list>
<ref bean="cat"/>
<ref bean="cat2"/>
</list>
</property>
</bean>

<bean class="com.antoniopeng.hello.spring.Cat" id="cat">
<property name="name" value="小白"/>
<property name="color" value="白色"/>
</bean>

<bean class="com.antoniopeng.hello.spring.Cat" id="cat2">
<property name="name" value="小黑"/>
<property name="color" value="黑色"/>
</bean>

注意,即可以通过 ref 使用外部定义好的 Bean,也可以直接在 list 或者 array 节点中定义 bean。

Map 注入

1
2
3
4
5
6
<property name="map">
<map>
<entry key="age" value="99"/>
<entry key="name" value="antoniopeng"/>
</map>
</property>

Properties 注入

1
2
3
4
5
6
<property name="info">
<props>
<prop key="age">99</prop>
<prop key="name">antoniopeng</prop>
</props>
</property>

Java 配置

在 Spring 中,想要将一个 Bean 注册到 Spring 容器中,整体上来说,有三种不同的方式。

  • XML 注入,如前文所说
  • Java 配置(通过 Java 代码将 Bean 注册到 Spring 容器中)
  • 自动化扫描

这里我们来看 Java 配置。Java 配置这种方式在 Spring Boot 出现之前,其实很少使用,自从有了 Spring Boot,Java 配置开发被广泛使用,因为在 Spring Boot 中,不使用一行 XML 配置。例如我有如下一个 Bean:

1
2
3
4
5
public class SayHello {
public String sayHello(String name) {
return "hello " + name;
}
}

在 Java 配置中,我们用一个 Java 配置类去代替之前的 applicationContext.xml 文件。

1
2
3
4
5
6
7
@Configuration
public class JavaConfig {
@Bean
SayHello sayHello() {
return new SayHello();
}
}

首先在配置类上有一个 @Configuration 注解,这个注解表示这个类不是一个普通类,而是一个配置类,它的作用相当于 applicationContext.xml。 然后,定义方法,方法返回对象,方法上添加 @Bean 注解,表示将这个方法的返回值注入的 Spring 容器中去。也就是说,@Bean 所对应的方法,就相当于 applicationContext.xml 中的 bean 节点。

既然是配置类,我们需要在项目启动时加载配置类。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
SayHello hello = ctx.getBean(SayHello.class);
System.out.println(hello.sayHello("antonio"));
}
}

注意,配置的加载,是使用 AnnotationConfigApplicationContext 来实现。

关于 Java 配置,这里有一个需要注意的问题:Bean 的名字是什么?

Bean 的默认名称是方法名。以上面的案例为例,Bean 的名字是 sayHello。如果想自定义方法名,也是可以的,直接在 @Bean 注解中进行过配置。如下配置表示修改 Bean 的名字为 antonio:

1
2
3
4
5
6
7
@Configuration
public class JavaConfig {
@Bean("antonio")
SayHello sayHello() {
return new SayHello();
}
}

自动化配置

在我们实际开发中,大量的使用自动配置。自动化配置既可以通过 Java 配置来实现,也可以通过 xml 配置来实现。

准备工作

例如我有一个 UserService,我希望在自动化扫描时,这个类能够自动注册到 Spring 容器中去,那么可以给该类添加一个 @Service,作为一个标记。

@Service 注解功能类似的注解,一共有四个:

  • @Component
  • @Repository
  • @Service
  • @Controller

这四个中,另外三个都是基于 @Component 做出来的,而且从目前的源码来看,功能也是一致的,那么为什么要搞三个呢?主要是为了在不同的类上面添加时方便。

  • 在 Service 业务层上,添加注解时使用 @Service
  • 在 Dao 数据持久层,添加注解时使用 @Repository
  • 在 Controller 控制器层,添加注解时使用 @Controller
  • 在其他组件上,添加注解时使用 @Component
1
2
3
4
@Service
public class UserService {

}

添加完成后,自动化扫描有两种方式,一种就是通过 Java 代码配置自动化扫描,另一种则是通过 xml 文件来配置自动化扫描。

Java 代码配置自动扫描

1
2
3
4
5
6
7
8
@Configuration
@ComponentScan(basePackages = "com.anotniopeng.hello.spring.service")
public class JavaConfig {

public String helloSpring() {
return "Hello Spring";
}
}

然后,在项目启动中加载配置类,在配置类中,通过 @ComponentScan 注解指定要扫描的包(如果不指定,默认情况下扫描的是配置类所在的包下载的 Bean 以及配置类所在的包下的子包下的类),然后就可以获取 UserService 的实例了:

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
UserService userService = ctx.getBean(UserService.class);
System.out.println(userService.helloSpring());
}
}

这里有几个问题需要注意:

  1. Bean 的名字叫什么?

    默认情况下,Bean 的名字是类名首字母小写。例如上面的 UserService,它的实例名,默认就是 userService。如果开发者想要自定义名字,就直接在 @Service 注解中添加即可。

  2. 有几种扫描方式?

    上面的配置,我们是按照包的位置来扫描的。也就是说,Bean 必须放在指定的扫描位置,否则,即使你有 @Service 注解,也扫描不到。除了按照包的位置来扫描,还有另外一种方式,就是根据注解来扫描。例如如下配置:

    1
    2
    3
    4
    @Configuration
    @ComponentScan(basePackages = "com.antoniopeng.hello.spring.javaconfig",useDefaultFilters = true,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Controller.class)})
    public class JavaConfig {
    }

    这个配置表示扫描 com.antoniopeng.hello.spring.javaconfig 下的所有 Bean,但是除了 Controller。

XML 配置自动化扫描

1
<context:component-scan base-package="com.antoniopeng.hello.spring.javaconfig"/>

上面这行配置表示扫描 com.antoniopeng.hello.spring.javaconfig 下的所有 Bean。当然也可以按照类来扫描。XML 配置完成后,在 Java 代码中加载 XML 配置即可。

1
2
3
4
5
6
7
public class XMLTest {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = ctx.getBean(UserService.class);
System.out.println(userService.helloSpring());
}
}

也可以在 XML 配置中按照注解的类型进行扫描:

1
2
3
<context:component-scan base-package="com.antoniopeng.hello.spring.javaconfig" use-default-filters="true">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

对象注入

自动扫描时的对象注入有三种方式:

  1. @Autowired
  2. @Resources
  3. @Injected

@Autowired 是根据类型去查找,然后赋值,这就有一个要求,这个类型只可以有一个对象,否则就会报错。如果有多个对象还非要使用 @Autowired,也是可以的,此时需要配合另外一个注解 @Qualifier,在 @Qualifier 中可以指定变量名,两个一起用(@Autowired@Qualifier )就可以实现通过变量名查找到变量。

@Resources 是根据名称去查找,默认情况下,定义的变量名,就是查找的名称,当然开发者也可以在 @Resources 注解中手动指定。所以,如果一个类存在多个实例,那么就应该使用 @Resources 去注入。

1
2
3
4
5
6
7
8
9
10
11
@Service
public class UserService {

@Autowired
UserDao userDao;

public String hello() {
return userDao.hello();
}

}

条件注解

条件注解就是在满足某一个条件的情况下,生效的配置。

使用示例

比如:在 Windows 中如何获取操作系统信息?Windows 中查看文件夹目录的命令是 dir,Linux 中查看文件夹目录的命令是 ls,我现在希望当系统运行在 Windows 上时,自动打印出 Windows 上的目录展示命令,Linux 运行时,则自动展示 Linux 上的目录展示命令。

1. 首先定义一个显示文件夹目录的接口

1
2
3
public interface ShowCmd {
String showCmd();
}

2. 分别实现 Windows 下的实例和 Linux 下的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class WinShowCmd implements ShowCmd {

@Override
public String showCmd() {
return "dir";
}
}

public class LinuxShowCmd implements ShowCmd {

@Override
public String showCmd() {
return "ls";
}
}

3. 接下来,定义两个条件,一个是 Windows 下的条件,另一个是 Linux 下的条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class WindowsCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().getProperty("os.name").toLowerCase().contains("windows");
}
}

public class LinuxCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().getProperty("os.name").toLowerCase().contains("linux");
}
}

4. 最后,在定义 Bean 的时候,就可以去配置条件注解了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class JavaConfig {

@Bean("showCmd")
@Conditional(WindowsCondition.class)
ShowCmd winCmd() {
return new WinShowCmd();
}

@Bean("showCmd")
@Conditional(LinuxCondition.class)
ShowCmd linuxCmd() {
return new LinuxShowCmd();
}
}

注意:这里,一定要给两个 Bean 取相同的名字,这样在调用时,才可以自动匹配。然后,给每一个 Bean 加上条件注解,当条件中的 matches 方法返回 true 的时候,这个 Bean 的定义就会生效。

1
2
3
4
5
6
7
public class JavaMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
ShowCmd showCmd = (ShowCmd) ctx.getBean("showCmd");
System.out.println(showCmd.showCmd());
}
}

条件注解有一个非常典型的使用场景,就是多环境切换。

多环境切换

开发中,如何在 开发/生产/测试 环境之间进行快速切换?Spring 中提供了 Profile 来解决这个问题,Profile 的底层就是条件注解。这个从 @Profile 注解的定义就可以看出来:

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
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {

/**
* The set of profiles for which the annotated component should be registered.
*/
String[] value();

}
class ProfileCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
return true;
}
}
return false;
}
return true;
}

}

1. 首先我们定义一个 DataSource

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
public class DataSource {
private String url;
private String username;
private String password;

@Override
public String toString() {
return "DataSource{" +
"url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}

public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

2. 然后,在配置 Bean 时,通过 @Profile 注解指定不同的环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean("ds")
@Profile("dev")
DataSource devDataSource() {
DataSource dataSource = new DataSource();
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/dev");
dataSource.setUsername("root");
dataSource.setPassword("123");
return dataSource;
}

@Bean("ds")
@Profile("prod")
DataSource prodDataSource() {
DataSource dataSource = new DataSource();
dataSource.setUrl("jdbc:mysql://192.158.222.33:3306/dev");
dataSource.setUsername("jkldasjfkl");
dataSource.setPassword("jfsdjflkajkld");
return dataSource;
}

3. 最后,再加载配置类,注意,需要先设置当前环境,然后再去加载配置类

1
2
3
4
5
6
7
8
9
10
public class JavaMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("dev");
ctx.register(JavaConfig.class);
ctx.refresh();
DataSource ds = (DataSource) ctx.getBean("ds");
System.out.println(ds);
}
}

这个是在 Java 代码中配置的环境的切换,也可以在 XML 文件中配置,如下配置在 XML 文件中,必须放在其他节点后面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<beans profile="dev">
<bean class="org.javaboy.DataSource" id="dataSource">
<property name="url" value="jdbc:mysql:///devdb"/>
<property name="password" value="root"/>
<property name="username" value="root"/>
</bean>
</beans>

<beans profile="prod">
<bean class="org.javaboy.DataSource" id="dataSource">
<property name="url" value="jdbc:mysql://111.111.111.111/devdb"/>
<property name="password" value="jsdfaklfj789345fjsd"/>
<property name="username" value="root"/>
</bean>
</beans>

启动类中设置当前环境并加载配置:

1
2
3
4
5
6
7
8
9
10
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext();
ctx.getEnvironment().setActiveProfiles("prod");
ctx.setConfigLocation("applicationContext.xml");
ctx.refresh();
DataSource dataSource = (DataSource) ctx.getBean("dataSource");
System.out.println(dataSource);
}
}

其他

Bean 的作用域

在 XML 配置中注册的 Bean,或者用 Java 配置注册的 Bean,如果我多次获取,获取到的对象是否是同一个?

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
User user = ctx.getBean("user", User.class);
User user2 = ctx.getBean("user", User.class);
System.out.println(user == user2);
}
}

如上,从 Spring 容器中多次获取同一个 Bean,默认情况下,获取到的实际上是同一个实例。当然我们可以自己手动配置获取不同的实例:

1
<bean class="com.antoniopeng.hello.spring.User" id="user" scope="prototype" />

这是在 XML 中的配置,在 Java 代码中,我们可以通过 @Scope 注解指定 Bean 的作用域:

1
2
3
4
5
6
7
8
@Configuration
public class JavaConfig {
@Bean
@Scope("prototype")
SayHello sayHello() {
return new SayHello();
}
}

通过设置 scope 属性,我们可以调整默认的实例个数。scope 的默认值为 singleton,表示这个 Bean 在 Spring 容器中,是以单例的形式存在,如果 scope 的值为 prototype,表示这个 Bean 在 Spring 容器中不是单例,多次获取将拿到多个不同的实例。

除了 singletonprototype 之外,还有两个取值,requestsession。这两个取值在 web 环境下有效。当然,在自动扫描配置中,也可以指定 Bean 的作用域。

1
2
3
4
5
6
7
8
@Repository
@Scope("prototype")
public class UserDao {

public String hello() {
return "userdao";
}
}

id 和 name 的区别

在 XML 配置中,我们可以看到,即可以通过 id 给 Bean 指定一个唯一标识符,也可以通过 name 来指定,大部分情况下这两个作用是一样的,有一个小小区别:

name 支持取多个。多个 name 之间,用 , 隔开:

1
<bean class="com.antoniopeng.hello.spring.User" name="user,user1" scope="prototype"/>

此时,通过 user、user1都可以获取到当前对象:

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
User user = ctx.getBean("user", User.class);
User user1 = ctx.getBean("user1", User.class);
System.out.println(user);
System.out.println(user1);
}
}

而 id 不支持有多个值。如果强行用 , 隔开,它还是一个值。

混合配置

混合配置就是 Java 配置+XML 配置。混用的话,可以在 Java 配置只通过 @ImportResource 注解可以导入一个 XML 配置。

1
2
3
4
5
@Configuration
@ImportResource("classpath:applicationContext.xml")
public class JavaConfig {

}

本文发于:https://antoniopeng.com


If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !