前言 開心一刻 一隻被二哈帶偏了的柴犬,我只想弄死隔壁的二哈 what:是什麼 BeanFactoryPostProcessor介面很簡單,只包含一個方法 推薦大家直接去讀它的源碼註釋,說的更詳細、更好理解 簡單來說,BeanFactoryPostProcessor是spring對外提供的介面,用來 ...
前言
開心一刻
一隻被二哈帶偏了的柴犬,我只想弄死隔壁的二哈
what:是什麼
BeanFactoryPostProcessor介面很簡單,只包含一個方法
/** * 通過BeanFactoryPostProcessor,我們自定義修改應用程式上下文中的bean定義 * * 應用上下文能夠在所有的bean定義中自動檢測出BeanFactoryPostProcessor bean, * 併在任何其他bean創建之前應用這些BeanFactoryPostProcessor bean * * BeanFactoryPostProcessor對自定義配置文件非常有用,可以覆蓋應用上下文已經配置了的bean屬性 * * PropertyResourceConfigurer就是BeanFactoryPostProcessor的典型應用 * 將xml文件中的占位符替換成properties文件中相應的key對應的value */ @FunctionalInterface public interface BeanFactoryPostProcessor { /** * 在應用上下文完成了標準的初始化之後,修改其內部的bean工廠 * 將載入所有bean定義,但尚未實例化任何bean. * 我們可以覆蓋或添加bean定義中的屬性,甚至是提前初始化bean */ void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException; }
推薦大家直接去讀它的源碼註釋,說的更詳細、更好理解
簡單來說,BeanFactoryPostProcessor是spring對外提供的介面,用來拓展spring,能夠在spring容器載入了所有bean的信息信息之後、bean實例化之前執行,修改bean的定義屬性;有人可能會問,這有什麼用?大家還記得spring配置文件中的占位符嗎? 我們會在spring配置中配置PropertyPlaceholderConfigurer(繼承PropertyResourceConfigurer)bean來處理占位符, 舉個例子大家就有印象了
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath:mysqldb.properties</value> </list> </property> </bean> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName"value="${jdbc.driverClassName}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}"/> <property name="password"value="${jdbc.password}" /> </bean> </beans>
mysqldb.properties
jdbc.driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://192.168.1.100:3306/mybatis jdbc.username=root jdbc.password=root
PropertyPlaceholderConfigurer類的繼承關係圖
how:怎麼用
怎麼用,這個問題比較簡單,我們實現BeanFactoryPostProcessor介面,然後將將其註冊到spring容器即可,在spring啟動過程中,在常規bean實例化之前,會執行BeanFactoryPostProcessor的postProcessBeanFactory方法(裡面有我們想要的邏輯),完成我們想要的操作;
重點應該是:用來乾什麼
上述占位符的例子是BeanFactoryPostProcessor的應用之一,但這是spring提供的BeanFactoryPostProcessor拓展,不是我們自定義的;實際工作中,自定義BeanFactoryPostProcessor的情況確實少,反正至少我是用的非常少的,但我還是有使用印象的,那就是對敏感信息的解密處理;上述資料庫的連接配置中,用戶名和密碼都是明文配置的,這就存在泄漏風險,還有redis的連接配置、shiro的加密演算法、rabbitmq的連接配置等等,凡是涉及到敏感信息的,都需要進行加密處理,信息安全非常重要
配置的時候以密文配置,在真正用到之前在spring容器中進行解密,然後用解密後的信息進行真正的操作,下麵我就舉個簡單的例子,用BeanFactoryPostProcessor來完整敏感信息的解密
加解密工具類:DecryptUtil.java
package com.lee.app.util; import org.apache.commons.lang3.StringUtils; import sun.misc.BASE64Decoder; import sun.misc.BASE64Encoder; import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import java.security.Key; import java.security.SecureRandom; public class DecryptUtil { private static final String CHARSET = "utf-8"; private static final String ALGORITHM = "AES"; private static final String RANDOM_ALGORITHM = "SHA1PRNG"; public static String aesEncrypt(String content, String key) { if (content == null || key == null) { return null; } Key secretKey = getKey(key); try { Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, secretKey); byte[] p = content.getBytes(CHARSET); byte[] result = cipher.doFinal(p); BASE64Encoder encoder = new BASE64Encoder(); String encoded = encoder.encode(result); return encoded; } catch (Exception e) { throw new RuntimeException(e); } } public static String aesDecrypt(String content, String key) { Key secretKey = getKey(key); try { Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, secretKey); BASE64Decoder decoder = new BASE64Decoder(); byte[] c = decoder.decodeBuffer(content); byte[] result = cipher.doFinal(c); String plainText = new String(result, CHARSET); return plainText; } catch (Exception e) { throw new RuntimeException(e); } } private static Key getKey(String key) { if (StringUtils.isEmpty(key)) { key = "hello!@#$world";// 預設key } try { SecureRandom secureRandom = SecureRandom.getInstance(RANDOM_ALGORITHM); secureRandom.setSeed(key.getBytes()); KeyGenerator generator = KeyGenerator.getInstance(ALGORITHM); generator.init(secureRandom); return generator.generateKey(); } catch (Exception e) { throw new RuntimeException(e); } } public static void main(String[] args) { // key可以隨意取,DecryptConfig中decryptKey與此相同即可 String newUserName= aesEncrypt("root", "hello!@#$world"); // QL34YffNntJi1OWG7zGqVw== System.out.println(newUserName); String originUserName = aesDecrypt(newUserName, "hello!@#$world"); System.out.println(originUserName); String newPassword = aesEncrypt("123456", "hello!@#$world"); // zfF/EU6k4YtzTnKVZ6xddw== System.out.println(newPassword); String orignPassword = aesDecrypt(newPassword, "hello!@#$world"); System.out.println(orignPassword); } }View Code
配置文件:application.yml
server: servlet: context-path: /app port: 8888 spring: #連接池配置 datasource: type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/mybatis?useSSL=false&useUnicode=true&characterEncoding=utf-8 #Enc[:解密標誌首碼,]:解密尾碼標誌,中間內容是需要解密的內容 username: Enc[QL34YffNntJi1OWG7zGqVw==] password: Enc[zfF/EU6k4YtzTnKVZ6xddw==] initial-size: 1 #連接池初始大小 max-active: 20 #連接池中最大的活躍連接數 min-idle: 1 #連接池中最小的活躍連接數 max-wait: 60000 #配置獲取連接等待超時的時間 pool-prepared-statements: true #打開PSCache,並且指定每個連接上PSCache的大小 max-pool-prepared-statement-per-connection-size: 20 validation-query: SELECT 1 FROM DUAL validation-query-timeout: 30000 test-on-borrow: false #是否在獲得連接後檢測其可用性 test-on-return: false #是否在連接放回連接池後檢測其可用性 test-while-idle: true #是否在連接空閑一段時間後檢測其可用性 #mybatis配置 mybatis: type-aliases-package: com.lee.app.entity #config-location: classpath:mybatis/mybatis-config.xml mapper-locations: classpath:mapper/*.xml # pagehelper配置 pagehelper: helperDialect: mysql #分頁合理化,pageNum<=0則查詢第一頁的記錄;pageNum大於總頁數,則查詢最後一頁的記錄 reasonable: true supportMethodsArguments: true params: count=countSql decrypt: prefix: "Enc[" suffix: "]" key: "hello!@#$world"View Code
工程中解密:DecryptConfig.java
package com.lee.app.config; import com.lee.app.util.DecryptUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.env.OriginTrackedMapPropertySource; import org.springframework.context.EnvironmentAware; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.stereotype.Component; import java.util.LinkedHashMap; import java.util.stream.Collectors; import java.util.stream.StreamSupport; /** * 敏感信息的解密 */ @Component public class DecryptConfig implements EnvironmentAware, BeanFactoryPostProcessor { private static final Logger LOGGER = LoggerFactory.getLogger(DecryptConfig.class); private ConfigurableEnvironment environment; private String decryptPrefix = "Enc["; // 解密首碼標誌 預設值 private String decryptSuffix = "]"; // 解密尾碼標誌 預設值 private String decryptKey = "hello!@#$world"; // 解密可以 預設值 @Override public void setEnvironment(Environment environment) { this.environment = (ConfigurableEnvironment) environment; } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { LOGGER.info("敏感信息解密開始....."); MutablePropertySources propSources = environment.getPropertySources(); StreamSupport.stream(propSources.spliterator(), false) .filter(ps -> ps instanceof OriginTrackedMapPropertySource) .collect(Collectors.toList()) .forEach(ps -> convertPropertySource((PropertySource<LinkedHashMap>) ps)); LOGGER.info("敏感信息解密完成....."); } /** * 解密相關屬性 * @param ps */ private void convertPropertySource(PropertySource<LinkedHashMap> ps) { LinkedHashMap source = ps.getSource(); setDecryptProperties(source); source.forEach((k,v) -> { String value = String.valueOf(v); if (!value.startsWith(decryptPrefix) || !value.endsWith(decryptSuffix)) { return; } String cipherText = value.replace(decryptPrefix, "").replace(decryptSuffix, ""); String clearText = DecryptUtil.aesDecrypt(cipherText, decryptKey); source.put(k, clearText); }); } /** * 設置解密屬性 * @param source */ private void setDecryptProperties(LinkedHashMap source) { decryptPrefix = source.get("decrypt.prefix") == null ? decryptPrefix : String.valueOf(source.get("decrypt.prefix")); decryptSuffix = source.get("decrypt.suffix") == null ? decryptSuffix : String.valueOf(source.get("decrypt.suffix")); decryptKey = source.get("decrypt.key") == null ? decryptKey : String.valueOf(source.get("decrypt.key")); } }View Code
主要就是3個文件,DecryptUtil對明文進行加密處理後,得到的值配置到application.yml中,然後工程啟動的時候,DecryptConfig會對密文進行解密,明文信息存到了spring容器,後續操作都是在spring容器的明文上進行的,所以與我們平時的不加密的結果一致,但是卻對敏感信息進行了保護;工程測試結果如下:
完整工程地址:spring-boot-BeanFactoryPostProcessor
有興趣的可以去看下jasypt-spring-boot的源碼,你會發現他的原理是一樣的,也是基於BeanFactoryPostProcessor的拓展
why:為什麼能這麼用
為什麼DecryptConfig實現了BeanFactoryPostProcessor,將DecryptConfig註冊到spring之後,DecryptConfig的postProcessBeanFactory方法就會執行?事出必有因,肯定是spring啟動過程中會調用DecryptConfig實例的postProcessBeanFactory方法,具體我們來看看源碼,我們從AbstractApplicationContext的refresh方法開始
不得不說,spring的命名、註釋確實寫得好,很明顯我們從refresh中的invokeBeanFactoryPostProcessors方法開始,大家可以仔細看下PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors方法,先按PriorityOrdered、Ordered、普通(沒有實現PriorityOrdered和Ordered介面)的順序調用BeanDefinitionRegistryPostProcessor,然後再按先按PriorityOrdered、Ordered、普通的順序調用BeanFactoryPostProcessor,這個順序還是值得大家註意下的,如果我們自定義的多個BeanFactoryPostProcessor有順序之分,而我們又沒有指定其執行順序,那麼可能出現的不是我們想要的結果
這裡可能會有會有人有這樣的的疑問:bean定義(BeanDefinition)是在什麼時候載入到spring容器的,如何保證BeanFactoryPostProcessor實例起作用之前,所有的bean定義都已經載入到了spring容器
ConfigurationClassPostProcessor實現了BeanDefinitionRegistryPostProcessor,在springboot的createApplicationContext階段註冊到spring容器的,也就是說在spring的refresh之前就有了ConfigurationClassPostProcessor實例;ConfigurationClassPostProcessor被應用的時候(調用其postProcessBeanDefinitionRegistry方法),會載入全部的bean定義(包括我們自定義的BeanFactoryPostProcessor實例:DecryptConfig)到spring容器,bean的載入詳情可查看:springboot2.0.3源碼篇 - 自動配置的實現,是你想象中的那樣嗎,那麼在應用BeanFactoryPostProcessor實例之前,所有的bean定義就已經載入到spring容器了,BeanFactoryPostProcessor實例也就能修改bean定義了
至此,BeanFactoryPostProcessor的機制我們就清楚了,為什麼能這麼用這個問題也就明瞭了
總結
1、BeanFactoryPostProcessor是beanFactory的後置處理器介面,通過BeanFactoryPostProcessor,我們可以自定義spring容器中的bean定義,BeanFactoryPostProcessor是在spring容器載入了bean的定義信息之後、bean實例化之前執行;
2、BeanFactoryPostProcessor類型的bean會被spring自動檢測,在常規bean實例化之前被spring調用;
3、BeanFactoryPostProcessor的常用場景包括spring中占位符的處理、我們自定義的敏感信息的解密處理,當然不局限與此;
其實只要我們明白了BeanFactoryPostProcessor的生效時機,哪些場景適用BeanFactoryPostProcessor也就很清楚了