一、代码类
(一) if 操作
if
要考虑后续还有没有别的操作, 如果是收窄操作, 可以直接反向if
, 然后直接return
(二) Resource 判断文件是否存在
- 在 classpath 中的资源文件, 路径总是以
/
开头, 我们先获取当前的 Class 对象, 然后调用getResourceAsStream()
就可以直接从classpath
读取任意的资源文件 - 如果资源文件不存在, 它将返回 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 对二进制数操作
- 按位与(AND)
- 符号:
&
- 描述: 对应位都为 1 时结果为 1,否则为 0。
- 例:
5 & 3
结果是1
(0101 & 0011 = 0001)。
- 按位或(OR)
- 符号:
|
- 描述: 对应位至少有一个为 1 时结果为 1。
- 例:
5 | 3
结果是7
(0101 | 0011 = 0111)。
- 按位异或(XOR)
- 符号:
^
- 描述: 对应位不同则结果为 1,相同则为 0。
- 例:
5 ^ 3
结果是6
(0101 ^ 0011 = 0110)。
- 按位取反(NOT)
- 符号:
~
- 描述: 反转每一位,0 变为 1,1 变为 0。
- 例:
~5
结果是-6
(在补码表示下)。
- 左移(Left Shift)
- 符号:
<<
- 描述: 将二进制数的所有位向左移动指定的位数,右侧填充 0。
- 例:
5 << 1
结果是10
(0101 变为 1010)。
- 右移(Right Shift)
- 符号:
>>
- 描述: 将二进制数的所有位向右移动,左侧根据符号位填充(算术右移)。
- 例:
5 >> 1
结果是2
(0101 变为 0010)。
- 无符号右移(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)
- Controller:
-
网页界面太简陋, 只能使用网页, 不符合接口开发习惯, 且全靠后端维护
二、其他错误
(一) 别在代码里记笔记,后续想起来不好找(外部很难搜索)
(二) 在调用方法时,一定要看方法实现
- 调用单据创建, 该方法一开始就会放缓存, 但调用
saveBill()
方法, 却没更新缓存, 导致重新取 Bill 时,单据状态billState
比对不上;后面换用quickSaveBill()
方法就好了
// billService.saveBill(bill);
billService.quickSaveBill(bill);
(END)