Poplar是一個(gè)社交主題的內(nèi)容社區(qū),但自身并不做社區(qū)爆办,旨在提供可快速二次開(kāi)發(fā)的開(kāi)源基礎(chǔ)套件难咕。前端基于React Native與Redux構(gòu)建,后端由Spring Boot、Dubbo余佃、Zookeeper組成微服務(wù)對(duì)外提供一致的API訪問(wèn)暮刃。
https://github.com/lvwangbeta/Poplar
前端React Native & Redux
React Native雖然提供跨平臺(tái)解決方案,但并未在性能與開(kāi)發(fā)效率上做出過(guò)度妥協(xié)爆土,尤其是對(duì)于有JS與CSS基礎(chǔ)的開(kāi)發(fā)人員入手不會(huì)很難椭懊,不過(guò)JSX語(yǔ)法糖需要一定的適應(yīng)時(shí)間,至于DOM結(jié)構(gòu)與樣式和JS處理寫(xiě)在一起是否喜歡就見(jiàn)仁見(jiàn)智了雾消,可這也是一個(gè)強(qiáng)迫你去模塊化解耦的比較好的方式灾搏。由于React組件的數(shù)據(jù)流是單向的,因此會(huì)引入一個(gè)很麻煩的問(wèn)題立润,組件之間很難高效通信狂窑,尤其是兩個(gè)層級(jí)很深的兄弟節(jié)點(diǎn)之間通信變得異常復(fù)雜,對(duì)上游所有父節(jié)點(diǎn)造成傳遞污染桑腮,維護(hù)成本極高泉哈。為此Poplar引入了Redux架構(gòu),統(tǒng)一管理應(yīng)用狀態(tài)破讨。
模塊化
APP由5個(gè)基礎(chǔ)頁(yè)面構(gòu)成丛晦,分別是Feed信息流主頁(yè)(MainPage)、探索發(fā)現(xiàn)頁(yè)面(ExplorePage)提陶、我的賬戶詳情頁(yè)(MinePage)烫沙、狀態(tài)創(chuàng)建于發(fā)送頁(yè)(NewFeed)、登錄注冊(cè)頁(yè)面(LoginRegPage)等隙笆。頁(yè)面又由基礎(chǔ)組件組成锌蓄,如Feed列表、Feed詳情撑柔、評(píng)論瘸爽、標(biāo)簽、相冊(cè)等等铅忿。如果與服務(wù)器交互剪决,則統(tǒng)一交由API層處理。
頁(yè)面底部由TabNavigator
包含5個(gè)TabNavigator.Item
構(gòu)成檀训,分別對(duì)應(yīng)基礎(chǔ)頁(yè)面柑潦,如果用戶未登錄,則在點(diǎn)擊主頁(yè)或新增Tab時(shí)呼出登錄注冊(cè)頁(yè)面肢扯。
Redux
引入Redux并不是趕潮流妒茬,而且早在2014年就已經(jīng)提出了Flux的概念。使用Redux主要是不得不用了蔚晨,Poplar組件結(jié)構(gòu)并非特別復(fù)雜,但嵌套關(guān)系較多,而且需要同時(shí)支持登錄與非登錄情況的信息流訪問(wèn)铭腕,這就需要一個(gè)統(tǒng)一的狀態(tài)管理器來(lái)協(xié)調(diào)組件之間的通信和狀態(tài)更新银择,而Redux很好的解決了這個(gè)問(wèn)題。
這里不枯燥的講解Redux的架構(gòu)模型了累舷,而是以Poplar中的登錄狀態(tài)為例來(lái)簡(jiǎn)單說(shuō)下Redux在Poplar項(xiàng)目中是如何使用的浩考。
Poplar使用React-Redux庫(kù),一個(gè)將Redux架構(gòu)在React的實(shí)現(xiàn)被盈。
1. 場(chǎng)景描述
在未登錄情況下析孽,如果用戶點(diǎn)擊Feed流頁(yè)面會(huì)彈出登錄/注冊(cè)頁(yè)面,登錄或注冊(cè)成功之后頁(yè)面收回只怎,同時(shí)刷新出信息流內(nèi)容袜瞬。下圖中的App組件是登錄頁(yè)面和信息流主頁(yè)兄弟節(jié)點(diǎn)的共同父組件。
這個(gè)需求看似簡(jiǎn)單身堡,但如果沒(méi)有Redux邓尤,在React中實(shí)現(xiàn)起來(lái)會(huì)很蹩腳而且會(huì)冗余很多無(wú)用代碼調(diào)用。
首先我們看下在沒(méi)有Redux的情況下是如何實(shí)現(xiàn)這一業(yè)務(wù)流程的贴谎?
在點(diǎn)擊Tabbar的第一個(gè)Item也就是信息流頁(yè)簽時(shí)汞扎,要做用戶是否登錄檢查,這個(gè)檢查可以通過(guò)查看應(yīng)用是否本地化存儲(chǔ)了token或其他驗(yàn)簽方式驗(yàn)證擅这,如果未登錄澈魄,需要主動(dòng)更新App組件的state狀態(tài),同時(shí)將這個(gè)狀態(tài)修改通過(guò)props的方式傳遞給LoginPage仲翎,LoginPage得知有新的props傳入后更新自己的state:{visible:true}來(lái)呼出自己痹扇,如果客戶輸入登錄信息并且登錄成功,則需要將LoginPage的state設(shè)置為{visible:false}來(lái)隱藏自己谭确,同時(shí)回調(diào)App傳給它的回調(diào)函數(shù)來(lái)告訴父附件用戶已經(jīng)登錄成功帘营,我們算一下這僅僅是兩個(gè)組件之間的通信就要消耗1個(gè)props變量1個(gè)props回調(diào)函數(shù)和2個(gè)state更新,到這里只是完成了LoginPage通知App組件目前應(yīng)用應(yīng)該處于已登錄狀態(tài)逐哈,但是還沒(méi)有刷新出用戶的Feed流芬迄,因?yàn)榇藭r(shí)MainPage還不知道用戶已登錄,需要App父組件來(lái)告知它已登錄請(qǐng)刷新昂秃,可怎樣通知呢禀梳?React是數(shù)據(jù)流單向的,要想讓下層組件更新只能傳遞變化的props屬性肠骆,這樣就又多了一個(gè)props屬性的開(kāi)銷算途,MainPage更新關(guān)聯(lián)的state同時(shí)刷新自己獲取Feed流,這才最終完成了一次登錄后的MainPage信息展示蚀腿。通過(guò)上面的分析可以看出Poplar在由未登錄到登錄的狀態(tài)轉(zhuǎn)變時(shí)冗余了很多但是又沒(méi)法避免的參數(shù)傳遞嘴瓤,因?yàn)樾值芄?jié)點(diǎn)LoginPage與MainPage之間無(wú)法簡(jiǎn)單的完成通信告知彼此的狀態(tài)扫外,就需要App父組件這個(gè)橋梁來(lái)先向上再向下的傳遞消息。
再來(lái)看下引入Redux之后是如何完成這一同樣的過(guò)程的:
還是在未登錄情況下點(diǎn)擊主頁(yè)廓脆,此時(shí)Poplar由于Redux的引入已經(jīng)為應(yīng)用初始了全局登錄狀態(tài){status: 'NOT_LOGGED_IN'}筛谚,當(dāng)用戶登錄成功之后會(huì)將該狀態(tài)更新為{status: 'LOGGED_IN'},同時(shí)LoginPage與此狀態(tài)進(jìn)行了綁定停忿,Redux會(huì)第一時(shí)間通知其更新組件自己的狀態(tài)為{visible:false}驾讲。與此同時(shí)App也綁定了這個(gè)由Redux管理的全局狀態(tài),因此也同樣可以獲得{status: 'LOGGED_IN'}的通知席赂,這樣就可以很簡(jiǎn)單的在客戶登錄之后隱藏LoginPage顯示MainPage吮铭,是不是很簡(jiǎn)單也很神奇,完全不用依賴參數(shù)的層層傳遞颅停,組件想要獲得哪個(gè)全局狀態(tài)就與其關(guān)聯(lián)就好谓晌,Redux會(huì)第一時(shí)間通知你。
2. 實(shí)現(xiàn)
以實(shí)際的代碼為例來(lái)講解下次場(chǎng)景的React-Redux實(shí)現(xiàn):
connect
在App組件中便监,通過(guò)connect方法將UI組件生成Redux容器組件扎谎,可以理解為架起了UI組件與Redux溝通的橋梁,將store于組件關(guān)聯(lián)在一起烧董。
import {showLoginPage, isLogin} from './actions/loginAction';
import {showNewFeedPage} from './actions/NewFeedAction';
export default connect((state) => ({
status: state.isLogin.status, //登錄狀態(tài)
loginPageVisible: state.showLoginPage.loginPageVisible
}), (dispatch) => ({
isLogin: () => dispatch(isLogin()),
showLoginPage: () => dispatch(showLoginPage()),
showNewFeedPage: () => dispatch(showNewFeedPage()),
}))(App)
connect方法的第一個(gè)參數(shù)是mapStateToProps
函數(shù)毁靶,建立一個(gè)store中的數(shù)據(jù)到UI組件props對(duì)象的映射關(guān)系,只要store更新了就會(huì)調(diào)用mapStateToProps
方法逊移,mapStateToProps
返回一個(gè)對(duì)象预吆,是一個(gè)UI組件props與store數(shù)據(jù)的映射。上面代碼中胳泉,mapStateToProps
接收state作為參數(shù)拐叉,返回一個(gè)UI組件登陸狀態(tài)與store中state的登陸狀態(tài)的映射關(guān)系以及一個(gè)登陸頁(yè)面是否顯示的映射關(guān)系。這樣App組件狀態(tài)就與Redux的store關(guān)聯(lián)上了扇商。
第二個(gè)參數(shù)mapDispatchToProps
函數(shù)允許將action作為props綁定到組件上凤瘦,返回一個(gè)UI組件props與Redux action的映射關(guān)系,上面代碼中App組件的isLogin
showLoginPage
showNewFeedPage
props與Redux的action建立了映射關(guān)系案铺。調(diào)用isLogin實(shí)際調(diào)用的是Redux中的store.dispatch(isLogin)
action蔬芥,dispatch完成對(duì)action到reducer的分發(fā)。
Provider
connect中的state是如何傳遞進(jìn)去的呢控汉?React-Redux 提供Provider
組件笔诵,可以讓容器組件拿到state
import React, { Component } from 'react';
import { Provider } from 'react-redux';
import configureStore from './src/store/index';
const store = configureStore();
export default class Root extends Component {
render() {
return (
<Provider store={store}>
<Main />
</Provider>
)
}
}
上面代碼中,Provider
在根組件外面包了一層姑子,這樣一來(lái)乎婿,App
的所有子組件就默認(rèn)都可以拿到state
了。
Action & Reducer
組件與Redux全局狀態(tài)的關(guān)聯(lián)已經(jīng)搞定了街佑,可如何實(shí)現(xiàn)狀態(tài)的流轉(zhuǎn)呢谢翎?登錄狀態(tài)是如何擴(kuò)散到整個(gè)應(yīng)用的呢捍靠?
這里就需要Redux中的Action和Reducer了,Action負(fù)責(zé)接收UI組件的事件岳服,Reducer負(fù)責(zé)響應(yīng)Action剂公,返回新的store希俩,觸發(fā)與store關(guān)聯(lián)的UI組件更新吊宋。
export default connect((state) => ({
loginPageVisible: state.showLoginPage.loginPageVisible,
}), (dispatch) => ({
isLogin: () => dispatch(isLogin()),
showLoginPage: (flag) => dispatch(showLoginPage(flag)),
showRegPage: (flag) => dispatch(showRegPage(flag)),
}))(LoginPage)
this.props.showLoginPage(false);
this.props.isLogin();
在這個(gè)登錄場(chǎng)景中,如上代碼颜武,LoginPage將自己的props與store和action綁定璃搜,如果登錄成功,調(diào)用showLoginPage(false)
action來(lái)隱藏自身鳞上,Reducer收到這個(gè)dispatch過(guò)來(lái)的action更新store狀態(tài):
//Action
export function showLoginPage(flag=true) {
if(flag == true) {
return {
type: 'LOGIN_PAGE_VISIBLE'
}
} else {
return {
type: 'LOGIN_PAGE_INVISIBLE'
}
}
}
//Reducer
export function showLoginPage(state=pageState, action) {
switch (action.type) {
case 'LOGIN_PAGE_VISIBLE':
return {
...state,
loginPageVisible: true,
}
break;
case 'LOGIN_PAGE_INVISIBLE':
return {
...state,
loginPageVisible: false,
}
break;
default:
return state;
}
}
同時(shí)調(diào)用isLogin這個(gè)action更新應(yīng)用的全局狀態(tài)為已登錄:
//Action
export function isLogin() {
return dispatch => {
Secret.isLogin((result, token) => {
if(result) {
dispatch({
type: 'LOGGED_IN',
});
} else {
dispatch({
type: 'NOT_LOGGED_IN',
});
}
});
}
}
//Reducer
export function isLogin(state=loginStatus, action) {
switch (action.type) {
case 'LOGGED_IN':
return {
...state,
status: 'LOGGED_IN',
}
break;
case 'NOT_LOGGED_IN':
return {
...state,
status: 'NOT_LOGGED_IN',
}
break;
default:
return state;
}
}
App組件由于已經(jīng)關(guān)聯(lián)了這個(gè)全局的登錄狀態(tài)这吻,在reducer更新了此狀態(tài)之后,App也會(huì)收到該更新篙议,進(jìn)而重新渲染自己唾糯,此時(shí)MainPage就會(huì)渲染出來(lái)了:
const {status} = this.props;
return (
<TabNavigator>
<TabNavigator.Item
selected={this.state.selectedTab === 'mainTab'}
renderIcon={() => <Image style={styles.icon}
source={require('./imgs/icons/home.png')} />}
renderSelectedIcon={() => <Image style={styles.icon}
source={require('./imgs/icons/home_selected.png')} />}
onPress={() => {
this.setState({ selectedTab: 'mainTab' });
if(status == 'NOT_LOGGED_IN') {
showLoginPage();
}
}
}
>
//全局狀態(tài)已由NOT_LOGGED_IN變?yōu)長(zhǎng)OGGED_IN
{status == 'NOT_LOGGED_IN'?<LoginPage {...this.props}/>:<MainPage {...this.props}/>}
后端微服務(wù)架構(gòu)
項(xiàng)目構(gòu)建 & 開(kāi)發(fā)
1. 項(xiàng)目結(jié)構(gòu)
poplar作為一個(gè)整體Maven項(xiàng)目,頂層不具備業(yè)務(wù)功能也不包含代碼鬼贱,對(duì)下層提供基礎(chǔ)的pom依賴導(dǎo)入
poplar-api有著兩重身份:API網(wǎng)關(guān)接收渠道層請(qǐng)求路由轉(zhuǎn)發(fā)移怯、作為微服務(wù)消費(fèi)者組織提供者服務(wù)調(diào)用完成服務(wù)串聯(lián)
poplar-user-service: 微服務(wù)提供者,提供注冊(cè)这难、登錄舟误、用戶管理等服務(wù)
poplar-feed-service: 微服務(wù)提供者,提供feed創(chuàng)建姻乓、生成信息流等服務(wù)
poplar-notice-service: 微服務(wù)提供者嵌溢, 提供通知消息服務(wù)
每個(gè)子項(xiàng)目以Module方式單獨(dú)創(chuàng)建
2. Maven聚合項(xiàng)目
Poplar由多個(gè)服務(wù)提供者、消費(fèi)者和公共組件構(gòu)成蹋岩,他們之間的依賴關(guān)系既有關(guān)聯(lián)關(guān)系又有父子從屬關(guān)系赖草,
為了簡(jiǎn)化配置也便于統(tǒng)一構(gòu)建,需要建立合理的依賴剪个。服務(wù)的提供者主要是Spring Boot項(xiàng)目秧骑,兼有數(shù)據(jù)庫(kù)訪問(wèn)等依賴;服務(wù)的消費(fèi)者同樣是是Spring Boot項(xiàng)目禁偎,但由于是API層腿堤,需要對(duì)外提供接口,所以需要支持Controller如暖;
服務(wù)消費(fèi)者笆檀、提供者通過(guò)Dubbo完成調(diào)用,這也需要共用的Dubbo組件盒至,所以我們可以發(fā)現(xiàn)消費(fèi)者酗洒、提供者共同依賴Spring Boot以及Dubbo士修,抽離出一個(gè)parent的pom即可,定義公共的父組件:
<groupId>com.lvwangbeta</groupId>
<artifactId>poplar</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>poplar</name>
<description>Poplar</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
...
</dependencies>
Poplar父組件除了引入公共的構(gòu)建包之外樱衷,還需要聲明其包含的子組件棋嘲,這樣做的原因是在Poplar頂層構(gòu)建的時(shí)候Maven可以在反應(yīng)堆計(jì)算出各模塊之間的依賴關(guān)系和構(gòu)建順序。我們引入服務(wù)提供者和消費(fèi)者:
<modules>
<module>poplar-common</module>
<module>poplar-api</module>
<module>poplar-feed-service</module>
<module>poplar-user-service</module>
</modules>
子組件的pom結(jié)構(gòu)就變的簡(jiǎn)單許多了矩桂,指定parent即可沸移,pom源為父組件的相對(duì)路徑
<groupId>com.lvwangbeta</groupId>
<artifactId>poplar-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>poplar-api</name>
<description>poplar api</description>
<parent>
<groupId>com.lvwangbeta</groupId>
<artifactId>poplar</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
</parent>
還有一個(gè)公共構(gòu)建包我們并沒(méi)有說(shuō),它主要包含了消費(fèi)者侄榴、提供者共用的接口雹锣、model、Utils方法等癞蚕,不需要依賴Spring也沒(méi)有數(shù)據(jù)庫(kù)訪問(wèn)的需求蕊爵,這是一個(gè)被其他項(xiàng)目引用的公共組件,我們把它聲明為一個(gè)package方式為jar的本地包即可桦山,不需要依賴parent:
<groupId>com.lvwangbeta</groupId>
<artifactId>poplar-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
在項(xiàng)目整體打包的時(shí)候攒射,Maven會(huì)計(jì)算出其他子項(xiàng)目依賴了這個(gè)本地jar包就會(huì)優(yōu)先將其打入本地Maven庫(kù)。
在Poplar項(xiàng)目根目錄執(zhí)行mvn clean install
查看構(gòu)建順序恒水,可以看到各子項(xiàng)目并不是按照我們?cè)赑oplar-pom中定義的那樣順序執(zhí)行的会放,而是Maven反應(yīng)堆計(jì)算各模塊的先后依賴來(lái)執(zhí)行構(gòu)建,先構(gòu)建公共依賴common包然后構(gòu)建poplar寇窑,最后構(gòu)建各消費(fèi)者鸦概、提供者。
[INFO] Reactor Summary:
[INFO]
[INFO] poplar-common ...................................... SUCCESS [ 3.341 s]
[INFO] poplar ............................................. SUCCESS [ 3.034 s]
[INFO] poplar-api ......................................... SUCCESS [ 25.028 s]
[INFO] poplar-feed-service ................................ SUCCESS [ 6.451 s]
[INFO] poplar-user-service ................................ SUCCESS [ 8.056 s]
[INFO] ------------------------------------------------------------------
如果我們只修改了某幾個(gè)子項(xiàng)目甩骏,并不需要全量構(gòu)建窗市,只需要用Maven的-pl選項(xiàng)指定項(xiàng)目同時(shí)-am構(gòu)建其依賴的模塊即可,我們嘗試單獨(dú)構(gòu)建poplar-api
這個(gè)項(xiàng)目饮笛,其依賴于poplar-common
和poplar
:
mvn clean install -pl poplar-api -am
執(zhí)行構(gòu)建發(fā)現(xiàn)Maven將poplar-api
依賴的poplar-common
和poplar
優(yōu)先構(gòu)建之后再構(gòu)建自己:
[INFO] Reactor Summary:
[INFO]
[INFO] poplar-common ...................................... SUCCESS [ 2.536 s]
[INFO] poplar ............................................. SUCCESS [ 1.756 s]
[INFO] poplar-api ......................................... SUCCESS [ 28.101 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
3. Dubbo & Zookeeper
上面所述的服務(wù)提供者和消費(fèi)者依托于Dubbo實(shí)現(xiàn)遠(yuǎn)程調(diào)用咨察,但還需要一個(gè)注冊(cè)中心,來(lái)完成服務(wù)提供者的注冊(cè)福青、通知服務(wù)消費(fèi)者的任務(wù)摄狱,Zookeeper就是一種注冊(cè)中心的實(shí)現(xiàn),poplar使用Zookeeper作為注冊(cè)中心无午。
3.1 Zookeeper安裝
下載解壓Zookeeper文件
$ cd zookeeper-3.4.6
$ mkdir data
創(chuàng)建配置文件
$ vim conf/zoo.cfg
tickTime = 2000
dataDir = /path/to/zookeeper/data
clientPort = 2181
initLimit = 5
syncLimit = 2
啟動(dòng)
$ bin/zkServer.sh start
停止
$ bin/zkServer.sh stop
3.2 Dubbo admin
Dubbo管理控制臺(tái)安裝
git clone https://github.com/apache/incubator-dubbo-ops
cd incubator-dubbo-ops && mvn package
然后就可以在target目錄下看到打包好的war包了媒役,將其解壓到tomcat webapps/ROOT
目錄下(ROOT目錄內(nèi)容要提前清空),可以查看下解壓后的dubbo.properties
文件宪迟,指定了注冊(cè)中心Zookeeper的IP和端口
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.admin.root.password=root
dubbo.admin.guest.password=guest
啟動(dòng)tomcat
./bin/startup.sh
訪問(wèn)
http://127.0.0.1:8080/
這樣Dubbo就完成了對(duì)注冊(cè)中心的監(jiān)控設(shè)置
4. 開(kāi)發(fā)
微服務(wù)的提供者和消費(fèi)者開(kāi)發(fā)模式與以往的單體架構(gòu)應(yīng)用雖有不同酣衷,但邏輯關(guān)系大同小異,只是引入了注冊(cè)中心需要消費(fèi)者和提供者配合實(shí)現(xiàn)一次請(qǐng)求次泽,這就必然需要在兩者之間協(xié)商接口和模型穿仪,保證調(diào)用的可用席爽。
文檔以用戶注冊(cè)為例展示從渠道調(diào)用到服務(wù)提供者、消費(fèi)者和公共模塊發(fā)布的完整開(kāi)發(fā)流程啊片。
4.1 公共
poplar-common作為公共模塊定義了消費(fèi)者和提供者都依賴的接口和模型只锻, 微服務(wù)發(fā)布時(shí)才可以被正常訪問(wèn)到
定義用戶服務(wù)接口
public interface UserService {
String register(String username, String email, String password);
}
4.2 服務(wù)提供者
UserServiceImpl實(shí)現(xiàn)了poplar-common中定義的UserService接口
@Service
public class UserServiceImpl implements UserService {
@Autowired
@Qualifier("userDao")
private UserDAO userDao;
public String register(String username, String email, String password){
if(email == null || email.length() <= 0)
return Property.ERROR_EMAIL_EMPTY;
if(!ValidateEmail(email))
return Property.ERROR_EMAIL_FORMAT;
...
}
可以看到這就是單純的Spring Boot Service
寫(xiě)法,但是@Service
注解一定要引入Dubbo包下的紫谷,才可以讓Dubbo掃描到該Service完成向Zookeeper注冊(cè):
dubbo.scan.basePackages = com.lvwangbeta.poplar.user.service
dubbo.application.id=poplar-user-service
dubbo.application.name=poplar-user-service
dubbo.registry.address=zookeeper://127.0.0.1:2181
dubbo.protocol.id=dubbo
dubbo.protocol.name=dubbo
dubbo.protocol.port=9001
4.3 服務(wù)消費(fèi)者
前面已經(jīng)說(shuō)過(guò)齐饮,poplar-api作為API網(wǎng)關(guān)的同時(shí)還是服務(wù)消費(fèi)者,組織提供者調(diào)用關(guān)系碴里,完成請(qǐng)求鏈路沈矿。
API層使用@Reference
注解來(lái)向注冊(cè)中心請(qǐng)求服務(wù),通過(guò)定義在poplar-common模塊中的UserService接口實(shí)現(xiàn)與服務(wù)提供者RPC通信
@RestController
@RequestMapping("/user")
public class UserController {
@Reference
private UserService userService;
@ResponseBody
@RequestMapping("/register")
public Message register(String username, String email, String password) {
Message message = new Message();
String errno = userService.register(username, email, password);
message.setErrno(errno);
return message;
}
}
application.properties
配置
dubbo.scan.basePackages = com.lvwangbeta.poplar.api.controller
dubbo.application.id=poplar-api
dubbo.application.name=poplar-api
dubbo.registry.address=zookeeper://127.0.0.1:2181
5.服務(wù)Docker化
如果以上步驟都已做完咬腋,一個(gè)完整的微服務(wù)架構(gòu)基本已搭建完成,可以開(kāi)始coding業(yè)務(wù)代碼了睡互,為什么還要再做Docker化改造根竿?首先隨著業(yè)務(wù)的復(fù)雜度增高,可能會(huì)引入新的微服務(wù)模塊就珠,在開(kāi)發(fā)新模塊的同時(shí)提供一個(gè)穩(wěn)定的外圍環(huán)境還是很有必要的寇壳,如果測(cè)試環(huán)境不理想,可以自己?jiǎn)?dòng)必要的docker容器妻怎,節(jié)省編譯時(shí)間壳炎;另外減少環(huán)境遷移帶來(lái)的程序運(yùn)行穩(wěn)定性問(wèn)題,便于測(cè)試逼侦、部署匿辩,為持續(xù)集成提供更便捷、高效的部署方式榛丢。
在poplar根目錄執(zhí)行 build.sh
可實(shí)現(xiàn)poplar包含的所有微服務(wù)模塊的Docker化和一鍵啟動(dòng):
cd poplar && ./build.sh
如果你有耐心铲球,可看下如下兩個(gè)小章節(jié),是如何實(shí)現(xiàn)的
5.1 構(gòu)建鏡像
Poplar采用了將各微服務(wù)與數(shù)據(jù)庫(kù)晰赞、注冊(cè)中心單獨(dú)Docker化的部署模式稼病,其中poplar-dubbo-admin
是dubbo管理控制臺(tái),poplar-api
poplar-tag-service
poplar-action-service
poplar-feed-service
poplar-user-service
是具體的服務(wù)化業(yè)務(wù)層模塊掖鱼,poplar-redis
poplar-mysql
提供緩存與持久化數(shù)據(jù)支持然走,poplar-zookeeper
為Zookeeper注冊(cè)中心
poplar-dubbo-admin
poplar-api
poplar-tag-service
poplar-action-service
poplar-feed-service
poplar-user-service
poplar-redis
poplar-mysql
poplar-zookeeper
poplar-api
poplar-tag-service
poplar-action-service
poplar-feed-service
poplar-user-service
業(yè)務(wù)層模塊可以在pom.xml
中配置docker-maven-plugin
插件構(gòu)建,在configuration中指定工作目錄戏挡、基礎(chǔ)鏡像等信息可省去Dockerfile:
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<version>1.0.0</version>
<configuration>
<imageName>lvwangbeta/poplar</imageName>
<baseImage>java</baseImage>
<maintainer>lvwangbeta lvwangbeta@163.com</maintainer>
<workdir>/poplardir</workdir>
<cmd>["java", "-version"]</cmd>
<entryPoint>["java", "-jar", "${project.build.finalName}.jar"]</entryPoint>
<skipDockerBuild>false</skipDockerBuild>
<resources>
<resource>
<targetPath>/poplardir</targetPath>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
</configuration>
</plugin>
如果想讓某個(gè)子項(xiàng)目不執(zhí)行docker構(gòu)建芍瑞,可設(shè)置子項(xiàng)目pom.xml的skipDockerBuild
為true
,如poplar-common
為公共依賴包增拥,不需要單獨(dú)打包成獨(dú)立鏡像:
<skipDockerBuild>true</skipDockerBuild>
在poplar項(xiàng)目根目錄執(zhí)行如下命令啄巧,完成整個(gè)項(xiàng)目的業(yè)務(wù)層構(gòu)建:
mvn package -Pdocker -Dmaven.test.skip=true docker:build
[INFO] Building image lvwangbeta/poplar-user-service
Step 1/6 : FROM java
---> d23bdf5b1b1b
Step 2/6 : MAINTAINER lvwangbeta lvwangbeta@163.com
---> Running in b7af524b49fb
---> 58796b8e728d
Removing intermediate container b7af524b49fb
Step 3/6 : WORKDIR /poplardir
---> e7b04b310ab4
Removing intermediate container 2206d7c78f6b
Step 4/6 : ADD /poplardir/poplar-user-service-2.0.0.jar /poplardir/
---> 254f7eca9e94
Step 5/6 : ENTRYPOINT java -jar poplar-user-service-2.0.0.jar
---> Running in f933f1f8f3b6
---> ce512833c792
Removing intermediate container f933f1f8f3b6
Step 6/6 : CMD java -version
---> Running in 31f52e7e31dd
---> f6587d37eb4d
Removing intermediate container 31f52e7e31dd
ProgressMessage{id=null, status=null, stream=null, error=null, progress=null, progressDetail=null}
Successfully built f6587d37eb4d
Successfully tagged lvwangbeta/poplar-user-service:latest
[INFO] Built lvwangbeta/poplar-user-service
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
5.2 啟動(dòng)運(yùn)行容器
由于poplar包含的容器過(guò)多寻歧,在此為其創(chuàng)建自定義網(wǎng)絡(luò)poplar-netwotk
docker network create --subnet=172.18.0.0/16 poplar-network
運(yùn)行以上構(gòu)建的鏡像的容器,同時(shí)為其分配同網(wǎng)段IP
啟動(dòng)Zookeeper注冊(cè)中心
docker run --name poplar-zookeeper --restart always -d --net poplar-network --ip 172.18.0.6 zookeeper
啟動(dòng)MySQL
docker run --net poplar-network --ip 172.18.0.8 --name poplar-mysql -p 3307:3306 -e MYSQL_ROOT_PASSWORD=123456 -d lvwangbeta/poplar-mysql
啟動(dòng)Redis
docker run --net poplar-network --ip 172.18.0.9 --name poplar-redis -p 6380:6379 -d redis
啟動(dòng)業(yè)務(wù)服務(wù)
docker run --net poplar-network --ip 172.18.0.2 --name=poplar-user-service -p 8082:8082 -t lvwangbeta/poplar-user-service
docker run --net poplar-network --ip 172.18.0.3 --name=poplar-feed-service -p 8083:8083 -t lvwangbeta/poplar-feed-service
docker run --net poplar-network --ip 172.18.0.4 --name=poplar-action-service -p 8084:8084 -t lvwangbeta/poplar-action-service
docker run --net poplar-network --ip 172.18.0.10 --name=poplar-api -p 8080:8080 -t lvwangbeta/poplar-api
至此秩仆,poplar項(xiàng)目的后端已完整的構(gòu)建和啟動(dòng)码泛,對(duì)外提供服務(wù),客戶端(無(wú)論是Web還是App)看到只有一個(gè)統(tǒng)一的API澄耍。
項(xiàng)目完整源碼:https://github.com/lvwangbeta/Poplar