Java 业务开发错误收集

一、代码类

(一) if 操作

  1. if 要考虑后续还有没有别的操作, 如果是收窄操作, 可以直接反向 if, 然后直接 return

(二) Resource 判断文件是否存在

  1. 在 classpath 中的资源文件, 路径总是以 / 开头, 我们先获取当前的 Class 对象, 然后调用 getResourceAsStream() 就可以直接从 classpath 读取任意的资源文件
  2. 如果资源文件不存在, 它将返回 null; 即getResourceAsStream() & getResource() 都是通过返回值是否为 null, 来判断是否存在
try (InputStream stream = ExecutorFactory.class.getClassLoader()
                                               .getResourceAsStream(path)) {
    if (stream == null) {
        log.info("文件 {} 不存在, 跳过", path);
        return null;
    }
    return CharStreams.toString(new InputStreamReader(stream, Charsets.UTF_8));
} catch (Exception e) {
    log.error("获取文件异常", e);
}
return null;

(三) ClassLoader.getResourceAsStream() 不能使用前导斜杠 /

No leading “/” (all names are absolute)

https://stackoverflow.com/questions/47900677/where-does-leading-slash-in-java-class-loader-getresource-leads-to
Leading slash works only for class.getResource() to override its default behavior. There is no leading slash concept for class.getClassLoader().getResource(), so it always returns null.

https://stackoverflow.com/questions/3803326/this-getclass-getclassloader-getresource-and-nullpointerexception
The reason you can’t use a leading / in the ClassLoader path is because all ClassLoader paths are absolute and so / is not a valid first character in the path.

(四) JdbcTemplate 批量执行 (TODO)

jdbcTemplate` 执行 `batchUpdate(multiSQL.split(";"))`, 参数必须是单个 sql 组成的 sql 数组, 进行 split() 切分时, 必须去掉前/后导空白字符, 否则执行到空白 sql 会报错 `Exception

如果要执行多条 SQL 语句, MySQL 5.7 及之前的版本的连接串, 后面需要添加 **?allowMultiQueries=true**

在 MySQL 8 以后, allowMultiQueries 的默认值为 true. (Starting from MySQL 8.0, the default value for allowMultiQueries is set to true.)

if (!StringUtils.hasText(sql)) {
    return;
}
String multiSQL = StringUtils.trimWhitespace(sql);
jdbcTemplate.batchUpdate(multiSQL.split(";"));

(五) ObjectMapper 构造 JavaType

objectMapper` 在使用 `TypeFactory` 时, 可以直接使用 `TypeFactory.defaultInstance()
// org.aspectj.weaver.TypeFactory
// 还有一个 TypeFactory.createParameterizedType 是完全不同的类, 完全不同的方法注意!!
JavaType javaType =
    // com.fasterxml.jackson.databind.type.TypeFactory
    TypeFactory.defaultInstance()
               .constructParametricType(List.class, ActionList.class);
getMapper().readValue(content, javaType);  // content must not be null.
// 获取 mapper 通常要复用
protected ObjectMapper getMapper() {
    if (mapper == null) {
        mapper = new ObjectMapper();
    }
    return mapper;
}
// 获取 XmlMapper
return XmlMapper.builder().enable(SerializationFeature.INDENT_OUTPUT).build();

(六) JsonNode 转 Map

使用 springboot 提供的 json 转换工具 org.springframework.boot.json.JsonParserFactory

private static Map<String, Object> setAllValue(JsonNode source, BillSubData target) {

    JsonParser jsonParser = JsonParserFactory.getJsonParser();

    Map<String, Object> sourceMap = jsonParser.parseMap(source.toString());
    for (Map.Entry<String, Object> entry : sourceMap.entrySet()) {
        target.set(entry.getKey(), entry.getValue());
    }
    return sourceMap;
}

(七) Ordered 不生效

因为 getBeansOfType 方法获取时, 只是从一个 map 里面进行获取, 里面默认是按 alphabet 排序的, 所以需要手动排序

private List<AbstractProvider> sortedProviders;
...

    Map<String, AbstractProvider> providers = applicationContext.getBeansOfType(AbstractProvider.class);
    sortedProviders = new ArrayList<>(providers.values());
    sortedProviders.sort(Comparator.comparingInt(Ordered::getOrder));
    // 手动排序, 因为 getBeansOfType 方法获取时, 只是从一个 map 里面进行获取, 里面默认是按 alphabet 排序的

(八) JsonNode 判断 null

// Method similar to #findValue(str), 
// but that will return a "missing node" instead of null if no field is found. 
// Missing node is a specific kind of node for which isMissingNode returns true;
JsonNode node = jsonNode.findPath("key"); // 必须使用 findPath 获取 JsonNode 对象

if (node.isNull()) {
    // 节点为空
}
if (node.isEmpty()) {
    // 节点为空(null、空对象、空数组)
}
if (node.isMissingNode()) {
    // 是否为缺失节点(不存在于 JSON 结构中)
}

(九) Excel 读取与遍历

Workbook workbook;
try (InputStream inputStream = file.getInputStream()) {
    workbook = WorkbookFactory.create(inputStream);
    Sheet firstSheet = workbook.getSheetAt(0);
    for (int rowNum = 1; rowNum <= firstSheet.getLastRowNum(); rowNum++) {
        Row row = firstSheet.getRow(rowNum);
        if (isEmptyRow(row)) {
            continue;
        }
        for (int i = 0; i < row.getPhysicalNumberOfCells(); i++) {
            Cell cell = row.getCell(i);
            Object cellValue = validateCell(cell);
        }
    }

// adjust
public static boolean isEmptyRow(Row row) {
    if (row == null || row.toString().isEmpty()) {
        return true;
    } else {
        Iterator<Cell> it = row.iterator();
        boolean isEmpty = true;
        while (it.hasNext()) {
            Cell cell = it.next();
            if (cell.getCellType() != CellType.BLANK) {
                isEmpty = false;
                break;
            }
        }
        return isEmpty;
    }
}

public static Object validateCell(Cell cell) {
    if (null == cell) {
        return null;
    }
    Object cellValue;
    switch (cell.getCellType()) {
        case STRING:
            cellValue = cell.getStringCellValue();
            break;
        case NUMERIC:
            DecimalFormat decimalFormat = new DecimalFormat("###################.###########");
            cellValue = decimalFormat.format(cell.getNumericCellValue());
            break;
        case BOOLEAN:
            cellValue = cell.getBooleanCellValue();
            break;
        case ERROR:
            cellValue = cell.getErrorCellValue();
            break;
        case BLANK:
            cellValue = null;
            break;
        default:
            cellValue = cell.toString();
    }
    return cellValue;
}

(十) 子类不能继承父类的构造器, 子类只能调用父类的构造器

class BizException extends RuntimeException { // 并不会默认拥有父类的构造签名
    public BizException(String msg) { // 父类的构造器只属于父类, 子类只能声明自己的构造器
        super(msg); 				  // 和调用父类构造器
        this.msg = msg;
    }

(十一) [数据字典/基础数据] 通用查询方法

  • 可直接复用
private PageVO<BaseDataDO> queryBaseData(String table,
                                         String code,
                                         String name,
                                         String... param) {

    BaseDataDTO query = new BaseDataDTO();
    query.setTableName(table);
    query.setAuthType(NONE);
    if (code != null) query.setCode(code);
    if (name != null) query.setName(name);
    if (param != null && param.length > 0) {
        for (int i = 0; i < param.length - 1; i++) {
            query.put(param[i++], param[i]);
        }
    }
    return baseDataService.list(query);
}

(十二) 请求响应异常处理

@ExceptionHandler({BizException.class})
public Response handleBizException(BizException e) {
    // 返回固定结构的响应体
    return Response.failed(e.getMessage());
}

(十三) SQL 中空值判断

必须使用 is null 而不是 = null

The reasoning is that a null means “unknown”, so the result of any comparison to a null is also “unknown”. So you’ll get no hit on rows by coding where my_column = null.

ANSI convention that NULL is an unknown value and cannot be compared with any other value, including other NULLs.

mysql: https://dev.mysql.com/doc/refman/8.0/en/comparison-operators.html

[ANSI_NULLS] sql server: https://learn.microsoft.com/en-us/sql/t-sql/language-elements/equals-transact-sql?view=sql-server-ver15

(NULL doesn’t equal NULL. NULL is not a value, and therefore cannot be compared to another value.)

select count(*) from md_org where PARENTCODE is null;

(十四) Optional 的使用

少写很多 if (xxx == null)

public Membership getAccountMembership_classic() {
    Account account = accountRepository.get("johnDoe");
    if (account == null || account.getMembership() == null) {
        throw new AccountNotEligible();
    }
    return account.getMembership();
}

public Membership getAccountMembership_optional() {
    return accountRepository.find("johnDoe")
                            .flatMap(Account::getMembershipOptional)
                            .orElseThrow(AccountNotEligible::new);
}

(十五) ORACLE 错误集

ORA-00904 表示无效的标识符: 不存在的列名或表达式 or 表或视图不存在
ORA-00918 column ambiguously defined 一般是字段的别名定义重复
ORA-01440: column to be modified must be empty to decrease precision or scale 要修改的列必须为空,才能减小其精度或比例
ORA-22859: invalid modification of columns 发生在 clob (存储方式较独特) 转换为 NVARCHAR2
ORA-01722: invalid number (ORA-01722 error occurs when an attempt is made to convert a character string into a number, and the string cannot be converted into a number)
ORA-24335: cannot support more than 1000 columns (无法支持 1000 列以上; i.e. 参数不允许超过1000个)
ORA-00933 sql未正确结束

(十六) Stack & Queue 的 Deque 接口

  • Queue (FIFO)

    • offer (!=add with Exception, !=addLast with Exception) 提案
    • poll (!=remove with Exception) 收集
    • peek (==peekFirst, !=getFirst with Exception) 预览
    • clear
  • Stack (LIFO)

    • push (==addFirst) 压栈
    • pop (==popFirst, !=removeFirst with Exception) 弹出

(十七) 位运算 Bitwise operators 对二进制数操作

  1. 按位与(AND)
  • 符号: &
  • 描述: 对应位都为 1 时结果为 1,否则为 0。
  • : 5 & 3 结果是 1(0101 & 0011 = 0001)。
  1. 按位或(OR)
  • 符号: |
  • 描述: 对应位至少有一个为 1 时结果为 1。
  • : 5 | 3 结果是 7(0101 | 0011 = 0111)。
  1. 按位异或(XOR)
  • 符号: ^
  • 描述: 对应位不同则结果为 1,相同则为 0。
  • : 5 ^ 3 结果是 6(0101 ^ 0011 = 0110)。
  1. 按位取反(NOT)
  • 符号: ~
  • 描述: 反转每一位,0 变为 1,1 变为 0。
  • : ~5 结果是 -6(在补码表示下)。
  1. 左移(Left Shift)
  • 符号: <<
  • 描述: 将二进制数的所有位向左移动指定的位数,右侧填充 0。
  • : 5 << 1 结果是 10(0101 变为 1010)。
  1. 右移(Right Shift)
  • 符号: >>
  • 描述: 将二进制数的所有位向右移动,左侧根据符号位填充(算术右移)。
  • : 5 >> 1 结果是 2(0101 变为 0010)。
  1. 无符号右移(Unsigned Right Shift)
  • 符号: >>>
  • 描述: 将二进制数向右移动,左侧填充 0,无论符号位。
  • : -5 >>> 1 结果是 2147483645(在某些语言中)。

(十八) 常量的设置

我之前认为常量设置有些多余, 因为一般都是复制粘贴值, 但实际上在批量替换时非常有用, 而且引用时也有技巧

必须使用 static 引用, 并在 import static 时, 直接 * 引用

import static com.arminzheng.constant.PrintConst.*; (包名中 const 为关键字不能直接使用)

与之相反, Builder 模式使用的相当折磨, 以后宁愿直接使用构造器或者 lombok, 也别用手动 builder, 维护火葬场

(十九) Swagger

个人忠告, 别用 swagger

使用 Postman 类似软件导入导出 (导出整个 Collection)

  • 依赖凌乱, 作为类库分了过多的层, 很多层没有复用的必要

  • 必须要手动打开白名单, 生产环境还得小心排除

  • 界面不稳定, 风格有好几版, 网页界面甚至有bug

  • 定义了好几个注解

    • Controller: @Api @ApiOperation @ApiParam @ApiResponses @ApiResponse(code=200, message="成功")
    • Model: @ApiModel(description="用户对象") @ApiModelProperty(value="用户姓名", required=true)
  • 网页界面太简陋, 只能使用网页, 不符合接口开发习惯, 且全靠后端维护

二、其他错误

(一) 别在代码里记笔记,后续想起来不好找(外部很难搜索)

(二) 在调用方法时,一定要看方法实现

  1. 调用单据创建, 该方法一开始就会放缓存, 但调用 saveBill() 方法, 却没更新缓存, 导致重新取 Bill 时,单据状态 billState 比对不上;后面换用 quickSaveBill() 方法就好了

img

// billService.saveBill(bill);
billService.quickSaveBill(bill);

(END)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值