罗列工作中实际使用的一些代码技巧或者叫工具类;知识无大小,希望大家都有收获
rpc服务出参统一化
什么,出参统一化有什么好说的??? 我不知道你们有没有遇到过多少五花八门的外部服务提供的返回对象,可能别人没有规范约束,我们管不了,但是从我们这里出去的,我们可以强制约束一下,不然发生新老交替,这代码还能看吗
首先出参都叫xxDTO的,阿里java开发手册提到过;再者我们是提供服务的一方,错误码code,错误信息msg,以及返回结果data都是要明确体现出来的,像下面这样
1publicclassTradeResultDTOimplementsSerializable{
2/**
3* 默认失败编码
4*/
5privatestaticfinalString DEFAULT_FAIL_CODE ="500";
6privatebooleansuccess;
7privateString code;
8privateString msg;
9privateT data;
10publicstaticTradeResultDTOsuccess(T data){
11returnbase("200",null,true, data);
12}
13
14publicstaticTradeResultDTOfail(String code, String msg){
15returnbase(code, msg,false,null);
16}
17
18publicstaticTradeResultDTOfail(String msg){
19returnbase(DEFAULT_FAIL_CODE, msg,false,null);
20}
21
22publicstaticTradeResultDTOsuccess(){
23returnbase("200",null,true,null);
24}
25
26publicstaticTradeResultDTOfail(IError iError){
27returnbase(iError.getErrorCode(), iError.getErrorMsg(),false,null);
28}
29}
统一对象返回的结构就是上面这样
接着这个我想说的是,作为服务提供方,如果这个接口提供了返回值,我拿创建订单接口举例
1/**
2* 创建交易单,业务系统发起
3*
4*@paramreq 创建入参
5*@return返回创建信息
6*/
7TradeResultDTOcreateOrder(TradeCreateOrderRequestDTO req)
8
比如这个TradeCreateOrderResponseDTO 返回了订单号之类的基本信息,这个接口对于具体业务场景只能产生一笔订单号,我之前遇到过对方只是提示什么的错误信息(订单已存在),是的没错,他做了幂等,但是他没有返回原值,那对应的调用方进入了死循环,可能对应的业务系统,需要返回的订单号落到自己的数据库,一直异常一直回滚重试,没有任何意义;所以作为一个负责人的服务提供方,类似这种情况,如果你的方法有幂等,那么请一定返回存在的那个对象;
异常统一化
统一化使用,杜绝项目出现各种各样的自定义异常
对外统一抛出异常
我使用的统一化有两个方面:
抛出的自定义异常不要五花八门,一个就够了;很多人喜欢写各种各样的异常,初衷其实没错,但是人多手杂,自定义异常可能越写越乱;
异常信息最好尽可能的具体化,描述出业务产生异常原因就可以了,比如入参校验的用户信息不存在之类的;或者在调用用户中心的时候,捕获了该异常,此时你只需定义调用用户中心异常就可以了
然后看下工作中比较推荐的:
首先,需要搞一个统一抛出异常的工具ExceptionUtil(这里Exceptions是公司内部统一前后端交互的,基于这个包装一个基础util,统一整个组抛异常的入口)
1publicclassExceptionUtil{
2publicstaticOptimusExceptionBasefail(IError error)throwsOptimusExceptionBase{
3returnExceptions.fail(errorMessage(error));
4}
5
6publicstaticOptimusExceptionBasefail(IError error, String... msg)throwsOptimusExceptionBase{
7returnExceptions.fail(errorMessage(error, msg));
8}
9
10publicstaticOptimusExceptionBasefault(IError error)throwsOptimusExceptionBase{
11returnExceptions.fault(errorMessage(error));
12}
13
14publicstaticOptimusExceptionBasefault(IError error, String... msg)throwsOptimusExceptionBase{
15returnExceptions.fault(errorMessage(error, msg));
16}
17
18
19privatestaticErrorMessageerrorMessage(IError error){
20if(error ==null) {
21error = CommonErrorEnum.DEFAULT_ERROR;
22}
23returnErrorMessage.errorMessage("500","["+ error.getErrorCode() +"]"+ error.getErrorMsg());
24}
25
26privatestaticErrorMessageerrorMessage(IError error, String... msg){
27if(error ==null) {
28error = CommonErrorEnum.DEFAULT_ERROR;
29}
30returnErrorMessage.errorMessage("500","["+ error.getErrorCode() +"]"+ MessageFormat.format(error.getErrorMsg(), msg));
31}
32}
其实上面代码里也体现出来IError这个接口了,我们的错误枚举都需要实现这个异常接口,方便统一获取对应的错误码和错误信息,这里列举一下通用异常的定义
1publicinterfaceIError{
2StringgetErrorCode();
3
4StringgetErrorMsg();
5}
6@AllArgsConstructor
7publicenumCommonErrorEnum implements IError {
8/**
9*
10*/
11DEFAULT_ERROR("00000000","系统异常"),
12REQUEST_OBJECT_IS_NULL_ERROR("00000001","入参对象为空"),
13PARAMS_CANNOT_BE_NULL_ERROR("00000002","参数不能为空"),
14BUILD_LOCK_KEY_ERROR("00000003","系统异常:lock key异常"),
15REPEAT_COMMIT_ERROR("00000004","正在提交中,请稍候");
16
17privateString code;
18privateString msg;
19
20@Override
21publicStringgetErrorCode(){
22returncode;
23}
24
25@Override
26publicStringgetErrorMsg(){
27returnmsg;
28}
29}
类似上面CommonErrorEnum的方式我们可以按照具体业务定义相应的枚举,比如OrderErrorEnum、PayErrorEnum之类,不仅具有区分度而且,也能瞬速定位问题;
所以对外抛出异常统一化就一把梭:统一util和业务错误枚举分类
对内统一捕获外部异常
很多时候我们需要调用别人的服务然后写了一些adapter适配类,然后在里面trycatch一把;其实这时候可以利用aop好好搞一把就完事了,并且统一输出adapter层里面的日志
1publicObjecttransferException(ProceedingJoinPoint joinPoint){
2try{
3Object result = joinPoint.proceed();
4log.info("adapter result:{}", JSON.toJSONString(result));
5returnresult;
6}catch(Throwable exception) {
7MethodSignature signature = (MethodSignature) joinPoint.getSignature();
8Method method = signature.getMethod();
9log.error("{}.{} throw exception", method.getDeclaringClass().getName(), method.getName(), exception);
10throwExceptionUtils.fault(CommonErrorEnum.ADAPTER_SERVICE_ERROR);
11returnnull;
12}
13}
上面这段统一捕获了外部服务,记录异常日志,避免了每个adapter类重复捕获的问题
**
**
用过swagger的应该了解api方法里有对应的注解属性约束是否必填项,但是如果判断不是在api入口又或者没有类似的注解,你们一般怎么做的,下面给出我自己的一种简单工具;有更好大佬的可以推荐一下
ParamCheckUtil.java
1@Slf4j
2publicclassParamCheckUtil{
3
4/**
5* 校验请求参数是否为空
6*
7*@paramrequestParams 请求入参
8*@paramkeys 属性值数组
9*/
10publicstaticvoidcheckParams(Object requestParams, String... keys){
11if(null== requestParams) {
12throwExceptionUtil.fault(CommonErrorEnum.REQUEST_OBJECT_IS_NULL_ERROR);
13}
14StringBuilder sb =newStringBuilder();
15for(String fieldName : keys) {
16Object value =null;
17Type type =null;
18try{
19String firstLetter = fieldName.substring(0,1).toUpperCase();
20String getter ="get"+ firstLetter + fieldName.substring(1);
21Method method = requestParams.getClass().getMethod(getter);
22value = method.invoke(requestParams);
23type = method.getReturnType();
24}catch(Exception e) {
25log.error("获取属性值出错,requestParams={}, fieldName={}", requestParams, fieldName);
26}finally{
27// 判空标志 String/Collection/Map特殊处理
28booleanisEmpty =
29(String.class == type && StringUtil.isEmpty((String) value))
30|| (Collection.class == type && CollectionUtils.isEmpty((Collection) value))
31|| (Map.class == type && CollectionUtils.isEmpty((Collection) value))
32|| (null== value);
33if(isEmpty) {
34if(sb.length() !=0) {
35sb.append(",");
36}
37sb.append(fieldName);
38}
39}
40}
41
42if(sb.length() >0) {
43log.error(sb.toString() + CommonErrorEnum.PARAMS_CANNOT_BE_NULL_ERROR.getErrorMsg());
44throwExceptionUtil.fault(CommonErrorEnum.PARAMS_CANNOT_BE_NULL_ERROR, sb.toString() + CommonErrorEnum.PARAMS_CANNOT_BE_NULL_ERROR.getErrorMsg());
45}
46}
47
48// test
49publicstaticvoidmain(String[] args){
50TradeCreateOrderRequestDTO tradeCreateOrderRequestDTO =newTradeCreateOrderRequestDTO();
51tradeCreateOrderRequestDTO.setBusinessNo("");
52ParamCheckUtil.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
2publicclassUserInfo{
3privateString id;
4privateString name;
5
6publicstaticvoidmain(String[] args){
7UserInfo userInfo = UserInfo.builder().id("a").name("name").build();
8}
9}
构造可变对象,推荐提供链式调用形式 使用lombok @Accessors(chain = true)注解
1@Data
2@Accessors(chain =true)
3publicclassCardInfo{
4privateString id;
5privateString name;
6publicstaticvoidmain(String[] args){
7CardInfo cardInfo =newCardInfo().setId("c").setName("name");
8}
9}
就一把梭:lambda工具类+mapstruct进行转换
BeanConvertUtil.java 通用的对象、list、Page转换
1publicclassBeanConvertUtil{
2/**
3* 对象转换
4*
5*@paramsource 源对象
6*@paramconvertFun T -> R lambda转换表达式
7*@param 输入类型
8*@param 输出类型
9*@return返回转化后输出类型的对象
10*/
11publicstaticRconvertObject(T source, Function convertFun){
12if(null== source) {
13returnnull;
14}
15returnconvertFun.apply(source);
16}
17
18/**
19* Page转换
20*
21*@parampage 源对象
22*@paramconvertFun T -> R lambda转换表达式
23*@param 输入类型
24*@param 输出类型
25*@return返回转化后输出类型的对象
26*/
27publicstaticPageconvertPage(Page page, Function convertFun){
28if(Objects.isNull(page)) {
29returnnewPage<>(0,1,10, Collections.emptyList());
30}
31List pageList = convertList(page.getItems(), convertFun);
32returnnewPage<>(page.getTotalNumber(), page.getCurrentIndex(), page.getPageSize(), pageList);
33}
34
35/**
36* ListData转换
37*
38*@paraminputList 数据源
39*@paramconvertFun T -> R lambda转换表达式
40*@param 输入类型
41*@param 输出类型
42*@return输出
43*/
44publicstaticListconvertList(List inputList, Function convertFun){
45if(org.springframework.util.CollectionUtils.isEmpty(inputList)) {
46returnLists.newArrayList();
47}
48returninputList
49.stream()
50.map(convertFun)
51.collect(Collectors.toList());
52}
53}
实战使用,在lambda方法进行转换: 先转换相同属性,再进行剩余属性赋值
1publicinterfaceOrderConverter{
2OrderConverter INSTANCE = Mappers.getMapper(OrderConverter.class);
3// 入参进行相同属性转换
4TradeOrderDOcreateOrder2TradeOrderDO(TradeCreateOrderRequestDTO req);
5}
6TradeOrderDO mainTradeOrder = BeanConvertUtil.convertObject(req, x -> {
7TradeOrderDO tod = OrderConverter.INSTANCE.createOrder2TradeOrderDO(req);
8tod.setOrderType(mainOrderType);
9tod.setOrderCode(snowflakeIdAdapterService.getId());
10tod.setOrderStatus(TradeStateEnum.ORDER_CREATED.getValue());
11tod.setDateCreate(newDate());
12tod.setDateUpdate(newDate());
13returntod;
14});
其实对象转换也可以完全通过mapstruct提供的一些表达式进行转换,但是有时候写那个感觉不是很直观,其实都可以,我比较喜欢我这种形式,大家有建议也可以提出
NPE解决指南
嵌套取值<3 推荐 null值判断(PS:强制null写在前面,别问为什么,问就是这样写你会意识到这里要NPE)
学会这点 基本意识有了
1null!=obj&&null!=obj.getXX()
## 2.1 Optional嵌套取值[强制]
参数>=3的取值操作
学会这点 基本告别NPE
这里以OrderInfo对象为例 获取purchaseType
1Optional optional = Optional.ofNullable(dto);
2Integer purchaseType = optional.map(OrderInfo::getOrderCarDTO)
3.map(OrderCarDTO::getPurchaseType)
4.orElse(null);
如果对取出的值如需再次进行判断操作 参考第1点
2.2 中断抛出异常[按需]
还是以上面的例子
1{
2// ...
3optional.map(OrderInfo::getOrderDTO).map(OrderDTO::getOrderBusinessType)
4.orElseThrow(() ->newException("获取cityCode失败"));
5}
如果依赖某些值,可尽早fail-fast
1Objects.equals(obj1,obj2);
弃用以下方式谢谢(PS:很多时候null判断你会丢的)
1null!=a&&a;
正确食用
1Boolean.TRUE.equals(a);
1if(CollectionUtils.isEmpty(list)) {
2// fail fast
3// return xxObj or return;
4}
5List 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}
特别是遍历过程中使用,需要判断是否为空。
1inti =0;
2list.forEach(item -> {
3if(Objects.nonNull(item.getType)){
4i += item.getType;//item.getType 返回 Integer
5}
6});
小结
融会贯通以上几招绝对告别NPE
未完待续,大家如果有好的建议,希望在留言中提出