1.不要为了方便,直接在代码中使用0,1等魔法值,应该要用enum枚举代替。
正例:
if(UserVipEnum.NOT_VIP.getCode.equals(userInfo.getVipFlag)){
//非会员,提示去开通会员
tipOpenVip(userInfo);
}else if(UserVipEnum.VIP.getCode.equals(userInfo.getVipFlag)){
//会员,加勋章返回
addMedal(userInfo);
}
public enum UserVipEnum {
VIP("1","会员"),
NOT_VIP("0","非会员"),:;
private String code;
private String desc;
UserVipEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
}
2.注意检验空指针,不要轻易相信业务,说正常逻辑某个参数不可能为空。
NullPointerException 在我们日常开发中非常常见,我们代码开发过程中,一定要对空指针保持灵敏的嗅觉。
主要有这几类空指针问题:
- 包装类型的空指针问题
- 级联调用的空指针问题
- Equals方法左边的空指针问题
- ConcurrentHashMap 类似容器不支持 k-v为 null。
- 集合,数组直接获取元素
- 对象直接获取属性
反例:
public class NullPointTest {
public static void main(String[] args) {
String s = null;
// s可能为空,会导致空指针问题
if (s.equals("666")) {
System.out.println("啦啦啦,啦啦啦!");
}
}
}
3. 通知类(如发邮件,有短信)的代码,建议异步处理。
假设业务流程这样:需要在用户登陆时,添加个短信通知它的粉丝。 很容易想到的实现流程如下:
假设提供sendMsgNotify服务的系统挂了,或者调用sendMsgNotify失败了,那么用户登陆就失败了。。。
一个通知功能导致了登陆主流程不可用,明显的捡了芝麻丢西瓜。那么有没有鱼鱼熊掌兼得的方法呢?有的,给发短信接口捕获异常处理,或者另开线程异步处理,如下:
因此,添加通知类等不是非主要,可降级的接口时,应该静下心来考虑是否会影响主要流程,思考怎么处理最好。
4. 处理Java日期时,当心YYYY格式设置的问题。
日常开发中,我们经常需要处理日期。我们要当时日期格式化的时候,年份是大写YYYY的坑。
Calendar calendar = Calendar.getInstance();
calendar.set(2019, Calendar.DECEMBER, 31);
Date testDate = calendar.getTime();
SimpleDateFormat dtf = new SimpleDateFormat("YYYY-MM-dd");
System.out.println("2019-12-31 转 YYYY-MM-dd 格式后 " + dtf.format(testDate));
运行结果:
2019-12-31 转 YYYY-MM-dd 格式后 2020-12-31
为什么明明是2019年12月31号,就转了一下格式,就变成了2020年12月31号了?因为YYYY是基于周来计算年的,它指向当天所在周属于的年份,一周从周日开始算起,周六结束,只要本周跨年,那么这一周就算下一年的了。正确姿势是使用yyyy格式。
5.static静态变量不要依赖spring实例化变量,可能会导致初始化出错
之前看到项目有类似的代码。静态变量依赖于spring容器的bean。
private static SmsService smsService = SpringContextUtils.getBean(SmsService.class);
这个静态的smsService有可能获取不到的,因为类加载顺序不是确定的,而以上的代码,静态的smsService初始化强制依赖spring容器的实例了。正确的写法可以这样,如下:
private static SmsService smsService =null;
//使用到的时候采取获取
public static SmsService getSmsService(){
if(smsService==null){
smsService = SpringContextUtils.getBean(SmsService.class);
}
return smsService;
}
6. 不要用一个Exception捕捉所有可能的异常。
反例:
public void test(){
try{
//…抛出 IOException 的代码调用
//…抛出 SQLException 的代码调用
}catch(Exception e){
//用基类 Exception 捕捉的所有可能的异常,如果多个层次都这样捕捉,会丢失原始异常的有效信息哦
log.info(“Exception in test,exception:{}”, e);
}
}
正例:
public void test(){
try{
//…抛出 IOException 的代码调用
//…抛出 SQLException 的代码调用
}catch(IOException e){
//仅仅捕捉 IOException
log.info(“IOException in test,exception:{}”, e);
}catch(SQLException e){
//仅仅捕捉 SQLException
log.info(“SQLException in test,exception:{}”, e);
}
}
7.注意Arrays.asList的几个坑
基本类型不能作为 Arrays.asList方法的参数,否则会被当做一个参数。
public class ArrayAsListTest {
public static void main(String[] args) {
int[] array = {1, 2, 3};
List list = Arrays.asList(array);
System.out.println(list.size());
}
}
运行结果
1
Arrays.asList 返回的 List 不支持增删操作。
public class ArrayAsListTest {
public static void main(String[] args) {
String[] array = {"1", "2", "3"};
List list = Arrays.asList(array);
list.add("5");
System.out.println(list.size());
}
}
运行结果
Exception in thread "main" java.lang.UnsupportedOperationException
at java.util.AbstractList.add(AbstractList.java:148)
at java.util.AbstractList.add(AbstractList.java:108)
at object.ArrayAsListTest.main(ArrayAsListTest.java:11)
Arrays.asList 返回的 List 并不是我们期望的 java.util.ArrayList,而是 Arrays 的内部类ArrayList。内部类的ArrayList没有实现add方法,而是父类的add方法的实现,是会抛出异常的呢。
使用Arrays.asLis的时候,对原始数组的修改会影响到我们获得的那个List
public class ArrayAsListTest {
public static void main(String[] args) {
String[] arr = {"1", "2", "3"};
List list = Arrays.asList(arr);
arr[1] = "4";
System.out.println("原始数组"+Arrays.toString(arr));
System.out.println("list数组" + list);
}
}
运行结果
原始数组[1, 4, 3]
list数组[1, 4, 3]
8.尽量使用函数内的基本类型临时变量
- 在方法函数内,基本类型参数以及临时变量,都是保存在栈中的,访问速度比较快。
- 对象类型的参数和临时变量的引用都保存在栈中,内容都保存在堆中,访问速度较慢。
- 在类中,任何类型的成员变量都保存在堆(Heap)中,访问速度较慢。
public class AccumulatorUtil {
private double result = 0.0D;
//反例
public void addAllOne( double[] values) {
for(double value : values) {
result += value;
}
}
//正例,先在方法内声明一个局部临时变量,累加完后,再赋值给方法外的成员变量
public void addAll1Two(double[] values) {
double sum = 0.0D;
for(double value : values) {
sum += value;
}
result += sum;
}
}
8. 修改对外老接口的时候,思考接口的兼容性。
很多bug都是因为修改了对外老接口,但是却不做兼容导致的。关键这个问题多数是比较严重的,可能直接导致系统发版失败的。
所以,如果你的需求是在原来接口上修改,,尤其这个接口是对外提供服务的话,一定要考虑接口兼容。举个例子吧,比如dubbo接口,原本是只接收A,B参数,现在你加了一个参数C,就可以考虑这样处理。
//老接口
void oldService(A,B);{
//兼容新接口,传个null代替C
newService(A,B,null);
}
//新接口,暂时不能删掉老接口,需要做兼容。
void newService(A,B,C);
9. 调用第三方接口,需要考虑异常处理,安全性,超时重试这几个点。
日常开发中,经常需要调用第三方服务,或者分布式远程服务的的话,需要考虑:
- 异常处理(比如,你调别人的接口,如果异常了,怎么处理,是重试还是当做失败)
- 超时(没法预估对方接口一般多久返回,一般设置个超时断开时间,以保护你的接口)
- 重试次数(你的接口调失败,需不需要重试,需要站在业务上角度思考这个问题)
简单一个例子,你一个http请求调别人的服务,需要考虑设置connect-time,和retry次数。
10. 使用spring事务功能时,注意这几个事务未生效的坑
日常业务开发中,我们经常跟事务打交道,事务失效主要有以下几个场景:
- 底层数据库引擎不支持事务
- 在非public修饰的方法使用
- rollbackFor属性设置错误
- 本类方法直接调用
- 异常被try...catch吃了,导致事务失效。
反例:
注解的事务方法给本类方法直接调用,事务失效
public class TransactionTest{
public void A(){
//插入一条数据
//调用方法B (本地的类调用,事务失效了)
B();
}
@Transactional
public void B(){
//插入数据
}
}
如果用异常catch住,那事务也是会失效,伪代码如下:
@Transactional
public void method(){
try{
//插入一条数据
insertA();
//更改一条数据
updateB();
}catch(Exception e){
logger.error("异常被捕获了,不发生回滚,事务就失效咯",e);
}
}
11. 接口需要考虑幂等性
接口是需要考虑幂等性的,尤其抢红包、转账这些重要接口。最直观的业务场景,就是用户连着点两次,你的接口有没有hold住。
幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。
一般幂等技术方案有这几种:
- 查询操作
- 唯一索引
- token机制,防止重复提交
- 数据库的delete/update操作
- 乐观锁
- 悲观锁
- Redis、zookeeper 分布式锁(以前抢红包需求,用了Redis分布式锁)
- 状态机幂等
12. 手动写完代码业务的SQL,先拿去数据库跑一下,同时也explain看下执行计划。
手动写完业务代码的SQL,可以先把它拿到数据库跑一下,看看有没有语法错误嘛。有些小伙伴不好的习惯就是,写完就把代码打包上去测试服务器,其实把SQL放到数据库执行一下,可以规避很多错误的。
同时,也用explain看下你Sql的执行计划,尤其走不走索引这一块。
explain select * from user where userid =10086 or age =18;
13. 协议方法参数值非空,避免不必要的空指针判断
协议编程,可以@NonNull和@Nullable标注参数,是否遵循全凭调用者自觉。
反例:
public static boolean isValid(UserDO user) {
if (Objects.isNull(user)) {
return false;
}
return Boolean.TRUE.equals(user.getIsValid());
}
正例:
public static boolean isValid(@NonNull UserDO user) {
return Boolean.TRUE.equals(user.getIsValid());
}
14.协议方法返回值非空,避免不必要的空指针判断
协议编程,可以@NonNull和@Nullable标注参数,是否遵循全凭实现者自觉。
反例:
// 定义接口
public interface OrderService {
public List<OrderVO> queryUserOrder(Long userId);
}
// 调用代码
List<OrderVO> orderList = orderService.queryUserOrder(userId);
if (CollectionUtils.isNotEmpty(orderList)) {
for (OrderVO order : orderList) {
...
}
}
正例:
// 定义接口
public interface OrderService {
@NonNull
public List<OrderVO> queryUserOrder(Long userId);
}
// 调用代码
List<OrderVO> orderList = orderService.queryUserOrder(userId);
for (OrderVO order : orderList) {
...
}