Spring配置文件中密碼明文改為密文處理的通用方式
一、背景
SpringBoot和SpringCloud中涉及多個配置文件,配置文件中對于密碼默認是明文方式,這種方式在生產(chǎn)環(huán)境一般是不被允許的。為避免配置文件中出現(xiàn)明文,應當在配置文件中配置為密文,然后在啟動時在程序內(nèi)部完成解密。
本文提供了通用的處理方式,可以適配以下幾類配置文件:
- 本地bootstrap.properties 在Spring的Bean創(chuàng)建之前的配置
- 本地application.properties 在Spring的配置,包括帶profile環(huán)境的配置
- 配置中心上的配置(例如nacos上的Data ID)
為了適應配置文件涉及密碼由明文改為密文,需要分為兩步:
①將配置文件中涉及密文的配置項配置為密文字符串(需自己加密計算得到);
②在Spring啟動中讀取密文字符串并解密還原。
二、思路
對于以上第②步Spring啟動時的處理,由于以上配置文件在Spring加載的時機和生命周期不同,有兩種處理方式:
A) 普通方式
由于Spring中的對本地application.properties或者配置中心上的配置(例如nacos上的Data ID)在Spring Bean創(chuàng)建過程中,會有對應的配置Bean(通過注解@Configuration申明的Java類),Spring會自動根據(jù)讀取解析配置文件并賦值給Bean。
因此,若需要對密文字符串并解密還原,可以對配置Bean(通過注解@Configuration申明的Java類)進行繼承,Override重寫對應的set方法,完成解密。
B) 適合bootstrap.properties方式
對于Spring Cloud,在bootstrap階段還未創(chuàng)建Bean,所以以上Override重寫對應的set方法并不適用。所以對于bootstrap.properties配置文件。可通過實現(xiàn)EnvironmentPostProcessor接口,來捕獲Environment配置,解密后將配置新值設(shè)置到Environment中。
三、示例
A) 普通方式(連接Redis集群)
下面以連接Redis集群為例進行說明,連接Redis集群的配置項可以在本地application.properties或者配置中心上的配置(例如nacos上的Data ID),且其中spring.redis.password配置項值已經(jīng)設(shè)置為密文。
下面代碼對配置Bean(通過注解@Configuration申明的Java類RedisProperties)進行繼承,Override重寫對應的set方法。Java代碼如下:
package 包指定忽略,請自定; import 忽略解密計算工具類SystemSecurityAlgorithm,請自定; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.util.StringUtils; /** * 連接Redis集群的配置類【通過@Configuration覆蓋原Bean機制】: * 1、連接Redis的連接password不得出現(xiàn)明文,故需在properties配置文件中配置為加密密文(加密算法Java類為:SystemSecurityAlgorithm),然后在啟動時通過本類解密 * 2、貴金屬應用服務采用多數(shù)據(jù)中心DataCenter部署。而每邏輯中心均有獨立的Redis集群。 應用服務應連接同邏輯中心內(nèi)的Redis集群,既北京的應用服務不應該連接合肥Redis集群 * 既:對于同服務的不同實例,應根據(jù)服務實例所在邏輯中心(具體見枚舉ServiceConstant.DataCenter定義的邏輯中心)連接相同邏輯中心下的Redis集群。 * 因此: * a).以Spring標準Redis連接配置為基礎(chǔ),對nodes值中各個IP端口配置,在各IP前增加一個大寫字母:該IP所在DataCenter數(shù)據(jù)中心的英文代碼 * b).以Spring標準Redis連接配置為基礎(chǔ),對password值改為可配多個密碼,以逗號分隔,每個密碼前增加一個大寫字母,該密碼是連接哪個Redis集群的DataCenter數(shù)據(jù)中心的英文代碼 * 為支持以上,定制化開發(fā)本類,實現(xiàn)處理最終還原至Spring標準連接Redis的配置,以供lettuce創(chuàng)建連接池。 * ----------------------------------------------------------- * 機制適用性: * 除了通過@Configuration覆蓋原Bean機制,還有通過實現(xiàn)EnvironmentPostProcessor接口機制。兩種機制適用性說明如下: * bootstrap.properties配置文件(bootstrap階段,還未創(chuàng)建Bean) →→適合→→ 【實現(xiàn)EnvironmentPostProcessor接口機制】 * 本地application.properties配置文件(正常SpringBoot啟動,通過@Configuration注解的Bean) →→適合→→ 【實現(xiàn)EnvironmentPostProcessor接口機制】和【通過@Configuration覆蓋原Bean機制】均可 * 從Nacos等配置中心獲取得到的配置文件 →→適合→→ 【通過@Configuration覆蓋原Bean機制】 * */ @Configuration @Primary // 由于默認RedisProperties作為配置類會自動創(chuàng)建Bean。 為避免存在兩個同類型(RedisProperties)Bean,所以本類通過注解Primary,使得只有本類生效。相當于替代默認RedisProperties public class GjsRedisProperties extends RedisProperties { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GjsRedisProperties.class); @Override public void setPassword(String orginPassword) { if(StringUtils.hasText(orginPassword)) { // 對密文解密并設(shè)置 if (StringUtils.hasText(orginPassword) && orginPassword.length() >= 32 ) { // 如果滿足密碼密文的長度及大小寫要求,視為密文,解密 String padStr = SystemSecurityAlgorithm.decryptStr(orginPassword); log.debug("連接Redis配置項spring.redis.password: 解密前orginPassword=[{}], 解密后padStr=[{}]", orginPassword, padStr); //為避免密碼泄露,僅debug才輸出明文 log.info("連接Redis配置項spring.redis.password: 對密文orginPassword=[{}]已完成解密", orginPassword); super.setPassword(padStr); } else { // 不滿足密碼密文的長度及大小寫要求(視為明文,不解密),保持不變 log.warn("連接Redis配置項spring.redis.password的:orginPassword=[{}]不滿足密碼密文的長度及大小寫要求(視為明文,不解密),保持不變", orginPassword); super.setPassword(orginPassword); } } } }
A) 普通方式(連接RocketMQ)
下面以連接RocketMQ為例進行說明,連接RocketMQ的配置項可以在本地application.properties或者配置中心上的配置(例如nacos上的Data ID),且其中rocketmq.producer.secret-key和rocketmq.consumer.secret-key配置項值已經(jīng)設(shè)置為密文。
下面代碼對配置Bean(通過注解@Configuration申明的Java類RocketMQProperties)進行繼承,Override重寫對應的set方法。Java代碼如下:
package 包指定忽略,請自定; import 忽略解密計算工具類SystemSecurityAlgorithm,請自定; import org.apache.rocketmq.spring.autoconfigure.RocketMQProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; import org.springframework.util.StringUtils; import java.util.HashMap; import java.util.Map; /** * 連接RocketMQ的配置類【通過@Configuration覆蓋原Bean機制】: * 因連接RocketMQ的secret-key不得出現(xiàn)明文,故需在properties配置文件中配置為加密密文(加密算法Java類為:SystemSecurityAlgorithm),然后在啟動時通過本類解密 * ----------------------------------------------------------- * 機制適用性: * 除了通過@Configuration覆蓋原Bean機制,還有通過實現(xiàn)EnvironmentPostProcessor接口機制。兩種機制適用性說明如下: * bootstrap.properties配置文件(bootstrap階段,還未創(chuàng)建Bean) →→適合→→ 【實現(xiàn)EnvironmentPostProcessor接口機制】 * 本地application.properties配置文件(正常SpringBoot啟動,通過@Configuration注解的Bean) →→適合→→ 【實現(xiàn)EnvironmentPostProcessor接口機制】和【通過@Configuration覆蓋原Bean機制】均可 * 從Nacos等配置中心獲取得到的配置文件 →→適合→→ 【通過@Configuration覆蓋原Bean機制】 * */ @Configuration @Primary // 由于默認RocketMQProperties作為配置類會自動創(chuàng)建Bean。 為避免存在兩個同類型(RocketMQProperties)Bean,所以本類通過注解Primary,使得只有本類生效。相當于替代默認RocketMQProperties public class GjsRocketMQProperties extends RocketMQProperties { final private String KEYNAME_PRODUCER_SECRET = "rocketmq.producer.secret-key"; final private String KEYNAME_CONSUMER_SECRET = "rocketmq.consumer.secret-key"; @Autowired ConfigurableApplicationContext springContext; private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(GjsRocketMQProperties.class); @Override public void setProducer(Producer producer) { final String orginSecretKey = producer.getSecretKey(); // 對密文解密并設(shè)置 if (StringUtils.hasText(orginSecretKey) && orginSecretKey.length() >= 32) { // 如果滿足密碼密文的長度及大小寫要求,視為密文,解密 String padStr = SystemSecurityAlgorithm.decryptStr(orginSecretKey); log.debug("連接RocketMQ配置項{}: 解密前orginSecretKey=[{}], 解密后padStr=[{}]", KEYNAME_PRODUCER_SECRET, orginSecretKey, padStr); //為避免密碼泄露,僅debug才輸出明文 log.info("連接RocketMQ配置項{}: 對密文orginSecretKey=[{}]已完成解密", KEYNAME_PRODUCER_SECRET, orginSecretKey); producer.setSecretKey(padStr); // 由于RocketMQ在構(gòu)建DefaultRocketMQListenerContainer過程中,會從Spring的Environment中獲取配置。 // 附調(diào)用關(guān)系簡要說明如下: // org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer.afterPropertiesSet() // org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer.initRocketMQPushConsumer() // org.apache.rocketmq.spring.support.RocketMQUtil.getRPCHookByAkSk() // org.springframework.core.env.AbstractEnvironment.resolveRequiredPlaceholders() // ...... // org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertyResolver.findPropertyValue() // 因此一并修改環(huán)境中的值,使其能取得新值 modifyEnvironmentValue(springContext.getEnvironment(), KEYNAME_PRODUCER_SECRET, padStr); } else { // 不滿足密碼密文的長度及大小寫要求(視為明文,不解密),保持不變 log.warn("連接RocketMQ配置項rocketmq.producer.secret-key值=[{}]不滿足密碼密文的長度及大小寫要求(視為明文,不解密),保持不變", orginSecretKey); } super.setProducer(producer); } @Override public void setConsumer(PushConsumer pushConsumer) { final String orginSecretKey = pushConsumer.getSecretKey(); // 對密文解密并設(shè)置 if (StringUtils.hasText(orginSecretKey) && orginSecretKey.length() >= 32 ) { // 如果滿足密碼密文的長度及大小寫要求,視為密文,解密 String padStr = SystemSecurityAlgorithm.decryptStr(orginSecretKey); log.debug("連接RocketMQ配置項{}: 解密前orginSecretKey=[{}], 解密后padStr=[{}]", KEYNAME_CONSUMER_SECRET, orginSecretKey, padStr); //為避免密碼泄露,僅debug才輸出明文 log.info("連接RocketMQ配置項{}: 對密文orginSecretKey=[{}]已完成解密", KEYNAME_CONSUMER_SECRET, orginSecretKey); pushConsumer.setSecretKey(padStr); // 由于RocketMQ在構(gòu)建DefaultRocketMQListenerContainer過程中,會從Spring的Environment中獲取配置。 // 附調(diào)用關(guān)系簡要說明如下: // org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer.afterPropertiesSet() // org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer.initRocketMQPushConsumer() // org.apache.rocketmq.spring.support.RocketMQUtil.getRPCHookByAkSk() // org.springframework.core.env.AbstractEnvironment.resolveRequiredPlaceholders() // ...... // org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertyResolver.findPropertyValue() // 因此一并修改環(huán)境中的值,使其能取得新值 modifyEnvironmentValue(springContext.getEnvironment(), KEYNAME_CONSUMER_SECRET, padStr); } else { // 不滿足密碼密文的長度及大小寫要求(視為明文,不解密),保持不變 log.warn("連接RocketMQ配置項{}的值=[{}]不滿足密碼密文的長度及大小寫要求(視為明文,不解密),保持不變", KEYNAME_CONSUMER_SECRET, orginSecretKey); } super.setConsumer(pushConsumer); } @Override public void setPullConsumer(PullConsumer pullConsumer) { final String orginSecretKey = pullConsumer.getSecretKey(); // 對密文解密并設(shè)置 if (StringUtils.hasText(orginSecretKey) && orginSecretKey.length() >= 32 ) { // 如果滿足密碼密文的長度及大小寫要求,視為密文,解密 String padStr = SystemSecurityAlgorithm.decryptStr(orginSecretKey); log.debug("連接RocketMQ配置項{}: 解密前orginSecretKey=[{}], 解密后padStr=[{}]", KEYNAME_CONSUMER_SECRET, orginSecretKey, padStr); //為避免密碼泄露,僅debug才輸出明文 log.info("連接RocketMQ配置項{}: 對密文orginSecretKey=[{}]已完成解密", KEYNAME_CONSUMER_SECRET, orginSecretKey); pullConsumer.setSecretKey(padStr); // 由于RocketMQ在構(gòu)建DefaultRocketMQListenerContainer過程中,會從Spring的Environment中獲取配置。 // 附調(diào)用關(guān)系簡要說明如下: // org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer.afterPropertiesSet() // org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer.initRocketMQPushConsumer() // org.apache.rocketmq.spring.support.RocketMQUtil.getRPCHookByAkSk() // org.springframework.core.env.AbstractEnvironment.resolveRequiredPlaceholders() // ...... // org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertyResolver.findPropertyValue() // 因此一并修改環(huán)境中的值,使其能取得新值 modifyEnvironmentValue(springContext.getEnvironment(), KEYNAME_CONSUMER_SECRET, padStr); } else { // 不滿足密碼密文的長度及大小寫要求(視為明文,不解密),保持不變 log.warn("連接RocketMQ配置項{}的值=[{}]不滿足密碼密文的長度及大小寫要求(視為明文,不解密),保持不變", KEYNAME_CONSUMER_SECRET, orginSecretKey); } super.setPullConsumer(pullConsumer); } /** * 對Spring的Environment的配置項的值修改為新值 * @param environment Spring的Environment對象 * @param keyName 配置項名 * @param newValue 新值 */ private void modifyEnvironmentValue(ConfigurableEnvironment environment, final String keyName, String newValue) { if(!environment.containsProperty(keyName)) { log.warn("當前Spring的environment中不存在名為{}的配置項", keyName); return; } if(environment.getProperty(keyName, "").equals(newValue)) { log.debug("當前Spring的environment中配置項{}的值已與新值相同,無需修改", keyName); return; } Map<String, Object> map = new HashMap<>(); //用于存放新值 map.put(keyName, newValue); // 若有map有值,則把該map作為PropertySource加入列表中,以實現(xiàn):把environment中對應key的value覆蓋為新值 // 必須加到First并且不能存在兩個相同的Name的MapPropertySource,值覆蓋才能生效 environment.getPropertySources().addFirst(new MapPropertySource("modifyEnvironmentValue-"+keyName, map)); log.info("已對Spring的Environment的配置項{}的值修改為新值", keyName); } }
B) 適合bootstrap.properties方式
下面以連接Nacos配置中心為例進行說明,需要在本地bootstrap.properties配置文件中指定連接Nacos配置中心的Nacos用戶名、密碼、服務端地址、Data ID等信息。bootstrap.properties配置文件有關(guān)連接Nacos配置中心類似如下:
#Nacos配置中心及注冊中心的authenticate鑒權(quán)用戶名和密碼(需Nacos服務端開啟auth鑒權(quán)) spring.cloud.nacos.username=nacos spring.cloud.nacos.password=760dee29f9fc82af0cc1d6074879dc39 #Nacos配置中心服務端的地址和端口(形式ip:port,ip:port,...) 。注:nacos-client1.x會按順序選其中地址進行連接(前個連接失敗則自動選后一個)。nacos-client2.x會隨機選其中地址進行連接(若連接失敗則自動另選) spring.cloud.nacos.config.server-addr=ip1:8848,ip2:8848,ip3:8848,ip4:8848 #Data ID的前綴(如果不設(shè)置,則默認取 ${spring.application.name}) #spring.cloud.nacos.config.prefix= #默認指定為開發(fā)環(huán)境 #spring.profiles.active= #Nacos命名空間,此處不設(shè)置,保持默認 #spring.cloud.nacos.config.namespace= #配置組(如果不設(shè)置,則默認為DEFAULT_GROUP) spring.cloud.nacos.config.group=G_CONFIG_GJS_SERVICE #指定文件后綴(如果不設(shè)置,則默認為properties) spring.cloud.nacos.config.file-extension=properties #以下為全局Data ID spring.cloud.nacos.config.shared-configs[0].data-id=NacosRegDiscoveryInfo.properties spring.cloud.nacos.config.shared-configs[0].group=G_CONFIG_GJS_GLOBALSHARED spring.cloud.nacos.config.shared-configs[0].refresh=true spring.cloud.nacos.config.shared-configs[1].data-id=XXXXX.properties spring.cloud.nacos.config.shared-configs[1].group=G_CONFIG_GJS_GLOBALSHARED spring.cloud.nacos.config.shared-configs[1].refresh=true spring.cloud.nacos.config.shared-configs[2].data-id=YYYYY.properties spring.cloud.nacos.config.shared-configs[2].group=G_CONFIG_GJS_GLOBALSHARED spring.cloud.nacos.config.shared-configs[2].refresh=true
其中spring.cloud.nacos.password配置項值已經(jīng)設(shè)置為密文。
下面的代碼通過實現(xiàn)EnvironmentPostProcessor接口,來捕獲配置,并將配置新值設(shè)置到Environment中。Java代碼如下:
package 包指定忽略,請自定; import 忽略解密計算工具類SystemSecurityAlgorithm,請自定; import org.apache.commons.logging.Log; import org.springframework.boot.ConfigurableBootstrapContext; import org.springframework.boot.SpringApplication; import org.springframework.boot.env.EnvironmentPostProcessor; import org.springframework.boot.logging.DeferredLogFactory; import org.springframework.core.Ordered; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; import org.springframework.util.StringUtils; import java.util.HashMap; import java.util.Map; /** * 本類通過實現(xiàn)EnvironmentPostProcessor接口,實現(xiàn)在Spring啟動過程中從environment中讀取指定的key值,處理后,然后把environment中對應key的value覆蓋為新值。 * 通過本類已經(jīng)實現(xiàn)對bootstrap階段的配置文件處理: * 因連接Nacos的password不得出現(xiàn)明文,故bootstrap配置文件中為加密密文(加密算法Java類為:SystemSecurityAlgorithm),然后在啟動時通過本類解密 * ----------------------------------------------------------- * 注意: * a) 需要在META-INF下的spring.factories文件中配置本類后,本類才會生效(才被Spring掃描識別到) * b) 因為本類是通過實現(xiàn)EnvironmentPostProcessor接口方式,所以本類在SpringCloud啟動過程中會被調(diào)用兩次: * 首先是在bootstrap配置文件加載后(SpringCloud為支持配置中心的bootstrap階段) * 其次是在application配置文件加載后(SpringBoot的正常啟動時加載配置文件階段) * 機制適用性: * 除了通過實現(xiàn)EnvironmentPostProcessor接口機制,還有通過@Configuration覆蓋原Bean機制。兩種機制適用性說明如下: * bootstrap.properties配置文件(bootstrap階段,還未創(chuàng)建Bean) →→適合→→ 【實現(xiàn)EnvironmentPostProcessor接口機制】 * 本地application.properties配置文件(正常SpringBoot啟動,通過@Configuration注解的Bean) →→適合→→ 【實現(xiàn)EnvironmentPostProcessor接口機制】和【通過@Configuration覆蓋原Bean機制】均可 * 從Nacos等配置中心獲取得到的配置文件 →→適合→→ 【通過@Configuration覆蓋原Bean機制】 * */ public class GjsEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { /** * The default order for the processor. 值越小,優(yōu)先級越高 * 因bootstrap配置文件是通過{@link org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor}完成加載處理 * 由于本EnvironmentPostProcessor類需等待SpringCloud對bootstrap配置文件后才能執(zhí)行,所以本EnvironmentPostProcessor類優(yōu)先級需更低 */ public static final int ORDER = Ordered.HIGHEST_PRECEDENCE + 50; private final DeferredLogFactory logFactory; private final Log logger; public GjsEnvironmentPostProcessor(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext) { this.logFactory = logFactory; this.logger = logFactory.getLog(getClass()); } @Override public int getOrder() { return ORDER; } /** * 從environment中讀取指定的key,并進行解密,解密后的結(jié)果放入map對象中 * @param environment 已經(jīng)有的Spring環(huán)境 * @param keyName 指定的key名 * @param map 若完成解密,則將解密后的結(jié)果放入map對象 */ private void decodePwd(ConfigurableEnvironment environment, String keyName, Map<String, Object> map ) { if(!environment.containsProperty(keyName)) { this.logger.debug("EnvironmentPostProcessor 當前Spring的environment中不存在名為"+keyName+"的配置項"); return; } final String origalValue = environment.getProperty(keyName); // 對密文解密并設(shè)置 if (StringUtils.hasText(origalValue) && origalValue.length() >= 32) { // 如果滿足密碼密文的長度及大小寫要求,視為密文,解密 String padStr = SystemSecurityAlgorithm.decryptStr(origalValue); this.logger.debug("EnvironmentPostProcessor 配置項"+keyName+"原值=["+origalValue+"], 解密后值=["+padStr+"]"); //為避免在日志中密碼泄露,僅debug才輸出明文 this.logger.info("EnvironmentPostProcessor 配置項"+keyName+"原值=["+origalValue+"]已完成解密"); map.put(keyName, padStr); }else { this.logger.warn("EnvironmentPostProcessor 配置項"+keyName+"值=["+origalValue+"]不滿足密碼密文的長度及大小寫要求(視為明文,不解密),保持不變"); } } @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { this.logger.debug("EnvironmentPostProcessor before PropertySources size=" + environment.getPropertySources().size()); this.logger.debug("EnvironmentPostProcessor before PropertySources : " + environment.getPropertySources()); Map<String, Object> map = new HashMap<>(); //用于存放新值 decodePwd(environment, "spring.cloud.nacos.password", map); if(!map.isEmpty()) { // 若有map有值,則把該map作為PropertySource加入列表中,以實現(xiàn):把environment中對應key的value覆蓋為新值 // 必須加到First并且不能存在兩個相同的Name的MapPropertySource,值覆蓋才能生效 environment.getPropertySources().addFirst(new MapPropertySource("afterDecodePassword", map)); } this.logger.debug("EnvironmentPostProcessor after PropertySources size=" + environment.getPropertySources().size()); this.logger.debug("EnvironmentPostProcessor after PropertySources : " + environment.getPropertySources()); } }
四、總結(jié)
通過以上兩種方式,可解決Spring各類配置文件對配置密文的適配和處理。
同時不僅僅用于密文,凡是需對配置文件的內(nèi)容在啟動時進行改變情況都可以按以上方式進行處理。例如啟動時對配置項值中多個IP進行動態(tài)使用等情形。
以上就是Spring配置文件中密碼明文改為密文處理的通用方式的詳細內(nèi)容,更多關(guān)于Spring密碼明文改為密文處理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
如何在SpringBoot 中使用 Druid 數(shù)據(jù)庫連接池
這篇文章主要介紹了SpringBoot 中使用 Druid 數(shù)據(jù)庫連接池的實現(xiàn)步驟,幫助大家更好的理解和學習使用SpringBoot,感興趣的朋友可以了解下2021-03-03優(yōu)雅地在Java應用中實現(xiàn)全局枚舉處理的方法
這篇文章主要給大家介紹了關(guān)于如何優(yōu)雅地在Java應用中實現(xiàn)全局枚舉處理的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧2019-02-02用Java設(shè)計模式中的觀察者模式開發(fā)微信公眾號的例子
這篇文章主要介紹了用Java設(shè)計模式中的觀察者模式開發(fā)微信公眾號的例子,這里Java的微信SDK等部分便不再詳述,只注重關(guān)鍵部分和開發(fā)過程中觀察者模式優(yōu)點的體現(xiàn),需要的朋友可以參考下2016-02-02流讀取導致StringBuilder.toString()亂碼的問題及解決
這篇文章主要介紹了流讀取導致StringBuilder.toString()亂碼的問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-11-11