C003.22届0913怪
C003.22届0913怪
1.URLClassPath.java 631-638
java/11.0.2-open 2019-01-18 16:22:01 /java.base/jdk/internal/loader/URLClassPath.java
Resource getResource(final String name, boolean check) {
final URL url;
try {
url = new URL(base, ParseUtil.encodePath(name, false));
} catch (MalformedURLException e) {
throw new IllegalArgumentException("name");
}
// ....
}
异常缺少有用信息
Caused by: java.lang.IllegalArgumentException: name
at URLClassPath$Loader.findResource(URLClassPath.java:600) ~[na:na]
at URLClassPath.findResource(URLClassPath.java:291) ~[na:na]
at URLClassLoader$2.run(URLClassLoader.java:655) ~[na:na]
日志中,可由堆栈信息定位代码,但无法获取参数name
运行时的值。 应该修改如下,注意数据脱敏,即对敏感信息进行替换,加密等。
throw new IllegalArgumentException("name=" + name);
2.BalanceAccountsServiceImpl.java 107-118
express-home 92ee7d223aca4c8d9dba281e121a3fc659d97dd1
SimpleResult<Map<String, Object>> result = SimpleResult.create(true);
// ... ...
// 判断客户类型
if (userTypeEnum == UserTypeEnum.PROXY_75) {
result.setSuccess(false);
result.setMessage("对于75折用户,我们使用每月账单结算!");
return result;
}
May The false be with you
- Result默认为失败,以
false
或failure
初始化 - 逻辑check中,直接设置消息并return
- 业务最后成功时,设定success及结果
消息文字的等宽性
- 此处未做I18n处理,硬编码消息。
- 中文内应该使用中文标点
3.StringTemplate.java 106-118
mirana c30e3f2c0e3bf61f38f05b56b4f341113b1cd61c
// arg = ThreadLocal.withInitial(P::new);
@NotNull
public String toString() {
final P p = arg.get();
try {
p.solid();
C c = cac.computeIfAbsent(p, k -> new C(k, txt, fix));
return c.build(p);
}
finally {
arg.remove();
p.clear();
}
}
不用搭特殊方法的便车
toString
方法,在IDE的debug,日志中,会被隐式调用。 非幂等的,有外部作用的方法,搭便车会存在隐藏问题。
ThreadLocal的内存泄露
arg
是ThreadLocal
实例,要注意内存泄露情形
- ①ThreadLocal引用存活较短,非static的,每次new的
- ②线程
Thread
存活较长,如线程池中,永活线程等
以上同时满足,会有①的引用被回收,但②内ThreadLocalMap.Entry.value不回收,发生泄露。
不同时满足①②,或者主动释放, 如try-close模式,都可以避免泄露。 注意,SoftReference作为value,只是以小对象替换大对象,延缓泄露。
ThreadLocal的方法泄露
ThreadLocal多用作Context使用,跨方法,跨层传递信息。 俗称方法泄露,即除了input,return,throw外, 还有其他数据通路,使得方法的数据流变得隐形和复杂。
ThreadLocal的线程切换
ThreadLocal是否能正常工作,依赖于业务的线程模型,跨线程时会丢失上下文。 简单的线程池,推荐使用阿里的TransmittableThreadLocal系列。
4.MetricAspect.java 23-24
fba_backend_v3 790e38947bdc5200536172ad1c2f638a11152803
@Around("@annotation(metricTime)")
public Object metric(ProceedingJoinPoint joinPoint, MetricTime metricTime) {
Spring AOP 和 Full AspectJ
@annotation
为in-place
的pointcut
语法,此处有2种写法,
- 全类名 - @Around("@annotation(com.xxx.xxx.annotation.MetricTime)")
- 参数自动 - @Around("@annotation(metricTime)")
- 参数手动 - @Around(value = "@annotation(metricTime)", argNames = "joinPoint,metricTime")
当Advice中不需要annotation参数时,可直接使用全类名形式。
详细资料: https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop
5.AddressCache.java 46-54
fba_backend_v3 790e38947bdc5200536172ad1c2f638a11152803
@Cacheable(cacheNames = WingsCache.Level.Forever + "Address")
public PageResult<AddressVo> findAddrPage(AddressRequest request, Long userId) {
log.info("find address of user {} from db", userId);
FbaAddressTable at = fbaAddressDao.getAlias();
FbaReceiverBookTable bt = fbaReceiverBookDao.getAlias();
val select = fbaAddressDao
.ctx()
.selectFrom(bt.leftJoin(at).on(bt.AddressId.eq(at.Id)))
.where(bt.onlyLiveData);
PageQuery pageQuery = request.getPageQuery();
缓存可能爆栈
从业务逻辑推断,此处Cacheable可能会溢出。
- Forever默认为无界,无过期缓存
- 缓存Key的组合过多
- 用户有关的缓存,不应该超过session生命周期
非业务瓶颈不要贸然增加缓存,缓存设置一定要基于实战指标。
select *
避免wings中引入jooq,其目的之一,就是要按需查询,避免偷懒的select *
.select(at.asterisk(), bt.UserId, bt.AddressId, bt.Code)
.from(bt.leftJoin(at).on(bt.AddressId.eq(at.Id)))
.where(bt.onlyLiveData);
6.BasicsOfferCache.java 39-40
fba_backend_v3 790e38947bdc5200536172ad1c2f638a11152803
log.info(String.format("获取报价缓存【%s,%s,%s,%s】", warehouse, feeType, apply, applyId));
// 不知哪个师傅教的
log.info(String.format("Received response for %s %s in %.1fms%n%s",
response.request().url(), response.code(), (t2 - t1) / 1e6d, response.headers()));
log的性能及可采集性
log.info("获取报价缓存【{},{},{},{}】", warehouse, feeType, apply, applyId);
日志性能,往往成为业务瓶颈之一,此外,日志应该具备可采集性,不要使用中文作为特征值。
不知哪个师傅教的
- 从来不允许
1e6d
的写法,使用普通人能懂的1_000_000D
,且d
要大写 - 时间转换,用TimeUnit.NANOSECONDS.toMillis(t2 - t1)
事后得知,是okhttp官方示例,我冒犯了。
7.FinanceAccountCache.java 30-34
fba_backend_v3 dfe83f2c8a02f45bdec841b25d8acbb846d61bb5
@Cacheable
public Long findByUserId(Long userId) {
final FbaFinanceAccount fbaFinanceAccount = fbaFinanceAccountDao.fetchOne(
table -> table.Currency.eq(Currency.USD).and(table.UserId.eq(userId)));
return fbaFinanceAccount == null ? EmptyValue.BIGINT : fbaFinanceAccount.getId();
}
select *
jooq误用及// 方式一,使用SelectOrderCondition包装
final FbaFinanceAccount fbaFinanceAccount = fbaFinanceAccountDao.fetchOne(t ->
SelectOrderCondition.of(t.Currency.eq(Currency.USD).and(t.UserId.eq(userId)), t.Id));
return fbaFinanceAccount == null ? EmptyValue.BIGINT : fbaFinanceAccount.getId();
// 方式二,使用Dsl,可以自定义表或别名
final FbaFinanceAccountTable t = fbaFinanceAccountDao.getTable();
final Long id = fbaFinanceAccountDao.fetchOne(Long.class, t,
t.Currency.eq(Currency.USD).and(t.UserId.eq(userId)),
t.Id);
return id == null ? EmptyValue.BIGINT : id;
8.OrderUsServiceImpl.java 476-483
express-home 938f45937c1a55d9ae9e203a840b8181de9e4b1e
BigDecimal waitPayFee = new BigDecimal(0.0);
waitPayFee = DecimalUtil.add(waitPayFee, orderMainPo.getCashFreight());
waitPayFee = DecimalUtil.add(waitPayFee, orderMainPo.getCashSurcharge());
if (orderMainPo.getCountry().equals(CountryEnum.fromCode(1).toString())) {
waitPayFee = orderMainPo.getCashDeerFee();
}
用心想问题,用脑写代码
BigDecimal waitPayFee;
if (orderMainPo.getCountry().equalsIgnoreCase(CountryEnum.US.name())) {
waitPayFee = Z.notNullSafe(ZERO, orderMainPo.getCashDeerFee());
} else {
waitPayFee = DecimalUtil.add(orderMainPo.getCashFreight(), orderMainPo.getCashSurcharge());
}
- BigDecimal绝对不可用浮点型构造,
ZERO
是常用初始值 - 毫无意义的赋值,一个
if
全白瞎 - Enum就是要直接用,不要使用fromCode(1)
- 若 waitPayFee 无后续覆盖,需要final
9.BalanceActsDetailController.java 208-234
express-home 938f45937c1a55d9ae9e203a840b8181de9e4b1e
List<BaggagePo> pos = new ArrayList<>();
int j = 0;
int k = 0;
FedexBillsPo fedexBillsPoT;
for (BaggagePo po : baggagePos) {
if (po.getIsDeleted() == 1) {
continue;
}
FedexBillsPo fedexBillsPo = fedexBillsService.getByTrackNum(po.getTrackNumUs());
// ... ...
if (po.getStatus() == OrderStatusEnum.USUS_PICK.code()) {
pos.add(k, po);
k++;
}
if (po.getStatus() != OrderStatusEnum.USUS_PICK.code() && (
po.getStatus() == OrderStatusEnum.USUS_CHECK.code() ||
po.getStatus() == OrderStatusEnum.USUS_BILLED.code() ||
po.getStatus() == OrderStatusEnum.USUS_COMPLETED.code()
)) {
weight.add(j, po.getWeightRaw());
j++;
}
// ... ...
}
Controller不要写业务逻辑
此Controller有300行业务代码,涉及数据库操作,上述仅是数据转换的一段。
- 业务代码 - 应该在Service层
- Mvc的C层 - 应该仅做数据检查,转换,以完成Service调度
不要在循环中查数据库
在循环中调用中资源,是非常不好的思维习惯,容易存在N+1
问题。
- 如有
N(1)
缓存,以减少代码复杂度的为目的,可以循环调用。 - 数据量不大时,应该在外层一次性获取
- 数据量很大时,应该按页处理,在外层按页处理。
要理解数据结构的特性
List及ArrayList是最常用类型,后者具备RandomAccess特性
- 移除计数器j和k,直接使用add()
- 最后的计数,使用 size()方法
要有逻辑判断及变量提取思想
- 本身业务语义很清晰,但两个
if
写的一塌糊涂。 po.getStatus()
应该提取为final变量,保持不变性和简洁