服務(wù)開發(fā)規(guī)范

服務(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)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,588評論 19 139
  • 關(guān)于Mongodb的全面總結(jié) MongoDB的內(nèi)部構(gòu)造《MongoDB The Definitive Guide》...
    中v中閱讀 32,309評論 2 89
  • 前言 本開發(fā)規(guī)范基于《阿里巴巴Java開發(fā)手冊終極版》修改,并集成我們自己的項(xiàng)目開發(fā)規(guī)范,整合而成。 為表示對阿里...
    4ea0af17fd67閱讀 5,751評論 0 5
  • 又是一天,除了窗口顏色的變化,鏡子里面更加粗糙的皮膚,溫暖的暖氣和被窩還是給了他一些力氣。想想已經(jīng)是三年了,三年前...
    閑置的魚閱讀 186評論 0 1
  • 昨天有一篇首頁推薦文章,題為:《簡書是個好平臺》。 我剛接觸簡書短短幾天,也深有同感。在我看來,除了那篇作者M(jìn)IC...
    故鄉(xiāng)圓月明閱讀 374評論 1 4

友情鏈接更多精彩內(nèi)容