Spring3

参考

B站颜群老师Spring教程,B站雷丰阳老师Spring教程,《Spring实战》
https://zhuanlan.zhihu.com/p/137507309
https://liayun.blog.csdn.net/article/details/115053350

容器

属性赋值

在之前探讨了如何向Spring的IOC容器中注册bean组件,并且还讲解了有
关bean组件的生命周期的知识。接下来学习一下@Value注解的用法

@Value

Spring中的@Value注解可以为bean中的属性赋值

1
2
3
4
5
6
7
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, 
ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Value {
String value();
}

@Value注解可以标注在字段、方法、参数以及注解上,而且在程序运行期间生效

@Value注解的用法

第一种是不通过配置文件注入属性的情况,通过@Value注解将外部的
值动态注入到bean的属性中

  1. 注入普通字符串
    1
    2
    @Value("天海")
    private String name; // 注入普通字符串
  2. 注入操作系统属性
    1
    2
    @Value("#{systemProperties['os.name']}")
    private String systemPropertiesName; // 注入操作系统属性
  3. 注入其他bean中属性的值
    1
    2
    @Value("#{person.name}")
    private String username; // 注入其他bean中属性的值,即注入person对象的name属性中的值
  4. 注入文件资源
    1
    2
    @Value("classpath:/config.properties")
    private Resource resourceFile; // 注入文件资源
  5. 注入URL资源
    1
    2
    @Value("http://www.baidu.com")
    private Resource url; // 注入URL资源

第二种是通过配置文件注入属性的情况,在项目的src/main/resources
目录下新建一个属性文件,例如person.properties

1
person.nickName=天海

新建一个MainConfigOfPropertyValues配置类,并在该类上使用
@PropertySource注解读取外部配置文件中的key/value并保存到
运行的环境变量中

1
2
3
4
5
6
7
8
@PropertySource(value={"classpath:/person.properties"})
@Configuration
public class MainConfigOfPropertyValues {
@Bean
public Person person() {
return new Person();
}
}

加载完外部的配置文件以后,接着我们就可以使用${key}取出配置文件
中key所对应的值,并将其注入到bean的属性中了

1
2
3
4
5
6
7
8
9
public class Person {
@Value("天海")
private String name;
@Value("#{20-2}")
private Integer age;
@Value("${person.nickName}")
private String nickName; // 昵称
...
}

@Value中#{···}和${···}的区别

在这里提供一个测试属性文件advance_value_inject.properties

1
2
server.name=server1,server2,server3
author.name=liayun

新建一个AdvanceValueInject类,并在该类上使用@PropertySource
注解读取外部属性文件中的key/value并保存到运行的环境变量中,即
加载外部的advance_value_inject.properties属性文件

1
2
3
4
5
@Component
@PropertySource(value={"classpath:/advance_value_inject.properties"})
public class AdvanceValueInject {
// ···
}
  1. ${···}的用法
    {}里面的内容必须符合SpEL表达式,通过@Value(“${spelDefault.
    value}”)我们可以获取属性文件中对应的值,但是如果属性文件中
    没有这个属性,那么就会报错。不过,我们可以通过赋予默认值来
    解决这个问题
    1
    2
    @Value("${author.name:meimeixia}")
    private String name;
    上述代码的含义是表示向bean的属性中注入属性文件中的author.name
    属性所对应的值,如果属性文件中没有author.name这个属性,那么便
    向bean的属性中注入默认值meimeixia
  2. #{···}的用法
    {}里面的内容同样也是必须符合SpEL表达式
    1
    2
    3
    4
    5
    6
    7
    // SpEL:调用字符串Hello World的concat方法
    @Value("#{'Hello World'.concat('!')}")
    private String helloWorld;

    // SpEL:调用字符串的getBytes方法,然后再调用其length属性
    @Value("#{'Hello World'.bytes.length}")
    private String helloWorldBytes;

${···}和#{···}可以混合使用,通过${server.name}从属性文件中获
取值并进行替换,然后就变成了执行SpEL表达式{‘server1,server2,
server3’.split(‘,’)}

1
2
3
4
/* SpEL:传入一个字符串,根据","切分后插入列表中, 
#{}和${}配合使用时,注意不能反过来${}在外面,而#{}在里面*/
@Value("#{'${server.name}'.split(',')}")
private List<String> severs;

必须#{}在外面,${}在里面,因为Spring执行${}的时机要早于#{},
当Spring执行外层的${}时,内部的#{}为空,所以会执行失败。小结

  1. #{···}:用于执行SpEl表达式,并将内容赋值给属性
  2. ${···}:主要用于加载外部属性文件中的值

加载配置文件

@PropertySource注解概述

@PropertySource注解是Spring 3.1c 开始引入的配置类注解。通过
@PropertySource注解可以将properties 配置文件中的key/value
存储到Spring的Environment中,Environment接口提供了方法去读
取配置文件中的值,参数是properties配置文件中定义的key值。也
可以使用@Value注解用${}占位符为bean的属性注入值

1
2
3
4
5
6
7
8
9
10
11
12
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(PropertySources.class)
public @interface PropertySource {
String name() default "";
String[] value();
boolean ignoreResourceNotFound() default false;
String encoding() default "";
Class<? extends PropertySourceFactory> factory()
default PropertySourceFactory.class;
}

可以通过@PropertySource注解指定多个properties文件

1
2
@PropertySource(value={"classpath:/person.properties", 
"classpath:/car.properties"})

@PropertySources注解概述

也可以使用@PropertySources注解来指定properties配置文件

1
2
3
4
5
6
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PropertySources {
PropertySource[] value();
}

指定配置文件

1
2
3
4
@PropertySources(value={
@PropertySource(value={"classpath:/person.properties"}),
@PropertySource(value={"classpath:/car.properties"}),
})

使用Environment获取值

使用@PropertySource注解读取外部配置文件中的key/value之后,是将
其保存到运行的环境变量中了,所以我们也可以通过运行环境来获取外部
配置文件中的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
public void test01() {
printBeans(applicationContext);
System.out.println("===================");

Person person = (Person) applicationContext.getBean("person");
System.out.println(person);

ConfigurableEnvironment environment = applicationContext.getEnvironment();
String property = environment.getProperty("person.nickName");
System.out.println(property);
// 关闭容器
applicationContext.close();
}

自动装配组件

@Autowired注解

@Autowired注解可以对类成员变量、方法和构造函数进行标注,完
成自动装配的工作。@Autowired注解可以放在类、接口以及方法上

1
2
3
4
5
6
7
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, 
ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
boolean required() default true;
}

@Autowired注解默认是优先按照类型去容器中找对应的组件,相当于
是调用了如下这个方法

1
applicationContext.getBean(类名.class);

如果找到多个相同类型的组件,那么是将属性名称作为组件的id,到IOC
容器中进行查找,这时就相当于是调用了如下这个方法

1
applicationContext.getBean("组件的id");

@Qualifier注解

@Autowired是根据类型进行自动装配的,如果需要按名称进行装配,那
么就需要配合@Qualifier注解来使用了

1
2
3
4
5
6
7
8
@Target({ElementType.FIELD, ElementType.METHOD, 
ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Qualifier {
String value() default "";
}

@Primary注解

在Spring中使用注解时,常常会使用到@Autowired这个注解,它默认是
根据类型Type来自动注入的。但有些特殊情况,对同一个接口而言,可能
会有几种不同的实现类,而在默认只会采取其中一种实现的情况下,就
可以使用@Primary注解来标注优先使用哪一个实现类

1
2
3
4
5
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Primary {
}

自动装配

Spring组件的自动装配就是Spring利用依赖注入,也就是我们通常所说的
DI,完成对IOC容器中各个组件的依赖关系赋值。
创建的BookDao、BookService和BookController为例进行说明

1
2
3
4
// 名字默认是类名首字母小写
@Repository
public class BookDao {
}

BookService

1
2
3
4
5
6
7
8
@Service
public class BookService {
@Autowired
private BookDao bookDao;
public void print() {
System.out.println(bookDao);
}
}

BookController

1
2
3
4
5
@Controller
public class BookController {
@Autowired
private BookService bookService;
}

测试在BookService类中使用@Autowired注解注入的BookDao(最后
输出了该BookDao的信息),和我们直接在Spring IOC容器中获取的
BookDao是不是同一个对象呢

1
2
3
4
5
6
7
8
9
10
11
12
public class IOCTest_Autowired {
@Test
public void test01() {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(MainConfigOfAutowired.class);
BookService bookService = applicationContext.getBean(BookService.class);
System.out.println(bookService);
BookDao bookDao = applicationContext.getBean(BookDao.class);
System.out.println(bookDao);
applicationContext.close();
}
}

如果在Spring容器中存在对多个BookDao对象,对BookDao类进行
改造,为其加上一个lable字段

1
2
3
4
5
6
// 名字默认是类名首字母小写
@Repository
public class BookDao {
private String lable = "1";
...
}

在MainConfigOfAutowired配置类中注入一个BookDao对象,并且显
示指定该对象在IOC容器中的bean的名称为bookDao2,并还为该对象
的lable字段赋值为2

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@ComponentScan({"com.atguigu.service", "com.atguigu.dao",
"com.atguigu.controller"})
public class MainConfigOfAutowired {
@Bean("bookDao2")
public BookDao bookDao() {
BookDao bookDao = new BookDao();
bookDao.setLable("2");
return bookDao;
}
}

在我们的IOC容器中就会注入两个BookDao对象,@Autowired注解默认
是优先按照类型去容器中找对应的组件,找到就赋值,如果找到多个相
同类型的组件,那么再将属性的名称作为组件的id,到IOC容器中进行
查找。如果让@Autowired注解装配bookDao2,只须将BookService类
中的bookDao属性的名称全部修改为bookDao2即可

测试@Qualifier注解

如果IOC容器中存在多个相同类型的组件时,想要显示指定@Autowired注
解装配哪个组件,就可以使用@Qualifier注解

1
2
3
4
5
6
@Service
public class BookService {
@Qualifier("bookDao")
@Autowired
private BookDao bookDao2;
}

如果IOC容器中无相应的组件那么就会抛出异常,如果不希望抛出异
常在@Autowired注解里面添加一个属性required=false

1
2
3
4
5
6
@Service
public class BookService {
@Qualifier("bookDao")
@Autowired(required=false)
private BookDao bookDao2;
}

测试@Primary注解

在Spring中,对同一个接口而言,可能会有几种不同的实现类,而默认
只会采取其中一种实现的情况下,就可以使用@Primary注解来标注优先
使用哪一个实现类。如果IOC容器中相同类型的组件有多个,那么我们不
可避免地就要来回用@Qualifier 注解来指定要装配哪个组件,这还是
比较麻烦的,Spring 正是帮我们考虑到了这样一种情况,就提供了这
样一个比较强大的注解,即@Primary。可以利用这个注解让Spring进
行自动装配的时候,默认使用首选的bean

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@ComponentScan({"com.meimeixia.service", "com.meimeixia.dao",
"com.meimeixia.controller"})
public class MainConfigOfAutowired {
@Primary
@Bean("bookDao2")
public BookDao bookDao() {
BookDao bookDao = new BookDao();
bookDao.setLable("2");
return bookDao;
}
}

注意需要注释掉BookService类中bookDao字段上的@Qualifier注解,
这是因为@Qualifier注解为显示指定装配哪个组件,如果使用了该注
解,无论是否使用了@Primary注解,都会装配@Qualifier注解标注
的对象

1
2
3
4
5
6
@Service
public class BookService {
//@Qualifier("bookDao") 要让首选装配起效果,@Qualifier自然就不能用了
@Autowired(required=false)
private BookDao bookDao;
}

@Resource注解

@Resource注解是Java规范里面的,也可以说它是JSR250规范里面定义
的一个注解。该注解默认按照名称进行装配,名称可以通过name 属性进
行指定,如果没有指定name属性,当注解写在字段上时,那么默认取字
段名将其作为组件的名称在IOC容器中进行查找,如果注解写在setter
方法上,那么默认取属性名进行装配。当找不到与名称匹配的bean时才
按照类型进行装配。但是需要注意的一点是,如果name属性一旦指定
,那么就只会按照名称进行装配

1
2
3
4
5
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Resources.class)
public @interface Resource {
}

@Inject注解

@Inject注解也是Java规范里面的,也可以说它是JSR330规范里面定义
的一个注解。该注解默认是根据参数名去寻找bean注入,支持Spring的
@Primary注解优先注入,@Inject 注解还可以增加@Named注解指定要
注入的bean。要想使用@Inject注解,需要在项目的pom.xml文件中添
加如下依赖

1
2
3
4
5
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>

可以查看源码

1
2
3
4
5
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Inject {
}

测试@Resource注解

使用@Resource 注解也能够自动装配组件,只不过此时自动装配的
是lable为1的bookDao,不支持@Primary 注解优先注入的功能的
,而且也不能像@Autowired注解一样能添加required=false属性

1
2
3
4
5
6
7
@Service
public class BookService {
// @Qualifier("bookDao") // 要让首选装配起效果,@Qualifier自然就不能用了
// @Autowired(required=false)
@Resource
private BookDao bookDao;
}

可以通过@Resource注解的name属性显示指定要装配的组件的名称

1
2
@Resource(name="bookDao2")
private BookDao bookDao;

测试@Inject注解

用@Inject注解默认输出的是lable为2的bookDao。这是因为@Inject
注解和@Autowired注解一样,默认优先装配使用了@Primary 注解标
注的组件,不能像@Autowired注解一样能添加required=false属性

1
2
3
4
5
6
7
8
@Service
public class BookService {
// @Qualifier("bookDao") // 要让首选装配起效果,@Qualifier自然就不能用了
// @Autowired(required=false)
// @Resource(name="bookDao2")
@Inject
private BookDao bookDao;
}

三个注解之间的区别

三种注解都可以实现bean的自动装配。不同点如下

  1. @Autowired是Spring中的专有注解,而@Resource是Java中JSR250
    规范里面定义的一个注解,@Inject是Java中JSR330规范里面定义的一
    个注解
  2. @Autowired支持参数required=false,而@Resource和@Inject都
    不支持
  3. @Autowired和@Inject支持@Primary注解优先注入,而@Resource
    不支持
  4. @Autowired通过@Qualifier指定注入特定bean,@Resource可以通
    过参数name指定注入bean,而@Inject需要通过@Named注解指定注入
    bean

方法、构造器位置的自动装配

@Autowired注解不仅可以标注在字段上,而且还可以标注在构造方法、
实例方法以及参数上,新建一个Boss类,在Boss类中有一个Car类的引
用,之前讲过可以在car字段上添加@Autowired注解,使其自动装配

1
2
3
4
5
6
7
8
9
10
11
//默认加在IOC容器中的组件,容器启动会调无参构造器创建对象,然后再进行初始化、赋值等操作
@Component
public class Boss {
private Car car;
public Car getCar() {
return car;
}
public void setCar(Car car) {
this.car = car;
}
}

标注在实例方法上

可以将@Autowired注解标注在setter方法上

1
2
3
4
@Autowired 
public void setCar(Car car) {
this.car = car;
}

当@Autowired注解标注在方法上时,Spring容器在创建当前对象的时
候,就会调用相应的方法为对象赋值。如果标注的方法存在参数时,那
么方法使用的参数和自定义类型的值,需要从IOC容器中获取

标注在构造方法上

为Boss类添加一个有参构造方法,然后去除setCar()方法上的@Autowired
注解,将@Autowired注解标注在有参构造方法上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class Boss {
private Car car;
@Autowired
public Boss(Car car) {
this.car = car;
System.out.println("Boss...有参构造器");
}
public Car getCar() {
return car;
}
public void setCar(Car car) {
this.car = car;
}
}

使用@Autowired注解标注在构造方法上时,如果组件中只有一个有参
构造方法,那么这个有参构造方法上的@Autowired注解可以省略,并
且参数位置的组件还是可以自动从IOC容器中获取

标注在参数上

我们也可以将@Autowired注解标注在参数上,例如,在Boss类中我们
将构造方法上的@Autowired注解标注在构造方法的参数上

1
2
3
4
public Boss(@Autowired Car car) {
this.car = car;
System.out.println("Boss...有参构造器");
}

也可以将@Autowired注解标注在setter方法的参数上

1
2
3
public void setCar(@Autowired Car car) {
this.car = car;
}

无论@Autowired注解是标注在字段上、实例方法上、构造方法上还是参
数上,参数位置的组件都是从IOC容器中获取

标注在方法位置

@Autowired注解可以标注在某个方法的位置上,新建一个Color类,在
Color类中有一个Car类型的成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Color {
public Car car;
public Car getCar() {
return car;
}
public void setCar(Car car) {
this.car = car;
}
@Override
public String toString() {
return "Color [car=" + car + "]";
}
}

MainConfigOfAutowired配置类中实例化Color类,此时的Color对
象中的Car对象为空

1
2
3
4
5
@Bean
public Color color() {
Color color = new Color();
return color;
}

可以将Car对象作为一个参数传递到MainConfigOfAutowired配置类的
color()方法中,并且将该Car对象设置到Color对象中

1
2
3
4
5
6
@Bean
public Color color(Car car) {
Color color = new Color();
color.setCar(car);
return color;
}

也可以使用@Autowired注解来标注color()方法中的car参数

1
2
3
4
5
6
@Bean
public Color color(@Autowired Car car) {
Color color = new Color();
color.setCar(car);
return color;
}

自定义组件

自定义组件要想使用Spring容器底层的一些组件,ApplicationContext
(IOC容器)、底层的BeanFactory 等等,那么只需要让自定义组件实现
XxxAware接口即可。此时,Spring在创建对象的时候,会调用XxxAware
接口中定义的方法注入相关的组件

XxxAware接口概览

之前创建的Dog 类,就实现了ApplicationContextAware 接口,实现
该接口的话,需要实现setApplicationContext() 方法。在IOC容器启
动并创建Dog 对象时,Spring会调用setApplicationContext()方法
,并且会将 ApplicationContext 对象传入到该方法中,我们只需要
在Dog 类中定义一个ApplicationContext 类型的成员变量来接收该
方法中的参数,便可在Dog类的其他方法中使用ApplicationContext
对象了

1
2
public interface Aware {
}

Spring中形如XxxAware这样的接口都继承了Aware接口

XxxAware接口案例

  1. 通过ApplicationContextAware接口我们可以获取到IOC容器
  2. 通过BeanNameAware接口获取到当前bean在Spring容器中的名称
  3. 通过EmbeddedValueResolverAware接口能够获取到String值解析器
    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
    public class Red implements ApplicationContextAware, 
    BeanNameAware, EmbeddedValueResolverAware {
    private ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext
    applicationContext) throws BeansException {
    System.out.println("传入的IOC:" + applicationContext);
    this.applicationContext = applicationContext;
    }
    /**
    * 参数name:IOC容器创建当前对象时,为这个对象起的名字
    */
    @Override
    public void setBeanName(String name) {
    System.out.println("当前bean的名字:" + name);
    }
    /*
    参数resolver:IOC容器启动时会自动地将这个String值的解析器传递
    过来给我们如果这个String类型的值里面有一些占位符,那么也会帮我
    们把这些占位符给解析出来,最后返回一个解析后的值
    */
    @Override
    public void setEmbeddedValueResolver(StringValueResolver
    resolver) {
    String resolveStringValue = resolver.resolveStringValue(
    "你好,${os.name},我的年龄是#{20*18}");
    System.out.println("解析的字符串:" + resolveStringValue);
    }
    }

XxxAware原理

XxxAware接口的底层原理是由XxxAwareProcessor实现类实现的
,每一个XxxAware 接口都有它自己对应的XxxAwareProcessor
实现类。例如ApplicationContextAware 接口的底层原理就是
由ApplicationContextAwareProcessor类实现的,其实现了
BeanPostProcessor接口,本质上是一个后置处理器

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
class ApplicationContextAwareProcessor implements BeanPostProcessor {
...
@Nullable
public Object postProcessBeforeInitialization(Object bean, String
beanName) throws BeansException {
if (!(bean instanceof EnvironmentAware) &&
!(bean instanceof EmbeddedValueResolverAware) &&
!(bean instanceof ResourceLoaderAware) &&
!(bean instanceof ApplicationEventPublisherAware) &&
!(bean instanceof MessageSourceAware) &&
!(bean instanceof ApplicationContextAware)) {
return bean;
} else {
...
} else {
//注意这里
this.invokeAwareInterfaces(bean);
}

return bean;
}
private void invokeAwareInterfaces(Object bean) {
...
//这里就把容器作为参数赋值
if (bean instanceof ApplicationContextAware) {
((ApplicationContextAware)bean).setApplicationContext(
this.applicationContext);
}
}
}

@Profile注解

在实际的企业开发环境中,往往都会将环境分为开发环境、测试环
境和生产环境, 并且每个环境基本上都是互相隔离的,也就是说
,开发环境、测试环境和生产环境它们之间是互不相通的。在以前
的开发过程中,如果开发人员完成相应的功能模块并通过单元测
试后,那么他会通过手动修改配置文件的形式,将项目的配置修
改成测试环境,发布到测试环境中进行测试。测试通过后,再将
配置修改为生产环境,发布到生产环境中。这样手动修改配置的
方式,不仅增加了开发和运维的工作量,而且总是手工修改各
项配置文件会很容易出问题

@Profile注解概述

在容器中如果存在同一类型的多个组件,那么可以使用@Profile注解
标识要获取的是哪一个bean。也可以说@Profile注解是Spring为我们
提供的可以根据当前环境,动态地激活和切换一系列组件的功能。这个
功能在不同的环境使用不同的变量的情景下特别有用,例如,开发环
境、测试环境、生产环境使用不同的数据源,在不改变代码的情况下
,可以使用这个注解来动态地切换要连接的数据库

1
2
3
4
5
6
7
8
//@Profile注解不仅可以标注在方法上,也可以标注在配置类上
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional({ProfileCondition.class})
public @interface Profile {
String[] value();
}
  1. 如果@Profile注解标注在配置类上,那么只有是在指定的环境的时
    候,整个配置类里面的所有配置才会生效
  2. 如果一个bean上没有使用@Profile注解进行标注,那么这个bean在
    任何环境下都会被注册到IOC容器中,当然了,前提是在整个配置类生
    效的情况下

实战案例

使用@Profile注解实现开发、测试和生产环境的配置和切换。这里以开发
过程中要用到的数据源为例(数据源也是一种组件)。我们希望在开发环
境中,数据源是连向A数据库的,在测试环境中,数据源是连向B数据库
的,而且在这一过程中,测试人员压根就不需要改动任何代码,最终项
目上线之后,数据源连向C数据库,而且最重要的一点是在整个过程中
,我们不希望改动大量的代码,而实现数据源的切换

环境搭建

需要在pom.xml文件中添加c3p0数据源和MySQL驱动的依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.44</version>
</dependency>

在项目中新建一个配置类,例如MainConfigOfProfile,并在该配置
类中模拟开发、测试、生产环境的数据源

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
@Configuration
public class MainConfigOfProfile {
@Bean("testDataSource")
public DataSource dataSourceTest() throws Exception {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setUser("root");
dataSource.setPassword("liayun");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test");
dataSource.setDriverClass("com.mysql.jdbc.Driver");
return dataSource;
}
@Bean("devDataSource")
public DataSource dataSourceDev() throws Exception {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setUser("root");
dataSource.setPassword("liayun");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/ssm_crud");
dataSource.setDriverClass("com.mysql.jdbc.Driver");
return dataSource;
}
@Bean("prodDataSource")
public DataSource dataSourceProd() throws Exception {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setUser("root");
dataSource.setPassword("liayun");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/scw_0515");
dataSource.setDriverClass("com.mysql.jdbc.Driver");
return dataSource;
}
}

该配置类这样写,是一点儿问题都没有的,但你有没有想过这一点,
在真实项目开发中,那些数据库连接的相关信息,例如用户名、密码
以及MySQL数据库驱动类的全名,这些都是要抽取在一个配置文件中
的,在项目的src/main/resources目录下新建一个配置文件,例
如dbconfig.properties

1
2
3
db.user=root
db.password=liayun
db.driverClass=com.mysql.jdbc.Driver

然后通过之前学习过的注解知识获取以上配置文件中的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@PropertySource("classpath:/dbconfig.properties") // 加载外部的配置文件
@Configuration
public class MainConfigOfProfile implements EmbeddedValueResolverAware {

@Value("${db.user}")
private String user;
private StringValueResolver valueResolver;
private String dirverClass;
...
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.valueResolver = resolver;
/*
我们通过该接口能够获取到String值解析器。也就是说,IOC容器启动时
会自动地将String值的解析器(即StringValueResolver)传递过来
给我们用,咱们可以用它来解析一些字符串
*/
dirverClass = valueResolver.resolveStringValue("${db.driverClass}");
}
}

根据环境注册bean

我们成功搭建环境之后,接下来就是要实现根据不同的环境来向IOC
容器中注册相应的bean了。也就是说,我们要实现在开发环境注册开
发环境下使用的数据源,在测试环境注册测试环境下使用的数据源
,在生产环境注册生产环境下使用的数据源。此时@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
@PropertySource("classpath:/dbconfig.properties") // 加载外部的配置文件
@Configuration
public class MainConfigOfProfile implements EmbeddedValueResolverAware {
...
//测试环境
@Profile("test")
@Bean("testDataSource")
public DataSource dataSourceTest(@Value("${db.password}") String pwd)
throws Exception {
...
}
//定义了一个环境标识,只有当dev环境被激活以后,我们这个bean才能被注册进来
@Profile("dev")
@Bean("devDataSource")
public DataSource dataSourceDev(@Value("${db.password}") String pwd)
throws Exception {
...
}
//生产环境
@Profile("prod")
@Bean("prodDataSource")
public DataSource dataSourceProd(@Value("${db.password}") String pwd)
throws Exception {
...
}
...
}

为不同的数据源添加@Profile注解后,默认是不会向IOC容器中注册
bean的,需要我们根据环境显示指定向IOC容器中注册相应的bean。
换句话说,通过@Profile 注解加了环境标识的bean,只有这个环境被
激活的时候,相应的bean才会被注册到IOC容器中。可以通过@Profile
(“default”)注解来标识一个默认的环境

1
2
3
4
5
6
7
8
9
10
11
@Profile("default")
@Bean("devDataSource")
public DataSource dataSourceDev(@Value("${db.password}") String pwd)
throws Exception {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setUser(user);
dataSource.setPassword(pwd);
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/ssm_crud");
dataSource.setDriverClass(dirverClass);
return dataSource;
}

根据不同的环境来注册相应的bean有两种方式

  1. 第一种方式就是根据命令行参数来确定环境,我们在运行程序的时
    候可以添加相应的命令行参数。例如,如果我们现在的环境是测试环境
    ,那么可以在运行程序的时候添加如下命令行参数
    1
    2
    #VM options
    -Dspring.profiles.active=test
  2. 第二种方式就是通过写代码的方式来激活某种环境,其实主要是通过
    AnnotationConfigApplicationContext类的无参构造方法来实现
  • 在bean上加@Profile注解,其value属性值为环境标识,可以自定义
  • 使用AnnotationConfigApplicationContext类的无参构造方法创建容器
  • 设置容器环境,其值为第1步设置的环境标识
  • 设置容器的配置类
  • 刷新容器

先在程序中调用AnnotationConfigApplicationContext类的无参构
造方法来创建一个IOC容器,然后在容器进行初始化之前,为其设置相
应的环境,接着再为容器设置主配置类,最后刷新一下容器。例如,
我们将IOC容器设置为测试环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Test
public void test02() {
// 1. 使用无参构造器创建一个IOC容器
/*
注意这里不能使用有参构造器,有参构造器里有refresh函数会
自动加载完成
*/
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext();
// 2. 在容器还没启动创建其他bean之前,先设置需要激活的环境(可设置激活多个环境)
applicationContext.getEnvironment().setActiveProfiles("test");
// 3. 注册主配置类
applicationContext.register(MainConfigOfProfile.class);
// 4. 启动刷新容器
applicationContext.refresh();
String[] namesForType = applicationContext.getBeanNamesForType(
DataSource.class);
for (String name : namesForType) {
System.out.println(name);
}
// 关闭容器
applicationContext.close();
}

@Profile注解不仅可以标注在方法上,也可以标注在配置类上。如果标
注在配置类上,那么只有是在指定的环境的时候,整个配置类里面的所
有配置才会生效。例如我们在MainConfigOfProfile 配置类上标注上
@Profile(“dev”)注解

1
2
3
4
5
6
@Profile("dev") // @Profile注解除了能写到bean上,还能写到类上
@PropertySource("classpath:/dbconfig.properties") // 加载外部的配置文件
@Configuration
public class MainConfigOfProfile implements EmbeddedValueResolverAware {
...
}

在test02()方法中指定当前环境为测试环境,而MainConfigOfProfile
配置类上标注的注解为@Profile(“dev”),说明该配置类中的所有配置
只有在开发环境下才会生效。所以,此时没有任何数据源注册到IOC容器
中,自然控制台中就不会输出任何信息了。如果一个bean 上没有使用
@Profile注解进行标注,那么这个bean在任何环境下都会被注册到
IOC容器中

Author: 高明
Link: https://skysea-gaoming.github.io/2021/04/21/Spring3/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.