跳转至

知学云 Java 编码规范

本规范根据产品中心实践过程中经验累积,结合美团、阿里等大厂公开规范整理出一套Java编码规范,为知学云Java开发人员提供规范与指引。

修订记录

提交者 更新日期 备注
产品中心 2020.1.8 修改
产品中心 2020.2.8 定稿
架构部 2020.5.12 优化正反例格式、添加错误码规范

通用规范

知学云规范

  1. 【强制】不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。

    正例:

    private static final String REG = "\\d+";
    boolean isMatch = Pattern.matches(REG, content);
    
    反例:
    // 不允许直接将常量直接使用
    boolean isMatch = Pattern.matches("\\d+", content);
    

  2. 【强制】关于基本数据类型与包装数据类型的使用标准如下

    • 所有的 Entity 类属性必须使用包装数据类型
    • RPC 方法的返回值和参数必须使用包装数据类型

    正例:

    // 考试分数,默认为 null,能够很好的区分是否参与考试获得分数
    Integer score;
    

    反例:

    // 考试分数,默认值 0 ,不能区分是参与考试获取的结果还是未参与考试
    int score;
    

  3. 【强制】Java类中配置文件通过注入Environment或者实现EnvironmentAware方式获取

    正例:

    private String value;
    
    @Autowired
    public void setEnvironment(Environment env) {
    value = env.getProperty("some.things", String.class, "some things");
    }
    

    反例:

    /**
    *  不允许用这种注解方式获取
    */
    @Value("#{xxxxx}")
    private String value;
    

  4. 【强制】所有的相同类型的包装类对象之间值的比较,全部使用equals方法比较

  5. 【强制】Objectequals方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals

    正例:

    "some string".equals(other);
    

    反例:

    other.equals("some string");
    

  6. 【强制】所有的覆写方法,必须加@Override 注解

  7. 【强制】禁止通过一个类的对象引用访问此类的静态变量或静态方法,直接用类名来访问即可

  8. 【强制】常量定义staticfinal前面

    正例:

    public static final String EXAMPLE = "example";
    

    反例:

    public final static String EXAMPLE = "example";
    

  9. 【强制】Optional严禁直接使用get()方法

    正例:

    Optional<T> opt = Optional.of(object);
    opt.ifPresent(o -> xxx);
    

    反例:

    Optional<T> opt = Optional.of(object);
    opt.get();
    
  10. 【强制】数据值是否为空不明确,则通过Optional.ofNullable()方法进行包装

    正例:

    Optional<T> opt = Optional.ofNullable(object);
    

    反例:

    Optional<T> opt = Optional.of(object);
    

  11. 【强制】工具类、常量类不应有public构造器,应该添加private构造函数,防止被实例化

    正例:

    public final class Constant {
        private Constant() {
    
        }
    }
    

    反例:

    public final class Constant {
        // 默认无参构造函数
        public Constant() {
    
        }
    }
    

  12. 【强制】不能使用过时的类或方法

    说明:接口提供方既然明确是过时接口,那么有义务同时提供新的接口;作为调用方来说,有义务去考证过时方法的新实现是什么,并及时作出调整。

  13. 【强制】通过stream转换成map的键值对,必须过滤空值key

    正例:

    examples.stream().filter(l -> null != key).collect(Collectors.toMap(key -> key, value -> value));
    

    反例:

    examples.stream().collect(Collectors.toMap(key -> key, value -> value));
    
  14. 【强制】maporElseorElseGetorElseThrow必须接收并使用返回值,否则使用ifPresent方法即可;orElseThrow修改为ErrorCode.xxx.throwIf(boolean)

    正例:

    error.throwIf(!Optional.ofNullable(object).isPresent)
    

    反例:

    Optional.orElseThrow(() -> new Exception(error));
    
  15. 【强制】方法复杂度值最大允许为15,否则必须整改以降低复杂度

    方法:根据实际情况将条件分支提取方法,减少复杂度,保证方法的可读性

    反例:

    if (条件1) {
    
    } else if (条件2 && 条件3) {
    
    } else if (条件4 && 条件5) {
    
    }
    ...
    
  16. 【强制】jooq中使用fetchOne多个值返回报异常;不确定数据量时,查询带上limit 1获取数据

  17. 【强制】数据获取必须按需查询,避免没有必要的资源消耗

    正例:

    Field<?>[] fields = {field1, field2};
    e.select(Fields.start().add(fields).end())
        .from(TABLE)
        .where(conditions)
        .fetch(r -> r.into(fields).into(TABLE.class)));
    
  18. 【强制】请求的url的长度大于2048,则需要由get请求修改成post请求

  19. 【推荐】所有的局部变量(jdk基本数据类型)使用基本数据类型

  20. 【推荐】Java类中Bean注入方式,将注解标注在set方法之上

    正例:

    private Dao dao;
    
    @Autowired
    public void setDao(Dao dao) {
        this.dao = dao;
    }
    

    反例:

    @Autowired
    private Dao dao;
    
  21. 【推荐】类内方法定义的顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter方法

  22. 【推荐】当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读

  23. 【推荐】方法尽量单一功能,有相同含义不同参数,使用方法重载

    正例:

    public int sum(int a, int b) { 
        return a + b;
    } 
    
    public int sum(int a, int b, int c) { 
        return sum(a, b) + c;
    }
    
  24. 【建议】递归的跳出条件需要前置,尽量写尾递归

    说明:在没有递归到底之前,那些中间变量会一直保存着,因此每一次递归都需要开辟一个新的栈空间,较大的数很容易就栈溢出了。便可以通过每轮递归结束后刷新当前的栈空间,复用了栈,解决递归的栈溢出问题,像这样的return后面不附带任何变量的递归写法,也就是递归发生在函数最尾部,我们称之为'尾递归'。

    正例:

    public int func(final int fact, final int num) {
        // 尾递归
        return num == 1 ? fact : func(fact * num, num - 1);
    }
    

    反例:

    public int func(final int num) { 
        if (num == 1) {
            return num;
        } else {
            return num * func(num - 1);
        } 
    }
    

命名风格

  1. 【强制】所有的字符串常量值里不能出现大写, 多个单词用-号连接, 包括URL

    // 不能写成 typeOne
    public static final String TYPE = "type-one"; 
    
    // 不能写成batchDelete
    @RequestMapping(value = "/user/batch-delete") 
    
  2. 【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。

    反例:_name / __name/ $name /name_ / name$/ name__

  3. 【强制】代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。不允许直接使用拼音 !!! note "" 说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,即使纯拼音命名方式也要避免采用。

    正例:exam / zhixueyun / shenzhen ,通用的中文名称(如城市名称、公司名称等)可视同英文。
    反例:KaoShiChengJi [考试成绩] / getPingfenByName() [评分] / int 某变量 = 3

  4. 【强制】类名使用 UpperCamelCase 风格。

    正例:UserService /ExamService / ErrorCode 反例:userService / examService /ErrorCODE

  5. 【强制】方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格,必须遵从驼峰形式。

    正例:localValue / getMessage() / memberId

  6. 【强制】抽象类命名使用 AbstractBase 开头;异常类命名使用 Exception 结尾;测试类命名以它要测试的类名开始,以 Test 结尾;枚举类命名使用Enum后缀;常量类命名使用Content后缀。

  7. 【强制】常量、枚举成员命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。

  8. 【强制】类型与中括号紧挨相连来定义数组。 正例:

    // 数组定义
    // 方式一:初始空数组,设置长度
    int[] arr = new int[10];
    // 方式二:初始带有数据的数组,没有数据则可以直接 arr = {}
    int[] arr = {1,2,3,.....};
    

  9. 【强制】Entity类中布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误。

    反例:boolean isSuccess;

  10. 【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。

  11. 【强制】杜绝完全不规范的缩写,避免望文不知义。

    反例:kscj[考试成绩]

  12. 【强制】long 或者 Long 初始赋值时,使用大写的 L,不能是小写的 l,小写容易跟数字 1混淆,造成误解。

  13. 【推荐】如果模块、接口、类、方法使用了设计模式,在命名时体现出具体模式。

    说明:将设计模式体现在名字中,有利于阅读者快速理解架构设计理念。

    正例:OrderFactory / LoginProxy / ResourceObserver

  14. 【推荐】为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达其意。

代码格式

  1. 【强制】大括号的使用约定。如果是大括号内为空,则简洁地写成{}即可,不需要换行;如果是非空代码块则:

    • 左大括号前不换行
    • 左大括号后换行
    • 右大括号前换行
    • 右大括号后还有 else 等代码则不换行;表示终止的右大括号后必须换行
  2. 【强制】 左小括号和字符之间不出现空格;同样,右小括号和字符之间也不出现空格

  3. 【强制】if / for / while / switch / do 等保留字与括号之间都必须加空格
  4. 【强制】任何二目、三目运算符的左右两边都需要加一个空格

    说明:运算符包括赋值运算符=、逻辑运算符&&、加减乘除符号等

  5. 【强制】采用 4 个空格缩进,禁止使用 tab字符

  6. 【强制】注释的双斜线与注释内容之间有且仅有一个空格。
  7. 【强制】单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则:

    • 第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进,参考下文示例
    • 运算符与下文一起换行
    • 方法调用的点符号与下文一起换行
    • 方法调用时,多个参数,需要换行时,在逗号后进行
    • 在括号前不要换行
  8. 【强制】方法参数在定义和传入时,多个参数逗号后边必须加空格。

    // 代码格式——正例
    public static void main(String[] args) {
        // 缩进 4 个空格
        String say = "hello";
        // 运算符的左右必须有一个空格 int flag = 0;
        // 关键词 if 与括号之间必须有一个空格,括号内的 f 与左括号,0 与右括号不需要空格 
        if (flag == 0) {
            System.out.println(say);
        }

        // 左大括号前加空格且不换行;左大括号后换行
        if (flag == 1) {
            System.out.println("world");
            // 右大括号前换行,右大括号后有 else,不用换行
        } else { 
            System.out.println("ok");
            // 在右大括号后直接结束,则必须换行
        }

        StringBuffer sb = new StringBuffer();
        // 超过 120 个字符的情况下,换行缩进 4 个空格,点号和方法名称一起换行
        sb.append("zi").append("xin")...
        .append("huang")...
        .append("huang")...
        .append("huang");
    }

集合处理

  1. 【强制】使用工具类 Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。

    说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。

     String[] str = new String[] { "you", "me" };
     List list = Arrays.asList(str);
     // 运行时异常
     list.add("he");
    
     // list.get(0) 也会随之修改
     str[0] = "she"
    
  2. 【强制】关于 hashCodeequals 的处理,遵循如下规则:

    • 只要重写 equals,就必须重写 hashCode
    • 因为 Set 存储的是不重复的对象,依据 hashCodeequals 进行判断,所以 Set 存储的对象必须重写这两个方法。
    • 如果自定义对象作为 Map 的键,那么必须重写 hashCodeequals

    说明:String 重写了 hashCodeequals 方法,所以我们可以非常愉快地使用 String 对象作为 key 来使用。

  3. 【强制】ArrayListsubList 结果不可强转成 ArrayList,否则会抛出 ClassCastException 异常,即 java.util.RandomAccessSubList cannot be cast to java.util.ArrayList

    说明:subList 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList ,而是ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。

  4. 【强制】不要在 foreach 循环里进行元素的 remove / add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。

    正例:

    Iterator<String> iterator = list.iterator();
    while (iterator.hasNext()) {
        String item = iterator.next();
        if (删除元素的条件) {
            iterator.remove();
        }
    }  
    

    反例: java List<String> list = new ArrayList<String>(); list.add("1"); list.add("2"); for (String item : list) { if ("1".equals(item)) { list.remove(item); } }

  5. 【强制】 在 JDK7 版本及以上,Comparator 要满足如下三个条件,不然 Arrays.sortCollections.sort 会报 IllegalArgumentException 异常。

    说明:三个条件如下
    1) x,y 的比较结果和 y,x 的比较结果相反
    2) x>y,y>z,则 x>z
    3) x=y,则 x,z 比较结果和 y,z 比较结果相同

    反例:下例中没有处理相等的情况,实际使用中可能会出现异常:

    new Comparator<Student>() {
        @Override
        public int compare(Student o1, Student o2) {
            return o1.getId() > o2.getId() ? 1 : -1;
        }
    };
    

  6. 【推荐】高度注意 Map 类集合 K/V 能不能存储 null 值的情况,如下表格:

    集合类 Key Value Super 说明
    Hashtable 不允许为 null 不允许为 null Dictionary 线程安全
    ConcurrentHashMap 不允许为 null 不允许为 null AbstractMap 锁分段技术(JDK8:CAS)
    TreeMap 不允许为 null 允许为 null AbstractMap 线程不安全
    HashMap 允许为 null 允许为 null AbstractMap 线程不安全

    由于 HashMap的干扰,很多人认为ConcurrentHashMap 是可以置入 null 值,而事实上,存储 null 值时会抛出 NPE异常。

Service API 规范

  1. 【强制】写service接口的时候, 必须能保证一个操作在一个方法里做完

  2. 【强制】所有接口方法都要打上@Transactional标记, 所有的查询方法都要加上readonly=true

方法名

  1. 【推荐】插入方法前缀insert, 更新方法前缀update, 删除方法前缀delete, 根据id查找前缀get, 其它查找前缀find

  2. 【推荐】如果方法名后缀与接口名前缀相同, 则省略

  3. 【推荐】如果同类型的方法有多个, 则根据参数添加后缀区分

    正例:

    public class RoleService {
        Role insert();
    
        Role get();
    
        // 这里的getByXxx根据参数添加名称后缀
        Role getByName(String name);
    
        Role getByType(Integer type);
    }
    

    反例:

    public class RoleService {
        // 这里的insertRole应该省略Role
        Role insertRole();
    
        // 同理getRole改为get
        Role getRole();
    }
    

方法参数

  1. 【强制】非必传参数必须用Optional包起来, 反言之, 没有用Optional包起来的参数都是必传参数

    正例:

    public String demo(String id, Optional<String> name);
    

  2. 【强制】参数没有超过10个以上, 不能传实体

    正例:

    public User insert(String name, Optional<String> fullName)   
    

    反例: java public User insert(User user)

方法返回值

  1. 【强制】接口方法不允许返回值为void
  2. 【强制】List类型的返回值不允许返回null, 如果没有数据返回空List

MQ 规范

下文的消息是指用户执行业务操作时系统生产的MQ消息

所有的消息都使用com.zxy.common.base.message.Message类发送与接收

消息分类(type属性)

所有的类型都是5位数, 从左到右定义如下:

  • 第1位表示模块类别, 如: 人资模块为 1, 考试模块为2

  • 第2-3位表示业务类别, 如: 人员操作为 01, 部门操作为02

  • 第4-5位表示操作类别, 如: 添加人员为01, 修改人员为02

完整示例: 10101 为添加人员消息

消息头(headers属性)

headers属性是键值对, 键和值都是String类型,一般用来存放消息相关的一些ID

消息体(payload属性)

payload属性可以用来传一些实体对象

提示

一般来说,这个属性是不允许设值,如果有需求,请在开发群里发起讨论,根据讨论结果确定(详见异步开发规范-4)。

消息定义方式

每个工程的api模块中都应该有以下消息定义常量类:

  • MessageTypeContent:消息类型
  • MessageHeaderContent:消息头

异步开发规范

  1. 【强制】异步中根据id查询数据必须使用getOptional, 否则容易出现异常

    如果异步消费时数据已经在其他场景删除了,则会抛出异常。

    正例:

    dao.getOptional(id).ifPresent(obj -> xxxx);
    

    反例:

    dao.get(id)
    

  2. 【强制】字段按需修改

    正例:

    dao.execute(e -> e.update(TABLE)
                   .set(TABLE.NAME, "example")
                   .where(TABLE.ID.eq("id"))
                   .execute());
    

    jooqDSLContext中update语句必须要调用execute才会执行。

    反例:

    dao.update(xxx);
    

  3. 【强制】未知数据大小使用分页处理数据,否则容易造成内存溢出

    正例:

    // 查询的时候分页,再处理
    List<Object> list = dao.limit(page * pageSize, pageSize).fetch(xxx);
    

    反例:

    // 不允许一次性查询list数据内存中处理
    List<Object> list = dao.fetch(xxx);
    

  4. 【强制】异步消息中参数传递,不使用Payload传数据,保证关键数据可查可追溯,否则MQ故障之后,数据无法找回

    真实案例:考试提交异步,将用户考试答题记录通过Payload传递,而MQ发送前未做任何存储处理,一旦MQ发生故障,数据丢失,是非常危险的。

  5. 【强制】参数大数据量时,在异步中查询数据,进行分批处理

  6. 【推荐】异步中数据处理,为提升性能、提高吞吐量以及避免调用rpc带来的消耗,可以直接使用Dao操作数据

错误码规范

  1. 【强制】需要自定义错误码的工程api模块中都应该有ErrorCode常量类,其实现com.zxy.common.base.exception.Code接口
  2. 【强制】系统自定义异常使用com.zxy.common.base.exception.UnprocessableException抛出
  3. 【推荐】系统自定义异常抛出的几种写法
    • 直接throw UnprocessableException
      throw new UnprocessableException(ErrorCode.CODE);
      
    • 使用Code.throwIf,当条件为true时抛出异常,适用于逻辑条件单一场景
      ErrorCode.CODE.throwIf(null == a);
      
    • 使用Code.throwUnless,当条件为false时抛出异常,适用于逻辑条件单一场景
      ErrorCode.CODE.throwUnless(a.isPresent());
      
    • 使用CodeSupplier接口的get()实现,适用于Optional.orElseThrow()
      Optional.ofNullable(a).orElseThrow(ErrorCode.CODE);
      
    • 使用Code接口的wrap方法,尝试执行wrap函数并捕获指定异常并自动转换为UnprocessableException抛出
      // 示例1:不指定要捕获的异常
      ErrorCode.CODE.wrap(() -> {
          // this is some try codes...
      });
      // 示例2:指定要捕获的异常类
      ErrorCode.CODE.wrap(() -> {
          // this is some try codes...
      }, IOException.class);
      
  4. 【附录】系统级错误码一览表

    模块 错误码 说明
    common-restful-support 900000 参数校验失败
    common-restful-support 900001 参数必填
    common-restful-support 900002 格式不匹配
    common-restful-support 900003 文件大小超出
    common-restful-support 900004 不允许的文件类型
    common-rpc 900005 数据不存在
    common-restful-support 900008 超出时间范围内允许访问次数
    common-restful-support 900009 超出当日允许访问次数
    common-rpc 910000 数据错误
    common-office 910001 数据类型错误
    common-office 910002 长度不匹配
    common-office 910003 必填
    common-office 910004 邮箱不合法
    common-office 910005 手机号码不合法
    common-office 910006 身份证不合法
    common-office 910007 编码不合法
    common-office 910008 序号不合法
    common-office 910009 超过最大限制行数

    业务错误码见业务系统错误码