服务交互的方式有很多种,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、杀连接。。。自我麻痹、毫无长进