服務(wù)交互的方式有很多種,dubbo、grpc敞映、soap webservice献宫、restful webservice、異步消息(rocketmq、kafka、rabbitmq等)
1 如何選型?
怎么選型鳖擒?看具體應(yīng)用場(chǎng)景,沒(méi)有一刀切烫止。只有理解了各種交互方式的優(yōu)缺點(diǎn)蒋荚,才能根據(jù)具體的場(chǎng)景做具體分析。你的應(yīng)用場(chǎng)景關(guān)注的是什么質(zhì)量屬性馆蠕,解耦期升、實(shí)時(shí)性惊奇、高性能、高并發(fā)播赁、易用性颂郎、可讀性?
一般來(lái)講:交易型接口用restful http服務(wù)容为;異步非實(shí)時(shí)類用MQ乓序;數(shù)據(jù)分析類接口需求采用kafka、rocketmq等進(jìn)行數(shù)據(jù)推送坎背。
1.1 不推薦的
不推薦使用soap協(xié)議webservice替劈,大家在使用soap協(xié)議的時(shí)候真正理解什么是soap嗎?什么是真正xml格式(不是返回xml格式的字符串)得滤?有什么優(yōu)缺點(diǎn)陨献?
1.2 推薦的方式
你是性能狂魔嗎?
如果是性能狂魔懂更,追求極致的性能湿故,那必須基于socket通信協(xié)議,例如阿里內(nèi)部廣泛采用netty做為底層通信框架膜蛔,來(lái)實(shí)現(xiàn)dubbo、HSF等分布式服務(wù)框架脖阵。
一般場(chǎng)景下無(wú)需追求極致的性能皂股,生態(tài)好、簡(jiǎn)單命黔、清理呜呐、易用、云原生等質(zhì)量屬性便是我們的追求----該restful服務(wù)出場(chǎng)了悍募。而且http服務(wù)同樣能通過(guò)保持長(zhǎng)連接蘑辑,達(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),無(wú)需嚴(yán)格按照網(wǎng)上的那些條條框框去開發(fā):
url代表資源喜鼓,通常情況下可以這么描述副砍,但實(shí)際業(yè)務(wù)場(chǎng)景很復(fù)雜,無(wú)需套用這種url命名方式庄岖,接口一定要簡(jiǎn)單豁翎、易懂。
get 隅忿、post心剥、put邦尊、delete對(duì)應(yīng)CRUD,但put优烧、delete對(duì)防火墻來(lái)說(shuō)都是不夠友好的蝉揍,通常會(huì)被過(guò)濾掉。所以棄用put匙隔、delete疑苫。get操作一般用于普通查詢,如果要傳復(fù)雜參數(shù)纷责,建議使用post方式捍掺,把參數(shù)放置于消息體中,而不是跟在url后面再膳。
2.2 原則與實(shí)現(xiàn)
1挺勿、接口是可持續(xù)運(yùn)營(yíng)的,不是上線就完事了喂柒,要能夠持續(xù)的版本更新迭代
2不瓶、向下兼容:如果一個(gè)功能以API的方式公布出來(lái),那么在發(fā)布以后灾杰,它的對(duì)外接口就已經(jīng)固定蚊丐,不能取消。接口簽名的改動(dòng)對(duì)調(diào)用方會(huì)造成非常大的影響
3艳吠、接口最好實(shí)現(xiàn)冪等性麦备。什么意思是,查詢是冪等的昭娩,無(wú)論查多少次凛篙,都不會(huì)對(duì)應(yīng)用數(shù)據(jù)造成影響。實(shí)現(xiàn)冪等性有什么意義呢栏渺?眾所周知呛梆,網(wǎng)絡(luò)是不穩(wěn)定了,調(diào)用者發(fā)生重試的時(shí)候磕诊,還能不能保持正確的數(shù)據(jù)狀態(tài)填物?CUD操作就不是冪等的。要實(shí)現(xiàn)冪等性要付出一定代價(jià)霎终,比如借助額外的token參數(shù)校驗(yàn)融痛,或者校驗(yàn)orderid、userid神僵、時(shí)間戳等參數(shù)來(lái)實(shí)現(xiàn)冪等操作雁刷。
4、接口功能應(yīng)單一化保礼,不能有歧義沛励,單一職責(zé)责语。
5、服務(wù)是無(wú)狀態(tài)的目派,不保存session信息坤候,不依賴于session或cookie。有狀態(tài)的服務(wù)難以使用與運(yùn)維企蹭。
2.3 接口簽名
什么叫簽名白筹,其實(shí)無(wú)非就是方法名、輸入谅摄、輸出參數(shù)徒河。
如何設(shè)計(jì)簡(jiǎn)單易用的接口呢?
鑒于PUT DELETE在網(wǎng)絡(luò)中可能被防火墻屏蔽送漠,在互聯(lián)網(wǎng)環(huán)境中不夠友好顽照,因此全部使用post來(lái)代替,舉例:
GET /user/getlist?condition=xxx:返回對(duì)象的列表
GET /user/getuser?id=xxx:返回單個(gè)對(duì)象
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)該返回新對(duì)象的id標(biāo)識(shí)
4植影、Controller做參數(shù)格式的轉(zhuǎn)換,不允許把json涎永,map這類對(duì)象傳到services去思币,也不允許services返回json、map
5土辩、參數(shù)中一般情況不允許出現(xiàn)Request,Response這些對(duì)象
6抢野、異常處理:業(yè)務(wù)異常拷淘、系統(tǒng)異常
controller一般不吃掉異常,拋出Businessexception指孤,統(tǒng)一交給AOP攔截器進(jìn)行最后的處理
7启涯、后臺(tái)異常一定要有通知機(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來(lái)獲取詳細(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)異常结洼、如何給用戶友好的錯(cuò)誤提示等等。
public class ResultBean<T> implements Serializable{
private int code=ExceptionEnum.SUCCESS.getCode();
/**
* 編號(hào)
*/
private String errStr;
//= ExceptionEnum.SUCCESS.toString();
/**
* 文本信息
*/
private String message="success";
/**
數(shù)據(jù)內(nèi)容
*/
private T data;
1叉跛、code字段描述了本次請(qǐng)求的狀態(tài)松忍,
SUCCESS(200),
RESOURCE_NOT_FOUND(404),
ARGUMENTS_INVALID(401),
BUSINESS_ERROR(400),
SERVER_ERROR(500);
2、errStr代表業(yè)務(wù)異常編碼筷厘,只有code為business_error時(shí)才需要填充此字段
3鸣峭、message 業(yè)務(wù)異常對(duì)應(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;
}
對(duì)需要校驗(yàn)的參數(shù)添加validated注解。springmvc校驗(yàn)失敗時(shí)會(huì)拋出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)行下去拉岁,總會(huì)出點(diǎn)毛病坷剧,出點(diǎn)bug。良好的異常處理也是一個(gè)攻城獅必備的能力膛薛。
1听隐、影響業(yè)務(wù)運(yùn)行的異常:業(yè)務(wù)層代碼出現(xiàn)異常要么不處理,要么捕獲處理拋再出業(yè)務(wù)異常
2哄啄、不影響正常邏輯的異常雅任,可直接吃掉
3、對(duì)業(yè)務(wù)異常進(jìn)行適當(dāng)?shù)漠惓>幋a咨跌,詳細(xì)代碼參考core模塊下的exception
4沪么、國(guó)際化文本提示:對(duì)每個(gè)業(yè)務(wù)異常編碼,將對(duì)應(yīng)的文本提示信息寫入國(guó)際化資源文件
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異常對(duì)象
* @return
*/
@ExceptionHandler
@ResponseStatus
public ResultBean<String> runtimeExceptionHandler(Exception e) {
logger.error("運(yùn)行時(shí)異常:【{}】", 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來(lái)建立日志體系殉摔。
日志要求:
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ù)請(qǐng)求者發(fā)送的httpheader中accept字段猎物,返回對(duì)應(yīng)的數(shù)據(jù)格式虎囚。springmvc采用管道過(guò)濾器來(lái)處理,在http處理鏈上注冊(cè)多個(gè)處理器蔫磨,層層攔截淘讥,符合條件就會(huì)被處理。
當(dāng)然了要設(shè)置一個(gè)默認(rèn)值堤如,默認(rèn)json蒲列,json處理器掛在第一個(gè)位置,既是默認(rèn)值搀罢。
本方案采用protostuff來(lái)支持protobuf數(shù)據(jù)格式蝗岖,并非使用google提供的jar包,無(wú)須每個(gè)對(duì)象都寫個(gè).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)的做法是寫個(gè)word文檔骇塘,洋洋灑灑幾百頁(yè)。實(shí)際上沒(méi)什么卵用:
1韩容、且不說(shuō)寫這么個(gè)文檔要耗費(fèi)多大的精力款违,文檔放在哪,便于大家調(diào)閱都是個(gè)問(wèn)題群凶。
2插爹、試問(wèn)誰(shuí)有耐心去看這么個(gè)冗長(zhǎng)的API文檔?
3座掘、開發(fā)者怎么進(jìn)行調(diào)試測(cè)試递惋?
4柔滔、服務(wù)更新怎么辦溢陪,還得去同步修改word文檔,一次兩次可以睛廊,N次呢形真,你會(huì)不會(huì)自亂陣腳?有人說(shuō),通過(guò)管理手段來(lái)保證咆霜,保證個(gè)蛋蛋哪邓馒,反人類思維。
是時(shí)候該swagger出場(chǎng)了蛾坯。
原理很簡(jiǎn)單光酣,通過(guò)掃描controller包下的接口類,對(duì)每個(gè)類脉课、每個(gè)方法救军、輸出輸出參數(shù)進(jìn)行解析,自動(dòng)化生成友好的API文檔倘零,也可以在線調(diào)試測(cè)試唱遭。保證文檔與代碼是實(shí)時(shí)統(tǒng)一的。
2.11 接口版本
在類呈驶、方法上添加注解@ApiVersion(1)拷泽,最終版本號(hào)呈現(xiàn)在url中
2.12 冪等性
由于宕機(jī),網(wǎng)絡(luò)抖動(dòng)袖瞻,超時(shí)等各種異常情況司致,還參與分布式事務(wù),我們通常會(huì)有重試機(jī)制來(lái)保證高可用虏辫。這就要求我們的服務(wù)對(duì)同一個(gè)請(qǐng)求的多次重試蚌吸,依然能正確響應(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)試測(cè)試
通常情況下佩微,使用服務(wù)提供者發(fā)布的swagger API在線文檔即可進(jìn)行調(diào)試測(cè)試。
復(fù)雜點(diǎn)的萌焰,比如要設(shè)置httpheader信息哺眯,則可通過(guò)postman之類的http調(diào)試工具進(jìn)行。
在此推薦一個(gè)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é)議的時(shí)候才能使用。添加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ù)格式來(lái)交互。
// 把自定義的ClientHttpRequestInterceptor添加到RestTemplate丐黄,可添加多個(gè)
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");
// 保證請(qǐng)求繼續(xù)被執(zhí)行
return execution.execute(request, body);
}
}
通過(guò)給resttemplate設(shè)置攔截器斋配,所有http請(qǐng)求統(tǒng)一添加頭信息。
3. 4提升可用性--重試機(jī)制
如果服務(wù)出現(xiàn)不可用灌闺、或者網(wǎng)絡(luò)抖動(dòng)怎么辦许起?
重試啊,重試賊好用菩鲜。
如果有結(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)蛛勉、限流鹿寻、日志
把這些通用處理剝離處理不要每個(gè)服務(wù)提供者都去實(shí)現(xiàn)一遍,統(tǒng)一交給API網(wǎng)關(guān)處理诽凌。具體不展開毡熏。
4.2 服務(wù)版本更新
版本更新、服務(wù)上下線侣诵,一定要管理痢法,該管的一定要管。
避免對(duì)下游業(yè)務(wù)造成不良影響杜顺。
5 不要濫用服務(wù)
服務(wù)是有成本的财搁,不要萬(wàn)事皆服務(wù)。
服務(wù)是可重用的躬络、可沉淀的尖奔、持續(xù)運(yùn)營(yíng)。
6 題外話:當(dāng)一只高效的程序猿
懶人改變世界穷当,不要盲目加班提茁,平時(shí)注重自身技術(shù)積累沉淀,養(yǎng)成良好的開發(fā)習(xí)慣馁菜,提升軟件質(zhì)量茴扁。
具備良好習(xí)慣的程序猿頂4個(gè)碼農(nóng),大家不要做真正的碼農(nóng)火邓、純體力活丹弱。機(jī)會(huì)總是留給有準(zhǔn)備的人的。
何必天天加班排查故障铲咨、找bug躲胳、殺連接。纤勒。坯苹。自我麻痹、毫無(wú)長(zhǎng)進(jìn)