Java实战项目开发的个人技巧分享

前言

笔者目前从事一线 Java 开发今年是第 3 个年头了,从 0-1的 SaaS、PaaS 的项目做过,多租户下定制化开发项目也做过,项目的 PM 也做过...

在实际的开发中积累了一些技巧和经验,包括线上 bug 处理、日常业务开发、团队开发规范等等。现在在这里分享出来,作为成长的记录和知识的更新,希望与大家共勉。

免责声明:以下所有demo、代码和测试都是出自笔者本人的构思和实践,不涉及企业隐私和商业机密,属于个人的知识分享。

一、枚举类的注解

看起来很常见的枚举,可能也隐藏着使用上的问题:你有没有在代码里不小心做过改变枚举值的操作?或者为怎么合理规范地写构造方法/成员方法而烦恼?

那么不妨来看看我的示例,注释写得比较清楚了:

@Getter// 只允许对属性 get,不允许 set
@RequiredArgsConstructor// 为枚举的每个属性生成有参构造
public enum ProjectStatusEnum {SURVEY("已调研"),APPROVAL("已立项"),PROGRESSING("进行中"),COMPLETED("已完成");// 对该成员变量使用 final 来修饰,表明一旦赋值就不可变private final String name;
}

为什么要分享这个呢?团队里开发的时候还真有人在使用枚举的时候 set() 改变了枚举值,编译通过但运行在一定条件触发后,导致了 bug 排查了一下午。


二、RESTful 接口

本节的内容其实更像是一种规范,因为见过不少别的部门同事写的项目代码,接口的风格真是迥异(写什么的都有),当我接手重构的时候真是头皮发麻。

首先就是禁止使用 Swagger 和 Knife4j 接口文档生成工具,原因无它:代码侵入性太强和需要写的注解太多,而且还是公司安全漏洞扫描单上的常客。

其次可以使用开源的 smart-doc 来代替,只要遵循 Javadoc 的标准注释写法即可。

  • 请求方式和参数

    在 Controller 中一般只使用 GET 请求和 POST 请求,无需使用 PUT 和 DELETE。

    GET 请求一般只使用 @GetMapping,主要配合 @RequestParam、@PathVariable 使用,请求的拼接参数一般不超过 5 个。

    POST 请求一般只使用 @PostMapping,主要配合 @RequestBody 使用,请求参数统一使用 DTO 对象。

    如果入参需要带上请求头,可以加上 @RequestHeader 注解;如果是提交表单,可以加上 @Valid 做参数校验,如 @NotBlank 和 @NotNull 等。

  • 统一返回

    主要包括:返回体(返回码+信息+返回数据)+ 封装VO + 统一异常,这些基础的东西肯定是遵顼团队/公司的开发规范,不需要再自己造轮子。

一个简单的 Controller 示例如下:

/*** 测试接口*/
@RestController
@RequestMapping("/study")
public class StudyController {@Resourceprivate StudyService studyService;/*** 新增xx* @return 是否成功*/@PostMapping("/add")//还可以加上其它必要注解,如:登录/权限/日志记录等public Response<Boolean> addStudy(@RequestBody @Valid StudyDTO studyDTO) {return ResultUtils.success(studyService.addStudy(studyDTO));}/*** xx列表(不分页)* @return 列表数据*/@GetMapping("/list")public Response<List<StudyListVO>> getList(@RequestParam("id") String id) {return ResultUtils.success(studyService.getList(id));}
}

三、类属性转换

在实际 Java 开发中,关于 VO、Entity、DTO 等对象属性之间的赋值是我们经常遇见的,最简单使用 @Data 去逐个 .set() 或者 @Builder 链式 .build(),其实都是很靠谱的办法,而且可以控制颗粒度。但属性一多起来的话,比如二十个以上,那么代码就会显得很长。所以有没有办法一行代码就搞定类属性转换呢?

首先不推荐使用 BeanUtils.copyProperties() 作类属性的拷贝,以下是几个常见的坑:

  1. 同一字段分别使用包装类型和基本类型,会出现转换异常,不会灵活识别转换
  2. null 值覆盖导致数据异常,即源属性有值为 null,但是目标属性有正常值,拷贝后会被 null 覆盖
  3. 内部类属性无法正常拷贝,即使类型和字段名均相同也无法拷贝成功,这个真的很坑

推荐泛型 + JSON组合的方式来实现类属性的转换,具体步骤如下:

  • 定义一个父类 CommonBean,让项目里所有 VO、Entity、DTO 等类都继承该类,类里面就只定义一个公共的泛型方法即可:

    public class CommonBean implements Serializable {/*** @apiNote 全局类型转换方法:入参和返参均支持泛型* @param target* @return 目标类型* @param <T>*/public <T> T copyProperties(Class<T> target) {//本质上就是进行了 Object -> json字符串 -> 到指定类型的转换return JSON.parseObject(JSON.toJSONString(this), target);}
    }
    
  • 在需要转换的地方,直接调用上面定义的方法即可完成转换:

        @Testpublic void testCopyProperties(){//Worker 和 WorkerVO 都需要 extends 上述的 CommonBeanWorker worker = new Worker();worker.setName("Alex");worker.setStatus(NumberUtils.INTEGER_ONE);//直接使用,得到需要的目标 VO 对象WorkerVO workerVO = worker.copyProperties(WorkerVO.class);log.info("转换结果:{}",workerVO);}
    

四、Stream 流

  • map() 流元素的映射、collect() 收集流,这两个就不展开讲了,几乎可以说是 Stream 流用的最多的方法,到处都是例子。

  • filter() 流按条件过滤和 sort() 流元素排序,这两个组合可以简单举个例子:

    /*** Stream 流的过滤与排序* @param id* @return 列表数据*/@Overridepublic List<StudyVO> getList(String id) {List<StudyVO> resultList = this.list(new LambdaQueryWrapper<Study>().eq(Study::getIsDelete, NumberUtils.INTEGER_ZERO)).stream().filter(e -> Constants.USER_ROLE_USER.equals(e.getUserRole())).sorted(Comparator.comparing(Study::getAge).reversed()).map(e -> e.copyProperties(StudyVO.class)).collect(Collectors.toList());return Optional.of(resultList).orElse(null);}

像上述从MySQL 里查表数据的例子,其实能在数据库做的操作就没必要在 Stream 流里操作。像 .select()、.eq()、.gt()、.orderByDesc() 等都可以完成,非数据库语句查询的情况下,使用 Stream 操作集合还是有必要的。

  • anyMatch() 平时用的不多,但当遇到了之后,它的用法还是能让人眼前一亮的:
 /*** 测试 Stream 的 AnyMatch 方法* @return*/public List<ArticleVO> testStreamAnyMatch(){List<Article> articleList = this.list(new LambdaQueryWrapper<Article>().eq(Article::getIsDelete, NumberUtils.INTEGER_ZERO));if (CollectionUtils.isNotEmpty(articleList)){//AnyMatch() 方法返回的是一个布尔,用来判断流中是否有满足条件的元素final boolean flag = articleList.parallelStream().anyMatch(e -> Objects.nonNull(e)//文章要有内容&& Objects.nonNull(e.getContent())//文章要有标题&& StringUtils.isNotBlank(e.getTitle()));if (flag){return articleList.parallelStream().map(e -> e.copyProperties(ArticleVO.class)).collect(Collectors.toList());}}return new ArrayList<>();}
  • skip() 跳过流的某些元素和 limit() 指定流元素的位置,组合起来使用可以实现自分页。这个在线上问题-如何避免集合内存溢出的文章中会拿出来单独讲一下。

五、判空和断言

5.1判空部分

首先什么情况下需要判空?基本是以下这 3 种情况:

  1. 当进行对象引用操作时

    为了避免 NPE 通常需要先判断该对象是否为 null。比如,在调用对象的方法或访问其属性之前,应该首先判断该对象不为 null 。

        @Testpublic void testNullMethod(){//以下对象为 null,即表示该对象的变量在内存中不引用任何对象地址Study study = null;//则下面调用该对象的 get() 方法试图获得其属性,则会导致 NPEString userName = study.getUserName();log.info("用户名称:{}", userName);}
    
  2. 从外部获取数据返回时

    比如:从数据库查询数据返回、调用另一个接口的方法返回的数据,有可能返回对象的结果为 null:

        @Overridepublic StudyVO detail(String id) {//查 MySQL 返回的集合数据,即使 null 赋值也没有问题List<Study> resultList = this.list(new LambdaQueryWrapper<Study>().eq(Study::getIsDelete, NumberUtils.INTEGER_ZERO).orderByDesc(Study::getAge));//但是同样需要对集合对象进行判空,有值再进行下一步if (CollectionUtils.isNotEmpty(resultList)){return resultList.get(NumberUtils.INTEGER_ZERO).copyProperties(StudyVO.class);}return new StudyVO();}
    
  3. 当集合进行元素操作时

    由于集合本身并没有提供直接的判空机制,所以在进行元素操作之前,需要进行集合是否为空的判断:

        /*** 举个反例*/@Overridepublic StudyVO detail(String id) {//调用其它方法,并将返回结果赋值,是 null 也没问题List<StudyVO> voList = this.getList(id);//但在使用 get() 方法前不判断集合里有没有元素,那么会报数组越界return voList.get(NumberUtils.INTEGER_ZERO);}
    

那么,常用判空的工具有哪些呢?从我个人的开发经验来说主要有以下几种:

  • 对象的判空

    推荐统一使用 java.util 包的 Objects.nonNull() 等方法。

  • 集合的判空

    推荐统一使用 org.apache.commons.collections.CollectionUtils 包的 .isNotEmpty() 等方法。

  • Map 对象判空

    推荐统一使用 Map 自带的 .isEmpty() 、 .containsKey()、.equals() 这三者配合使用。

  • 字符串的判空

    推荐统一使用 org.apache.commons.lang3.StringUtils 包的 .isNotBlank() 等方法。

  • Optional类

     Optional//of(T value)方法用于创建一个包含指定值的 Optional 对象,该方法接收一个非 null 值作为参数.of()//ofNullable(T value)方法用于创建一个包含指定值的 Optional 对象,该方法接收一个可能为 null 的值作为参数.ofNullable()//isPresent()方法用于判断 Optional 对象中是否存在非 null 值,有值就返回 true ,否则返回 false.isPreset()//orElse(T other)方法顾名思义,泛型 T 表示其它的类型.orElse()//ifPresent(Consumer<? super T> consumer) 判断该对象是否值,有则调用传入的 Consumer 类型函数处理该值。否则,什么也不做.ifPresent()  //map(Function<? super T, ? extends U> mapper) 用于对 Optional 对象中的值进行映射,并返回一个新的 Optional 对象.map()//filter(Predicate<? super T> predicate) 用于过滤 Optional 对象中的值,只有当值满足特定条件时才保留.filter()
    

什么情况下可以不需要判空?

答:一般当方法允许直接返回 null 时,可以不对返回值进行判空。

5.2断言部分

断言的应用场景可能就比较单一了,我自己的理解是:在业务因为无数据且需要强制中断业务时,可以使用到断言。那么接口允许返回空、或者返回约定的状态码等这些情况除外。

一定程度上可以简化“先判断,再抛异常”的代码。即替换以下代码,三行变一行:

    if (Objects.nonNull(Object obj)){throw new BusinessException("obj error");}
//对象型断言:如果该对象为 null ,则抛出 String 类型的异常信息
Assert.notNull(@Nullable Object object, String message);

常用的断言如下:

//布尔型断言:如果布尔表达式为 false,则抛出 String 类型的异常信息
Assert.isTrue(boolean expression, String message);//对象型断言:如果该对象为 null ,则抛出 String 类型的异常信息
Assert.notNull(@Nullable Object object, String message);//字符串型断言:如果该字符串无内容,则抛出 String 类型的异常信息
Assert.hasText(@Nullable String text, String message)//集合型断言:如果集合(包括 Map)对象无内容,则抛出 String 类型的异常信息
Assert.notEmpty(@Nullable Map<?, ?> map, String message);
Assert.notEmpty(@Nullable Collection<?> collection, String message);

文章小结

作为一个系列文章的开头,本文的内容偏基础。在之后的文章中我会分享一些关于真实项目中关于线上 bug 处理、缓存的使用、异步/解耦等内容,敬请期待。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/20000.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NetApp财季报告亮点:全闪存阵列需求强劲,云计算收入增长放缓但AI领域前景乐观

在最新的财季报告中&#xff0c;NetApp的收入因全闪存阵列的强劲需求而显著增长。截至2024年4月26日的2024财年第四季度&#xff0c;NetApp的收入连续第三个季度上升&#xff0c;达到了16.7亿美元&#xff0c;较前一年同期增长6%&#xff0c;超出公司指导中值。净利润为2.91亿美…

前端开发:$nextTick()的使用及原理

目录 前言 $nextTick()的概念 $nextTick()的用法和原理 1、$nextTick()用法 2、$nextTick()原理 $nextTick()的具体使用示例 拓展&#xff1a;面试中考察$nextTick()的底层原理 最后 前言 在前端开发中&#xff0c;涉及到JS原生的使用原理是非常重要的知识点&#xff0…

Kafka 消息保留策略及其影响详解

目录 Kafka 消息保留策略及其影响详解消息保留策略超过保留时间的消息删除过程删除过程对正在消费的消息和偏移量的影响不同保留时间设置的区别总结 博客示例题目&#xff1a;详解 Kafka 消息保留策略及其影响消息保留策略超过保留时间的消息删除过程删除过程对正在消费的消息和…

使用pytorch搭建textCNN、BERT、transformer进行文本分类

首先展示数据处理后的类型&#xff1a; 第一列为文本&#xff0c;第二类为标注的标签&#xff0c;数据保存在xlsx的表格中&#xff0c;分为训练集和验证集。 textCNN 直接上整个工程代码&#xff1a; import pandas as pd import numpy as np import torch from torch.util…

springMVC返回对象或List集合时报错 无法解析

报错信息 &#xff1a;No converter found for return value of type: class java.util.ArrayList 原因&#xff1a;没有导入json 所以java在返回对象时解析错误 解决&#xff1a;1、添加所需的依赖&#xff1a;确保在项目的依赖管理中添加了适当的 JSON 库&#xff0c;如 J…

c++函数基础总结

在给出的代码片段中&#xff0c;我们看到两部分内容&#xff1a;一个类定义和一个全局函数声明。让我们逐一分析它们&#xff1a; 类定义&#xff1a; cpp复制代码 class { public: void a(); }; 这个类定义是不完整的&#xff0c;因为它没有类名。但为了说明&#xff0c;我…

Linux开发工具(个人使用)

Linux开发工具 1.Linux yum软件包管理器1.1Linux安装程序有三种方式1.2注意事项1.3如何查看&#xff0c;安装&#xff0c;卸载软件包1.3.1查看软件包1.3.2安装软件包1.3.3卸载软件 2.Linux vim编辑器2.1vim的基本操作2.2vim正常模式命令集2.3vim底行模式命令集2.4vim配置 3.Lin…

金色晚年的温暖旋律

​在小镇的东头&#xff0c;有一座带着些许岁月痕迹的二层小楼。这里住着一位刚刚退休的老人&#xff0c;李大爷。他的退休生活并不像人们想象的那样枯燥无味&#xff0c;反而充满了色彩和活力。 退休后的李大爷并没有选择整日坐在摇椅上晒太阳&#xff0c;而是找到了新的生活…

如何设置eclipse中web.xml 文件的地址

新学了一个项目 &#xff0c;项目结构与平常自己构建的web项目不同 &#xff0c;用eclipse打开之后&#xff0c;eclipse竟然自己创建了一个web.xml 而项目里面原本的web.xml 文件eclipse没有识别出来&#xff0c;导致后来浏览器访问任何路径都报错404 一、修改项目中web.xml的…

Centos7.9环境下安装Keepalived(亲测版)

目录 一、在线安装 二、离线安装 (1)、 下载 (2)、安装依赖包 (3)、解压文件 (4)、编译 (4.1)、进入 keepalived-2.2.8 目录中 (4.2)、安装Keepalived (5)、配置文件修改 (6)、启动 (7)、检查启动状态 (8)、 设置开机自启 (9)、配置从节点 (10)、启动从节点keepalived…

vue3中实现鼠标点击后出现点击特效

一、效果展示 图片下方为效果体验地址 缓若江海凝清光 二、代码 js中&#xff1a; <script setup lang"ts"> window.addEventListener("click", (e: MouseEvent) > {const pointer document.createElement("div");pointer.classLi…

Oracle索引的在线添加方法

在Oracle中&#xff0c;索引的在线添加方法主要是通过使用CREATE INDEX语句时添加ONLINE选项来实现的。以下是关于Oracle索引在线添加方法的详细步骤和要点&#xff1a; 了解在线添加索引的概念&#xff1a; 在线添加索引意味着在创建索引的过程中&#xff0c;其他数据库操作…

头歌数据结构与算法课程设计中-硬币找零

给定n种不同面值的硬币k_i和每种硬币的数量x_i以及一个总金额k,请编写一个程序计算最少需要几枚硬币凑出这个金额k,凑出的方案是什么? 如果凑不出则输出“凑不出” 输入描述: 第一行两个正整数,n和k 然后n行每行两个数k_i和x_i 表示k_i面值的硬币有x_i个,中间以空格分隔 输…

k8s处于pending状态的原因有哪些

k8s处于pending状态的原因 资源不足&#xff1a;集群中的资源&#xff08;如CPU、内存&#xff09;不足以满足Pod所需的资源请求&#xff0c;导致Pod无法调度。 调度器问题&#xff1a;调度器无法为Pod找到合适的节点进行调度&#xff0c;可能是由于节点资源不足或调度策略配置…

数模混合芯片之可靠性设计

一、可靠性设计目的 数模混合芯片设计之所以需要可靠性设计&#xff0c;主要原因有以下几点&#xff1a; 工艺与环境影响&#xff1a; 半导体制造工艺存在着不可避免的随机和系统性偏差&#xff0c;这可能导致芯片内部的模拟电路和数字电路参数发生变化&#xff0c;影响性能…

CobaltStrike基本渗透

目录 CobaltStrike简介 主要功能&#xff1a; 使用注意&#xff1a; 在使用CobaltStrike进行渗透测试时&#xff0c;务必遵守法律法规&#xff0c;并获得合法授权。 CobaltStrike安装 前提 安装 服务端安装 windows安装 CS基本使用 监听器配置 一些基本的攻击…

算法(十四)动态规划

算法概念 动态规划&#xff08;Dynamic Programming&#xff09;是一种分阶段求解的算法思想&#xff0c;通过拆分问题&#xff0c;定义问题状态和状态之间的关系&#xff0c;使得问题能够以递推&#xff08;分治&#xff09;的方式去解决。动态规划中有三个重点概念&#xff…

【监控】prometheus自定义指标 exporter

一、【写在前面】 prometheus自定义指标本质是用代码自己写一个网络访问的采集器&#xff0c;你可以在官网看到&#xff0c;Client libraries | Prometheus官方支持的语言有GO JAVA PYTHON RUBY RUST, 第三方的库就支持的更多了&#xff0c;有BASH C CPP LUA C# JS PHP R PER…

智慧医院物联网建设-统一管理物联网终端及应用

近年来&#xff0c;国家卫健委相继出台的政策和评估标准体系中&#xff0c;都涵盖了强化物联网建设的内容。物联网建设已成为智慧医院建设的核心议题之一。 作为医院高质量发展的关键驱动力&#xff0c;物联网的顶层设计与网络架构设计规划&#xff0c;既需要结合现代信息技术的…

Keras深度学习框架基础第四讲:层接口(layers API)“层权重约束”

1、层权重约束概述 1.1 层权重约束的定义 Keras层权重约束的定义主要涉及到在训练神经网络模型时&#xff0c;对层的权重参数施加一定的限制或约束&#xff0c;以提高模型的泛化能力和稳定性。以下是关于Keras层权重约束的详细定义&#xff1a; 约束的目的&#xff1a; 防止…