分享幾個 SpringBoot 實用的小技巧

crossoverJie發表於2018-10-15

前言

最近分享的一些原始碼、框架設計的東西。我發現大家熱情不是特別高,想想大多數應該還是正兒八經寫程式碼的居多;這次就分享一點接地氣的: SpringBoot 使用中的一些小技巧。

算不上多高大上的東西,但都還挺有用。

遮蔽外部依賴

第一個是遮蔽外部依賴,什麼意思呢?

比如大家日常開發時候有沒有這樣的煩惱:

專案是基於 SpringCloud 或者是 dubbo 這樣的分散式服務,你需要依賴許多基礎服務。

比如說某個訂單號的生成、獲取使用者資訊等。

由於服務拆分,這些功能都是在其他應用中以介面的形式提供,單測還好我還可以利用 Mock 把它遮蔽掉。

但如果自己想把應用啟動起來同時把自己相關的程式碼跑一遍呢?

通常有幾種做法:

  • 本地把所有的服務都啟動起來。
  • 把註冊中心換為開發環境,依賴開發環境的服務。
  • 直接把程式碼推送到開發環境自測。

看起來三種都可以,以前我也是這麼幹的。但還是有幾個小問題:

  • 本地啟動有可能服務很多,全部起來電腦能不能撐住還兩說,萬一服務有問題就進行不下去了。
  • 依賴開發環境的前提是網路打通,還有一個問題就是開發環境程式碼很不穩定很大可能會影響你的測試。
  • 推送到開發環境應該是比較靠譜的方案,但如果想除錯只有日誌大法,沒有本地 debug 的效率高效。

那如何解決問題呢?既可以在本地除錯也不用啟動其他服務。

其實也可以利用單測的做法,把其他外部依賴 Mock 掉就行了。

大致的流程分為以下幾步:

  • SpringBoot 啟動之後在 Spring 中找出你需要遮蔽的那個 APIbean(通常情況下這個介面都是交給 Spring 管理的)。
  • 手動從 bean 容器中刪除該 bean
  • 重新建立一個該 API 的物件,只不過是通過 Mock 出來的。
  • 再手動註冊進 bean 容器中。

以下面這段程式碼為例:

    @Override
    public BaseResponse<OrderNoResVO> getUserByHystrix(@RequestBody UserReqVO userReqVO) {

        OrderNoReqVO vo = new OrderNoReqVO();
        vo.setAppId(123L);
        vo.setReqNo(userReqVO.getReqNo());
        BaseResponse<OrderNoResVO> orderNo = orderServiceClient.getOrderNo(vo);
        return orderNo;
    }
複製程式碼

這是一個 SpringCloud 應用。

它依賴於 orderServiceClient 獲取一個訂單號。

其中的 orderServiceClient 就是一個外部 API,也是被 Spring 所管理。

替換原有的 Bean

下一步就是替換原有的 Bean。

@Component
public class OrderMockServiceConfig implements CommandLineRunner {

    private final static Logger logger = LoggerFactory.getLogger(OrderMockServiceConfig.class);

    @Autowired
    private ApplicationContext applicationContext;

    @Value("${excute.env}")
    private String env;

    @Override
    public void run(String... strings) throws Exception {

        // 非本地環境不做處理
        if ("dev".equals(env) || "test".equals(env) || "pro".equals(env)) {
            return;
        }

        DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();

        OrderServiceClient orderServiceClient = defaultListableBeanFactory.getBean(OrderServiceClient.class);
        logger.info("======orderServiceClient {}=====", orderServiceClient.getClass());

        defaultListableBeanFactory.removeBeanDefinition(OrderServiceClient.class.getCanonicalName());

        OrderServiceClient mockOrderApi = PowerMockito.mock(OrderServiceClient.class,
                invocationOnMock -> BaseResponse.createSuccess(DateUtil.getLongTime() + "", "mock orderNo success"));

        defaultListableBeanFactory.registerSingleton(OrderServiceClient.class.getCanonicalName(), mockOrderApi);

        logger.info("======mockOrderApi {}=====", mockOrderApi.getClass());
    }
}
複製程式碼

其中實現了 CommandLineRunner 介面,可以在 Spring 容器初始化完成之後呼叫 run() 方法。

程式碼非常簡單,簡單來說首先判斷下是什麼環境,畢竟除開本地環境其餘的都是需要真正呼叫遠端服務的。

之後就是獲取 bean 然後手動刪除掉。

關鍵的一步:

OrderServiceClient mockOrderApi = PowerMockito.mock(OrderServiceClient.class,
                invocationOnMock -> BaseResponse.createSuccess(DateUtil.getLongTime() + "", "mock orderNo success"));

defaultListableBeanFactory.registerSingleton(OrderServiceClient.class.getCanonicalName(), mockOrderApi);
複製程式碼

建立了一個新的 OrderServiceClient 物件並手動註冊進了 Spring 容器中。

第一段程式碼使用的是 PowerMockito.mock 的 API,他可以建立一個代理物件,讓所有呼叫 OrderServiceClient 的方法都會做預設的返回。

BaseResponse.createSuccess(DateUtil.getLongTime() + "", "mock orderNo success"))
複製程式碼

測試一下,當我們沒有替換時呼叫剛才那個介面並且本地也沒有啟動 OrderService

分享幾個 SpringBoot 實用的小技巧

因為沒有配置 fallback 所以會報錯,表示找不到這個服務。

替換掉 bean 時:

分享幾個 SpringBoot 實用的小技巧

再次請求沒有報錯,並且獲得了我們預設的返回。

分享幾個 SpringBoot 實用的小技巧

通過日誌也會發現 OrderServiceClient 最後已經被 Mock 代理了,並不會去呼叫真正的方法。

配置加密

下一個則是配置加密,這應該算是一個基本功能。

比如我們配置檔案中的一些賬號和密碼,都應該是密文儲存的。

因此這次使用了一個開源元件來實現加密與解密,並且對 SpringBoot 非常友好只需要幾段程式碼即可完成。

  • 首先根據加密密碼將需要加密的配置加密為密文。
  • 替換原本明文儲存的配置。
  • 再使用時進行解密。

使用該包也只需要引入一個依賴即可:

<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>1.14</version>
</dependency>
複製程式碼

同時寫一個單測根據密碼生成密文,密碼也可儲存在配置檔案中:

jasypt.encryptor.password=123456
複製程式碼

接著在單測中生成密文。

    @Autowired
    private StringEncryptor encryptor;

    @Test
    public void getPass() {
        String name = encryptor.encrypt("userName");
        String password = encryptor.encrypt("password");
        System.out.println(name + "----------------");
        System.out.println(password + "----------------");

    }
複製程式碼

之後只需要使用密文就行。

由於我這裡是對資料庫使用者名稱和密碼加密,所以還得有一個解密的過程。

利用 Spring Bean 的一個增強介面即可實現:

@Component
public class DataSourceProcess implements BeanPostProcessor {


    @Autowired
    private StringEncryptor encryptor;

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {

        if (bean instanceof DataSourceProperties){
            DataSourceProperties dataSourceProperties = (DataSourceProperties) bean;
            dataSourceProperties.setUsername(encryptor.decrypt(dataSourceProperties.getUsername())) ;
            dataSourceProperties.setPassword(encryptor.decrypt(dataSourceProperties.getPassword()));
            return dataSourceProperties ;
        }

        return bean;
    }
}
複製程式碼

這樣就可以在真正使用時還原為明文。

同時也可以在啟動命令中配置剛才的密碼:

java -Djasypt.encryptor.password=password -jar target/jasypt-spring-boot-demo-0.0.1-SNAPSHOT.jar
複製程式碼

總結

這樣兩個小技巧就講完了,大家有 SpringBoot 的更多使用技巧歡迎留言討論。

上文的一些例項程式碼可以在這裡找到:

github.com/crossoverJi…

歡迎關注公眾號一起交流:

分享幾個 SpringBoot 實用的小技巧

相關文章