需求
系統遇到這樣一個需求,線上環境在配置文件發生變動時,可以不用經過重啟,通過刷新接口的方式得到配置文件的加載,主要目的是為了迅速部署,避免因手動重啟,處理不及時導致積壓的問題
問題
1.程序中如何獲取修改后的配置
2.某些配置值是應用在bean里面的,在程序初始化的時候已經注入,如何修改這一部分。
例如,程序是分主備環境的,kafka consumer的groupid或topic根據環境不同而改變,那么consumer若寫在bean中,修改配置文件,要一並更新到bean中
//示例代碼,KafkaConsumer是自己封裝的類 @Bean(name = "consumer") public KafkaConsumer createKafkaConsumer( @Qualifier("service") ExcutorService excutorService) { //groupid + topic //currEnv 主環境 ““ 備環境”RE_BAK_“ KafkaConsumer consumer = new KafkaConsumer(currEnv+groupid,topic){ public void function(ConsumerRecords<String, String> records) { //...業務代碼} } }
解決
1.程序中獲取修改后的配置,去處理業務邏輯,參考了這篇文章戳我
第一步,添加適合自己springboot版本的Springcloud context依賴,若Springboot版本低,maven可能會引不上高版本的context
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-context</artifactId> <version>?</version> </dependency>
第二步,在需要得到刷新配置值的類上引入@RefreshScope
第三步,在對應變量上引入@Value
第四步,編寫refresh接口
package com.example.zzy.zzttest.controller; import com.example.zzy.zzttest.config.BizConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RefreshScope @RestController public class TestController { @Autowired private BizConfig bizConfig; @Autowired private ContextRefresher contextRefresher; @Value("${biz.refresh}") private String refresh; @RequestMapping(path = "/show") public String show() { //String ref = bizConfig.getRefresh(); System.out.println(refresh); return refresh; } @RequestMapping(path = "/refresh") public String refresh() throws Exception{ new Thread( () -> contextRefresher.refresh() ).start(); return show(); } }
p.s.這種方式僅適用於springboot默認管理的配置文件,比如包內的或者包外的application.properties 或者application.yml。一般我們都要更新配置文件了那基本動的都是包外的配置文件,可以直接外置application.properties,把它放在部署包的同級目錄,可以達到在controller中獲取配置值的目的。關於springboot加載配置文件路徑及優先級戳我,總結的很清楚。關於ContextRefresher相關源碼解讀戳我
p.s.順便記錄一下另外一個問題,我的實際項目沒有用到springboot默認的項目內外的application.properties,而是在部署包路徑下的/hadoopConf/props.properties中,項目中獲取配置的時候使用了工具類,這樣做的目的是因為有很多普通項目也引用了這里面的配置,這里不延伸。在這種情況下,因為不是springboot管理的配置文件,所以用上面的方法是不成功的,解決方法是在啟動參數里加入
--spring.config.location=./hadoopConf/props.properties
這樣springboot會把這個配置文件納入管理,相當於把它認為是applicaiton.properties,就可以正常使用上面的步驟獲取更新的配置值了
2.解決刷新bean的問題,參考鏈接
spring boot 動態注入bean
方法一
SpringContextUtil
public class SpringContextUtil { private static ApplicationContext applicationContext; //獲取上下文 public static ApplicationContext getApplicationContext() { return applicationContext; } //設置上下文 public static void setApplicationContext(ApplicationContext applicationContext) { SpringContextUtil.applicationContext = applicationContext; } //通過名字獲取上下文中的bean public static Object getBean(String name){ return applicationContext.getBean(name); } //通過類型獲取上下文中的bean public static Object getBean(Class<?> requiredType){ return applicationContext.getBean(requiredType); } }
啟動類
@SpringBootApplication
public class Application { public static void main(String[] args) { ApplicationContext app = SpringApplication.run(Application.class, args); SpringContextUtil.setApplicationContext(app); } }
測試bean
@Component public class TestService { public String doService(String contxt){ System.err.printf(contxt+"hello service"); return "hello service"; }
}
//無注入
public class TestController implements InitializingBean { @Autowired private TestService testService; @Override public void afterPropertiesSet() throws Exception { System.out.println("我是動態注冊的你,不是容器啟動的時候注冊的你"); } public String toAction(String content){ return "-->" + testService.doService(content); } }
測試
@RestController public class CallCSBController { @GetMapping("/bean") public String registerBean() { //將applicationContext轉換為ConfigurableApplicationContext ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) SpringContextUtil.getApplicationContext(); // 獲取bean工廠並轉換為DefaultListableBeanFactory DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory(); // 通過BeanDefinitionBuilder創建bean定義 BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(TestController.class); // 設置屬性userService,此屬性引用已經定義的bean:userService,這里userService已經被spring容器管理了. // beanDefinitionBuilder.addPropertyReference("testService", "testService"); // 注冊bean defaultListableBeanFactory.registerBeanDefinition("testController", beanDefinitionBuilder.getRawBeanDefinition()); TestController userController = (TestController) SpringContextUtil.getBean("testController"); return userController.toAction("動態注冊生成調用"); //刪除bean. //defaultListableBeanFactory.removeBeanDefinition("testService"); } }
以上參考
鏈接:https://www.jianshu.com/p/41c716e7c31b
方法二(略有不同)
工具類
package com.theeternity.beans.applicationContextRegister;
import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Component; /** * @program: apiboot * @description: 獲取ApplicationContext, 實現動態注入bean * @author: TheEternity Zhang * @create: 2019-06-22 12:15 */ @Component @Slf4j public class ApplicationContextRegister implements ApplicationContextAware { private static ApplicationContext APPLICATION_CONTEXT; /** * 設置spring上下文 * @param applicationContext spring上下文 * @throws BeansException * */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { log.debug("ApplicationContext registed-->{}", applicationContext); APPLICATION_CONTEXT = applicationContext; } /** * 獲取容器 * @return */ public static ApplicationContext getApplicationContext() { return APPLICATION_CONTEXT; } /** * 獲取容器對象 * @param type * @param <T> * @return */ public static <T> T getBean(Class<T> type) { return APPLICATION_CONTEXT.getBean(type); } public static <T> T getBean(String name,Class<T> clazz){ return APPLICATION_CONTEXT.getBean(name, clazz); } public static Object getBean(String name){ return APPLICATION_CONTEXT.getBean(name); } }
測試bean
@Component public class TestService { public String doService(String contxt){ System.err.printf(contxt+"hello service"); return "hello service"; } } //無注入 public class TestController implements InitializingBean { @Autowired private TestService testService; @Override public void afterPropertiesSet() throws Exception { System.out.println("我是動態注冊的你,不是容器啟動的時候注冊的你"); } public String toAction(String content){ return "-->" + testService.doService(content); } }
測試
@RestController public class CallCSBController { @GetMapping("/bean") public String registerBean() { //將applicationContext轉換為ConfigurableApplicationContext ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) ApplicationContextRegister.getApplicationContext(); // 獲取bean工廠並轉換為DefaultListableBeanFactory DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory(); // 通過BeanDefinitionBuilder創建bean定義 BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(TestController.class); // 設置屬性userService,此屬性引用已經定義的bean:userService,這里userService已經被spring容器管理了. // beanDefinitionBuilder.addPropertyReference("testService", "testService"); // 注冊bean defaultListableBeanFactory.registerBeanDefinition("testController", beanDefinitionBuilder.getRawBeanDefinition()); TestController userController = (TestController) ApplicationContextRegister.getBean("testController"); return userController.toAction("動態注冊生成調用"); //刪除bean. //defaultListableBeanFactory.removeBeanDefinition("testService"); } 第一種方法的另外一種形式 /** //獲取ApplicationContext ApplicationContext ctx=ApplicationContextRegister.getApplicationContext(); //獲取BeanFactory DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) ctx.getAutowireCapableBeanFactory(); //創建bean信息. BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(TestService.class); beanDefinitionBuilder.addPropertyValue("name","張三"); //動態注冊bean. defaultListableBeanFactory.registerBeanDefinition("testService", beanDefinitionBuilder.getBeanDefinition()); //獲取動態注冊的bean. TestService testService =ctx.getBean(TestService.class);、testService.print(); */ @GetMapping("/bean2") public String registerBean2() { TestController userController = (TestController) ApplicationContextRegister.getBean(TestController.class); return userController.toAction("動態注冊生成調用"); } }
以上參考:
主力:https://www.jianshu.com/p/41c716e7c31b
輔助:https://www.jb51.net/article/140157.htm
拓展理解
我們通過getBean來獲得對象,但這些對象都是事先定義好的,我們有時候要在程序中動態的加入對象.因為如果采用配置文件或者注解,我們要加入對象的話,還要重啟服務,如果我們想要避免這一情況就得采用動態處理bean,包括:動態注入,動態刪除。
本節大綱 :
(1)動態注入bean思路; (2)動態注入實現代碼; (3)多次注入同一個bean的情況; (4)動態刪除;
接下來我們看下具體的內容:
(1)動態注入bean思路;
在具體進行代碼實現的時候,我們要知道,Spring管理bean的對象是BeanFactory,具體的是DefaultListableBeanFactory,在這個類當中有一個注入bean的方法:registerBeanDefinition,在調用registerBeanDefinition方法時,需要BeanDefinition參數,那么這個參數怎么獲取呢?Spring提供了BeanDefinitionBuilder可以構建一個BeanDefinition,那么我們的問題就是如何獲取BeanFactory了,這個就很簡單了,只要獲取到ApplicationContext對象即可獲取到BeanFacory了。
(2)動態注入實現代碼;
綜上所述,如果我們要編寫一個簡單里的例子的話,那么分以個幾個步驟進行編碼即可進行動態注入了:
<1>. 獲取ApplicationContext; <2>. 通過ApplicationContext獲取到BeanFacotory; <3>. 通過BeanDefinitionBuilder構建BeanDefiniton; <4>. 調用beanFactory的registerBeanDefinition注入beanDefinition; <5>. 使用ApplicationContext.getBean獲取bean進行測試;
很明顯我們需要先定義個類進行測試,比如TestService代碼如下:
package com.kfit.demo.service;
public class TestService { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public void print(){ System.out.println("動態載入bean,name="+name); } } 注意:這里沒有使用@Service和配置文件進行注入TestService。
那么下面我們的目標就是動態注入TestService了,根據以上的分析,我們進行編碼,具體代碼如下:
//獲取context. -- Angel -守護天使 ApplicationContext ctx = (ApplicationContext) SpringApplication.run(App.class, args); //獲取BeanFactory DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) ctx.getAutowireCapableBeanFactory(); //創建bean信息. BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(TestService.class); beanDefinitionBuilder.addPropertyValue("name","張三"); //動態注冊bean. defaultListableBeanFactory.registerBeanDefinition("testService", beanDefinitionBuilder.getBeanDefinition()); //獲取動態注冊的bean. TestService testService =ctx.getBean(TestService.class); testService.print();
執行代碼我們會在控制台看到如下打印信息:
動態載入bean,name=張三
到這里,就證明我們的代碼很成功了。
(3)多次注入同一個bean的情況;
多次注入同一個bean的,如果beanName不一樣的話,那么會產生兩個Bean;如果beanName一樣的話,后面注入的會覆蓋前面的。
第一種情況:beanName一樣的代碼:
beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(TestService.class); defaultListableBeanFactory.registerBeanDefinition("testService", beanDefinitionBuilder.getBeanDefinition()); TestService testService =ctx.getBean(TestService.class); testService.print();
運行看控制台: 動態載入bean,name=李四
第二種情況:beanName不一樣的代碼:
beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(TestService.class); defaultListableBeanFactory.registerBeanDefinition("testService1", beanDefinitionBuilder.getBeanDefinition()); TestService testService =ctx.getBean(TestService.class); testService.print();
此時如果沒有更改別的代碼直接運行的話,是會報如下錯誤的:
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [com.kfit.demo.service.TestService] is defined: expected single matching bean but found 2: testService1,testService
大體意思就是在getBean的時候,找到了兩個bean,這時候就不知道要獲取哪個了,所以在獲取的時候,我們就要指定我們是要獲取的testService還是testService1,只需要修改一句代碼:
將代碼:
TestService testService =ctx.getBean(TestService.class);
修改為:
TestService testService =ctx.getBean("testService");
(4)動態刪除;
相對於動態注入,動態刪除就很簡單了,直接奉上代碼:
//刪除bean. defaultListableBeanFactory.removeBeanDefinition("testService");
拓展參考:
https://412887952-qq-com.iteye.com/blog/2348445
https://www.cnblogs.com/eternityz/p/12241143.html