【进阶篇】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 {
@Resource
private 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() 作类属性的拷贝,以下是几个常见的坑:
- 同一字段分别使用包装类型和基本类型,会出现转换异常,不会灵活识别转换
- null 值覆盖导致数据异常,即源属性有值为 null,但是目标属性有正常值,拷贝后会被 null 覆盖
- 内部类属性无法正常拷贝,即使类型和字段名均相同也无法拷贝成功,这个真的很坑
推荐泛型 + 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);
}
}
在需要转换的地方,直接调用上面定义的方法即可完成转换:
@Test
public void testCopyProperties(){
//Worker 和 WorkerVO 都需要 extends 上述的 CommonBean
Worker 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 列表数据
*/
@Override
public 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 种情况:
当进行对象引用操作时
为了避免 NPE 通常需要先判断该对象是否为 null。比如,在调用对象的方法或访问其属性之前,应该首先判断该对象不为 null 。
@Test
public void testNullMethod(){
//以下对象为 null,即表示该对象的变量在内存中不引用任何对象地址
Study study = null;
//则下面调用该对象的 get() 方法试图获得其属性,则会导致 NPE
String userName = study.getUserName();
log.info("用户名称:{}", userName);
}
从外部获取数据返回时
比如:从数据库查询数据返回、调用另一个接口的方法返回的数据,有可能返回对象的结果为 null:
@Override
public 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();
}
当集合进行元素操作时
由于集合本身并没有提供直接的判空机制,所以在进行元素操作之前,需要进行集合是否为空的判断:
/**
* 举个反例
*/
@Override
public 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 处理、缓存的使用、异步/解耦等内容,敬请期待。
那么 Java 实际开发中值得注意的几个小技巧的分享到这里就暂时结束了,如有不足和错误,还请大家指正。或者你有其它想说的,也欢迎大家在评论区交流!
【进阶篇】Java 实际开发中积累的几个小技巧(一)的更多相关文章
- Java项目开发中实现分页的三种方式一篇包会
前言 Java项目开发中经常要用到分页功能,现在普遍使用SpringBoot进行快速开发,而数据层主要整合SpringDataJPA和MyBatis两种框架,这两种框架都提供了相应的分页工具,使用 ...
- Java Web开发中路径问题小结
Java Web开发中,路径问题是个挺麻烦的问题,本文小结了几个常见的路径问题,希望能对各位读者有所帮助. (1) Web开发中路径的几个基本概念 假设在浏览器中访问了如下的页面,如图1所示: 图1 ...
- Java Web 开发中路径相关问题小结
Java Web开发中路径问题小结 (1) Web开发中路径的几个基本概念 假设在浏览器中访问了如下的页面,如图1所示: 图1 Eclipse中目录结构如图2所示: 图2 那么针对这个站点的几个基本概 ...
- 《Maven在Java项目开发中的应用》论文笔记(十七)
标题:Maven在Java项目开发中的应用 一.基本信息 时间:2019 来源:山西农业大学 关键词:Maven:Java Web:仓库:开发人员:极限编程; 二.研究内容 1.Maven 基本原理概 ...
- SQL开发中容易忽视的一些小地方(二)
原文:SQL开发中容易忽视的一些小地方(二) 目的:继上一篇:SQL开发中容易忽视的一些小地方(一) 总结SQL中的null用法后,本文我将说说表联接查询. 为了说明问题,我创建了两个表,分别是学生信 ...
- SQL开发中容易忽视的一些小地方( 三)
原文:SQL开发中容易忽视的一些小地方( 三) 目的:这篇文章我想说说我在工作中关于in和union all 的用法. 索引定义 : 微软的SQL SERVER提供了两种索引:聚集索引(cluster ...
- SQL开发中容易忽视的一些小地方(四)
原文:SQL开发中容易忽视的一些小地方(四) 本篇我想针对网上一些对于非聚集索引使用场合的某些说法进行一些更正. 下面引用下MSDN对于非聚集索引结构的描述. 非聚集索引结构: 1:非聚集索引与聚集索 ...
- SQL开发中容易忽视的一些小地方(五)
原文:SQL开发中容易忽视的一些小地方(五) 背景: 索引分类:众所周知,索引分为聚集索引和非聚集索引. 索引优点:加速数据查询. 问题:然而我们真的清楚索引的应用吗?你写的查询语句是否能充分应用上索 ...
- SQL开发中容易忽视的一些小地方(一)
原文:SQL开发中容易忽视的一些小地方(一) 写此系列文章缘由: 做开发三年来(B/S),发现基于web 架构的项目技术主要分两大方面: 第一:C#,它是程序的基础,也可是其它开发语言,没有开发语言也 ...
- SQL开发中容易忽视的一些小地方(六)
原文:SQL开发中容易忽视的一些小地方(六) 本文主旨:条件列上的索引对数据库delete操作的影响. 事由:今天在博客园北京俱乐部MSN群中和网友讨论了关于索引对delete的影响问题,事后感觉非常 ...
随机推荐
- 技术文档丨 OpenSCA技术原理之npm依赖解析
本文主要介绍基于npm包管理器的组件成分解析原理. npm介绍 npm(全称Node Package Manager)是Node.js标准的软件包管理器. npm的依赖管理文件是package.jso ...
- Serverless 奇点已来,下一个十年将驶向何方?
本文整理自 QCon 上海站 2022 丁宇(叔同)的演讲内容. 以前构建应用,需要买 ECS 实例,搭建开源软件体系然后维护它,流量大了扩容,流量小了缩容,整个过程非常复杂繁琐. 用了 Server ...
- C#查找算法2:插值查找
插值查找,有序表的一种查找方式.插值查找是根据查找关键字与查找表中最大最小记录关键字比较后的查找方法.插值查找基于二分查找,将查找点的选择改进为自适应选择,提高查找效率. 原理: (midInd ...
- longjmp 使 C++ RAII 失效
C 语言的 longjmp 没有进行栈展开,而是直接跳转.从 longjmp 到 setjmp 之间的所有析构函数都没有调用,也就是 RAII 失效. #include <setjmp.h> ...
- Angular系列教程之路由守卫
.markdown-body { line-height: 1.75; font-weight: 400; font-size: 16px; overflow-x: hidden; color: rg ...
- js - 元素 scrollTop 设置无效的原因 及 解决办法
原因 : 元素 display : flex ; 解决方法 : display : block;
- CSS - 滤镜的妙用 - 制作炫彩圆环(外加动画)
效果图如下: 话不多说,上代码: <!DOCTYPE html> <html lang="en"> <head> <meta charse ...
- 幻兽帕鲁 Palworld 私有服务器一键部署教程
<幻兽帕鲁>(日语:パルワールド,英语:Palworld) 是由日本开发商 Pocket Pair 推出的一款动作冒险生存游戏.游戏设定在一个由类似动物的生物 "帕鲁" ...
- [转帖]TiKV 内存参数性能调优
https://docs.pingcap.com/zh/tidb/stable/tune-tikv-memory-performance 本文档用于描述如何根据机器配置情况来调整 TiKV 的参数,使 ...
- [转帖]使用 Logical Import Mode
https://docs.pingcap.com/zh/tidb/v6.5/tidb-lightning-logical-import-mode-usage 配置及使用 可以通过以下配置文件使用 Lo ...