1.注释尽可能全面,写有意义的注释
接口方法、类、复杂的业务逻辑,都应该添加有意义的注释
- 对于接口方法的注释,应该包含详细的入参和结果说明,有异常抛出的情况也要详细叙述
- 类的注释应该包含类的功能说明、作者和修改者。
- 如果是业务逻辑很复杂的代码,真的非常有必要写清楚注释。
清楚的注释,更有利于后面的维护。
2.项目拆分合理的目录结构
记得读大学那会,刚学做各种各样的管理系统,都是用MVC模式,也就是controller
、service
、mapper
、entity
。如果未来业务扩展,你没有拆分业务结构的话,很可能就会发现,一个service包下,有上百个服务。。。
正确的做法,如果服务过多,应该根据不同的业务进行划分,比如订单、登陆、积分等等
当然,你也可以根据不同的业务划分模块,比如建一个moudles包,然后按订单、登陆等业务划分,每个业务都有自己的controller、service、mapper、entity。
我们拆分的目的,就是让项目结构更清晰,可读性更强,更容易维护而已。
3. 不在循环里远程调用、或者数据库操作,优先考虑批量进行。
远程操作或者数据库操作都是比较耗网络、IO资源的,所以尽量不在循环里远程调用、不在循环里操作数据库,能批量一次性查回来尽量不要循环多次去查。(但是呢,如果是操作数据库,也不要一次性查太多数据哈,可以分批500一次酱紫)。
正例:
remoteBatchQuery(param);
反例:
for(int i=0;i<n;i++){ remoteSingleQuery(param) }
4. 封装方法形参
如果你的方法参数过多,要封装一个对象出来。反例如下:
public void getUserInfo(String name,String age,String sex,String mobile,String idNo){ // do something ... }
如果参数很多,做新老接口兼容处理也比较麻烦。建议写个对象出来,如下:
public void getUserInfo(UserInfoParamDTO userInfoParamDTO){ // do something ... } class UserInfoParamDTO{ private String name; private String age; private String sex; private String mobile; private String idNo; }
5. 封装通用模板
一个优秀的后端开发,应该具备封装通用模板的编码能力。
我们来看一个业务需求:假设我们有这么一个业务场景:
内部系统不同商户,调用我们系统接口,去跟外部第三方系统交互(http方式)。
走类似这么一个流程,如下:
一个请求都会经历这几个流程:
- 查询商户信息
- 对请求报文加签
- 发送http请求出去
- 对返回的报文验签
通过HTTP发请求出去时,有的商户可能是走代理的,有的是走直连。假设当前有A,B商户接入,不少伙伴可能这么实现,伪代码如下:
// 商户A处理句柄 CompanyAHandler implements RequestHandler { Resp hander(req){ //查询商户信息 queryMerchantInfo(); //加签 signature(); //http请求(A商户假设走的是代理) httpRequestbyProxy() //验签 verify(); } } // 商户B处理句柄 CompanyBHandler implements RequestHandler { Resp hander(Rreq){ //查询商户信息 queryMerchantInfo(); //加签 signature(); // http请求(B商户不走代理,直连) httpRequestbyDirect(); // 验签 verify(); } }
假设新加一个C商户接入,你需要再实现一套这样的代码。显然,这样代码就重复了。这时候我们可以封装一个通用模板!我们就可以定义一个抽象类,包含请求流程的几个方法,伪代码如下:
abstract class AbstractMerchantService { //模板方法流程 Resp handlerTempPlate(req){ //查询商户信息 queryMerchantInfo(); //加签 signature(); //http 请求 httpRequest(); // 验签 verifySinature(); } // Http是否走代理(提供给子类实现) abstract boolean isRequestByProxy(); }
然后所有商户接入,都做这个流程。如果这个通用模板是你抽取的,别的小伙伴接到开发任务,都是接入你的模板;
封装通用模板,就是抽个模板模式嘛?其实不仅仅是,而是自己对需求、代码的思考与总结,一种编程思想的升华。
6. 封装复杂的逻辑判断条件
我们来看下这段代码:
public void test(UserStatus userStatus){ if (userStatus != UserStatus.BANNED && userStatus != UserStatus.DELETED && userStatus != UserStatus.FROZEN) { //doSomeThing return } }
这段代码有什么问题呢?是的,逻辑判断条件太复杂啦,我们可以封装一下它。如下:
public void test(UserStatus userStatus){ if (isUserActive(userStatus)) { //doSomeThing } } private boolean isUserActive(UserStatus userStatus) { return userStatus != UserStatus.BANNED && userStatus != UserStatus.DELETED && userStatus != UserStatus.FROZEN; }
7. 保持优化性能的嗅觉
优秀的后端开发,应该保持优化性能的嗅觉。比如避免创建比必要的对象、异步处理、使用缓冲流,减少IO操作等等。
比如,我们设计一个APP首页的接口,它需要查用户信息、需要查banner信息、需要查弹窗信息等等。假设耗时如下:
查用户信息200ms,查banner信息100ms、查弹窗信息50ms,那一共就耗时350ms了。如果还查其他信息,那耗时就更大了。如何优化它呢?可以并行发起,耗时可以降为200ms。如下:
采用线程异步的方式对代码进行优化;
8. 可变参数的配置化处理
日常开发中,我们经常会遇到一些可变参数,比如用户多少天没登录注销、运营活动,不同节日红包皮肤切换、订单多久没付款就删除等等。对于这些可变的参数,不用该直接写死在代码。优秀的后端,要做配置化处理,你可以把这些可变参数,放到数据库一个配置表里面,也可以放到项目的配置文件或者apollo上。
比如产品经理提了个红包需求,圣诞节的时候,红包皮肤为圣诞节相关的,春节的时候,为春节红包皮肤等。如果在代码写死控制,可有类似以下代码:
if(duringChristmas){ img = redPacketChristmasSkin; }else if(duringSpringFestival){ img = redSpringFestivalSkin; }
如果到了元宵节的时候,运营小姐姐突然又有想法,红包皮肤换成灯笼相关的,这时候,是不是要去修改代码了,重新发布了?
从一开始接口设计时,可以实现一张红包皮肤的配置表,将红包皮肤做成配置化呢?更换红包皮肤,只需修改一下表数据就好了。当然,还有一些场景适合一些配置化的参数:一个分页多少数量控制、某个抢红包多久时间过期这些,都可以搞到参数配置化表里面。这也是扩展性思想的一种体现。
9. 会总结并使用工具类。
很多小伙伴,判断一个list是否为空,会这么写:
if (list == null || list.size() == 0) { return null; }
这样写呢,逻辑是没什么问题的。但是更建议用工具类,比如:
if (CollectionUtils.isEmpty(list)) { return null; }
StringUtils.isEmpty() StringUtils.isBlank()
日常开发中,我们既要会用工具类,更要学会自己去总结工具类。比如去文件处理工具类、日期处理工具类等等。这些都是优秀后端开发的一些好习惯。
10. 控制方法函数复杂度
你的方法不要写得太复杂,逻辑不要混乱,也不要太长。一个函数不能超过80行。写代码不仅仅是能跑就行,而是为了以后更好的维护。
反例如下:
public class Test { private String name; private Vector<Order> orders = new Vector<Order>(); public void printOwing() { //print banner System.out.println("****************"); System.out.println("*****customer Owes *****"); System.out.println("****************"); //calculate totalAmount Enumeration env = orders.elements(); double totalAmount = 0.0; while (env.hasMoreElements()) { Order order = (Order) env.nextElement(); totalAmount += order.getAmout(); } //print details System.out.println("name:" + name); System.out.println("amount:" + totalAmount); ...... } }
其实可以使用Extract Method,抽取功能单一的代码段,组成命名清晰的小函数,去解决长函数问题,正例如下:
public class Test { private String name; private Vector<Order> orders = new Vector<Order>(); public void printOwing() { //print banner printBanner(); //calculate totalAmount double totalAmount = getTotalAmount(); //print details printDetail(totalAmount); } void printBanner(){ System.out.println("****************"); System.out.println("*****customer Owes *****"); System.out.println("****************"); } double getTotalAmount(){ Enumeration env = orders.elements(); double totalAmount = 0.0; while (env.hasMoreElements()) { Order order = (Order) env.nextElement(); totalAmount += order.getAmout(); } return totalAmount; } void printDetail(double totalAmount){ System.out.println("name:" + name); System.out.println("amount:" + totalAmount); } }
11. 在finally块中对资源进行释放
应该大家都有过这样的经历,windows系统桌面如果打开太多文件或者系统软件,就会觉得电脑很卡。当然,我们linux服务器也一样,平时操作文件,或者数据库连接,IO资源流如果没关闭,那么这个IO资源就会被它占着,这样别人就没有办法用了,这就造成资源浪费。
我们操作完文件资源,需要在在finally块中对资源进行释放。
FileInputStream fdIn = null; try { fdIn = new FileInputStream(new File("/公众号_捡田螺的小男孩.txt")); } catch (FileNotFoundException e) { log.error(e); } catch (IOException e) { log.error(e); }finally { try { //此处判断是否为空根据自己开启的流进行对应的代码关闭 if (fdIn != null) { fdIn.close(); } } catch (IOException e) { log.error(e); } }
12.把日志打印好
日常开发中,一定需要把日志打印好。比如:你实现转账业务,转个几百万,然后转失败了,接着客户投诉,然后你还没有打印到日志,想想那种水深火热的困境下,你却毫无办法。。。
一般情况,方法入参、出参需要打印日志,异常的时候,也要打印日志等等,如下:
public void transfer(TransferDTO transferDTO){ log.info("invoke tranfer begin"); //打印入参 log.info("invoke tranfer,paramters:{}",transferDTO); try { res= transferService.transfer(transferDTO); }catch(Exception e){ log.error("transfer fail,account:{}", transferDTO.getAccount()) log.error("transfer fail,exception:{}",e); } log.info("invoke tranfer end"); }
12.1 选择恰当的日志级别
常见的日志级别有5种,分别是error、warn、info、debug、trace。日常开发中,我们需要选择恰当的日志级别,不要反手就是打印info哈~
- error:错误日志,指比较严重的错误,对正常业务有影响,需要运维配置监控的;
- warn:警告日志,一般的错误,对业务影响不大,但是需要开发关注;
- info:信息日志,记录排查问题的关键信息,如调用时间、出参入参等等;
- debug:用于开发DEBUG的,关键逻辑里面的运行时数据;
- trace:最详细的信息,一般这些信息只记录到日志文件中。
12.2 日志要打印出方法的入参、出参
我们并不需要打印很多很多日志,只需要打印可以快速定位问题的有效日志。有效的日志,是甩锅的利器!
哪些算得的上有效关键的日志呢?比如说,方法进来的时候,打印入参。再然后呢,在方法返回的时候,就是打印出参,返回值。入参的话,一般就是userId或者bizSeq这些关键信息。正例如下:
public String testLogMethod(Document doc, Mode mode){ log.debug(“method enter param:{}”,userId); String id = "666"; log.debug(“method exit param:{}”,id); return id; }
12. 3 选择合适的日志格式
理想的日志格式,应当包括这些最基本的信息:如当前时间戳(一般毫秒精确度)、日志级别,线程名字等等。在logback日志里可以这么配置:
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{HH:mm:ss.SSS} %-5level [%thread][%logger{0}] %m%n</pattern> </encoder> </appender>
如果我们的日志格式,连当前时间都沒有记录,那连请求的时间点都不知道了?
12.4 遇到if…else…等条件时,每个分支首行都尽量打印日志
当你碰到if…else…或者switch这样的条件时,可以在分支的首行就打印日志,这样排查问题时,就可以通过日志,确定进入了哪个分支,代码逻辑更清晰,也更方便排查问题了。
正例:
if(user.isVip()){ log.info("该用户是会员,Id:{},开始处理会员逻辑",user,getUserId()); //会员逻辑 }else{ log.info("该用户是非会员,Id:{},开始处理非会员逻辑",user,getUserId()) //非会员逻辑 }
12.5 日志级别比较低时,进行日志开关判断
对于trace/debug这些比较低的日志级别,必须进行日志级别的开关判断。
正例:
User user = new User(666L, "公众号", "捡田螺的小男孩"); if (log.isDebugEnabled()) { log.debug("userId is: {}", user.getId()); }
因为当前有如下的日志代码:
logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);
如果配置的日志级别是warn的话,上述日志不会打印,但是会执行字符串拼接操作,如果symbol是对象, 还会执行toString()方法,浪费了系统资源,执行了上述操作,最终日志却没有打印,因此建议加日志开关判断。
12.6. 日志系统选择
不能直接使用日志系统(Log4j、Logback)中的 API,而是使用日志框架SLF4J中的API。
SLF4J 是门面模式的日志框架,有利于维护和各个类的日志处理方式统一,并且可以在保证不修改代码的情况下,很方便的实现底层日志框架的更换。
正例:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; private static final Logger logger = LoggerFactory.getLogger(TianLuoBoy.class);
12.7 建议使用参数占位{},而不是用+拼接。
反例:
logger.info("Processing trade with id: " + id + " and symbol: " + symbol);
上面的例子中,使用+操作符进行字符串的拼接,有一定的性能损耗。
正例如下:
logger.info("Processing trade with id: {} and symbol : {} ", id, symbol);
我们使用了大括号{}来作为日志中的占位符,比于使用+操作符,更加优雅简洁。并且,相对于反例,使用占位符仅是替换动作,可以有效提升性能。
12.8. 建议使用异步的方式来输出日志。
- 日志最终会输出到文件或者其它输出流中的,IO性能会有要求的。如果异步,就可以显著提升IO性能。
- 除非有特殊要求,要不然建议使用异步的方式来输出日志。以logback为例吧,要配置异步很简单,使用AsyncAppender就行
<appender name="FILE_ASYNC" class="ch.qos.logback.classic.AsyncAppender"> <appender-ref ref="ASYNC"/> </appender>
12.9 不要使用e.printStackTrace()
反例:
try{ // 业务代码处理 }catch(Exception e){ e.printStackTrace(); }
正例:
try{ // 业务代码处理 }catch(Exception e){ log.error("你的程序有异常啦",e); }
理由:
e.printStackTrace()打印出的堆栈日志跟业务代码日志是交错混合在一起的,通常排查异常日志不太方便。
e.printStackTrace()语句产生的字符串记录的是堆栈信息,如果信息太长太多,字符串常量池所在的内存块没有空间了,即内存满了,那么,用户的请求就卡住啦~
12.10 异常日志不要只打一半,要输出全部错误信息
反例1:
try { //业务代码处理 } catch (Exception e) { // 错误 LOG.error('你的程序有异常啦'); }
异常e都没有打印出来,所以压根不知道出了什么类型的异常。
反例2:
try { //业务代码处理 } catch (Exception e) { // 错误 LOG.error('你的程序有异常啦', e.getMessage()); }
e.getMessage()不会记录详细的堆栈异常信息,只会记录错误基本描述信息,不利于排查问题。
正例:
try { //业务代码处理 } catch (Exception e) { // 错误 LOG.error('你的程序有异常啦', e); }
12.11 禁止在线上环境开启 debug
禁止在线上环境开启debug,这一点非常重要。
因为一般系统的debug日志会很多,并且各种框架中也大量使用 debug的日志,线上开启debug不久可能会打满磁盘,影响业务系统的正常运行。
12.12 不要记录了异常,又抛出异常
反例如下:
log.error("IO exception", e); throw new MyException(e);
这样实现的话,通常会把栈信息打印两次。这是因为捕获了MyException异常的地方,还会再打印一次。
这样的日志记录,或者包装后再抛出去,不要同时使用!否则你的日志看起来会让人很迷惑。
13.考虑异常,处理好异常
优秀的后端开发,应当考虑到异常,并做好异常处理。给大家提了10个异常处理的建议:
- 尽量不要使用e.printStackTrace(),而是使用log打印。因为e.printStackTrace()语句可能会导致内存占满。
- catch住异常时,建议打印出具体的exception,利于更好定位问题
- 不要用一个Exception捕捉所有可能的异常
- 记得使用finally关闭流资源或者直接使用try-with-resource。
- 捕获异常与抛出异常必须是完全匹配,或者捕获异常是抛异常的父类
- 捕获到的异常,不能忽略它,至少打点日志吧
- 注意异常对你的代码层次结构的侵染
- 自定义封装异常,不要丢弃原始异常的信息Throwable cause
- 运行时异常RuntimeException ,不应该通过catch的方式来处理,而是先预检查,比如:NullPointerException处理
- 注意异常匹配的顺序,优先捕获具体的异常
14. 考虑系统、接口的兼容性
优秀的后端开发,会考虑系统、接口的兼容性。
如果修改了对外旧接口,但是却不做兼容。这个问题可能比较严重,甚至会直接导致系统发版失败的。新手程序员很容易犯这个错误哦~
因此,如果你的需求是在原来接口上修改,尤其这个接口是对外提供服务的话,一定要考虑接口兼容。举个例子吧,比如dubbo接口,原本是只接收A,B参数,现在你加了一个参数C,就可以考虑这样处理:
//老接口 void oldService(A,B){ //兼容新接口,传个null代替C newService(A,B,null); } //新接口,暂时不能删掉老接口,需要做兼容。 void newService(A,B,C){ ... }
15. 采取措施避免运行时错误
优秀的后端开发,应该在编写代码阶段,就采取措施,避免运行时错误,如数组边界溢出,被零整除,空指针等运行时错误。类似代码比较常见:
String name = list.get(1).getName(); //list可能越界,因为不一定有2个元素哈
所以,应该采取措施,预防一下数组边界溢出,正例如下:
if(CollectionsUtil.isNotEmpty(list)&& list.size()>1){ String name = list.get(1).getName(); }
文章来源:1,