跳至主要內容

C003.22届0913怪

trydofor原创技债大约 5 分钟

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默认为失败,以falsefailure初始化
  • 逻辑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的内存泄露

argThreadLocal实例,要注意内存泄露情形

  • ①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

@annotationin-placepointcut语法,此处有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#aopopen in new window

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官方示例open in new window,我冒犯了。

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();
}

jooq误用及select *

// 方式一,使用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变量,保持不变性和简洁