服務(wù)交互的方式有很多種,dubbo、grpc、soap webservice、restful webservice、異步消息(rocketmq、kafka、rabbitmq等)
1 如何選型?
怎么選型?看具體應(yīng)用場景,沒有一刀切。只有理解了各種交互方式的優(yōu)缺點(diǎn),才能根據(jù)具體的場景做具體分析。你的應(yīng)用場景關(guān)注的是什么質(zhì)量屬性,解耦、實(shí)時性、高性能、高并發(fā)、易用性、可讀性?
一般來講:交易型接口用restful http服務(wù);異步非實(shí)時類用MQ;數(shù)據(jù)分析類接口需求采用kafka、rocketmq等進(jìn)行數(shù)據(jù)推送。
1.1 不推薦的
不推薦使用soap協(xié)議webservice,大家在使用soap協(xié)議的時候真正理解什么是soap嗎?什么是真正xml格式(不是返回xml格式的字符串)?有什么優(yōu)缺點(diǎn)?
1.2 推薦的方式
你是性能狂魔嗎?
如果是性能狂魔,追求極致的性能,那必須基于socket通信協(xié)議,例如阿里內(nèi)部廣泛采用netty做為底層通信框架,來實(shí)現(xiàn)dubbo、HSF等分布式服務(wù)框架。
一般場景下無需追求極致的性能,生態(tài)好、簡單、清理、易用、云原生等質(zhì)量屬性便是我們的追求----該restful服務(wù)出場了。而且http服務(wù)同樣能通過保持長連接,達(dá)到很高的性能。
服務(wù)開發(fā)模板:https://github.com/wuzuquan/microservice
2 服務(wù)開發(fā)規(guī)范
2.1 rest只是一種風(fēng)格,并非標(biāo)準(zhǔn)
什么意思呢,rest并不是一種技術(shù)標(biāo)準(zhǔn),無需嚴(yán)格按照網(wǎng)上的那些條條框框去開發(fā):
url代表資源,通常情況下可以這么描述,但實(shí)際業(yè)務(wù)場景很復(fù)雜,無需套用這種url命名方式,接口一定要簡單、易懂。
get 、post、put、delete對應(yīng)CRUD,但put、delete對防火墻來說都是不夠友好的,通常會被過濾掉。所以棄用put、delete。get操作一般用于普通查詢,如果要傳復(fù)雜參數(shù),建議使用post方式,把參數(shù)放置于消息體中,而不是跟在url后面。
2.2 原則與實(shí)現(xiàn)
1、接口是可持續(xù)運(yùn)營的,不是上線就完事了,要能夠持續(xù)的版本更新迭代
2、向下兼容:如果一個功能以API的方式公布出來,那么在發(fā)布以后,它的對外接口就已經(jīng)固定,不能取消。接口簽名的改動對調(diào)用方會造成非常大的影響
3、接口最好實(shí)現(xiàn)冪等性。什么意思是,查詢是冪等的,無論查多少次,都不會對應(yīng)用數(shù)據(jù)造成影響。實(shí)現(xiàn)冪等性有什么意義呢?眾所周知,網(wǎng)絡(luò)是不穩(wěn)定了,調(diào)用者發(fā)生重試的時候,還能不能保持正確的數(shù)據(jù)狀態(tài)?CUD操作就不是冪等的。要實(shí)現(xiàn)冪等性要付出一定代價(jià),比如借助額外的token參數(shù)校驗(yàn),或者校驗(yàn)orderid、userid、時間戳等參數(shù)來實(shí)現(xiàn)冪等操作。
4、接口功能應(yīng)單一化,不能有歧義,單一職責(zé)。
5、服務(wù)是無狀態(tài)的,不保存session信息,不依賴于session或cookie。有狀態(tài)的服務(wù)難以使用與運(yùn)維。
2.3 接口簽名
什么叫簽名,其實(shí)無非就是方法名、輸入、輸出參數(shù)。
如何設(shè)計(jì)簡單易用的接口呢?
鑒于PUT DELETE在網(wǎng)絡(luò)中可能被防火墻屏蔽,在互聯(lián)網(wǎng)環(huán)境中不夠友好,因此全部使用post來代替,舉例:
GET /user/getlist?condition=xxx:返回對象的列表
GET /user/getuser?id=xxx:返回單個對象
POST /user/create 創(chuàng)建
POST /user/update 修改
POST /users/delete?id=xxx:刪除
1、返回統(tǒng)一的數(shù)據(jù)格式ResultBean,考慮異常返回
2、參數(shù)中不能出現(xiàn) jsonstring map之類的復(fù)雜參數(shù)
3、create應(yīng)該返回新對象的id標(biāo)識
4、Controller做參數(shù)格式的轉(zhuǎn)換,不允許把json,map這類對象傳到services去,也不允許services返回json、map
5、參數(shù)中一般情況不允許出現(xiàn)Request,Response這些對象
6、異常處理:業(yè)務(wù)異常、系統(tǒng)異常
controller一般不吃掉異常,拋出Businessexception,統(tǒng)一交給AOP攔截器進(jìn)行最后的處理
7、后臺異常一定要有通知機(jī)制
2.4 接口代碼示例
/**
* <p>
* </p>
*
* @author wuzuquan
* @date 2018-06-28 09:09:00
* @version
*/
@RestController
@RequestMapping(value = "/tbempdata")
@ApiVersion(1)
public class TbEmpDataController {
Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private HttpServletRequest request;
@Autowired
private HttpServletResponse response;
@Autowired
private TbEmpDataMapper mapper;
@ApiOperation(value="獲取單條記錄", notes="根據(jù)url的id來獲取詳細(xì)信息")
@RequestMapping(value = "/get",method = RequestMethod.GET)
public ResultBean<TbEmpData> get(String id){
TbEmpData item= mapper.selectByPrimaryKey(id);
if(item!=null){
return new ResultBean<TbEmpData>(item);
}else {
return new ResultBean<TbEmpData>(ExceptionEnum.RESOURCE_NOT_FOUND,null,"找不到該記錄",null);
}
}
@RequestMapping(value = "/getlist",method = RequestMethod.GET)
public ResultBean<List<TbEmpData>> getList(){
List<TbEmpData> list= mapper.selectAll();
ResultBean<List<TbEmpData>> resultBean=new ResultBean<List<TbEmpData>>(list);
return resultBean;
}
@RequestMapping(value = "/create",method = RequestMethod.POST)
public ResultBean<String> create(@Validated TbEmpData item){
int result= mapper.insert(item);
logger.info("create TbEmpData success,record,{}"+ JsonUtil.bean2Json(item));
ResultBean<String> resultBean=new ResultBean<String>("");
return resultBean;
}
@RequestMapping(value = "/update",method = RequestMethod.POST)
public ResultBean<String> update(@Validated TbEmpData item){
int result= mapper.updateByPrimaryKey(item);
logger.info("update TbEmpData success,record,{}"+ JsonUtil.bean2Json(item));
ResultBean<String> resultBean=new ResultBean<String>("");
return resultBean;
}
@RequestMapping(value = "/deleteByID",method = RequestMethod.POST)
public ResultBean<Integer> delete(String id){
int result= mapper.deleteByPrimaryKey(id);
logger.info("delete TbEmpData success,record id,{}"+ id);
ResultBean<Integer> resultBean=new ResultBean<Integer>(result);
return resultBean;
}
@RequestMapping(value = "/delete",method = RequestMethod.POST)
public ResultBean<Integer> delete(TbEmpData item){
int result= mapper.updateByPrimaryKey(item);
ResultBean<Integer> resultBean=new ResultBean<Integer>(result);
return resultBean;
}
}
2.5 返回的數(shù)據(jù)結(jié)構(gòu)ResultBean
不能只考慮正常執(zhí)行返回結(jié)果,還有考慮各種業(yè)務(wù)異常、系統(tǒng)異常、如何給用戶友好的錯誤提示等等。
public class ResultBean<T> implements Serializable{
private int code=ExceptionEnum.SUCCESS.getCode();
/**
* 編號
*/
private String errStr;
//= ExceptionEnum.SUCCESS.toString();
/**
* 文本信息
*/
private String message="success";
/**
數(shù)據(jù)內(nèi)容
*/
private T data;
1、code字段描述了本次請求的狀態(tài),
SUCCESS(200),
RESOURCE_NOT_FOUND(404),
ARGUMENTS_INVALID(401),
BUSINESS_ERROR(400),
SERVER_ERROR(500);
2、errStr代表業(yè)務(wù)異常編碼,只有code為business_error時才需要填充此字段
3、message 業(yè)務(wù)異常對應(yīng)的文字描述
4、data 真實(shí)的業(yè)務(wù)數(shù)據(jù),泛型
2.6 參數(shù)校驗(yàn)
統(tǒng)一的參數(shù)校驗(yàn),使用hibernate validator組件,在controller層統(tǒng)一處理
@RequestMapping(value = "/create",method = RequestMethod.POST)
public ResultBean<String> create(@Validated TbEmpData item){
int result= mapper.insert(item);
logger.info("create TbEmpData success,record,{}"+ JsonUtil.bean2Json(item));
ResultBean<String> resultBean=new ResultBean<String>("");
return resultBean;
}
對需要校驗(yàn)的參數(shù)添加validated注解。springmvc校驗(yàn)失敗時會拋出bindexception,統(tǒng)一在攔截器里處理此異常
@ControllerAdvice(annotations = RestController.class)
@ResponseBody
public class CommonExceptionHandler {
/**
* logback new instance
*/
Logger logger = LoggerFactory.getLogger(this.getClass());
/**
* 統(tǒng)一處理bean驗(yàn)證拋出的參數(shù)校驗(yàn)異常
* 參數(shù)校驗(yàn)失敗,統(tǒng)一采用warn記錄日志
* @see javax.validation.Valid
* @see org.springframework.validation.Validator
* @see org.springframework.validation.DataBinder
*/
@ExceptionHandler(BindException.class)
public ResultBean<List<FieldError>> validExceptionHandler(BindException e, WebRequest request, HttpServletResponse response) {
logger.warn("參數(shù)校驗(yàn)失敗,{}", JsonUtil.bean2Json(e.getTarget()));
List<FieldError> fieldErrors=e.getBindingResult().getFieldErrors();
return new ResultBean<>(ExceptionEnum.ARGUMENTS_INVALID,null,"arguments invalid",fieldErrors);
}
2.7 異常處理
程序不可能按照人的意志,永遠(yuǎn)完美的運(yùn)行下去,總會出點(diǎn)毛病,出點(diǎn)bug。良好的異常處理也是一個攻城獅必備的能力。
1、影響業(yè)務(wù)運(yùn)行的異常:業(yè)務(wù)層代碼出現(xiàn)異常要么不處理,要么捕獲處理拋再出業(yè)務(wù)異常
2、不影響正常邏輯的異常,可直接吃掉
3、對業(yè)務(wù)異常進(jìn)行適當(dāng)?shù)漠惓>幋a,詳細(xì)代碼參考core模塊下的exception
4、國際化文本提示:對每個業(yè)務(wù)異常編碼,將對應(yīng)的文本提示信息寫入國際化資源文件
5、最終在異常攔截器里處理參數(shù)校驗(yàn)異常、業(yè)務(wù)異常、未捕獲的系統(tǒng)異常
/**
* 統(tǒng)一處理bean驗(yàn)證拋出的參數(shù)校驗(yàn)異常
* 參數(shù)校驗(yàn)失敗,統(tǒng)一采用warn記錄日志
* @see javax.validation.Valid
* @see org.springframework.validation.Validator
* @see org.springframework.validation.DataBinder
*/
@ExceptionHandler(BindException.class)
public ResultBean<List<FieldError>> validExceptionHandler(BindException e, WebRequest request, HttpServletResponse response) {
logger.warn("參數(shù)校驗(yàn)失敗,{}", JsonUtil.bean2Json(e.getTarget()));
List<FieldError> fieldErrors=e.getBindingResult().getFieldErrors();
return new ResultBean<>(ExceptionEnum.ARGUMENTS_INVALID,null,"arguments invalid",fieldErrors);
}
/**
* 統(tǒng)一攔截處理業(yè)務(wù)異常
*/
@ExceptionHandler(BusinessException.class)
public ResultBean<String> validExceptionHandler(BusinessException e) {
logger.warn("業(yè)務(wù)異常:【{}】", e.getMessage(),e);
ResultBean<String> result=new ResultBean<String>();
result.setCode(ExceptionEnum.BUSINESS_ERROR.getCode());
result.setErrStr(e.getErrCode());
result.setMessage(e.getMessage());
result.setData(JsonUtil.bean2Json(e.getData()));
return result;
}
/**
* 默認(rèn)統(tǒng)一異常處理方法
* @param e 默認(rèn)Exception異常對象
* @return
*/
@ExceptionHandler
@ResponseStatus
public ResultBean<String> runtimeExceptionHandler(Exception e) {
logger.error("運(yùn)行時異常:【{}】", e.getMessage(),e);
ResultBean<String> result=new ResultBean<String>();
result.setCode(ExceptionEnum.SERVER_ERROR.getCode());
result.setMessage(e.getMessage()+"-- traceid:"+ MDC.get("traceId"));
return result;
}
2.8 記錄日志
推薦使用log4j2 或logback+kafka+elk來建立日志體系。
日志要求:
1、能定位到機(jī)器IP
2、定位到用戶干了啥,用戶ID
3、修改新增操作必須打印日志
4、重要參數(shù)必須打印參數(shù)值
5、日志記錄的內(nèi)容,不允許使用字符串拼接++ ,浪費(fèi)資源
6、推業(yè)務(wù)消息,必須記錄返回值,便于跟蹤
2.9 返回多樣性的數(shù)據(jù)格式
服務(wù)提供者要支持常用的json、xml、protobuf,根據(jù)請求者發(fā)送的httpheader中accept字段,返回對應(yīng)的數(shù)據(jù)格式。springmvc采用管道過濾器來處理,在http處理鏈上注冊多個處理器,層層攔截,符合條件就會被處理。
當(dāng)然了要設(shè)置一個默認(rèn)值,默認(rèn)json,json處理器掛在第一個位置,既是默認(rèn)值。
本方案采用protostuff來支持protobuf數(shù)據(jù)格式,并非使用google提供的jar包,無須每個對象都寫個.proto文件,這操作也是反人類。
在webconfig中添加如下代碼,具體參考代碼工程:https://github.com/wuzuquan/microservice
//添加protobuf支持,需要client指定accept-type:application/x-protobuf
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
stringConverter.setDefaultCharset(Charset.forName("utf-8"));
List<MediaType> list = new ArrayList<MediaType>();
list.add(MediaType.TEXT_PLAIN);
stringConverter.setSupportedMediaTypes(list);
MappingJackson2XmlHttpMessageConverter xmlConverter=new MappingJackson2XmlHttpMessageConverter();
xmlConverter.setDefaultCharset(Charset.forName("utf-8"));
List<MediaType> list2 = new ArrayList<MediaType>();
list2.add(MediaType.APPLICATION_XML);
xmlConverter.setSupportedMediaTypes(list2);
converters.add(0,stringConverter);
converters.add(0,xmlConverter);
converters.add(0,new ProtostuffHttpMessageConverter());
converters.add(0,getCustomJacksonConverter(objectMapper));
}
不建議在controller方法上寫死返回的數(shù)據(jù)格式,幫倒忙。
2.10 API接口文檔--swagger
服務(wù)是要公布給其他開發(fā)者調(diào)用的,怎么跟別人描述你提供了多少服務(wù),怎么調(diào)用,注意事項(xiàng)是什么?
傳統(tǒng)的做法是寫個word文檔,洋洋灑灑幾百頁。實(shí)際上沒什么卵用:
1、且不說寫這么個文檔要耗費(fèi)多大的精力,文檔放在哪,便于大家調(diào)閱都是個問題。
2、試問誰有耐心去看這么個冗長的API文檔?
3、開發(fā)者怎么進(jìn)行調(diào)試測試?
4、服務(wù)更新怎么辦,還得去同步修改word文檔,一次兩次可以,N次呢,你會不會自亂陣腳?有人說,通過管理手段來保證,保證個蛋蛋哪,反人類思維。
是時候該swagger出場了。
原理很簡單,通過掃描controller包下的接口類,對每個類、每個方法、輸出輸出參數(shù)進(jìn)行解析,自動化生成友好的API文檔,也可以在線調(diào)試測試。保證文檔與代碼是實(shí)時統(tǒng)一的。
2.11 接口版本
在類、方法上添加注解@ApiVersion(1),最終版本號呈現(xiàn)在url中
2.12 冪等性
由于宕機(jī),網(wǎng)絡(luò)抖動,超時等各種異常情況,還參與分布式事務(wù),我們通常會有重試機(jī)制來保證高可用。這就要求我們的服務(wù)對同一個請求的多次重試,依然能正確響應(yīng)。
講那么多廢話,最關(guān)鍵的一點(diǎn)就是業(yè)務(wù)邏輯實(shí)現(xiàn)去重,可以借助redis db 等進(jìn)行去重,返回正確的數(shù)據(jù)。
3 服務(wù)調(diào)用者最佳實(shí)踐--打通最后一公里
3.1 調(diào)試測試
通常情況下,使用服務(wù)提供者發(fā)布的swagger API在線文檔即可進(jìn)行調(diào)試測試。
復(fù)雜點(diǎn)的,比如要設(shè)置httpheader信息,則可通過postman之類的http調(diào)試工具進(jìn)行。
在此推薦一個chrome插件 https://chrome.google.com/webstore/detail/restlet-client-rest-api-t/aejoelaoggembcahagimdiliamlcdmfm
3.2 提升穩(wěn)定性與性能--使用okhttp
okhttp在穩(wěn)定性、連接池方面處理的很好,推薦使用。在springboot工程中,推薦結(jié)合resttemplate使用。
@Bean
public OkHttpClient okHttpClient() {
//注意:只有明確知道服務(wù)端支持H2C協(xié)議的時候才能使用。添加H2C支持,
OkHttpClient.Builder builder = new OkHttpClient.Builder();
// .protocols(Collections.singletonList(Protocol.H2_PRIOR_KNOWLEDGE));
Dispatcher dispatcher=new Dispatcher(
httpTracing.tracing().currentTraceContext()
.executorService(new Dispatcher().executorService())
);
//設(shè)置連接池大小
dispatcher.setMaxRequests(1000);
dispatcher.setMaxRequestsPerHost(200);
ConnectionPool pool = new ConnectionPool(20, 30, TimeUnit.MINUTES);
builder.connectTimeout(2000, TimeUnit.MILLISECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.connectionPool(pool)
.dispatcher(dispatcher)
//鏈路監(jiān)控埋點(diǎn)
.addNetworkInterceptor(TracingInterceptor.create(httpTracing))
//.addInterceptor(new OkHttpInterceptor())
.retryOnConnectionFailure(true);
return builder.build();
}
詳細(xì)配置代碼參看HttpClientConfig,自行DIY。
3.3 提升性能--我要protobuf
protobu具備體積小、高性能等特性,如果服務(wù)提供者支持protobuf格式,可使用此數(shù)據(jù)格式來交互。
// 把自定義的ClientHttpRequestInterceptor添加到RestTemplate,可添加多個
restTemplate.setInterceptors(Collections.singletonList(new ProtobufHeaderInterceptor()));
public class ProtobufHeaderInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
HttpHeaders headers = request.getHeaders();
// 加入自定義字段
headers.clear();
headers.add("Accept","application/x-protobuf");
// 保證請求繼續(xù)被執(zhí)行
return execution.execute(request, body);
}
}
通過給resttemplate設(shè)置攔截器,所有http請求統(tǒng)一添加頭信息。
3. 4提升可用性--重試機(jī)制
如果服務(wù)出現(xiàn)不可用、或者網(wǎng)絡(luò)抖動怎么辦?
重試啊,重試賊好用。
如果有結(jié)合ribbon客戶端負(fù)載工具,直接配載ribbon重試策略。
如果未使用ribbon,okhttp也支持配置重試策略。
3.5 代碼示例
@Qualifier("signleTemplate")
@Autowired
private RestTemplate restTemplate;
@Test
public void testListService() throws Exception {
String url = "http://localhost:8080/v1/organ/getlist?organCode=10.230";
ParameterizedTypeReference<ResultBean<List<A1001>>> typeRef = new ParameterizedTypeReference<ResultBean<List<A1001>>>() {};
ResponseEntity<ResultBean<List<A1001>>> responseEntity = restTemplate.exchange(
url, HttpMethod.GET,null , typeRef);
ResultBean<List<A1001>> myModelClasses = responseEntity.getBody();
Assert.assertEquals(myModelClasses.getData().get(0).getOrganCode(),"10.230");
}
4 服務(wù)治理
4.1 認(rèn)證、鑒權(quán)、限流、日志
把這些通用處理剝離處理不要每個服務(wù)提供者都去實(shí)現(xiàn)一遍,統(tǒng)一交給API網(wǎng)關(guān)處理。具體不展開。
4.2 服務(wù)版本更新
版本更新、服務(wù)上下線,一定要管理,該管的一定要管。
避免對下游業(yè)務(wù)造成不良影響。
5 不要濫用服務(wù)
服務(wù)是有成本的,不要萬事皆服務(wù)。
服務(wù)是可重用的、可沉淀的、持續(xù)運(yùn)營。
6 題外話:當(dāng)一只高效的程序猿
懶人改變世界,不要盲目加班,平時注重自身技術(shù)積累沉淀,養(yǎng)成良好的開發(fā)習(xí)慣,提升軟件質(zhì)量。
具備良好習(xí)慣的程序猿頂4個碼農(nóng),大家不要做真正的碼農(nóng)、純體力活。機(jī)會總是留給有準(zhǔn)備的人的。
何必天天加班排查故障、找bug、殺連接。。。自我麻痹、毫無長進(jìn)