Spring系列第25篇:@Value【用法、数据来源、动态刷新】

@Value的用法


系统中需要连接db,连接db有很多配置信息。

系统中需要发送邮件,发送邮件需要配置邮件服务器的信息。

还有其他的一些配置信息。

我们可以将这些配置信息统一放在一个配置文件中,上线的时候由运维统一修改。

那么系统中如何使用这些配置信息呢,spring中提供了@Value注解来解决这个问题。

通常我们会将配置信息以key=value的形式存储在properties配置文件中。

通过@Value(“${配置文件中的key}”)来引用指定的key对应的value。

@Value使用步骤

步骤一:使用@PropertySource注解引入配置文件

将@PropertySource放在类上面,如下

@PropertySource({“配置文件路径1”,“配置文件路径2”…})

@PropertySource注解有个value属性,字符串数组类型,可以用来指定多个配置文件的路径。

如:

@Component

@PropertySource({“classpath:com/javacode2018/lesson002/demo18/db.properties”})

public class DbConfig {

}

步骤二:使用@Value注解引用配置文件的值

通过@Value引用上面配置文件中的值:

语法

@Value(“${配置文件中的key:默认值}”)

@Value(“${配置文件中的key}”)

如:

@Value(“${password:123}”)

上面如果password不存在,将123作为值

@Value(“${password}”)

上面如果password不存在,值为${password}

假如配置文件如下

jdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8

jdbc.username=javacode

jdbc.password=javacode

使用方式如下:

@Value(“${jdbc.url}”)

private String url;

@Value(“${jdbc.username}”)

private String username;

@Value(“${jdbc.password}”)

private String password;

下面来看案例

案例

来个配置文件db.properties

jdbc.url=jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8

jdbc.username=javacode

jdbc.password=javacode

来个配置类,使用@PropertySource引入上面的配置文件

package com.javacode2018.lesson002.demo18.test1;

import org.springframework.beans.factory.annotation.Configurable;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.PropertySource;

@Configurable

@ComponentScan

@PropertySource({“classpath:com/javacode2018/lesson002/demo18/db.properties”})

public class MainConfig1 {

}

来个类,使用@Value来使用配置文件中的信息

package com.javacode2018.lesson002.demo18.test1;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.stereotype.Component;

@Component

public class DbConfig {

@Value(“${jdbc.url}”)

private String url;

@Value(“${jdbc.username}”)

private String username;

@Value(“${jdbc.password}”)

private String 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;

}

@Override

public String toString() {

return “DbConfig{” +

“url='” + url + ‘’’ +

“, username='” + username + ‘’’ +

“, password='” + password + ‘’’ +

‘}’;

}

}

上面重点在于注解@Value注解,注意@Value注解中的

来个测试用例

package com.javacode2018.lesson002.demo18;

import com.javacode2018.lesson002.demo18.test1.DbConfig;

import com.javacode2018.lesson002.demo18.test1.MainConfig1;

import org.junit.Test;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ValueTest {

@Test

public void test1() {

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

context.register(MainConfig1.class);

context.refresh();

DbConfig dbConfig = context.getBean(DbConfig.class);

System.out.println(dbConfig);

}

}

运行输出

DbConfig{url=‘jdbc:mysql://localhost:3306/javacode2018?characterEncoding=UTF-8’, username=‘javacode’, password=‘javacode’}

上面用起来比较简单,很多用过的人看一眼就懂了,这也是第一个问题,多数人都是ok的,下面来看@Value中数据来源除了配置文件的方式,是否还有其他方式。

@Value数据来源


通常情况下我们@Value的数据来源于配置文件,不过,还可以用其他方式,比如我们可以将配置文件的内容放在数据库,这样修改起来更容易一些。

我们需要先了解一下@Value中数据来源于spring的什么地方。

spring中有个类

org.springframework.core.env.PropertySource

可以将其理解为一个配置源,里面包含了key->value的配置信息,可以通过这个类中提供的方法获取key对应的value信息

内部有个方法:

public abstract Object getProperty(String name);

通过name获取对应的配置信息。

系统有个比较重要的接口

org.springframework.core.env.Environment

用来表示环境配置信息,这个接口有几个方法比较重要

String resolvePlaceholders(String text);

MutablePropertySources getPropertySources();

resolvePlaceholders用来解析${text}的,@Value注解最后就是调用这个方法来解析的。

getPropertySources返回MutablePropertySources对象,来看一下这个类

public class MutablePropertySources implements PropertySources {

private final List<PropertySource<?>> propertySourceList = new CopyOnWriteArrayList<>();

}

内部包含一个propertySourceList列表。

spring容器中会有一个Environment对象,最后会调用这个对象的resolvePlaceholders方法解析@Value。

大家可以捋一下,最终解析@Value的过程:

1. 将@Value注解的value参数值作为Environment.resolvePlaceholders方法参数进行解析

2. Environment内部会访问MutablePropertySources来解析

3. MutablePropertySources内部有多个PropertySource,此时会遍历PropertySource列表,调用PropertySource.getProperty方法来解析key对应的值

通过上面过程,如果我们想改变@Value数据的来源,只需要将配置信息包装为PropertySource对象,丢到Environment中的MutablePropertySources内部就可以了。

下面我们就按照这个思路来一个。

来个邮件配置信息类,内部使用@Value注入邮件配置信息

package com.javacode2018.lesson002.demo18.test2;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.stereotype.Component;

/**

* 邮件配置信息

*/

@Component

public class MailConfig {

@Value(“${mail.host}”)

private String host;

@Value(“${mail.username}”)

private String username;

@Value(“${mail.password}”)

private String password;

public String getHost() {

return host;

}

public void setHost(String host) {

this.host = host;

}

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;

}

@Override

public String toString() {

return “MailConfig{” +

“host='” + host + ‘’’ +

“, username='” + username + ‘’’ +

“, password='” + password + ‘’’ +

‘}’;

}

}

再来个类DbUtilgetMailInfoFromDb方法模拟从db中获取邮件配置信息,存放在map中

package com.javacode2018.lesson002.demo18.test2;

import java.util.HashMap;

import java.util.Map;

public class DbUtil {

/**

* 模拟从db中获取邮件配置信息

* @return

*/

public static Map<String, Object> getMailInfoFromDb() {

Map<String, Object> result = new HashMap<>();

result.put(“mail.host”, “smtp.qq.com”);

result.put(“mail.username”, “路人”);

result.put(“mail.password”, “123”);

return result;

}

}

来个spring配置类

package com.javacode2018.lesson002.demo18.test2;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.Configuration;

@Configuration

@ComponentScan

public class MainConfig2 {

}

下面是重点代码

@Test

public void test2() {

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

/下面这段是关键 start/

//模拟从db中获取配置信息

Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();

//将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)

MapPropertySource mailPropertySource = new MapPropertySource(“mail”, mailInfoFromDb);

//将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高

context.getEnvironment().getPropertySources().addFirst(mailPropertySource);

/上面这段是关键 end/

context.register(MainConfig2.class);

context.refresh();

MailConfig mailConfig = context.getBean(MailConfig.class);

System.out.println(mailConfig);

}

注释比较详细,就不详细解释了。

直接运行,看效果

MailConfig{host=‘smtp.qq.com’, username=‘路人’, password=‘123’}

有没有感觉很爽,此时你们可以随意修改DbUtil.getMailInfoFromDb,具体数据是从db中来,来时从redis或者其他介质中来,任由大家发挥。

上面重点是下面这段代码,大家需要理解

/下面这段是关键 start/

//模拟从db中获取配置信息

Map<String, Object> mailInfoFromDb = DbUtil.getMailInfoFromDb();

//将其丢在MapPropertySource中(MapPropertySource类是spring提供的一个类,是PropertySource的子类)

MapPropertySource mailPropertySource = new MapPropertySource(“mail”, mailInfoFromDb);

//将mailPropertySource丢在Environment中的PropertySource列表的第一个中,让优先级最高

context.getEnvironment().getPropertySources().addFirst(mailPropertySource);

/上面这段是关键 end/

咱们继续看下一个问题

如果我们将配置信息放在db中,可能我们会通过一个界面来修改这些配置信息,然后保存之后,希望系统在不重启的情况下,让这些值在spring容器中立即生效。

@Value动态刷新的问题的问题,springboot中使用@RefreshScope实现了。

实现@Value动态刷新


先了解一个知识点

这块需要先讲一个知识点,用到的不是太多,所以很多人估计不太了解,但是非常重要的一个点,我们来看一下。

这个知识点是自定义bean作用域,对这块不了解的先看一下这篇文章:bean作用域详解

bean作用域中有个地方没有讲,来看一下@Scope这个注解的源码,有个参数是:

ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;

这个参数的值是个ScopedProxyMode类型的枚举,值有下面4中

public enum ScopedProxyMode {

DEFAULT,

NO,

INTERFACES,

TARGET_CLASS;

}

前面3个,不讲了,直接讲最后一个值是干什么的。

当@Scope中proxyMode为TARGET_CLASS的时候,会给当前创建的bean通过cglib生成一个代理对象,通过这个代理对象来访问目标bean对象。

理解起来比较晦涩,还是来看代码吧,容易理解一些,来个自定义的Scope案例。

自定义一个bean作用域的注解

package com.javacode2018.lesson002.demo18.test3;

import org.springframework.context.annotation.Scope;

import org.springframework.context.annotation.ScopedProxyMode;

import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Scope(BeanMyScope.SCOPE_MY) //@1

public @interface MyScope {

/**

* @see Scope#proxyMode()

*/

ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;//@2

}

@1:使用了@Scope注解,value为引用了一个常量,值为my,一会下面可以看到。

@2:注意这个地方,参数名称也是proxyMode,类型也是ScopedProxyMode,而@Scope注解中有个和这个同样类型的参数,spring容器解析的时候,会将这个参数的值赋给@MyScope注解上面的@Scope注解的proxyMode参数,所以此处我们设置proxyMode值,最后的效果就是直接改变了@Scope中proxyMode参数的值。此处默认值取的是ScopedProxyMode.TARGET_CLASS

@MyScope注解对应的Scope实现如下

package com.javacode2018.lesson002.demo18.test3;

import org.springframework.beans.factory.ObjectFactory;

import org.springframework.beans.factory.config.Scope;

import org.springframework.lang.Nullable;

/**

* @see MyScope 作用域的实现

*/

public class BeanMyScope implements Scope {

public static final String SCOPE_MY = “my”; //@1

@Override

public Object get(String name, ObjectFactory<?> objectFactory) {

System.out.println(“BeanMyScope >>>>>>>>> get:” + name); //@2

return objectFactory.getObject(); //@3

}

@Nullable

@Override

public Object remove(String name) {

return null;

}

@Override

public void registerDestructionCallback(String name, Runnable callback) {

}

@Nullable

@Override

public Object resolveContextualObject(String key) {

return null;

}

@Nullable

@Override

public String getConversationId() {

return null;

}

}

@1:定义了一个常量,作为作用域的值

@2:这个get方法是关键,自定义作用域会自动调用这个get方法来创建bean对象,这个地方输出了一行日志,为了一会方便看效果

@3:通过objectFactory.getObject()获取bean实例返回。

下面来创建个类,作用域为上面自定义的作用域

package com.javacode2018.lesson002.demo18.test3;

import org.springframework.stereotype.Component;

import java.util.UUID;

@Component

@MyScope //@1

public class User {

private String username;

public User() {

System.out.println(“---------创建User对象” + this); //@2

this.username = UUID.randomUUID().toString(); //@3

}

public String getUsername() {

return username;

}

public void setUsername(String username) {

this.username = username;

}

}

@1:使用了自定义的作用域@MyScope

@2:构造函数中输出一行日志

@3:给username赋值,通过uuid随机生成了一个

来个spring配置类,加载上面@Compontent标注的组件

package com.javacode2018.lesson002.demo18.test3;

import org.springframework.context.annotation.ComponentScan;

import org.springframework.context.annotation.Configuration;

@ComponentScan

@Configuration

public class MainConfig3 {

}

下面重点来了,测试用例

@Test

public void test3() throws InterruptedException {

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();

//将自定义作用域注册到spring容器中

context.getBeanFactory().registerScope(BeanMyScope.SCOPE_MY, new BeanMyScope());//@1

context.register(MainConfig3.class);

context.refresh();

System.out.println(“从容器中获取User对象”);

User user = context.getBean(User.class); //@2

System.out.println(“user对象的class为:” + user.getClass()); //@3

System.out.println(“多次调用user的getUsername感受一下效果\n”);

for (int i = 1; i <= 3; i++) {

System.out.println(String.format(“********\n第%d次开始调用getUsername”, i));

System.out.println(user.getUsername());

System.out.println(String.format(“第%d次调用getUsername结束\n********\n”, i));

}

}

@1:将自定义作用域注册到spring容器中

@2:从容器中获取User对应的bean

@3:输出这个bean对应的class,一会认真看一下,这个类型是不是User类型的

代码后面又搞了3次循环,调用user的getUsername方法,并且方法前后分别输出了一行日志。

见证奇迹的时候到了,运行输出

从容器中获取User对象

user对象的class为:class com.javacode2018.lesson002.demo18.test3.User E n h a n c e r B y S p r i n g C G L I B EnhancerBySpringCGLIB EnhancerBySpringCGLIB80233127

多次调用user的getUsername感受一下效果


第1次开始调用getUsername

BeanMyScope >>>>>>>>> get:scopedTarget.user

---------创建User对象com.javacode2018.lesson002.demo18.test3.User@6a370f4

7b41aa80-7569-4072-9d40-ec9bfb92f438

第1次调用getUsername结束



第2次开始调用getUsername

BeanMyScope >>>>>>>>> get:scopedTarget.user

---------创建User对象com.javacode2018.lesson002.demo18.test3.User@1613674b

01d67154-95f6-44bb-93ab-05a34abdf51f

第2次调用getUsername结束



第3次开始调用getUsername

BeanMyScope >>>>>>>>> get:scopedTarget.user

---------创建User对象com.javacode2018.lesson002.demo18.test3.User@27ff5d15

76d0e86f-8331-4303-aac7-4acce0b258b8

第3次调用getUsername结束


从输出的前2行可以看出:

  1. 调用context.getBean(User.class)从容器中获取bean的时候,此时并没有调用User的构造函数去创建User对象

  2. 第二行输出的类型可以看出,getBean返回的user对象是一个cglib代理对象。

后面的日志输出可以看出,每次调用user.getUsername方法的时候,内部自动调用了BeanMyScope#get 方法和 User的构造函数。

通过上面的案例可以看出,当自定义的Scope中proxyMode=ScopedProxyMode.TARGET_CLASS的时候,会给这个bean创建一个代理对象,调用代理对象的任何方法,都会调用这个自定义的作用域实现类(上面的BeanMyScope)中get方法来重新来获取这个bean对象。

动态刷新@Value具体实现

那么我们可以利用上面讲解的这种特性来实现@Value的动态刷新,可以实现一个自定义的Scope,这个自定义的Scope支持@Value注解自动刷新,需要使用@Value注解自动刷新的类上面可以标注这个自定义的注解,当配置修改的时候,调用这些bean的任意方法的时候,就让spring重启初始化一下这个bean,这个思路就可以实现了,下面我们来写代码。

先来自定义一个Scope:RefreshScope

package com.javacode2018.lesson002.demo18.test4;

import org.springframework.context.annotation.Scope;

import org.springframework.context.annotation.ScopedProxyMode;

import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Scope(BeanRefreshScope.SCOPE_REFRESH)

@Documented

public @interface RefreshScope {

ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; //@1

}

要求标注@RefreshScope注解的类支持动态刷新@Value的配置

@1:这个地方是个关键,使用的是ScopedProxyMode.TARGET_CLASS

这个自定义Scope对应的解析类

下面类中有几个无关的方法去掉了,可以忽略

package com.javacode2018.lesson002.demo18.test4;

import org.springframework.beans.factory.ObjectFactory;

import org.springframework.beans.factory.config.Scope;

import org.springframework.lang.Nullable;

import java.util.concurrent.ConcurrentHashMap;

public class BeanRefreshScope implements Scope {

public static final String SCOPE_REFRESH = “refresh”;

private static final BeanRefreshScope INSTANCE = new BeanRefreshScope();

//来个map用来缓存bean

private ConcurrentHashMap<String, Object> beanMap = new ConcurrentHashMap<>(); //@1

private BeanRefreshScope() {

}

public static BeanRefreshScope getInstance() {

return INSTANCE;

}

/**

* 清理当前

*/

public static void clean() {

INSTANCE.beanMap.clear();

}

@Override

public Object get(String name, ObjectFactory<?> objectFactory) {

Object bean = beanMap.get(name);

if (bean == null) {

bean = objectFactory.getObject();

beanMap.put(name, bean);

}

return bean;

}

}

上面的get方法会先从beanMap中获取,获取不到会调用objectFactory的getObject让spring创建bean的实例,然后丢到beanMap中

上面的clean方法用来清理beanMap中当前已缓存的所有bean

来个邮件配置类,使用@Value注解注入配置,这个bean作用域为自定义的@RefreshScope

package com.javacode2018.lesson002.demo18.test4;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.stereotype.Component;

  • 18
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值