[java基础系列]日常工作中我所使用的java代码技巧

7,039 阅读18分钟
Man, Glasses, Hipster, Beard, Adult
Man, Glasses, Hipster, Beard, Adult

前言

罗列工作中实际使用的一些代码技巧或者叫工具类;知识无大小,希望大家都有收获

实用技巧

rpc服务出参统一化

什么,出参统一化有什么好说的????? 我不知道你们有没有遇到过多少五花八门的外部服务提供的返回对象,可能别人没有规范约束,我们管不了,但是从我们这里出去的,我们可以强制约束一下,不然发生新老交替,这代码还能看吗

首先出参都叫xxDTO的,阿里java开发手册提到过;再者我们是提供服务的一方,错误码code,错误信息msg,以及返回结果data都是要明确体现出来的,像下面这样

 1public class TradeResultDTO<Timplements Serializable {
2    /**
3     * 默认失败编码
4     */

5    private static final String DEFAULT_FAIL_CODE = "500";
6    private boolean success;
7    private String code;
8    private String msg;
9    private T data;
10        public static <T> TradeResultDTO<T> success(T data) {
11        return base("200"nulltrue, data);
12    }
13
14    public static <T> TradeResultDTO<T> fail(String code, String msg) {
15        return base(code, msg, falsenull);
16    }
17
18    public static <T> TradeResultDTO<T> fail(String msg) {
19        return base(DEFAULT_FAIL_CODE, msg, falsenull);
20    }
21
22    public static <T> TradeResultDTO<T> success() {
23        return base("200"nulltruenull);
24    }
25
26    public static <T> TradeResultDTO<T> fail(IError iError) {
27        return base(iError.getErrorCode(), iError.getErrorMsg(), falsenull);
28    }
29}

统一对象返回的结构就是上面这样

接着这个我想说的是,作为服务提供方,如果这个接口提供了返回值,我拿创建订单接口举例

1/**
2 * 创建交易单,业务系统发起
3 *
4 * @param req 创建入参
5 * @return 返回创建信息
6 */

7TradeResultDTO<TradeCreateOrderResponseDTO> createOrder(TradeCreateOrderRequestDTO req)
8

比如这个TradeCreateOrderResponseDTO 返回了订单号之类的基本信息,这个接口对于具体业务场景只能产生一笔订单号,我之前遇到过对方只是提示什么的错误信息(订单已存在),是的没错,他做了幂等,但是他没有返回原值,那对应的调用方进入了死循环,可能对应的业务系统,需要返回的订单号落到自己的数据库,一直异常一直回滚重试,没有任何意义;所以作为一个负责人的服务提供方,类似这种情况,如果你的方法有幂等,那么请一定返回存在的那个对象;

异常统一化

统一化使用,杜绝项目出现各种各样的自定义异常

对外统一抛出异常

我使用的统一化有两个方面:

  • 抛出的自定义异常不要五花八门,一个就够了;很多人喜欢写各种各样的异常,初衷其实没错,但是人多手杂,自定义异常可能越写越乱;

  • 异常信息最好尽可能的具体化,描述出业务产生异常原因就可以了,比如入参校验的用户信息不存在之类的;或者在调用用户中心的时候,捕获了该异常,此时你只需定义调用用户中心异常就可以了

然后看下工作中比较推荐的:

首先,需要搞一个统一抛出异常的工具 ExceptionUtil(这里Exceptions是公司内部统一前后端交互的,基于这个包装一个基础util,统一整个组抛异常的入口)

 1public class ExceptionUtil {
2    public static OptimusExceptionBase fail(IError error) throws OptimusExceptionBase {
3        return Exceptions.fail(errorMessage(error));
4    }
5
6    public static OptimusExceptionBase fail(IError error, String... msg) throws OptimusExceptionBase {
7        return Exceptions.fail(errorMessage(error, msg));
8    }
9
10    public static OptimusExceptionBase fault(IError error) throws OptimusExceptionBase {
11        return Exceptions.fault(errorMessage(error));
12    }
13
14    public static OptimusExceptionBase fault(IError error, String... msg) throws OptimusExceptionBase {
15        return Exceptions.fault(errorMessage(error, msg));
16    }
17
18
19    private static ErrorMessage errorMessage(IError error) {
20        if (error == null) {
21            error = CommonErrorEnum.DEFAULT_ERROR;
22        }
23        return ErrorMessage.errorMessage("500""[" + error.getErrorCode() + "]" + error.getErrorMsg());
24    }
25
26    private static ErrorMessage errorMessage(IError error, String... msg) {
27        if (error == null) {
28            error = CommonErrorEnum.DEFAULT_ERROR;
29        }
30        return ErrorMessage.errorMessage("500""[" + error.getErrorCode() + "]" + MessageFormat.format(error.getErrorMsg(), msg));
31    }
32}

其实上面代码里也体现出来IError这个接口了,我们的错误枚举都需要实现这个异常接口,方便统一获取对应的错误码和错误信息,这里列举一下通用异常的定义

 1public interface IError {
2    String getErrorCode();
3
4    String getErrorMsg();
5}
6@AllArgsConstructor
7public enum CommonErrorEnum implements IError {
8    /**
9     *
10     */

11    DEFAULT_ERROR("00000000""系统异常"),
12    REQUEST_OBJECT_IS_NULL_ERROR("00000001""入参对象为空"),
13    PARAMS_CANNOT_BE_NULL_ERROR("00000002""参数不能为空"),
14    BUILD_LOCK_KEY_ERROR("00000003""系统异常:lock key异常"),
15    REPEAT_COMMIT_ERROR("00000004""正在提交中,请稍候");
16
17    private String code;
18    private String msg;
19
20    @Override
21    public String getErrorCode() {
22        return code;
23    }
24
25    @Override
26    public String getErrorMsg() {
27        return msg;
28    }
29}

类似上面CommonErrorEnum的方式我们可以按照具体业务定义相应的枚举,比如OrderErrorEnum、PayErrorEnum之类,不仅具有区分度而且,也能瞬速定位问题;

所以对外抛出异常统一化就一把梭:统一util和业务错误枚举分类

对内统一捕获外部异常

很多时候我们需要调用别人的服务然后写了一些adapter适配类,然后在里面trycatch一把;其实这时候可以利用aop好好搞一把就完事了,并且统一输出adapter层里面的日志

 1    public Object transferException(ProceedingJoinPoint joinPoint) {
2        try {
3            Object result = joinPoint.proceed();
4            log.info("adapter result:{}", JSON.toJSONString(result));
5            return result;
6        } catch (Throwable exception) {
7            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
8            Method method = signature.getMethod();
9            log.error("{}.{} throw exception", method.getDeclaringClass().getName(), method.getName(), exception);
10            throw ExceptionUtils.fault(CommonErrorEnum.ADAPTER_SERVICE_ERROR);
11            return null;
12        }
13    }

上面这段统一捕获了外部服务,记录异常日志,避免了每个adapter类重复捕获的问题

参数校验

用过swagger的应该了解api方法里有对应的注解属性约束是否必填项,但是如果判断不是在api入口又或者没有类似的注解,你们一般怎么做的,下面给出我自己的一种简单工具;有更好大佬的可以推荐一下

ParamCheckUtil.java

 1@Slf4j
2public class ParamCheckUtil {
3
4    /**
5     * 校验请求参数是否为空
6     *
7     * @param requestParams 请求入参
8     * @param keys          属性值数组
9     */

10    public static void checkParams(Object requestParams, String... keys) {
11        if (null == requestParams) {
12            throw ExceptionUtil.fault(CommonErrorEnum.REQUEST_OBJECT_IS_NULL_ERROR);
13        }
14        StringBuilder sb = new StringBuilder();
15        for (String fieldName : keys) {
16            Object value = null;
17            Type type = null;
18            try {
19                String firstLetter = fieldName.substring(01).toUpperCase();
20                String getter = "get" + firstLetter + fieldName.substring(1);
21                Method method = requestParams.getClass().getMethod(getter);
22                value = method.invoke(requestParams);
23                type = method.getReturnType();
24            } catch (Exception e) {
25                log.error("获取属性值出错,requestParams={}, fieldName={}", requestParams, fieldName);
26            } finally {
27                // 判空标志 String/Collection/Map特殊处理
28                boolean isEmpty =
29                        (String.class == type && StringUtil.isEmpty((String) value))
30                                || (Collection.class == type && CollectionUtils.isEmpty((Collection<? extends Object>) value))
31                                || (Map.class == type && CollectionUtils.isEmpty((Collection<? extends Object>) value))
32                                || (null == value);
33                if (isEmpty) {
34                    if (sb.length() != 0) {
35                        sb.append(",");
36                    }
37                    sb.append(fieldName);
38                }
39            }
40        }
41
42        if (sb.length() > 0) {
43            log.error(sb.toString() + CommonErrorEnum.PARAMS_CANNOT_BE_NULL_ERROR.getErrorMsg());
44            throw ExceptionUtil.fault(CommonErrorEnum.PARAMS_CANNOT_BE_NULL_ERROR, sb.toString() + CommonErrorEnum.PARAMS_CANNOT_BE_NULL_ERROR.getErrorMsg());
45        }
46    }
47
48    // test
49    public static void main(String[] args) {
50        TradeCreateOrderRequestDTO tradeCreateOrderRequestDTO = new TradeCreateOrderRequestDTO();
51        tradeCreateOrderRequestDTO.setBusinessNo("");
52        ParamCheckUtil.checkParams(tradeCreateOrderRequestDTO, "businessNo""tradeType""tradeItemDTOS");
53    }
54
55}

基于了上面统一异常的形式,只要参数校验出空我就抛出异常中断程序,并且告知其缺什么参数

我在业务代码需要判断字段非空的地方只需要一行就够了,就行下面这样

1ParamCheckUtil.checkParams(tradeCreateOrderRequestDTO, "businessNo""tradeType""tradeItemDTOS");

而不是我们常用的一堆判断,像下面这样;看到这些我人都晕了,一次两次就算了,一大段全是这种

 1if (null == tradeCreateOrderRequestDTO) {
2// 提示tradeCreateOrderRequestDTO为空
3}
4if (StringUtil.isEmpty(tradeCreateOrderRequestDTO.getBusinessNo())) {
5// 提示businessNo为空
6}
7if (StringUtil.isEmpty(tradeCreateOrderRequestDTO.getTradeType())) {
8// 提示tradeType为空
9}
10if (CollectionUtils.isEmpty(tradeCreateOrderRequestDTO.getTradeItemDTOS())) {
11// 提示tradeItemDTOS列表为空
12}

如果你是上面说的这种形式,不妨试试我提供的这种

bean相关

对象的构造

关于对象的构造,我想提两点,构造变的对象和不变的对象

  • 构造不变对象,使用builder,不提供set方法,推荐使用lombok @Builder
1@Builder
2public class UserInfo {
3    private String id;
4    private String name;
5
6    public static void main(String[] args) {
7        UserInfo userInfo = UserInfo.builder().id("a").name("name").build();
8    }
9}
  • 构造可变对象,推荐提供链式调用形式 使用lombok @Accessors(chain = true)注解
1@Data
2@Accessors(chain = true)
3public class CardInfo {
4    private String id;
5    private String name;
6    public static void main(String[] args) {
7        CardInfo cardInfo = new CardInfo().setId("c").setName("name");
8    }
9}

对象转换

就一把梭:lambda工具类+mapstruct进行转换

BeanConvertUtil.java 通用的对象、list、Page转换

 1public class BeanConvertUtil {
2    /**
3     * 对象转换
4     *
5     * @param source     源对象
6     * @param convertFun T -> R lambda转换表达式
7     * @param <T>        输入类型
8     * @param <R>        输出类型
9     * @return 返回转化后输出类型的对象
10     */

11    public static <T, R> convertObject(T source, Function<T, R> convertFun) {
12        if (null == source) {
13            return null;
14        }
15        return convertFun.apply(source);
16    }
17
18    /**
19     * Page转换
20     *
21     * @param page       源对象
22     * @param convertFun T -> R lambda转换表达式
23     * @param <T>        输入类型
24     * @param <R>        输出类型
25     * @return 返回转化后输出类型的对象
26     */

27    public static <T, R> Page<R> convertPage(Page<T> page, Function<T, R> convertFun) {
28        if (Objects.isNull(page)) {
29            return new Page<>(0110, Collections.emptyList());
30        }
31        List<R> pageList = convertList(page.getItems(), convertFun);
32        return new Page<>(page.getTotalNumber(), page.getCurrentIndex(), page.getPageSize(), pageList);
33    }
34
35    /**
36     * ListData转换
37     *
38     * @param inputList  数据源
39     * @param convertFun T -> R lambda转换表达式
40     * @param <T>        输入类型
41     * @param <R>        输出类型
42     * @return 输出
43     */

44    public static <T, R> List<R> convertList(List<T> inputList, Function<T, R> convertFun) {
45        if (org.springframework.util.CollectionUtils.isEmpty(inputList)) {
46            return Lists.newArrayList();
47        }
48        return inputList
49                .stream()
50                .map(convertFun)
51                .collect(Collectors.toList());
52    }
53}

实战使用,在lambda方法进行转换: 先转换相同属性,再进行剩余属性赋值

 1 public interface OrderConverter {
2    OrderConverter INSTANCE = Mappers.getMapper(OrderConverter.class);
3        // 入参进行相同属性转换
4    TradeOrderDO createOrder2TradeOrderDO(TradeCreateOrderRequestDTO req);
5}
6 TradeOrderDO mainTradeOrder = BeanConvertUtil.convertObject(req, x -> {
7     TradeOrderDO tod = OrderConverter.INSTANCE.createOrder2TradeOrderDO(req);
8     tod.setOrderType(mainOrderType);
9     tod.setOrderCode(snowflakeIdAdapterService.getId());
10     tod.setOrderStatus(TradeStateEnum.ORDER_CREATED.getValue());
11     tod.setDateCreate(new Date());
12     tod.setDateUpdate(new Date());
13     return tod;
14});

其实对象转换也可以完全通过mapstruct提供的一些表达式进行转换,但是有时候写那个感觉不是很直观,其实都可以,我比较喜欢我这种形式,大家有建议也可以提出

NPE解决指南

1.null值手动判断[强制]

嵌套取值<3 推荐 null值判断(PS:强制null写在前面,别问为什么,问就是这样写你会意识到这里要NPE)

学会这点 基本意识有了

1null!=obj&&null!=obj.getXX()

2.Optional

2.1 Optional嵌套取值[强制]

参数>=3的取值操作
学会这点 基本告别NPE

这里以OrderInfo对象为例 获取purchaseType

1Optional<OrderInfo> optional = Optional.ofNullable(dto);
2Integer purchaseType = optional.map(OrderInfo::getOrderCarDTO)
3                                 .map(OrderCarDTO::getPurchaseType)
4                                 .orElse(null);

如果对取出的值如需再次进行判断操作 参考第1点

2.2 中断抛出异常[按需]

还是以上面的例子

1{
2    // ...
3    optional.map(OrderInfo::getOrderDTO).map(OrderDTO::getOrderBusinessType)
4              .orElseThrow(() -> new Exception("获取cityCode失败"));
5}

如果依赖某些值,可尽早fail-fast

3.对象判空[强制]

1Objects.equals(obj1,obj2);

4.Boolean值判断[强制]

弃用以下方式谢谢(PS:很多时候null判断你会丢的)

1null!=a&&a;

正确食用

1Boolean.TRUE.equals(a);

5.list正确判空姿势[强制]

1if (CollectionUtils.isEmpty(list)) {
2    // fail fast
3    // return xxObj or return;
4}
5List<Object> safeList = list.stream().filter(Objects::nonNull).collect(Collectors.toList());
6if (CollectionUtils.isEmpty(safeList)) {
7    // fail fast
8    // return xxObj or return;
9}

6.String正确判空姿势[强制]

1// 不为空
2if (StringUtil.isNotEmpty(s)) {
3    // ...
4}

7.包装类型转换判空[强制]

特别是遍历过程中使用,需要判断是否为空。

1int i = 0;
2list.forEach(item -> {
3    if(Objects.nonNull(item.getType)){
4     i += item.getType; //item.getType 返回 Integer   
5    }
6});

小结

融会贯通以上几招绝对告别NPE

END

未完待续,大家如果有好的建议,希望在留言中提出
原文git