跳至主要內容

C002.22届0818怪

trydofor原创技债大约 5 分钟

C002.22届0818怪

1.pom.xml 60-63

tail-backend 2022-08-18 18:39:03 /tail-common/pom.xml

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>2.2.8</version>
</dependency>

maven版本号统一定义

Maven的实践中,版本号在parent的porperties中定义, 格式为*.version,如easyexcel.version

这也就要求,任何一个依赖必须明确定义,知道去用途,特性,尤其漏洞等。

2.TruckController.java 82-83

bbl_backend 2022-09-04 12:20:23

logger.info("truck rate {}", JSONObject.toJSONString(request));

日志性能和碎片

使用slf4j的占位符{},其目标之一是避免无意义的性能消耗和内存碎片。 此处不管日志级别如何,都会执行toJSONString。应修改为

// 常规写法,推荐,简单明了
if (logger.isInfoEnabled()) {
    logger.info("truck rate {}", JSONObject.toJSONString(request));
}
// 2.0+的fluent+lambda写法,视情况而定,不推荐
logger.atInfo()
.setMessage("truck rate {}")
.addArgument(() -> JSONObject.toJSONString(request))
.log();

3.CommonShipResponse.java 47-49

bbl_backend 2022-09-04 17:52:27

final String str = JSONObject.toJSONString(response);
final CommonResponse res = JSONObject.parseObject(str, CommonResponse.class);

低效不安全的复制

业务代码中,经常有Dto值赋值或克隆。按以下优先级从高到低考虑。

  • MapStruct - 强类型,编译时,静态赋值。
  • BeanUtil - 弱类型,运行时属性反射赋值或浅clone
  • 序列化反序列化 - 弱类型,深度复制,kryo优于json。

4.ExpressAccountCache.java 55-58

bbl_backend 2022-09-04 19:01:07

private Optional<ExpressAccount> getExpressAccountById(Long id) {
    final ExpressAccount ea = expressAccountDao.fetchOneById(id);
    return Optional.ofNullable(ea);
}

getter命名误导

慎用get动词前缀,以免误导为getterset同。 在JavaBean体系中,带有getter特征,意味在简单取值,无副作用。

对于此类方法,应该采用能够翻译任务层面和轻重的动词。

  • 服务层 - make/save/load/find
  • 服务层 - create/remove/modify/search
  • 数据层 - purge/store/fetch
  • sql层 - insert/delete/update/select

容器Optional及值对象

非函数式,不推荐用Optional,因java中stream+lambda非常丑陋。 Longlong,后者@NotNull,前者要判断NPE

5.TruckServiceImpl.java 239-242

bbl_backend 2022-09-05 11:31:12

if (StringUtils.isBlank(request.getDelivery().getTimeZone()) ||
    TimeZone.getTimeZone(request.getDelivery().getTimeZone()).toZoneId() == null) {
    throw new BblException("请先确认时区");
}

重复链式调用

无法确定链式调用是否具有幂等性的时候,必须提取final变量。 此外,推荐提取变量以减少代码长度,除非是非幂等性需要。

final String timeZone = request.getDelivery().getTimeZone();
if (StringUtils.isBlank(timeZone) || TimeZone.getTimeZone(timeZone).toZoneId() == null) {
    throw new BblException("请先确认时区");
}

使用I18n异常

在需要I18n的场景,需要使用Code或Enum转换业务Message。 此处异常,应该换成无堆栈的MessageException,可提高性能。

6.MotherShipServiceImpl.java 66-68

bbl_backend 2022-09-05 18:49:36

final String format = String.format("?zip=%s&city=%s&state=%s&street=%s", zip, city, state, street);
final String s = format.replaceAll("\\+", "%20");

String.format效果不经济

应该避免使用C语言printf风格的字符串格式化,效果不经济。 建议直接使用字符串拼接,或支持预编译的工具类,如StringTemplate

与此Formatter情景类似的还有Pattern的split, replace 要注意线程安全,单例安全编译,多线程下高性能使用。

URL中的空格是+还是%20

  • replace("+", "%20") - 字符串替换全部
  • replaceAll("\\+", "%20") - 正则替换全部

空格在RFC3986约定为%20,如js的encodeURI。 在RFC1866约定为+,如java中的URLEncoder。

以下工具默认%20,就不必使用蹩脚的replace了

  • spring-web的UriUtils
  • guava的UrlEscapers.urlFragmentEscaper()

7.MotherShipServiceImpl.java 171-174

bbl_backend 2022-09-06 17:47:25

if (response.isSuccessful()) {
    return JSON.parseObject(Objects.requireNonNull(response.body()).string(), \
    new TypeReference<MotherCommonResponse<List<T>>>(clz) {
    }).getData();
}

单行多调用代码

含有多调用的单行代码,不宜过长,影响可读性外,重要的是模糊了异常错误的行数,建议段成短句。

if (response.isSuccessful()) {
   final String str = Objects.requireNonNull(response.body()).string();
   final Type typ = new TypeReference<MotherCommonResponse<List<T>>>(clz) {}.getType();
   final MotherCommonResponse<List<T>> obj = JSON.parseObject(str, typ);
   return obj.getData();
}

Json反序列化的泛型

在FastJson使用中,wings禁用AutoType,并和jackson有场景区分。 对于Json的反序列化,两者提供了相同的TypeReference类和用法。推荐用法如下,

  • 免编译擦除的单行声明 new TypeReference.getType()
  • spring的ResolvableType.getType()
  • jackson的TypeFactory获取JavaType

8.AliPayService.java 62-72

bbl_backend 2022-09-07 09:25:32

Map<String, String> params = new HashMap<>();
Map requestParams = request.getParameterMap();
for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
    String name = (String) iter.next();
    String[] values = (String[]) requestParams.get(name);
    String valueStr = "";
    for (int i = 0; i < values.length; i++) {
        valueStr = (i == values.length - 1) ? valueStr + values[i]
                : valueStr + values[i] + ",";
    }
    params.put(name, valueStr);
}

数据结构和编程底子薄

场景的key-val签名算法,不过有些官方文档的代码很不负责人,写的非常HelloWorld

  • 字母序排key,用TreeMap完成,按先后顺序则LinkedHashMap
  • 有泛型的要补全,不要用raw,这点根据IDE提示修改就好
  • Map同时去key-val的loop,用entry,不要获得key再get,非二遍事
  • 集合及数组的join,有现成的工具类。
  • 如果非要loop,参考注释的代码,使用substring和setLength
Map<String, String> params = new TreeMap<>();
Map<String, String[]> requestParams = request.getParameterMap();
// final StringBuilder sb = new StringBuilder(512);
for (Map.Entry<String, String[]> entry : requestParams.entrySet()) {
    String name = entry.getKey();
    String valueStr = String.join(",", entry.getValue());
    params.put(name, valueStr);
    // for (String s: entry.getValue()) {
    //     sb.append(',').append(s);
    // }
    // params.put(name, sb.length() > 0 ? sb.substring(1) : "");
    // sb.setLength(0); // 需要复用时
}

9.BblProgressService.java 122-124

bbl_backend 2022-09-07 11:37:32

@Transactional
@Retryable(maxAttempts = 5, value = Exception.class, backoff = @Backoff(delay = 1000L, multiplier = 2))
public void updateStatus(int status, String message, Long id, BigDecimal rate, String express) {
    // ...
}

Retry和Transaction共存的隐患

多个切面存在于同一方法,是按照切面的Order顺序来执行的,上述代码堆栈如下

  • updateStatus(BblProgressService.java:124)
  • proceedWithInvocation(TransactionInterceptor.java:123)
  • doWithRetry(RetryOperationsInterceptor.java:93)

可以看出,实际为在Retry中执行Transaction,但这样存在隐患,导致事务rollback。 即在@Transactional中调用updateStatus方法时,虽然retry成功,但实际会回滚。

此类场景,最好解耦,由调度类中明确是在Retry中Tx,还是在Tx中Retry。

@SpringBootTest
public class RetryTest {

    @Setter(onMethod_ = {@Autowired})
    protected ServiceB serviceB;

    @Test
    public void testB1() {
        assertThrows(UnexpectedRollbackException.class, () -> serviceB.b1());
    }

    @Test
    public void testB2() {
        serviceB.b2();
    }
}
//
@Service
public class ServiceB {
    @Setter(onMethod_ = {@Autowired})
    protected ServiceA serviceA;

    @Transactional
    public void b1() {
        serviceA.a();
    }

    public void b2() {
        serviceA.a();
    }
}
//
@Service
public class ServiceA {
    final AtomicInteger count = new AtomicInteger(0);

    @Transactional
    @Retryable(maxAttempts = 2, value = Exception.class)
    public void a() {
        if (count.getAndIncrement() % 2 == 0) throw new IllegalStateException("");
    }
}