服务开发规范

服务交互的方式有很多种,dubbo、grpc、soap webservice、restful webservice、异步消息(rocketmq、kafka、rabbitmq等)

1 如何选型?

怎么选型?看具体应用场景,没有一刀切。只有理解了各种交互方式的优缺点,才能根据具体的场景做具体分析。你的应用场景关注的是什么质量属性,解耦、实时性、高性能、高并发、易用性、可读性?

一般来讲:交易型接口用restful http服务;异步非实时类用MQ;数据分析类接口需求采用kafka、rocketmq等进行数据推送。

1.1 不推荐的

不推荐使用soap协议webservice,大家在使用soap协议的时候真正理解什么是soap吗?什么是真正xml格式(不是返回xml格式的字符串)?有什么优缺点?

1.2 推荐的方式

你是性能狂魔吗?
如果是性能狂魔,追求极致的性能,那必须基于socket通信协议,例如阿里内部广泛采用netty做为底层通信框架,来实现dubbo、HSF等分布式服务框架。
一般场景下无需追求极致的性能,生态好、简单、清理、易用、云原生等质量属性便是我们的追求----该restful服务出场了。而且http服务同样能通过保持长连接,达到很高的性能。
服务开发模板:https://github.com/wuzuquan/microservice

2 服务开发规范

2.1 rest只是一种风格,并非标准

什么意思呢,rest并不是一种技术标准,无需严格按照网上的那些条条框框去开发:
url代表资源,通常情况下可以这么描述,但实际业务场景很复杂,无需套用这种url命名方式,接口一定要简单、易懂。
get 、post、put、delete对应CRUD,但put、delete对防火墙来说都是不够友好的,通常会被过滤掉。所以弃用put、delete。get操作一般用于普通查询,如果要传复杂参数,建议使用post方式,把参数放置于消息体中,而不是跟在url后面。

2.2 原则与实现

1、接口是可持续运营的,不是上线就完事了,要能够持续的版本更新迭代
2、向下兼容:如果一个功能以API的方式公布出来,那么在发布以后,它的对外接口就已经固定,不能取消。接口签名的改动对调用方会造成非常大的影响
3、接口最好实现幂等性。什么意思是,查询是幂等的,无论查多少次,都不会对应用数据造成影响。实现幂等性有什么意义呢?众所周知,网络是不稳定了,调用者发生重试的时候,还能不能保持正确的数据状态?CUD操作就不是幂等的。要实现幂等性要付出一定代价,比如借助额外的token参数校验,或者校验orderid、userid、时间戳等参数来实现幂等操作。
4、接口功能应单一化,不能有歧义,单一职责。
5、服务是无状态的,不保存session信息,不依赖于session或cookie。有状态的服务难以使用与运维。

2.3 接口签名

什么叫签名,其实无非就是方法名、输入、输出参数。
如何设计简单易用的接口呢?

鉴于PUT DELETE在网络中可能被防火墙屏蔽,在互联网环境中不够友好,因此全部使用post来代替,举例:

GET /user/getlist?condition=xxx:返回对象的列表
GET /user/getuser?id=xxx:返回单个对象
POST /user/create 创建
POST /user/update 修改
POST /users/delete?id=xxx:删除

1、返回统一的数据格式ResultBean,考虑异常返回
2、参数中不能出现 jsonstring map之类的复杂参数
3、create应该返回新对象的id标识
4、Controller做参数格式的转换,不允许把json,map这类对象传到services去,也不允许services返回json、map
5、参数中一般情况不允许出现Request,Response这些对象
6、异常处理:业务异常、系统异常
controller一般不吃掉异常,抛出Businessexception,统一交给AOP拦截器进行最后的处理
7、后台异常一定要有通知机制

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="根据url的id来获取详细信息")
    @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 返回的数据结构ResultBean

不能只考虑正常执行返回结果,还有考虑各种业务异常、系统异常、如何给用户友好的错误提示等等。

public class  ResultBean<T> implements Serializable{

    private int code=ExceptionEnum.SUCCESS.getCode();
    /**
     * 编号
     */
    private String errStr;
    //= ExceptionEnum.SUCCESS.toString();

    /**
     * 文本信息
     */
    private String message="success";

    /**
     数据内容
     */
    private  T data;

1、code字段描述了本次请求的状态,

SUCCESS(200),
    RESOURCE_NOT_FOUND(404),
    ARGUMENTS_INVALID(401),
    BUSINESS_ERROR(400),
    SERVER_ERROR(500);

2、errStr代表业务异常编码,只有code为business_error时才需要填充此字段
3、message 业务异常对应的文字描述
4、data 真实的业务数据,泛型

2.6 参数校验

统一的参数校验,使用hibernate validator组件,在controller层统一处理

  @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;
    }

对需要校验的参数添加validated注解。springmvc校验失败时会抛出bindexception,统一在拦截器里处理此异常

@ControllerAdvice(annotations = RestController.class)
@ResponseBody
public class CommonExceptionHandler {
    /**
     * logback new instance
     */
    Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 统一处理bean验证抛出的参数校验异常
     * 参数校验失败,统一采用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("参数校验失败,{}", JsonUtil.bean2Json(e.getTarget()));
        List<FieldError> fieldErrors=e.getBindingResult().getFieldErrors();

        return  new ResultBean<>(ExceptionEnum.ARGUMENTS_INVALID,null,"arguments invalid",fieldErrors);

    }

2.7 异常处理

程序不可能按照人的意志,永远完美的运行下去,总会出点毛病,出点bug。良好的异常处理也是一个攻城狮必备的能力。
1、影响业务运行的异常:业务层代码出现异常要么不处理,要么捕获处理抛再出业务异常
2、不影响正常逻辑的异常,可直接吃掉
3、对业务异常进行适当的异常编码,详细代码参考core模块下的exception
4、国际化文本提示:对每个业务异常编码,将对应的文本提示信息写入国际化资源文件
5、最终在异常拦截器里处理参数校验异常、业务异常、未捕获的系统异常

/**
     * 统一处理bean验证抛出的参数校验异常
     * 参数校验失败,统一采用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("参数校验失败,{}", JsonUtil.bean2Json(e.getTarget()));
        List<FieldError> fieldErrors=e.getBindingResult().getFieldErrors();

        return  new ResultBean<>(ExceptionEnum.ARGUMENTS_INVALID,null,"arguments invalid",fieldErrors);

    }


    /**
     * 统一拦截处理业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public ResultBean<String> validExceptionHandler(BusinessException e) {
        logger.warn("业务异常:【{}】", 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;
    }

    /**
     * 默认统一异常处理方法
     * @param e 默认Exception异常对象
     * @return
     */
    @ExceptionHandler
    @ResponseStatus
    public ResultBean<String> runtimeExceptionHandler(Exception e) {
        logger.error("运行时异常:【{}】", 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、能定位到机器IP
2、定位到用户干了啥,用户ID
3、修改新增操作必须打印日志
4、重要参数必须打印参数值
5、日志记录的内容,不允许使用字符串拼接++ ,浪费资源
6、推业务消息,必须记录返回值,便于跟踪

2.9 返回多样性的数据格式

服务提供者要支持常用的json、xml、protobuf,根据请求者发送的httpheader中accept字段,返回对应的数据格式。springmvc采用管道过滤器来处理,在http处理链上注册多个处理器,层层拦截,符合条件就会被处理。
当然了要设置一个默认值,默认json,json处理器挂在第一个位置,既是默认值。
本方案采用protostuff来支持protobuf数据格式,并非使用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方法上写死返回的数据格式,帮倒忙。

2.10 API接口文档--swagger

服务是要公布给其他开发者调用的,怎么跟别人描述你提供了多少服务,怎么调用,注意事项是什么?
传统的做法是写个word文档,洋洋洒洒几百页。实际上没什么卵用:
1、且不说写这么个文档要耗费多大的精力,文档放在哪,便于大家调阅都是个问题。
2、试问谁有耐心去看这么个冗长的API文档?
3、开发者怎么进行调试测试?
4、服务更新怎么办,还得去同步修改word文档,一次两次可以,N次呢,你会不会自乱阵脚?有人说,通过管理手段来保证,保证个蛋蛋哪,反人类思维。

是时候该swagger出场了。

原理很简单,通过扫描controller包下的接口类,对每个类、每个方法、输出输出参数进行解析,自动化生成友好的API文档,也可以在线调试测试。保证文档与代码是实时统一的。

2.11 接口版本

在类、方法上添加注解@ApiVersion(1),最终版本号呈现在url中

2.12 幂等性

由于宕机,网络抖动,超时等各种异常情况,还参与分布式事务,我们通常会有重试机制来保证高可用。这就要求我们的服务对同一个请求的多次重试,依然能正确响应。
讲那么多废话,最关键的一点就是业务逻辑实现去重,可以借助redis db 等进行去重,返回正确的数据。

3 服务调用者最佳实践--打通最后一公里

3.1 调试测试

通常情况下,使用服务提供者发布的swagger API在线文档即可进行调试测试。
复杂点的,比如要设置httpheader信息,则可通过postman之类的http调试工具进行。
在此推荐一个chrome插件 https://chrome.google.com/webstore/detail/restlet-client-rest-api-t/aejoelaoggembcahagimdiliamlcdmfm

3.2 提升稳定性与性能--使用okhttp

okhttp在稳定性、连接池方面处理的很好,推荐使用。在springboot工程中,推荐结合resttemplate使用。

 @Bean
    public OkHttpClient okHttpClient() {
        //注意:只有明确知道服务端支持H2C协议的时候才能使用。添加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())
        );
        //设置连接池大小
        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)
               //链路监控埋点
                .addNetworkInterceptor(TracingInterceptor.create(httpTracing))
                //.addInterceptor(new OkHttpInterceptor())
                .retryOnConnectionFailure(true);
        return builder.build();
    }

详细配置代码参看HttpClientConfig,自行DIY。

3.3 提升性能--我要protobuf

protobu具备体积小、高性能等特性,如果服务提供者支持protobuf格式,可使用此数据格式来交互。

 // 把自定义的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");
        // 保证请求继续被执行
        return execution.execute(request, body);
    }
}

通过给resttemplate设置拦截器,所有http请求统一添加头信息。

3. 4提升可用性--重试机制

如果服务出现不可用、或者网络抖动怎么办?
重试啊,重试贼好用。
如果有结合ribbon客户端负载工具,直接配载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 服务治理

4.1 认证、鉴权、限流、日志

把这些通用处理剥离处理不要每个服务提供者都去实现一遍,统一交给API网关处理。具体不展开。

4.2 服务版本更新

版本更新、服务上下线,一定要管理,该管的一定要管。
避免对下游业务造成不良影响。

5 不要滥用服务

服务是有成本的,不要万事皆服务。

服务是可重用的、可沉淀的、持续运营。

6 题外话:当一只高效的程序猿

懒人改变世界,不要盲目加班,平时注重自身技术积累沉淀,养成良好的开发习惯,提升软件质量。

具备良好习惯的程序猿顶4个码农,大家不要做真正的码农、纯体力活。机会总是留给有准备的人的。

何必天天加班排查故障、找bug、杀连接。。。自我麻痹、毫无长进

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,894评论 2 89
  • 前言 本开发规范基于《阿里巴巴Java开发手册终极版》修改,并集成我们自己的项目开发规范,整合而成。 为表示对阿里...
    4ea0af17fd67阅读 5,622评论 0 5
  • 又是一天,除了窗口颜色的变化,镜子里面更加粗糙的皮肤,温暖的暖气和被窝还是给了他一些力气。想想已经是三年了,三年前...
    闲置的鱼阅读 166评论 0 1
  • 昨天有一篇首页推荐文章,题为:《简书是个好平台》。 我刚接触简书短短几天,也深有同感。在我看来,除了那篇作者MIC...
    故乡圆月明阅读 314评论 1 4