如何编写优雅的 Java 代码
不知道各位在项目开发过程中有没有过这种体会,接手上一任的代码,看到代码的那一刻,有一种想要砸电脑的冲动,一个方法体内写了无数行代码,到处皆可看到复制粘贴的代码,变量命名也让人看不懂。
各位在编码时,是否有想过,我如何才能写出高质量的代码,写出优雅的代码,写出高度可扩展的代码。我相信大家都希望能写出那样的代码,让人佩服的五体投地,可不知道如何写,那么本文就是为帮助大家提高编码质量而生的。
我相信,大家在看完本文后,一定会有所领悟。下面,我们就进入主题。
在介绍 lombok 之前,我们先来看一段代码:
这段代码大家应该都很熟悉,我们在开发 JavaWeb 项目时,定义一个 Bean,会先写好属性,然后设置 getter/setter 方法,这段代码本身没有任何问题,也必须这样写。
但是每个 bean 都需要些 getter/setter,这样写的话就不够优雅了,这段代码我们如何优雅的写呢?接下来就轮到强大的 lombok 出场了。
lombok 是一个可以通过注解的形式来简化我们的代码的一个插件,要使用它,我们应该先安装插件,安装步骤如下:
1.
打开 IDEA 的 Plugins,点击 Browse repositories。
2.
搜索 lombok,并安装它,安装好后重启 IDEA。
3.
打开 Settings,找到 Annotaion Processors,将 Enable annotaion processing 勾选上。
4.单纯这样还不够,我们要用到 lombok 的注解还需要添加其依赖:
我们可以看到,在类上加入了 Getter 和 Setter 两个注解,将之前写的 getter/setter 方法干掉了,这种代码看着清爽多了,写个 main 方法来测试下:
我们并没有写任何 setter/getter 方法,只是加了两个注解就可以调用了,这是为什么呢?这是因为 lombok 提供的 Getter 和 Setter 注解是编译时注解,也就是在编译时,lombok 会自动为我们添加 getter/setter 方法,因此我们不需要显式地去写 getter/setter 方法而可以直接调用,这样的代码是不是看着非常优雅。
当然,lombok 的功能不止于此,它提供了很多注解以简化我们的代码,下面,我将分别介绍它的其他常用注解:
该注解的作用是是否开启链式调用,比如我们开启链式调用:
构建者模式,我们在使用第三方框架时经常能看到 Builder 模式,比如 HttpClient 的:
那么,通过 Builder 注解可以很方便的实现它:
编译时添加 getter、setter、toString、equals 和 hashCode 方法,如:
添加输入输出流 close 方法,如:
我们通过断点调试发现,它会调用 close(),说明 lombok 帮我们生成了 close():
通过 lombok 的注解,可以极大的减少我们的代码量,并且更加清爽,更加优雅。
lombok 还有很多注解,如:
- @ToString: 生成 toString() 方法。
- @EqualsAndHashCode: 生成 equals 和 hashCode 方法。
日志相关注解(当然需要添加相关日志依赖):
- Slf4j、Log4j 等。
- NoArgsConstructor、AllArgsConstructor: 生成无参和全参构造函数。
- Value: 生成 getter、toString、equals、hashCode 和全参构造函数。
- NonNull: 标注在字段和方法上,表示非空,会在第一次使用时判断。
一说到设计模式,我相信大家都能说出那 23 种设计模式,并且还能说出每种设计模式的用法,但是大多数同学应该都没有在实际应用中真正运用过设计模式,还只是停留在理论阶段。
不知道各位是否有过这个感觉,整个应用被相同的代码充斥着,自己也知道这种代码不好,但是不知道怎么做优化,虽然知道有 23 种设计模式,却不知道怎么运用。
本节我就将以实际的例子教大家如何在实际应用中灵活运用所学的设计模式。
(实际应用中,一个场景可能不只包含一个设计模式,很有可能需要多种设计模式配合使用才能写出优雅的高质量的代码。)
我们在做后台管理系统时,会有这样一个需求,根据后台的数据统计导出报表,需要支持 Excel、Word、PPT、PDF 等格式。
对于以上需求,一般做法是:为每一个导出报表的方法提供一个方法,然后在 Service 里判断,如果为 excel,则调用 excel 的方法,如果为 Word 则调用 word 的方法,
如:
这样写本身没有问题,也能实现需求,但是它有以下缺点:
- 1.代码不够优雅,业务方法内存在太多 if – else。
- 2.扩展性不强,每增加一个报表格式,就需要修改业务方法,增加一个 if – else。
我们在开发时需要遵循有一个原则:一个方法做你该做的事。也就是无论增加什么样的报表格式,业务方法 exportReport 的作用依然是导出功能,除非业务需求发生改变,否则不能修改业务方法。
那么,我们该怎么改造呢?
我们发现,导出报表可以导出不同的格式,这些格式我们可以理解为产品,需要由一个地方产出,因此马上就能想到可以利用工厂模式对其进行改造,下面是改造后的代码:
这样就完成了工厂模式对报表导出的改造,在业务方法内,通过 TemplateFactory 创建 template,然后调用 template 的 read 或 write 方法,以后我们每增加一个格式,只需要实现 Template 的相应方法,在 factory 实例化它即可,无需修改业务方法。
此场景用到的设计模式有:简单工厂模式。
这种场景也比较多见,比如:
- 1.我们实现一个注册功能,注册的字段比较多,可能就会分步骤进行,第一步,填写手机号验证码,第二步,填写头像昵称。
- 2.我们发布一篇文章,第一步,填写标题和内容,第二步,设置定时任务,第三步,设置文章打赏规则。
针对这些情况,一般做法也是在业务方法内,做个 if – else 判断,如果是第一步,则执行第一步的业务,如果是第二步,则执行第二步的业务,这种方式同场景 1 一样,代码也比较难看。
对于这样的场景,我们同样可以使用设计模式来实现,因为每一步都是有关联的,执行完第一步,才能执行第二步,执行完第二步才能执行第三步,它很像一条链子将它们联系起来,所以很容易想到可以采用责任链模式。
下面,请看具体的实现:
业务类传入一个 step,通过 HandlerFactory 实例化 handler,通过 handler 就可以执行指定的步骤,同样地,增加一个步骤,业务类无需任何变动。
有些时候,我们会使用多重循环,直接带业务方法里写,看着很不优雅,就像这样:
我们可以将其进行封装改造,将循环细节封装起来,只将一些方法暴露给业务方调用:
上面的 main 方法就是我们业务调用时需要调用的方法,可以看出,我们将循环细节封装到 Lists 里面,使调用方的代码更加优雅。
此场景用到的设计模式有:构建者模式、观察者模式。
在实际应用中,我们看到最多的代码便是 if – else,这样的代码在业务场景中出现太多的话,看着就不太优雅了,前面的场景其实已经多次将 if – else 用设计模式替换,本场景,我将会用新的设计模式来替换讨厌的 if – else,那就是策略模式。
策略模式,通俗点讲,就是根据不同的情况,采取不同的策略,我们把它转化成 if – else,即:
这样,我们就避免了在业务场景中大量地使用 if – else 了。
通过以上的学习,我们其实是可以写出很多优雅的代码,各位在实际中如果有什么问题,或者在实际应用中发现一段代码不知道如何优化也可以再本 chat 的读者圈随时向我提问。
接下来,我将告诉大家一些 Java 编程的小技巧,利用这些技巧,可以避免一些低级 bug,也可以写出一些优雅的代码。
我们在集成一个类时,可能会重写父类方法,大家务必加上 Override 注解,请看下面的代码:
我们的本意是要重写 method,但是参数类型写错了,变成了重载,编译器不会报错,如果我们加上 @Override,编译器会报错,我们就能马上发现代码的错误,而避免运行一段时间导致的 bug。
为什么这么说呢?错误我们马上就能发现,而且如果是编译时错误,都无法运行,但是警告并不影响编译和运行,举个例子:
我的本意是 for 循环用 i,但是却写成了 j,这时编译不会报错,但是 IDE 会给出警告:
它告诉我们i这个变量没有使用到,如果忽略警告,那么很可能运行一段时间出现致命性的 bug,但是如果我们重视警告,当编译器提出这个警告时,我们就会想,i为什么没有用到呢,检查代码,马上就能发现隐藏的 bug。
在开发数据库项目时,经常会有一些譬如状态、类型、性别等具有固定值的字段,一般我们会用数字表示,在业务中,也会经常判断,比如状态为 1 时,执行什么操作,如果直接这样写数字,必须要写注释,否则很难懂。类似这种字段,尽量封装成枚举类型,如:
我们在使用时直接调用枚举,可读性增加了,也利于扩展。
小王是公司的 Android 开发工程师,在开发应用时,封装了一些常量,用于提示语。
架构师在 code review 时发现,变量命名很成问题,如:
架构师要求小王更正,但小王给我的理由是这种编码是产品经理定的,我可以在每个调用者上面加上注释,而保留现状。
很明显,这样的代码是不可取的,如果换成一个可读变量名是不是更清晰呢?比如:
这个原则很好理解,即一个方法只做一件事,如果一个方法做了太多的事,请考虑重构此方法,合理运用类似上面提到的设计模式。
下面对两种代码进行比较:
如果变量放在前面,一旦变量为 null,则会出现空指针异常,但是常量放在前面,则不会出现空指针异常。
各位看到网上经常再说,位运算效率高怎么怎么样的,但事实真的如此吗,我们不妨做个测试:
以上代码,我分别测试了 1 万次,10 万次和 100 万次,得出的结论是 1 万次速度一样,10 万次和 100 万次都只相差 2 毫秒,如今计算机计算性能越来越好,利用位运算和四则运算效率相差太小,而位运算的可读性非常低,除非有详细的注释,否则一般人真看不懂。
因此,尽量少用位运算。当然有些场景是避免不了的,比如:密码生成、图像处理等,但实际应用中,我们很少自己写这类算法。
我们如果要精确计算浮点数,切记不要用 float 和 double,它们的计算结果往往不是你想要的,比如:
计算结果为:
我们要精确计算,需要用 BigDecimal 类,如:
这样就能得出精确的值:
java8 为我们带来了 lambda 表达式,也带来了集合的流式运算,java8 以前,我们循环集合是这样的:
java8 以后,我们可以这样做:
通过集合的流式操作,我们可以很方便的过滤元素、分组、排序等,如:
除了,集合的流式操作,通过 lambda 表示,我们还可以实例化匿名类,如:
可以看出,使用 lambda 表达式,让我们的代码更加简洁,也更加优雅,同学们,请拥抱 lambda 吧!
听说你还不知道Java代码是怎么运行的?
作者:Jay_huaxiao
作为一名Java程序员,我们需要知道Java代码是怎么运行的。最近复习了深入理解Java虚拟机这本书,做了一下笔记,希望对大家有帮助,如果有不正确的地方,欢迎提出,感激不尽。
java 代码运行主要流程
本文主要讲解流程如下:
- java源文件编译为class字节码
- 类加载器把字节码加载到虚拟机的方法区。
- 运行时创建对象
- 方法调用,执行引擎解释为机器码
- CPU执行指令
- 多线程切换上下文
编译
我们都知道,java代码是运行在Java虚拟机上的。但是java是一门面向对象的高级语言,它不仅语法非常复杂,抽象程度也非常高,并不能直接运行在计算机硬件机器上。
Java虚拟机(Java Virtual Machine 简称JVM)是运行所有Java程序的抽象计算机,是Java语言的运行环境。
因此,在运行Java程序之前,需要编译器把代码编译成java虚拟机所能识别的指令程序,这就是Java字节码,即class文件。
所以,Java代码运行的第一步是:把Java源代码编译成.class 字节码文件。
类加载
在Class文件中描述的各种信息,需要被加载到虚拟机之后才能运行和使用。因此,需要把class字节码文件加载到Java虚拟机来。
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。
加载
在加载阶段,虚拟机需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
加载阶段完成后,这些二进制字节流按照虚拟机所需的格式存储在方法区之中。
验证
为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,不会危害虚拟机的安全,Java虚拟机对输入的字节流走验证过程。
验证阶段包括四个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。
- 文件格式验证: 验证字节流是否符合Class文件格式规范,如:是否以魔数0xCAFEBABE开头。
- 元数据验证: 对字节码描述的信息进行语义分析,如:这个类的父类是否继承了不允许被继承的类(被final修饰的类);
- 字节码验证: 主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。如:保证跳转指令不会跳转到方法体以外的字节码指令上。
- 符号引用验证: 发生在虚拟机将符号引用转化为直接引用的时候,如:校验符号引用中通过字符串描述的全限定名是否能找到对应的类。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。如:
public static int value =123;
变量value在准备阶段过后的初始值是0而不是123。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
比如:com.User类引用com.Tool类,在编译时,User类不知道Tool类的实际内存地址,因此只能使用符号com.Tool(假设)来表示。而在类加载加载User类的时候,可以通过虚拟机获取Tool类的实际内存地址,因此便可以将符号com.Tool替换为Tool类的实际内存地址,即直接引用地址。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行。
初始化
到了初始化阶段,才真正开始执行类中定义的Java字节码。在这个阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。
创建对象
Java虚拟机是如何执行字节码的呢?我们先来看一下运行时创建对象。
Java是面向对象的编程语言,程序的运行是以对象为调用单位的。
- 字节码文件加载到虚拟机的方法区后,在程序运行过程,通过 class字节码文件创建与其对应的对象信息 。
- 创建对象的方式有:new关键字,反射等。
- Java堆内存是线程共享的区域,创建后的对象信息就保存在Java堆内存中。
方法调用
JVM的调用单位是对象,但是真正执行功能性的代码还是对象上的方法。
在运行过程中,每当调用进入一个java方法,java虚拟机会在当前线程的java方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。方法栈内存是线程私有的,每个线程都有自己的方法栈。如果对应的方法是本地方法,则对应的就是本地方法栈。
java运行时数据区域如下:
解释
当调用Java对象的某个方法时,JVM执行引擎会将该方法的字节码文件翻译成计算机所能识别的机器码,机器码信息保存在方法区中。翻译有解释执行和即时编译两种方式。
两种翻译方式的区别如下:
解释执行来一行代码,解释一行,大部分不常用的代码,都是采用这种方式。
即使编译
对于部分热点代码,将一个方法包含的所有字节码翻译成机器指令,以提高java虚拟机的运行效率。
即时编译是建立经典的二八定律上,即20%代码占据了80%的计算资源。
执行指令
- Java程序被加载入内存后,指令也在内存中了。
- 指令的指令寄存器IP,指向下一条待执行指令的地址。
- CPU的控制单元根据IP寄存器的指向,将主存中的指令装载到指令寄存器,这些加载的指令就是一串二进制码,还需要译码器进行解码。
- 解码后,如果需要获取操作数,则从内存中取数据,调用运算单元进行计算。
多线程上下文切换
CPU一通上电,就会周而复始从内存中获取指令、译码、执行。
- 为了支持多任务,CPU 将执行时间这个资源划分成时间片,每个程序执行一段时间。
- java虚拟机的多线程是通过线程轮流切换分配处理执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条程序中的指令。
- 假设当前线程在运行中,CPU分配的时间执行完了,总得保存运行过的结果信息吧,要不然白白浪费之前的工作了,因此,程序计数器(PC寄存器)作用体现出来了,它是一块较小的内存空间,线程私有,可以看作当前线程执行的字节码的行号指示器。当CPU又给它分配时间跑的时候,可以把数据恢复,接着上一次执行到的位置继续执行就可以了。
原文:https://juejin.im/entry/5e6ccc05e51d4527110aa25f
我这样写代码,比直接使用 MyBatis 效率提高了 100 倍
对一个 Java 后端程序员来说,mybatis、hibernate、data-jdbc 等都是我们常用的 ORM 框架。它们有时候很好用,比如简单的 CRUD,事务的支持都非常棒。但有时候用起来也非常繁琐,比如接下来我们要聊到的一个常见的开发需求,而对这类需求,本文会给出一个比直接使用这些 ORM 开发效率至少会提高 100 倍的方法(绝无夸张)。
用户表(user):(简单起见,假设只有 4 个字段)
角色表(role):(简单起见,假设只有 2 个字段)
这个查询有点复杂,它的要求如下:
- 可按用户名字段查询,要求: 可精确匹配(等于某个值) 可全模糊匹配(包含给定的值) 可后模糊查询(以…开头) 可前模糊查询(以.. 结尾) 可指定以上四种匹配是否可以忽略大小写
- 可按年龄字段查询,要求: 可精确匹配(等于某个年龄) 可大于匹配(大于某个值) 可小于匹配(小于某个值) 可区间匹配(某个区间范围)
- 可按角色ID查询,要求:精确匹配
- 可按用户ID查询,要求:同年龄字段
- 可指定只输出哪些列(例如,只查询 ID 与 用户名 列)
- 支持分页(每次查询后,页面都要显示满足条件的用户总数)
- 查询时可选择按 ID、用户名、年龄 等任意字段排序
试想一下,对于这种要求的查询,后端接口里的代码如果用 mybatis、hibernate、data-jdbc 直接来写的话,100 行代码 能实现吗?
反正我是没这个信心,算了,我还是直接坦白,面对这种需求后端如何 只用一行代码搞定 吧(有兴趣的同学可以 mybatis 等写个试试,最后可以对比一下)
首先,重点人物出场啦:Bean Searcher, 它就是专门来对付这种列表检索的,无论简单的还是复杂的,统统一行代码搞定!而且它还非常轻量,Jar 包体积仅不到 100KB,无第三方依赖。
假设我们项目使用的框架是 Spring Boot(当然 Bean Searcher 对框架没有要求,但在 Spring Boot 中使用更加方便)
Maven :
Gradle :
接口路径就叫 /user/index 吧:
上述代码中的 MapUtils 是 Bean Searcher 提供的一个工具类,MapUtils.flat(request.getParameterMap()) 只是为了把前端传来的请求参数统一收集起来,然后剩下的,就全部交给 MapSearcher 检索器了。
- GET /user/index
- 返回结果:
- GET /user/index? page=2 & size=10
- 返回结果:结构同 (1)(只是每页 10 条,返回第 2 页)
参数名 size 和 page 可自定义, page 默认从 0 开始,同样可自定义,并且可与其它参数组合使用
- GET /user/index? sort=age & order=desc
- 返回结果:结构同 (1)(只是 dataList 数据列表以 age 字段降序输出)
参数名 sort 和 order 可自定义,可与其它参数组合使用
- GET /user/index? onlySelect=id,name,role
- GET /user/index? selectExclude=age,roleId
- 返回结果:( 列表只含 id,name 与 role 三个字段)
参数名 onlySelect 和 selectExclude 可自定义,可与其它参数组合使用
- GET /user/index? age=20
- GET /user/index? age=20 & age-op=eq
- 返回结果:结构同 (1)(但只返回 age = 20 的数据)
参数 age-op = eq 表示 age 的 字段运算符 是 eq(Equal 的缩写),表示参数 age 与参数值 20 之间的关系是 Equal,由于 Equal 是一个默认的关系,所以 age-op = eq 也可以省略
参数名 age-op 的后缀 -op 可自定义,且可与其它字段参数 和 上文所列的参数(分页、排序、指定字段)组合使用,下文所列的字段参数也是一样,不再复述。
- GET /user/index? age=20 & age-op=ne
- 返回结果:结构同 (1)(但只返回 age != 20 的数据,ne 是 NotEqual 的缩写)
- GET /user/index? age=20 & age-op=ge
- 返回结果:结构同 (1)(但只返回 age >= 20 的数据,ge 是 GreateEqual 的缩写)
- GET /user/index? age=20 & age-op=le
- 返回结果:结构同 (1)(但只返回 age <= 20 的数据,le 是 LessEqual 的缩写)
- GET /user/index? age=20 & age-op=gt
- 返回结果:结构同 (1)(但只返回 age > 20 的数据,gt 是 GreateThan 的缩写)
- GET /user/index? age=20 & age-op=lt
- 返回结果:结构同 (1)(但只返回 age < 20 的数据,lt 是 LessThan 的缩写)
- GET /user/index? age-0=20 & age-1=30 & age-op=bt
- 返回结果:结构同 (1)(但只返回 20 <= age <= 30 的数据,bt 是 Between 的缩写)
参数 age-0 = 20 表示 age 的第 0 个参数值是 20。上述提到的 age = 20 实际上是 age-0 = 20 的简写形式。另:参数名 age-0 与 age-1 中的连字符 – 可自定义。
- GET /user/index? age-0=20 & age-1=30 & age-2=40 & age-op=mv
- 返回结果:结构同 (1)(但只返回 age in (20, 30, 40) 的数据,mv 是 MultiValue 的缩写,表示有多个值的意思)
- GET /user/index? name=Jack & name-op=in
- 返回结果:结构同 (1)(但只返回 name 包含 Jack 的数据,in 是 Include 的缩写)
- GET /user/index? name=Jack & name-op=sw
- 返回结果:结构同 (1)(但只返回 name 以 Jack 开头的数据,sw 是 StartWith 的缩写)
- GET /user/index? name=Jack & name-op=ew
- 返回结果:结构同 (1)(但只返回 name 以 Jack 结尾的数据,sw 是 EndWith 的缩写)
- GET /user/index? name-op=ey
- 返回结果:结构同 (1)(但只返回 name 为空 或为 null 的数据,ey 是 Empty 的缩写)
- GET /user/index? name-op=ny
- 返回结果:结构同 (1)(但只返回 name 非空 的数据,ny 是 NotEmpty 的缩写)
- GET /user/index? name=Jack & name-ic=true
- 返回结果:结构同 (1)(但只返回 name 等于 Jack (忽略大小写) 的数据,ic 是 IgnoreCase 的缩写)
参数名 name-ic 中的后缀 -ic 可自定义,该参数可与其它的参数组合使用,比如这里检索的是 name 等于 Jack 时忽略大小写,但同样适用于检索 name 以 Jack 开头或结尾时忽略大小写。
查询 name 以 Jack (忽略大小写) 开头,且 roleId = 1,结果以 id 字段排序,每页加载 10 条,查询第 2 页:
- GET /user/index? name=Jack & name-op=sw & name-ic=true & roleId=1 & sort=id & size=10 & page=2
- 返回结果:结构同 (1)
OK,效果看完了,/user/index 接口里我们确实只写了一行代码,它便可以支持这么多种的检索方式,有没有觉得现在 你写的一行代码 就可以 干过别人的一百行 呢?
本例中,我们只使用了 Bean Searcher 提供的 MapSearcher 检索器的一个 search 方法,其实,它有很多 search 方法。
- searchCount(Class<T> beanClass, Map<String, Object> params) 查询指定条件下的数据 总条数
- searchSum(Class<T> beanClass, Map<String, Object> params, String field) 查询指定条件下的 某字段 的 统计值
- searchSum(Class<T> beanClass, Map<String, Object> params, String[] fields) 查询指定条件下的 多字段 的 统计值
- search(Class<T> beanClass, Map<String, Object> params) 分页 查询指定条件下数据 列表 与 总条数
- search(Class<T> beanClass, Map<String, Object> params, String[] summaryFields) 同上 + 多字段 统计
- searchFirst(Class<T> beanClass, Map<String, Object> params) 查询指定条件下的 第一条 数据
- searchList(Class<T> beanClass, Map<String, Object> params) 分页 查询指定条件下数据 列表
- searchAll(Class<T> beanClass, Map<String, Object> params) 查询指定条件下 所有 数据 列表
另外,Bean Searcher 除了提供了 MapSearcher 检索器外,还提供了 BeanSearcher 检索器,它同样拥有 MapSearcher 拥有的方法,只是它返回的单条数据不是 Map,而是一个 泛型 对象。
另外,如果你是在 Service 里使用 Bean Searcher,那么直接使用 Map<String, Object> 类型的参数可能不太优雅,为此, Bean Searcher 特意提供了一个参数构建工具。
例如,同样查询 name 以 Jack (忽略大小写) 开头,且 roleId = 1,结果以 id 字段排序,每页加载 10 条,加载第 2 页,使用参数构建器,代码可以这么写:
这里使用的是 BeanSearcher 检索器,以及它的 searchList(Class<T> beanClass, Map<String, Object> params) 方法。
上文我们看到,Bean Searcher 对实体类中的每一个字段,都直接支持了很多的检索方式。
但某同学:哎呀!检索方式太多了,我根本不需要这么多,我的数据量几十亿,用户名字段的前模糊查询方式利用不到索引,万一把我的数据库查崩了怎么办呀?
好办,Bean Searcher 支持运算符的约束,实体类的用户名 name 字段只需要注解一下即可:
如上,通过 @DbField 注解的 onlyOn 属性,指定这个用户名 name 只能适用与 精确匹配 和 后模糊查询,其它检索方式它将直接忽略。
上面的代码是限制了 name 只能有两种检索方式,如果再严格一点,只允许 精确匹配,那其实有两种写法。
该同学又:哎呀!我的数据量还是很大,age 字段没有索引,我不想让它参与 where 条件,不然很可能就出现慢 SQL 啊!
不急,Bean Searcher 还支持条件的约束,让这个字段直接不能作为条件:
如上,通过 @DbField 注解的 conditional 属性, 就直接不允许 age 字段参与条件了,无论前端怎么传参,Bean Searcher 都不搭理。
该同学仍:哎呀!哎呀 …
别怕! Bean Searcher 还支持配置全局参数过滤器,可自定义任何参数过滤规则,在 Spring Boot 项目中,只需要配置一个 Bean:
- 参数名是否奇怪,这其实看个人喜好,如果你不喜欢中划线 -,不喜欢 op、ic 后缀,完全可以自定义,参考这篇文档:
searcher.ejlchina.com/guide/lates…
- 参数个数的多少,其实是和需求的复杂程度相关的。如果需求很简单,那么很多参数没必要让前端传,后端直接塞进去就好。比如:name 只要求后模糊匹配,age 只要求区间匹配,则可以:
这样前端就不用传 name-op 与 age-op 这两个参数了。
其实还有一种更简单的方法,那就是 运算符约束(当约束存在时,运算符默认就是 onlyOn 属性中指定的第一个值,前端可以省略不传):
其实,Bean Searcher 的检索器只是需要一个 Map<String, Object> 类型的参数,至于这个参数是怎么来的,和 Bean Searcher 并没有直接关系。前文之所以从 request 里取,只是因为这样代码看起来简洁,如果你喜欢声明参数,完全可以把代码写成这样:
本文介绍了 Bean Searcher 在复杂列表检索领域的超强能力。它之所以可以极大提高这类需求的研发效率,根本上归功于它 独创 的 动态字段运算符 与 多表映射机制,这是传统 ORM 框架所没有的。但由于篇幅所限,它的特性本文不能尽述,比如它还:
- 支持 聚合查询
- 支持 Select|Where|From子查询
- 支持 实体类嵌入参数
- 支持 字段转换器
- 支持 Sql
- 支持 多数据源
- 支持 自定义注解
- 等等
要了解更多,先来点个 Star 吧 : Github 、Gitee。
Bean Searcher 是我在工作中总结封装出来的一个小工具,公司内部使用了 4 年,经历大小项目三四十个,只是最近才着手完善文档分享给大家,如果你喜欢,一定去点个 Star 哦 ^_^。
再奉上 Bean Searcher 的详细文档:searcher.ejlchina.com/
- Spring Boot 框架中使用 demo
- github.com/ejlchina/be…
- gitee.com/ejlchina-zh…
- Grails 框架中使用 demo
- github.com/ejlchina/be…
- gitee.com/ejlchina-zh…
代码,也喜欢纯手工的,因为这样才能造出真正的艺术品。
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。