上周接手了一個微服務(wù)遷移的項目,即從原來的騰訊云遷移到阿里云的EDAS平臺,這個項目非常的簡單技術(shù)層面基本是沒有什么可說的,接口也不多,而且都是使用請求阿里獲取一些數(shù)據(jù)。但是隨著和測試以及甲方的對接有了一些新的變化。
本來測試環(huán)境和生產(chǎn)是兩套代碼,區(qū)別在于測試環(huán)境是有數(shù)據(jù)庫的,而生產(chǎn)環(huán)境沒有數(shù)據(jù)庫的(至于為啥生產(chǎn)環(huán)境不能加一個數(shù)據(jù)庫….這個說來話長)。測試環(huán)境需要根據(jù)接口請求參數(shù)來判斷是直接從數(shù)據(jù)庫返回數(shù)據(jù)還是調(diào)用阿里接口獲取數(shù)據(jù)后返回。原來的測試環(huán)境代碼邏輯其實也簡單,就是添加了一個攔截器獲取請求參數(shù),然后根據(jù)這個參數(shù)去查詢數(shù)據(jù)庫,數(shù)據(jù)庫有結(jié)果直接返回,沒有則需要調(diào)用阿里接口獲取結(jié)果并返回。對接之后需求變了,首先測試環(huán)境和生產(chǎn)環(huán)境代碼是一套,生產(chǎn)依然沒有數(shù)據(jù)庫,這就引起了一個很尷尬的問題(下面會詳細說),還有就是本來測試環(huán)境的請求參數(shù)是一個額外添加的RequestParam,現(xiàn)在沒有這個參數(shù)了,需要根據(jù)接口的具體請求參數(shù)來進行判斷,關(guān)鍵部分接口的參數(shù)是在RequestBody…..本著盡量不改動原有代碼的情況下,我決定繼續(xù)使用原來的攔截器,接下來就是遇到了幾個問題。
一、RequestBody丟失問題
第一個問題就是在攔截器我需要獲取用戶請求參數(shù),這里可能會有一個疑問,攔截器不是能夠拿到HttpServletRequest嗎,直接獲取就行了呀,這么想確實沒問題,但是如果是POST請求,且請求參數(shù)在RequestBody中就會出現(xiàn)問題,看下面攔截器的preHandle方法代碼:
@Override
publicboolean?preHandle(HttpServletRequestrequest,?HttpServletResponseresponse,?Object?handler)?throws?Exception?{
log.info(">>>>?requestInterceptor?preHandle?method?start?<<<<");
StringjsonRequest?=?HttpRequestUtil.getRequestBody(request);
StringrequestParam?="";
if(StringUtils.isNotBlank(jsonRequest))?{
Map?requestMap?=newGson().fromJson(jsonRequest,Map.class);
requestParam?=?requestMap?!=null??requestMap.get("username")?:"";
}else{
requestParam?=request.getParameter("username");
}
if(StringUtils.isNotBlank(requestParam))?{
User?user?=?userRepository.findByUsername(requestParam);
if(user?!=null)?{
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(newGson().toJson(user));
returnfalse;
}
}
returntrue;
}
上面的代碼中使用一個工具類讀取HttpServletRequest的請求體,代碼如下:
@Slf4j
publicclassHttpRequestUtil{
publicstaticStringgetRequestBody(HttpServletRequest?request)?{
StringBufferstringBuffer?=newStringBuffer();
try(ServletInputStream?servletInputStream?=?request.getInputStream()){
Stringline?=null;
BufferedReader?bufferedReader?=newBufferedReader(newInputStreamReader(servletInputStream));
while((line?=?bufferedReader.readLine())?!=null)?{
stringBuffer.append(line);
}
}catch(IOException?e)?{
log.error(">>>>?error?occurred?while?get?request?inputStream,?error?message={}?<<<<",e.getMessage());
e.printStackTrace();
}
returnstringBuffer.toString();
}
}
如果在攔截器中獲取到了相應(yīng)的參數(shù)是沒問題的,但是一旦preHandle方法返回true,即將HttpServletRequest向后傳遞,那么就會出現(xiàn)問題:
Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: I/O error while reading input message; nested exception is java.io.IOException: Stream closed
原因就是在攔截器已經(jīng)讀取了請求體中的內(nèi)容,這時候請求的流中已經(jīng)沒有了數(shù)據(jù),開始我只是以為是HttpRequestUtil中關(guān)閉流的問題,后面修改以后還是不行,報錯信息是:
Failed to read HTTP message: org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public org.springframework.http.ResponseEntity
是因為請求體丟失,也就是說HttpServletRequest請求體中的內(nèi)容一旦讀取就不不存在了,所以直接讀取是不行的。后面網(wǎng)上看到一種方案,就是使用一個自定義的包裝類來實現(xiàn),因此自定義一個包裝類CustomRequestWrapper繼承HttpServletRequestWrapper,代碼如下:
publicclassCustomRequestWrapperextendsHttpServletRequestWrapper{
privatebyte[]?requestBody;
publicCustomRequestWrapper(HttpServletRequest?request){
super(request);
requestBody?=?HttpRequestUtil.getRequestBody(request).getBytes();
}
publicbyte[]?getRequestBody()?{
returnrequestBody;
}
@Override
publicServletInputStreamgetInputStream()throwsIOException{
ByteArrayInputStream?byteArrayInputStream?=newByteArrayInputStream(requestBody);
ServletInputStream?servletInputStream?=newServletInputStream()?{
@Override
publicbooleanisFinished(){
returnfalse;
}
@Override
publicbooleanisReady(){
returnfalse;
}
@Override
publicvoidsetReadListener(ReadListener?readListener){
}
@Override
publicintread()throwsIOException{
returnbyteArrayInputStream.read();
}
};
returnservletInputStream;
}
@Override
publicBufferedReadergetReader()throwsIOException{
returnnewBufferedReader(newInputStreamReader(getInputStream()));
}
}
這樣成員變量requestBody保存了請求體的內(nèi)容,根據(jù)其構(gòu)造函數(shù)可以看出,其先會調(diào)用父類構(gòu)造,然后將HttpServletRequest的請求體內(nèi)容賦值給成員變量requestBody。這樣似乎應(yīng)該沒問題了,畢竟賦值是調(diào)用父類構(gòu)造之后進行的,只要在之后的過程中將自定義的CustomRequestWrapper向后進行傳遞就行了,這么說好像沒問題,但是實際上在攔截中沒辦法實現(xiàn)這點,這時候就需要引入一個Filter,因為Filter先執(zhí)行這樣能夠保證在過濾的時候?qū)ttpServletRequest替換成我們自定義的CustomRequestWrapper向后進行傳遞。定義一個Filter,代碼如下:
@Slf4j
@Component
@WebFilter(urlPatterns?=?{"/user/*"},filterName?="customFilter")
publicclassCustomFilterimplementsFilter{
@Override
publicvoidinit(FilterConfig?filterConfig)throwsServletException{
log.info(">>>>?customFilter?init?<<<<");
}
@Override
publicvoiddoFilter(ServletRequest?servletRequest,?ServletResponse?servletResponse,?FilterChain?filterChain)throwsIOException,?ServletException{
log.info(">>>>?customFilter?doFilter?start?<<<<");
CustomRequestWrapper?requestWapper?=null;
if(servletRequestinstanceofHttpServletRequest)?{
requestWapper?=newCustomRequestWrapper((HttpServletRequest)?servletRequest);
}
if(requestWapper?!=null)?{
filterChain.doFilter(requestWapper,servletResponse);
}else{
filterChain.doFilter(servletRequest,servletResponse);
}
}
@Override
publicvoiddestroy(){
log.info(">>>>?customFilter?destroy?<<<<");
}
}
上面的代碼中在自定義Filter執(zhí)行doFilter時判斷ServletRequest是不是一個HttpServletRequest實例,是的話,則創(chuàng)建一個自定義的CustomRequestWrapper對象,并將其向后傳遞,這樣之后的代碼中我們獲取到的HttpServletRequest其實都是一個CustomRequestWrapper對象。
這樣應(yīng)該就沒什么問題了,接下來debug模式重啟項目測試一下,我們現(xiàn)在過濾器看看創(chuàng)建的CustomRequestWrapper和在攔截器中的HttpServletRequest對象是不是一個,見圖-1和圖-2。
圖-1.png
圖-2.png
也就是說在自定義的過濾器之后,其實傳遞的都是CustomRequestWrapper對象。這里需要說明一點,就是自定義的CustomRequestWrapper中必須要重寫getInputStream和getReader這兩個方法(這兩個方法都是返回的請求體),不然依然無法獲取到請求體的內(nèi)容。當(dāng)然我覺得最好參考這個代碼實現(xiàn)。
另外這里還遇到了一個關(guān)于Filter的小問題,根據(jù)自定義的代碼可以看出來我配置的過濾路徑是/user/*,但是實際在啟動日志中卻并不是這樣的,如下:
圖-3.png
可見我配置的過濾路徑并沒有生效,依然是過濾/*所有請求,雖然不影響功能的實現(xiàn),但是我還是覺得還是根據(jù)具體需求來比較好。網(wǎng)上找資料說需要在啟動類使用@ServletComponentScan注解,指定basePackages即自定義Filter的包名或者使用basePackageClasses指定具體的Filter類即可,具體原因尚不清楚。
二、不同環(huán)境下服務(wù)啟動
前面介紹了對接后的變更,這里還有個令人難受的問題,那就是生產(chǎn)環(huán)境和測試環(huán)境不同的問題,測試環(huán)境需要使用數(shù)據(jù)庫,生產(chǎn)沒有數(shù)據(jù)庫。如果直接將現(xiàn)在代碼部署到生產(chǎn)環(huán)境,服務(wù)是無法啟動的,因為在服務(wù)啟動過程會涉及到創(chuàng)建數(shù)據(jù)庫鏈接,但是沒有數(shù)據(jù)源。開始的想法是能不能在測試環(huán)境配置數(shù)據(jù)源,而在生產(chǎn)環(huán)境不配置,但是因為spring boot是自動配置,那么可以禁用自動配置,即在測試環(huán)境使用自定義數(shù)據(jù)源配置,而在生產(chǎn)環(huán)境不指定。先按照生產(chǎn)環(huán)境代碼優(yōu)先的原則,先排除掉所有和數(shù)據(jù)庫相關(guān)的自動配置,比如DataSourceAutoConfiguration和HibernateJpaAutoConfiguration,代碼如下:
@SpringBootApplication(exclude?=?{DataSourceAutoConfiguration.class,?HibernateJpaAutoConfiguration.class})
@EnableDiscoveryClient
@ServletComponentScan(basePackageClasses?=?{CustomFilter.class})
publicclassNacosApplication{
publicstaticvoidmain(String[]?args)?{
SpringApplication.run(NacosApplication.class,?args);
}
}
但是啟動過程依然報錯,因為創(chuàng)建RequestInterceptor時需要依賴UserRepository,但是因為排除了HibernateJpaAutoConfiguration,所以無法創(chuàng)建UserRepository,當(dāng)時想著要不然直接使用JdbcTemplate算了,雖然需要自己寫sql,這樣就不會涉及到JPA的內(nèi)容了。后來想起其實攔截器這部分代碼只在測試環(huán)境使用,那問題就容易解決了,只要自定義攔截器和注冊攔截器的配置類只在測試環(huán)境下創(chuàng)建就可以了,修改自定義攔截器和其注冊配置類,代碼如下:
@Slf4j
@Configuration
@Profile("dev")
publicclassInterceptorConfigimplementsWebMvcConfigurer{
@Autowired
privateRequestInterceptor?requestInterceptor;
@Override
publicvoidaddInterceptors(InterceptorRegistry?registry){
log.info(">>>>?registry?interceptor?start?<<<<");
registry.addInterceptor(requestInterceptor).addPathPatterns("/user/**");
}
}
//?攔截器
@Slf4j
@Component
@Profile("dev")
publicclassRequestInterceptorimplementsHandlerInterceptor{
@Autowired
privateUserRepository?userRepository;
@Override
publicbooleanpreHandle(HttpServletRequest?request,?HttpServletResponse?response,?Object?handler)throwsException{
log.info(">>>>?requestInterceptor?preHandle?method?start?<<<<");
CustomRequestWrapper?requestWrapper?=newCustomRequestWrapper(request);
String?jsonRequest?=newString(requestWrapper.getRequestBody(),?Charset.forName("UTF-8"));
String?requestParam?="";
if(StringUtils.isNotBlank(jsonRequest))?{
Map?requestMap?=newGson().fromJson(jsonRequest,Map.class);
requestParam?=?requestMap?!=null??requestMap.get("username")?:"";
}else{
requestParam?=?request.getParameter("username");
}
if(StringUtils.isNotBlank(requestParam))?{
User?user?=?userRepository.findByUsername(requestParam);
if(user?!=null)?{
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().write(newGson().toJson(user));
returnfalse;
}
}
returntrue;
}
}
當(dāng)修改spring.profiles.active=prod時啟動服務(wù),服務(wù)終于可以正常啟動,日志如下:
圖-4.png
可見啟動日志中沒有任何和數(shù)據(jù)庫相關(guān)的內(nèi)容,測試一下接口也是正常的。
但是改回spring.profiles.active=dev的時候就無法啟動了,因為在啟動類上我們排除了
DataSourceAutoConfiguration和HibernateJpaAutoConfiguration,所以必須修改啟動類上的注解,即將:
(exclude={DataSourceAutoConfiguration.class, HibernateJpaAutoConfiguration.class})去掉。但是生產(chǎn)環(huán)境確實不能有這兩個自動配置項,所以改為在生產(chǎn)環(huán)境配置文件,在
application-prod.properties添加以下配置,排除兩個自動配置項:
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
然后切換到dev環(huán)境啟動服務(wù),啟動日志如下:
圖-5.png
從上圖可以看出啟動過程中輸出了HikariPool和Hibernate相關(guān)日志,且調(diào)用接口也返回了測試數(shù)據(jù),說明整個服務(wù)根據(jù)配置文件切換環(huán)境的功能完成了。
其實在解決這個問題的過程中也有同事建議我使用一個內(nèi)存型的數(shù)據(jù)庫,但是自己對這方面了解的比較少,因此沒有按照他的思路去做,不知道可不可行。另外其實就功能實現(xiàn)上來講我覺得使用JdbcTemplate而不使用JPA應(yīng)該也是可行的,但是我覺得盡量不動原來的代碼比較好,所以還是按照原有方案解決了。
當(dāng)然,實際工作中解決方案可能不止一種,感興趣的話可以嘗試下不同的解決方案,這樣也可以加深對相關(guān)知識點的了解。