JAVA|后端编码规范

目录

零、引言

一、基础

二、集合

三、并发

四、日志

五、安全


零、引言

规范等级:

  • 【强制】:强制遵守,来源于线上历史故障,将通过工具进行检查。
  • 【推荐】:推荐遵守,来源于日常代码审查、开发人员反馈和行业经验。

一、基础

序号

等级

规范

示例  

说明

1

强制

在POJO类中定义布尔类型成员变量时,禁止用is作变量名前缀。

反例:                                                                                                                                          

private boolean isDeleted;

// Getter方法:IDEA生成,与变量同名。

public boolean isDeleted() {

    return isDeleted;

}

// Setter方法:IDEA生成

public void setDeleted(boolean deleted) {

    isDeleted = deleted;

}

is作变量名前缀的布尔型成员变量,在一些IDE(如:IDEA)中,默认生成的getter方法与变量名相同,导致部分框架(如:Jackson、Fastjson)在反向解析时会引发“找不到指定成员变量名”的错误。
2

强制

在对象之间做相等比较时,应当使用Objects工具类(java.util.Objects)的equals方法。

正例:

String versionNo = "6.6.1";

// 如param为null,不会出现异常。

Objects.equals(param, versionNo);

反例:

String versionNo = "6.6.1";

// 如param为null,会出现异常。

param.equals(versionNo);

当对象为null时,直接调用equals会出现空指针异常。

注意:Objects的equals方法内部会利用参数对象的equals方法进行比较,对数组、集合之类的对象并不会做内部元素的一一比较。

3

强制

在BigDecimal之间做等值比较时,禁止使用equals方法。

正例:

// 商品原价格

BigDecimal skuPrice = new BigDecimal("120.00");

// 商品优惠后价格

BigDecimal skuPromoPrice = new BigDecimal("120.0");

// 使用compareTo做比较,不会比较精度。

if (skuPrice.compareTo(skuPromoPrice) == 0) { // true

    // ...

}

反例:

// 商品原价格

BigDecimal skuPrice = new BigDecimal("120.00");

// 商品优惠后价格

BigDecimal skuPromoPrice = new BigDecimal("120.0");

// 使用equals做比较,会比较精度。

if (skuPrice.equals(skuPromoPrice)) { // false

    // ...

}

BigDecimal的equals方法会比较精度,如1.0与1.00比较的结果为false,推荐使用其

compareTo方法做比较。

4强制在浮点数之间做等值比较时,基本类型禁止使用==,包装类型禁止使用equals。

正例:

// 下方减法运算是为了故意引起浮点误差

// 时段一的单价:充电站单价 - 优惠价

float timePrice1 = 1.0F - 0.9F;

// 时段二的单价:充电站单价 - 优惠价

float timePrice2 = 0.9F - 0.8F;

// 工具类推荐:org.apache.commons.lang3.math.NumberUtils

// 将浮点数转为BigDecimal并指定保留位数和取舍方式

BigDecimal decimalTimePrice1 = NumberUtils.toScaledBigDecimal(timePrice1, 2, RoundingMode.HALF_UP);

BigDecimal decimalTimePrice2 = NumberUtils.toScaledBigDecimal(timePrice2, 2, RoundingMode.HALF_UP);

if (decimalTimePrice1.compareTo(decimalTimePrice2) == 0) { // true

    // ...

}

反例:

// 下方减法运算是为了故意引起浮点误差

// 时段一的单价:充电站单价 - 优惠价

float timePrice1 = 1.0F - 0.9F;

// 时段二的单价:充电站单价 - 优惠价

float timePrice2 = 0.9F - 0.8F;

//  可能存在浮点误差,等式不成立。

if (timePrice1 == timePrice2) { // false

    // ...

}

反例:

// 下方减法运算是为了故意引起浮点误差

// 时段一的单价:充电站单价 - 优惠价

Float timePrice1 = 1.0F - 0.9F;

// 时段二的单价:充电站单价 - 优惠价

Float timePrice2 = 0.9F - 0.8F;

// 可能存在浮点误差,等式不成立。

if (timePrice1.equals(timePrice2)) { // false

    // ...

}

浮点数先转成BigDecimal,再用compareTo方法做比较,以避免精度问题影响结果。

浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。二进制无法精确表示大部分的十进制小数。

5

强制

将浮点数转换为BigDecimal 时,禁止直接使用构造方法。

正例:

// 商品SKU单价

double skuPrice = 0.1D;

// 工具类推荐:org.apache.commons.lang3.math.NumberUtils

// 精度不影响,仍然是0.1。

BigDecimal decimalSkuPrice = NumberUtils.createBigDecimal(Double.toString(skuPrice));

反例:

// 商品SKU单价

double skuPrice = 0.1D;

// 精度受影响,可能是0.1000000000000000055511151231257827021181583404541015625。

BigDecimal decimalSkuPrice = new BigDecimal(skuPrice);

BigDecimal的浮点数构造方法存在精度损失风险,在精确计算或值比较的场景中会导致业务逻辑异常。

注意:和上一条的正例不同的是,前者在经过浮点计算后已经形成了误差,在转换时用toScaledBigDecimal方法限定了精度。在此处,是为了避免因直接利用浮点数构造而导致的误差。

6强制在空指针异常易发的场景中,应当对对象做null判断。

正例:

@Resource

private UserService userService;

public String queryCityName(String userId) {

    User userObj = userService.queryUserInfo(userId);

    Optional<User> userData = Optional.ofNullable(userObj);

    Optional<UserAddress> userAddress = userData.map(User::getUserAddress);

    // 如用户地址为null,不会空指针异常。

    Optional<String> optionalCityName = userAddress.map(UserAddress::getCiyName);

    // null时返回默认值

    String cityName = optionalCityName.orElse(DEFAULT_CITY);

    return cityName;

}  

反例:

@Resource

private UserService userService;

public String queryCityName(String userId) {

    User userObj = userService.queryUserInfo(userId);

    // 如用户地址为null,会空指针异常。

    String cityName = userObj.getUserAddress().getCiyName();

    return cityName;

}

在对象未判断null的情况下直接引用,容易发生空指针异常,推荐用Optional类更优雅的处理null对象。

一些空指针异常(NPE)易发的场景:

  1. 返回类型为基本数据类型,return包装数据类型的对象时,自动拆箱有可能产生NPE。
  2. 数据库的查询结果可能为null
  3. 集合里的元素即使isNotEmpty,取出的数据元素也可能为null。
  4. 远程调用返回对象时,一律要求进行空指针判断,防止NPE。
  5. 对于Session中获取的数据,建议进行NPE检查,避免空指针。
  6. 级联调用obj.getA().getB().getC(),一连串调用,易产生NPE。

7

强制

在日期格式化时,应当使用DateUtils工具类

正例

Calendar calendar = Calendar.getInstance();

// 日期是2023-12-31

calendar.set(20231131);

// strDateTime:2023-12-31

String strDateTime = DateUtils.datesToString(calendar.getTime());

反例

Calendar calendar = Calendar.getInstance();

// 日期是2023-12-31

calendar.set(20231131);

// 误用了大写Y格式化年份

SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYY-MM-dd");

// strDateTime:2024-12-31

String strDateTime = simpleDateFormat.format(calendar.getTime());

DateUtils包装了常用的日期格式,避免了手动格式化时的误用风险,如:年份格式化误书写成YYYY,如本周存在跨年的情况,返回的就是下一年。

8强制POJO类属性,禁止使用基本数据类型。

正例

@Resource

private UserRepository userRepository;

public Integer queryUserId(String phone) {

    // getUserIdByPhone返回的是Integer类型,当返回null时,不会出现空指针异常。

    Integer userId = userRepository.getUserIdByPhone(phone);

    // 可以做防御性校验,以避免直接空指针

    if(userId != null){

        // …

    }

    // …

}

反例:

@Resource

private UserRepository userRepository;

public int queryUserId(String phone) {

    //  getUserIdByPhone返回的是Integer类型,当返回null时,会出现空指针异常。

    int userId = userRepository.getUserIdByPhone(phone);

    // …

}

POJO类属性使用包装数据类型有如下优点:

  1. 若值为null时,能显式的提醒使用者做相应的处理,而不是使用基本数据类型的默认值。
  2. 避免对象之间的自动拆装箱时出现NPE。

二、集合

序号

等级

规范

示例

说明

1

强制

在需要对List的subList方法返回结果进行遍历、增加、删除元素时,禁止直接变更原List中的元素。

反例:

List<String> cityList = new ArrayList<>();

cityList.add("北京");

cityList.add("上海");

cityList.add("广州");

List<String> citySubList = cityList.subList(01);

// subList之后变更原List

cityList.add("深圳");

// 出现ConcurrentModificationException异常

for (int i = 0; i < citySubList.size(); i++) {

    // ...

}

List的subList方法返回的是一个List的内部类对象,它是List的一个视图,对原List所有的操作都会反映到这个对象上。

同时,对原List进行元素的增加或删除,会被计数(modCount,结构变更次数),此数值和原subList时记录的数值(expectedModCount)不一致,会引发ConcurrentModification

Exception异常。

2强制

对集合进行for循环时,禁止在循环体内用remove/add方法。

正例:

List<String> cityList = new ArrayList<>();

cityList.add("北京");

cityList.add("上海");

cityList.add("广州");

Iterator<String> iterator = cityList.iterator();

while (iterator.hasNext()) {

    String city = iterator.next();

    // 工具类推荐:org.apache.commons.lang3.StringUtils

    if (StringUtils.equals("广州", city)) {

        iterator.remove();

    }

}

反例:

List<String> cityList = new ArrayList<>();

cityList.add("北京");

cityList.add("上海");

cityList.add("广州");

// 出现ConcurrentModificationException异常

for (String city : cityList) {

    // 工具类推荐:org.apache.commons.lang3.StringUtils

    if (StringUtils.equals("广州", city)) {

        cityList.remove(city);

    }

}

若在for循环体内对集合元素进行remove/add操作,可能导致异常,建议使用iterator方式处理。

三、并发

序号

等级

规范

示例

说明

1

强制

动态线程池只允许使用通义管理平台定义的(比如Poseidon),禁止自行创建。

正例:

// poseidonThreadPool在Poseidon管理平台已创建

@Resource(name = "poseidonThreadPool")

private ThreadPoolExecutor poseidonThreadPool;

CompletableFuture.supplyAsync(() -> {

    // ...

}, poseidonThreadPool);

反例:

// 自建线程池,未接入Poseidon。

ThreadPoolExecutor customThreadPool = new ThreadPoolExecutor(corePoolSize,

                    maximumPoolSize,

                    keepAliveTime,

                    unit,

                    workQueue,

                    new ThreadPoolExecutor.AbortPolicy());

CompletableFuture.supplyAsync(() -> {

    // ...

}, customThreadPool);

通过Poseidon创建的线程池将能较好的进行管理和监控:

  1. 支持通过Poseidon动态修改线上线程池参数(线程数量、队列长度、拒绝策略),无需重启服务即可生效。
  2. CAT平台能对线程池状态进行监控和告警。
2强制

在使用线程池时,禁止将拒绝策略设置为DiscardPolicy。

反例:

// poseidonThreadPool在Poseidon管理平台已创建,且配置了DiscardPolicy拒绝策略。

@Resource(name = "poseidonThreadPool")

private ThreadPoolExecutor poseidonThreadPool;

public void process(){

    // ...

    // 队列满且已达到最大线程数后,任务会被自动抛弃。

    Future<String> resultFuture = poseidonThreadPool.submit(task);

    // ...

}

若配置了DiscardPolicy,当线程池队列排满且已达到了最大线程数后,新增任务会被直接丢弃,无任何提示,并且在结合future.get()运行时,存在阻塞的风险。

3强制

父子任务禁止使用同一个线程池。

反例:

class OrderManager {

    // Poseidon配置的线程池

    @Resource(name = "poseidonThreadPool"

    private ThreadPoolExecutor poseidonThreadPool;

    @Autowired

    private ProductManager productManager;

    public List<Order> queryOrders() throws Exception {

        // ...

        // 如果子任务出现阻塞,父任务一直等待。

        Future<Order> orderFuture = poseidonThreadPool.submit(() -> {

            // ...

            ProductInfo product = productManager.queryProducts(orderId);

            // ...

        });

        orderFuture.get();

        // ...

    }

}

class ProductManager {

    // Poseidon配置的线程池

    @Resource(name = "poseidonThreadPool")

    private ThreadPoolExecutor poseidonThreadPool;

    public List<ProductInfo> queryProducts(String orderId) throws Exception {

        // ...

        // 子任务存在多线程处理,且使用了同一个线程池。

        Future<ProductInfo> productFuture = poseidonThreadPool.submit(() -> {

            // ...

        });

        productFuture.get();

        // ...

    }

}

父子任务使用同一个线程池容易相互影响,线程数达上限时,子任务等待线程资源,而同时,父任务因子任务未完成,其资源得不到释放,最终可能导致相互等待或死锁。

4强制在多线程环境下,禁止直接使用HashMap。

正例:

// poseidonThreadPool在Poseidon管理平台已创建

@Resource(name = "poseidonThreadPool")

private ThreadPoolExecutor poseidonThreadPool;

public void process() {

     // ConcurrentHashMap是线程安全的

    Map<String, String> map = new ConcurrentHashMap<>();

    CompletableFuture.supplyAsync(() -> {

        // ...

        map.put("key1""value1");

        // ...

    }, poseidonThreadPool);

    CompletableFuture.supplyAsync(() -> {

        // ...

        map.put("key2""value2");

        // ...

    }, poseidonThreadPool);

}

反例:

// poseidonThreadPool在Poseidon管理平台已创建

@Resource(name = "poseidonThreadPool")

private ThreadPoolExecutor poseidonThreadPool;

public void process() {

    // HashMap是线程不安全的

    Map<String, String> map = new HashMap<>();

    CompletableFuture.supplyAsync(() -> {

        // ...

        map.put("key1""value1");

        // ...

    }, poseidonThreadPool);

    CompletableFuture.supplyAsync(() -> {

        // ...

       map.put("key2""value2");

        // ...

    }, poseidonThreadPool);

}

HashMap是线程不安全的,在容量不够进行resize时,可能因并发出现死链,导致CPU飙升。

四、日志

序号

等级

规范

示例

说明

1

强制

日志级别只允许使用ERROR、WARN、INFO、DEBUG。

正例:

// ...

// info打印业务关键信息

log.info("操作人id:{}", operatorUserId);

// ...

List<UserData> userDataList = userListService.selectUsers();

// debug打印开发调试信息

log.debug("开始执行时间:{}", System.currentTimeMillis());

userDataList.stream().forEach(userData -> {

    // ...

});

log.debug("结束执行时间:{}", System.currentTimeMillis());

  • ERROR:业务功能受损或无法完成预期操作,可能会造成线上故障需要预警并及时解决,否则该功能将无法正常运行。
  • WARN:异常符合预期的情况且业务不受损,不会出现线上故障,可根据实际情况选择性预警,解决时效要求不高,但需要关注。
  • INFO:用于记录系统运行过程或重要信息点,主要为故障定位、过程追溯、数据分析等提供辅助。
  • DEBUG:用于在测试或本地的非生产环境中使用,可以记录详细的信息,主要为了方便开发调试程序,在生产环境中禁止使用。
2

强制

业务受损或预期外的异常场景,应当打印ERROR日志。

正例:

try {

    // ...

    // 发送短信验证码

    loginService.sendVerificationCode(cellphone);

    // ...

catch (Exception ex) {

    // 预期外的系统错误,业务受损。

    log.error("异常场景:{}, 异常数据:{}""用户H5登录", userId, ex);

    // 异常处理

}

反例:

try {

    // ...

    // 发送短信验证码

    loginService.sendVerificationCode(cellphone);

    // ...

catch (Exception ex) {

    log.info("异常场景:{}, 异常数据:{}""用户H5登录", userId, ex);

    // 异常处理

}

ERROR日志用于描述异常不可控的场景,当该类异常发生的时候会给业务和系统带来伤害,需要第一时间告警并介入排查修复。

3强制业务不受损且预期内的异常场景,应当打印WARN日志。

正例:

public List<User> selectUserByHeight(Integer height) {

    // ...

    // 该校验偶发于用户录入出错的情况下,需观测是否频繁以指导进一步处理。

    // 如:可能上游系统BUG导致度量单位搞错

    if (height > 270) {

        log.warn("异常场景:{},身高:{} 不能大于270cm""用户H5注册",  height);

        // ...

    }

    // ...

}

WARN日志用于描述异常可控的场景,当该类异常发生的时候不会给业务和系统带来伤害,用于记录和观测,指导进一步处理。
4

推荐

打印日志时,建议使用占位符的方式拼装内容。

正例:

// 用{}填充

log.info("用户注册ID:{}", userId);

反例:

// 用+拼接

log.info("用户注册ID:" + userId);

”+“ 拼接会多次调用StringBuilder的append()方式,每一次append的时候会计算字符串的长度以及重新分配一次内存,对性能有一定的损耗。

此外,“+”拼接方式无论本条日志是否打印都会计算长度和分配内存,而占位符的方式仅在打印的时候才进行内存分配。

5

推荐

打印日志时,不建议使用JSON工具将对象转换成String。

正例

public class UserInfo {

    private String id;

    public String getUserName() throws Exception {

        throw new Exception();

    }

    @Override

    public String toString() {

        return "id:" + id;

    }

}

public void doSth (UserInfo user) {

    // ...

   if(user != null){

        // ...

        log.info("user = {}", user.toString());

    }

    // ...

}

反例:

public class UserInfo {

    // ...

    public String getUserName() throws Exception {

        throw new Exception();

    }

}

public void doSth(UserInfo user) {

    // 会抛出异常。

    log.info("user = {}", JSON.toJSONString(user));

    // ...

}

如果对象里某些get方法被覆写,存在抛出异常的风险,进而影响正常业务流程。

6

推荐

异常日志内容中应当包含三要素:异常场景、异常数据、异常堆栈。

正例:

try {

    // ...

catch (Exception ex) {

    // 日志内容里记录异常场景、异常数据、异常堆栈信息。

    log.error("异常场景:{}, 异常数据:{}""用户H5登录", userId, ex);

    // 异常处理

}

反例:

try {       

    // ...

catch (Exception ex) {

    // 打印日志不完整,没有打印异常堆栈信息、异常数据。

    log.error("系统异常");

    // 异常处理

}

异常日志内容应记录关键的信息(异常场景、异常数据、异常堆栈),为问题排查提供有效帮助,能更高效的处理线上故障。

三要素包含:

异常场景:出现异常的业务场景说明。

异常数据:出现异常的数据(比如下单场景,需要记录商品ID、用户ID等信息)。

异常堆栈:异常堆栈信息。

五、安全

序号

等级

规范

示例

说明

1强制

用户敏感数据禁止直接展示、禁止用Get方式提交。

反例:

@Resource

private UserService userService;

public BizResponse<List<UserDTO>> getAllUsers() {

    List<User> users = userService.selectAllUsers();

    List<UserDTO> userDTOs = new ArrayList<>();

    users.forEach(user -> {

        UserDTO userDTO = new UserDTO();

        // 手机号码此处没有脱敏,直接返回到前端。

        userDTO.setPhone(user.getPhone());

        // ...

        userDTOs.add(userDTO);

    });

    return BizResponse.success(userDTOs);

}

手机号、银行卡卡号、身份证、车牌、车架号等都属于用户敏感信息,不能直接展示。

脱敏方式:

  • 手机号保留前3位和后4位;
  • 身份证号保留前6位和后3位;
  • 银行卡前6位和后4位;
  • 车牌将视情况按照保留地区和流水号后2位;
  • 车架号将视情况按照保留后6位;

禁止用Get方式提交,这种方式在URL上带有敏感数据,将会在wan/lan日志中出现这些元数据。

2强制

用户输入的参数,禁止直接拼接到SQL访问数据库。

反例:

<!-- Mapper XML -->

<select id="findUser" resultType="com.tuhu.mysql.User">

    select * from userInfo where username = ${userName}

</select>

// Mapper接口

public UserInfo findUser(String userName)

用户输入的参数可能带有SQL片段,存在SQL注入的风险,需要使用参数绑定的技术来防范。

3强制未经许可,禁止外发公司任何程序代码。

反例:

为了交流学习,将公司代码上传至GitHub,以方便各方来阅读和讨论,导致APPID、API等信息泄漏。

程序代码属于公司资产,在未经许可的情况下不得以任何方式(邮件、IM软件、纸质打印等)向外传输或公开,包括但不仅限于:

  • 第三方代码托管平台(Github、Gitee等)
  • 第三方网盘(百度网盘、阿里网盘等)
  • 第三方网站(优快云、博客园等)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值