上周接手了一個微服務(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ù)庫,這就引起了一個很尷尬的問題(下面會詳細(xì)說),還有就是本來測試環(huán)境的請求參數(shù)是一個額外添加的RequestParam,現(xiàn)在沒有這個參數(shù)了,需要根據(jù)接口的具體請求參數(shù)來進(jìn)行判斷,關(guān)鍵部分接口的參數(shù)是在RequestBody.....本著盡量不改動原有代碼的情況下,我決定繼續(xù)使用原來的攔截器,接下來就是遇到了幾個問題。
一、RequestBody丟失問題
第一個問題就是在攔截器我需要獲取用戶請求參數(shù),這里可能會有一個疑問,攔截器不是能夠拿到HttpServletRequest嗎,直接獲取就行了呀,這么想確實沒問題,但是如果是POST請求,且請求參數(shù)在RequestBody中就會出現(xiàn)問題,看下面攔截器的preHandle方法代碼:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info(">>>> requestInterceptor preHandle method start <<<<");
String jsonRequest = HttpRequestUtil.getRequestBody(request);
String requestParam = "";
if (StringUtils.isNotBlank(jsonRequest)) {
Map<String,String> requestMap = new Gson().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(new Gson().toJson(user));
return false;
}
}
return true;
}
上面的代碼中使用一個工具類讀取HttpServletRequest的請求體,代碼如下:
@Slf4j
public class HttpRequestUtil {
public static String getRequestBody(HttpServletRequest request) {
StringBuffer stringBuffer = new StringBuffer();
try (ServletInputStream servletInputStream = request.getInputStream()){
String line = null;
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(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();
}
return stringBuffer.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,代碼如下:
public class CustomRequestWrapper extends HttpServletRequestWrapper {
private byte[] requestBody;
public CustomRequestWrapper(HttpServletRequest request) {
super(request);
requestBody = HttpRequestUtil.getRequestBody(request).getBytes();
}
public byte[] getRequestBody() {
return requestBody;
}
@Override
public ServletInputStream getInputStream() throws IOException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestBody);
ServletInputStream servletInputStream = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
return servletInputStream;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}
這樣成員變量requestBody保存了請求體的內(nèi)容,根據(jù)其構(gòu)造函數(shù)可以看出,其先會調(diào)用父類構(gòu)造,然后將HttpServletRequest的請求體內(nèi)容賦值給成員變量requestBody。這樣似乎應(yīng)該沒問題了,畢竟賦值是調(diào)用父類構(gòu)造之后進(jìn)行的,只要在之后的過程中將自定義的CustomRequestWrapper向后進(jìn)行傳遞就行了,這么說好像沒問題,但是實際上在攔截中沒辦法實現(xiàn)這點,這時候就需要引入一個Filter,因為Filter先執(zhí)行這樣能夠保證在過濾的時候?qū)?code>HttpServletRequest替換成我們自定義的CustomRequestWrapper向后進(jìn)行傳遞。定義一個Filter,代碼如下:
@Slf4j
@Component
@WebFilter(urlPatterns = {"/user/*"},filterName = "customFilter")
public class CustomFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info(">>>> customFilter init <<<<");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
log.info(">>>> customFilter doFilter start <<<<");
CustomRequestWrapper requestWapper = null;
if (servletRequest instanceof HttpServletRequest) {
requestWapper = new CustomRequestWrapper((HttpServletRequest) servletRequest);
}
if (requestWapper != null) {
filterChain.doFilter(requestWapper,servletResponse);
} else {
filterChain.doFilter(servletRequest,servletResponse);
}
}
@Override
public void destroy() {
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


也就是說在自定義的過濾器之后,其實傳遞的都是
CustomRequestWrapper對象。這里需要說明一點,就是自定義的CustomRequestWrapper中必須要重寫getInputStream和getReader這兩個方法(這兩個方法都是返回的請求體),不然依然無法獲取到請求體的內(nèi)容。當(dāng)然我覺得最好參考這個代碼實現(xiàn)。
另外這里還遇到了一個關(guān)于Filter的小問題,根據(jù)自定義的代碼可以看出來我配置的過濾路徑是/user/*,但是實際在啟動日志中卻并不是這樣的,如下:

可見我配置的過濾路徑并沒有生效,依然是過濾
/*所有請求,雖然不影響功能的實現(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})
public class NacosApplication {
public static void main(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")
public class InterceptorConfig implements WebMvcConfigurer {
@Autowired
private RequestInterceptor requestInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
log.info(">>>> registry interceptor start <<<<");
registry.addInterceptor(requestInterceptor).addPathPatterns("/user/**");
}
}
// 攔截器
@Slf4j
@Component
@Profile("dev")
public class RequestInterceptor implements HandlerInterceptor {
@Autowired
private UserRepository userRepository;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info(">>>> requestInterceptor preHandle method start <<<<");
CustomRequestWrapper requestWrapper = new CustomRequestWrapper(request);
String jsonRequest = new String(requestWrapper.getRequestBody(), Charset.forName("UTF-8"));
String requestParam = "";
if (StringUtils.isNotBlank(jsonRequest)) {
Map<String,String> requestMap = new Gson().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(new Gson().toJson(user));
return false;
}
}
return true;
}
}
當(dāng)修改spring.profiles.active=prod時啟動服務(wù),服務(wù)終于可以正常啟動,日志如下:

可見啟動日志中沒有任何和數(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ù),啟動日志如下:

從上圖可以看出啟動過程中輸出了
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)知識點的了解。
最后:自己在微信開了一個個人號:
超超學(xué)堂,都是自己之前寫過的一些文章,另外關(guān)注還有Java免費自學(xué)資料,歡迎大家關(guān)注。
二維碼.jpg
