开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

50行代码串行Promise,koa洋葱模型原来是这么实现?

发表于 2021-09-08
  1. 前言

大家好,我是若川。欢迎关注我的公众号若川视野,最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 参与,长期交流学习。

之前写的《学习源码整体架构系列》 包含jQuery、underscore、lodash、vuex、sentry、axios、redux、koa、vue-devtools、vuex4十余篇源码文章。其中最新的两篇是:

Vue 3.2 发布了,那尤雨溪是怎么发布 Vue.js 的?

初学者也能看懂的 Vue3 源码中那些实用的基础工具函数

写相对很难的源码,耗费了自己的时间和精力,也没收获多少阅读点赞,其实是一件挺受打击的事情。从阅读量和读者受益方面来看,不能促进作者持续输出文章。

所以转变思路,写一些相对通俗易懂的文章。其实源码也不是想象的那么难,至少有很多看得懂。

之前写过 koa 源码文章学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理比较长,读者朋友大概率看不完,所以本文从koa-compose50行源码讲述。

本文涉及到的 koa-compose 仓库 文件,整个index.js文件代码行数虽然不到 50 行,而且测试用例test/test.js文件 300 余行,但非常值得我们学习。

歌德曾说:读一本好书,就是在和高尚的人谈话。 同理可得:读源码,也算是和作者的一种学习交流的方式。

阅读本文,你将学到:

1
2
3
bash复制代码1. 熟悉 koa-compose 中间件源码、可以应对面试官相关问题
2. 学会使用测试用例调试源码
3. 学会 jest 部分用法
  1. 环境准备

2.1 克隆 koa-compose 项目

本文仓库地址 koa-compose-analysis,求个star~

1
2
3
4
bash复制代码# 可以直接克隆我的仓库,我的仓库保留的 compose 仓库的 git 记录
git clone https://github.com/lxchuan12/koa-compose-analysis.git
cd koa-compose/compose
npm i

顺带说下:我是怎么保留 compose 仓库的 git 记录的。

1
2
3
4
5
bash复制代码# 在 github 上新建一个仓库 `koa-compose-analysis` 克隆下来
git clone https://github.com/lxchuan12/koa-compose-analysis.git
cd koa-compose-analysis
git subtree add --prefix=compose https://github.com/koajs/compose.git main
# 这样就把 compose 文件夹克隆到自己的 git 仓库了。且保留的 git 记录

关于更多 git subtree,可以看这篇文章用 Git Subtree 在多个 Git 项目间双向同步子项目,附简明使用手册

接着我们来看怎么根据开源项目中提供的测试用例调试源码。

2.2 根据测试用例调试 compose 源码

用VSCode(我的版本是 1.60 )打开项目,找到 compose/package.json,找到 scripts 和 test 命令。

1
2
3
4
5
6
7
8
9
json复制代码// compose/package.json
{
"name": "koa-compose",
// debug (调试)
"scripts": {
"eslint": "standard --fix .",
"test": "jest"
},
}

在scripts上方应该会有debug或者调试字样。点击debug(调试),选择 test。

VSCode 调试

接着会执行测试用例test/test.js文件。终端输出如下图所示。

koa-compose 测试用例输出结果

接着我们调试 compose/test/test.js 文件。
我们可以在 45行 打上断点,重新点击 package.json => srcipts => test 进入调试模式。
如下图所示。

koa-compose 调试

接着按上方的按钮,继续调试。在compose/index.js文件中关键的地方打上断点,调试学习源码事半功倍。

更多 nodejs 调试相关 可以查看官方文档

顺便详细解释下几个调试相关按钮。

    1. 继续(F5): 点击后代码会直接执行到下一个断点所在位置,如果没有下一个断点,则认为本次代码执行完成。
    1. 单步跳过(F10):点击后会跳到当前代码下一行继续执行,不会进入到函数内部。
    1. 单步调试(F11):点击后进入到当前函数的内部调试,比如在 compose 这一行中执行单步调试,会进入到 compose 函数内部进行调试。
    1. 单步跳出(Shift + F11):点击后跳出当前调试的函数,与单步调试对应。
    1. 重启(Ctrl + Shift + F5):顾名思义。
    1. 断开链接(Shift + F5):顾名思义。

接下来,我们跟着测试用例学源码。

  1. 跟着测试用例学源码

分享一个测试用例小技巧:我们可以在测试用例处加上only修饰。

1
2
js复制代码// 例如
it.only('should work', async () => {})

这样我们就可以只执行当前的测试用例,不关心其他的,不会干扰调试。

3.1 正常流程

打开 compose/test/test.js 文件,看第一个测试用例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
js复制代码// compose/test/test.js
'use strict'

/* eslint-env jest */

const compose = require('..')
const assert = require('assert')

function wait (ms) {
return new Promise((resolve) => setTimeout(resolve, ms || 1))
}
// 分组
describe('Koa Compose', function () {
it.only('should work', async () => {
const arr = []
const stack = []

stack.push(async (context, next) => {
arr.push(1)
await wait(1)
await next()
await wait(1)
arr.push(6)
})

stack.push(async (context, next) => {
arr.push(2)
await wait(1)
await next()
await wait(1)
arr.push(5)
})

stack.push(async (context, next) => {
arr.push(3)
await wait(1)
await next()
await wait(1)
arr.push(4)
})

await compose(stack)({})
// 最后输出数组是 [1,2,3,4,5,6]
expect(arr).toEqual(expect.arrayContaining([1, 2, 3, 4, 5, 6]))
})
}

大概看完这段测试用例,context是什么,next又是什么。

在koa的文档上有个非常代表性的中间件 gif 图。

中间件 gif 图

而compose函数作用就是把添加进中间件数组的函数按照上面 gif 图的顺序执行。

3.1.1 compose 函数

简单来说,compose 函数主要做了两件事情。

    1. 接收一个参数,校验参数是数组,且校验数组中的每一项是函数。
    1. 返回一个函数,这个函数接收两个参数,分别是context和next,这个函数最后返回Promise。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
js复制代码/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
function compose (middleware) {
// 校验传入的参数是数组,校验数组中每一项是函数
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}

/**
* @param {Object} context
* @return {Promise}
* @api public
*/

return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch(i){
// 省略,下文讲述
}
}
}

接着我们来看 dispatch 函数。

3.1.2 dispatch 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
js复制代码function dispatch (i) {
// 一个函数中多次调用报错
// await next()
// await next()
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 取出数组里的 fn1, fn2, fn3...
let fn = middleware[i]
// 最后 相等,next 为 undefined
if (i === middleware.length) fn = next
// 直接返回 Promise.resolve()
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}

值得一提的是:bind函数是返回一个新的函数。第一个参数是函数里的this指向(如果函数不需要使用this,一般会写成null)。
这句fn(context, dispatch.bind(null, i + 1),i + 1 是为了 let fn = middleware[i] 取middleware中的下一个函数。
也就是 next 是下一个中间件里的函数。也就能解释上文中的 gif图函数执行顺序。
测试用例中数组的最终顺序是[1,2,3,4,5,6]。

3.1.3 简化 compose 便于理解

自己动手调试之后,你会发现 compose 执行后就是类似这样的结构(省略 try catch 判断)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
js复制代码// 这样就可能更好理解了。
// simpleKoaCompose
const [fn1, fn2, fn3] = stack;
const fnMiddleware = function(context){
return Promise.resolve(
fn1(context, function next(){
return Promise.resolve(
fn2(context, function next(){
return Promise.resolve(
fn3(context, function next(){
return Promise.resolve();
})
)
})
)
})
);
};

也就是说koa-compose返回的是一个Promise,从中间件(传入的数组)中取出第一个函数,传入context和第一个next函数来执行。

第一个next函数里也是返回的是一个Promise,从中间件(传入的数组)中取出第二个函数,传入context和第二个next函数来执行。

第二个next函数里也是返回的是一个Promise,从中间件(传入的数组)中取出第三个函数,传入context和第三个next函数来执行。

第三个…

以此类推。最后一个中间件中有调用next函数,则返回Promise.resolve。如果没有,则不执行next函数。
这样就把所有中间件串联起来了。这也就是我们常说的洋葱模型。

洋葱模型图如下图所示:

不得不说非常惊艳,“玩还是大神会玩”。

3.2 错误捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
js复制代码it('should catch downstream errors', async () => {
const arr = []
const stack = []

stack.push(async (ctx, next) => {
arr.push(1)
try {
arr.push(6)
await next()
arr.push(7)
} catch (err) {
arr.push(2)
}
arr.push(3)
})

stack.push(async (ctx, next) => {
arr.push(4)
throw new Error()
})

await compose(stack)({})
// 输出顺序 是 [ 1, 6, 4, 2, 3 ]
expect(arr).toEqual([1, 6, 4, 2, 3])
})

相信理解了第一个测试用例和 compose 函数,也是比较好理解这个测试用例了。这一部分其实就是对应的代码在这里。

1
2
3
4
5
js复制代码try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}

3.3 next 函数不能调用多次

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码it('should throw if next() is called multiple times', () => {
return compose([
async (ctx, next) => {
await next()
await next()
}
])({}).then(() => {
throw new Error('boom')
}, (err) => {
assert(/multiple times/.test(err.message))
})
})

这一块对应的则是:

1
2
3
4
5
6
js复制代码index = -1
dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
}

调用两次后 i 和 index 都为 1,所以会报错。

compose/test/test.js文件中总共 300余行,还有很多测试用例可以按照文中方法自行调试。

  1. 总结

虽然koa-compose源码 50行 不到,但如果是第一次看源码调试源码,还是会有难度的。其中混杂着高阶函数、闭包、Promise、bind等基础知识。

通过本文,我们熟悉了 koa-compose 中间件常说的洋葱模型,学会了部分 jest 用法,同时也学会了如何使用现成的测试用例去调试源码。

相信学会了通过测试用例调试源码后,会觉得源码也没有想象中的那么难。

开源项目,一般都会有很全面的测试用例。除了可以给我们学习源码调试源码带来方便的同时,也可以给我们带来的启发:自己工作中的项目,也可以逐步引入测试工具,比如 jest。

此外,读开源项目源码是我们学习业界大牛设计思想和源码实现等比较好的方式。

看完本文,非常希望能自己动手实践调试源码去学习,容易吸收消化。另外,如果你有余力,可以继续看我的 koa-compose 源码文章:学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

最后欢迎加我微信 ruochuan12 交流,参与 源码共读 活动,大家一起学习源码,共同进步。


关于 && 交流群

最近组织了源码共读活动,感兴趣的可以加我微信 ruochuan12 参与,长期交流学习。

作者:常以若川为名混迹于江湖。欢迎加我微信ruochuan12。前端路上 | 所知甚少,唯善学。

关注公众号若川视野,每周一起学源码,学会看源码,进阶高级前端。

若川的博客

segmentfault若川视野专栏,开通了若川视野专栏,欢迎关注~

掘金专栏,欢迎关注~

知乎若川视野专栏,开通了若川视野专栏,欢迎关注~

github blog,求个star^_^~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java多线程:从基本概念到避坑指南

发表于 2021-09-08

本文为掘金社区首发签约文章,未获授权禁止转载

多核的机器,现在已经非常常见了。即使是一块手机,也都配备了强劲的多核处理器。通过多进程和多线程的手段,就可以让多个CPU同时工作,来加快任务的执行。

多线程,是编程中一个比较高级的话题。由于它涉及到共享资源的操作,所以在编码时非常容易出现问题。Java的concurrent包,提供了非常多的工具,来帮助我们简化这些变量的同步,但学习应用之路依然充满了曲折。

本篇文章,将简单的介绍一下Java中多线程的基本知识。然后着重介绍一下初学者在多线程编程中一些最容易出现问题的地方,很多都是血泪经验。规避了这些坑,就相当于规避了90%凶残的多线程bug。

  1. 多线程基本概念

1.1 轻量级进程

在JVM中,一个线程,其实是一个轻量级进程(LWP)。所谓的轻量级进程,其实是用户进程调用系统内核,所提供的一套接口。实际上,它还要调用更加底层的内核线程(KLT)。

实际上,JVM的线程创建销毁以及调度等,都是依赖于操作系统的。如果你看一下Thread类里面的多个函数,你会发现很多都是native的,直接调用了底层操作系统的函数。

下图是JVM在Linux上简单的线程模型。

image.png

可以看到,不同的线程在进行切换的时候,会频繁在用户态和内核态进行状态转换。这种切换的代价是比较大的,也就是我们平常所说的上下文切换(Context Switch)。

1.2 JMM

在介绍线程同步之前,我们有必要介绍一个新的名词,那就是JVM的内存模型JMM。

JMM并不是说堆、metaspace这种内存的划分,它是一个完全不同的概念,指的是与线程相关的Java运行时线程内存模型。

由于Java代码在执行的时候,很多指令都不是原子的,如果这些值的执行顺序发生了错位,就会获得不同的结果。比如,i++的动作就可以翻译成以下的字节码。

1
2
3
4
java复制代码getfield      // Field value:I
iconst_1
iadd
putfield      // Field value:I

这还只是代码层面的。如果再加上CPU每核的各级缓存,这个执行过程会变得更加细腻。如果我们希望执行完i++之后,再执行i--,仅靠初级的字节码指令,是无法完成的。我们需要一些同步手段。

image.png

上图就是JMM的内存模型,它分为主存储器(Main Memory)和工作存储器(Working Memory)两种。我们平常在Thread中操作这些变量,其实是操作的主存储器的一个副本。当修改完之后,还需要重新刷到主存储器上,其他的线程才能够知道这些变化。

1.3 Java中常见的线程同步方式

为了完成JMM的操作,完成线程之间的变量同步,Java提供了非常多的同步手段。

  1. Java的基类Object中,提供了wait和notify的原语,来完成monitor之间的同步。不过这种操作我们在业务编程中很少遇见
  2. 使用synchronized对方法进行同步,或者锁住某个对象以完成代码块的同步
  3. 使用concurrent包里面的可重入锁。这套锁是建立在AQS之上的
  4. 使用volatile轻量级同步关键字,实现变量的实时可见性
  5. 使用Atomic系列,完成自增自减
  6. 使用ThreadLocal线程局部变量,实现线程封闭
  7. 使用concurrent包提供的各种工具,比如LinkedBlockingQueue来实现生产者消费者。本质还是AQS
  8. 使用Thread的join,以及各种await方法,完成并发任务的顺序执行

从上面的描述可以看出,多线程编程要学的东西可实在太多了。幸运的是,同步方式虽然千变万化,但我们创建线程的方式却没几种。

第一类就是Thread类。大家都知道有两种实现方式。第一可以继承Thread覆盖它的run方法;第二种是实现Runnable接口,实现它的run方法;而第三种创建线程的方法,就是通过线程池。

其实,到最后,就只有一种启动方式,那就是Thread。线程池和Runnable,不过是一种封装好的快捷方式罢了。

多线程这么复杂,这么容易出问题,那常见的都有那些问题,我们又该如何避免呢?下面,我将介绍10个高频出现的坑,并给出解决方案。

  1. 避坑指南

image.png

2.1. 线程池打爆机器

首先,我们聊一个非常非常低级,但又产生了严重后果的多线程错误。

通常,我们创建线程的方式有Thread,Runnable和线程池三种。随着Java1.8的普及,现在最常用的就是线程池方式。

有一次,我们线上的服务器出现了僵死,就连远程ssh,都登录不上,只能无奈的重启。大家发现,只要启动某个应用,过不了几分钟,就会出现这种情况。最终定位到了几行让人啼笑皆非的代码。

有位对多线程不太熟悉的同学,使用了线程池去异步处理消息。通常,我们都会把线程池作为类的静态变量,或者是成员变量。但是这位同学,却将它放在了方法内部。也就是说,每当有一个请求到来的时候,都会创建一个新的线程池。当请求量一增加,系统资源就被耗尽,最终造成整个机器的僵死。

1
2
3
4
java复制代码void realJob(){
ThreadPoolExecutor exe = new ThreadPoolExecutor(...);
exe.submit(new Runnable(){...})
}

这种问题如何去避免?只能通过代码review。所以多线程相关的代码,哪怕是非常简单的同步关键字,都要交给有经验的人去写。即使没有这种条件,也要非常仔细的对这些代码进行review。

2.2. 锁要关闭

相比较synchronized关键字加的独占锁,concurrent包里面的Lock提供了更多的灵活性。可以根据需要,选择公平锁与非公平锁、读锁与写锁。

但Lock用完之后是要关闭的,也就是lock和unlock要成对出现,否则就容易出现锁泄露,造成了其他的线程永远了拿不到这个锁。

如下面的代码,我们在调用lock之后,发生了异常,try中的执行逻辑将被中断,unlock将永远没有机会执行。在这种情况下,线程获取的锁资源,将永远无法释放。

1
2
3
4
5
6
7
8
9
java复制代码private final Lock lock = new ReentrantLock();
void doJob(){
try{
lock.lock();
//发生了异常
lock.unlock();
}catch(Exception e){
}
}

正确的做法,就是将unlock函数,放到finally块中,确保它总是能够执行。

由于lock也是一个普通的对象,是可以作为函数的参数的。如果你把lock在函数之间传来传去的,同样会有时序逻辑混乱的情况。在平时的编码中,也要避免这种把lock当参数的情况。

2.3. wait要包两层

Object作为Java的基类,提供了四个方法wait wait(timeout) notify notifyAll ,用来处理线程同步问题,可以看出wait等函数的地位是多么的高大。在平常的工作中,写业务代码的同学使用这些函数的机率是比较小的,所以一旦用到很容易出问题。

但使用这些函数有一个非常大的前提,那就是必须使用synchronized进行包裹,否则会抛出IllegalMonitorStateException。比如下面的代码,在执行的时候就会报错。

1
2
3
4
java复制代码final Object condition = new Object();
public void func(){
condition.wait();
}

类似的方法,还有concurrent包里的Condition对象,使用的时候也必须出现在lock和unlock函数之间。

为什么在wait之前,需要先同步这个对象呢?因为JVM要求,在执行wait之时,线程需要持有这个对象的monitor,显然同步关键字能够完成这个功能。

但是,仅仅这么做,还是不够的,wait函数通常要放在while循环里才行,JDK在代码里做了明确的注释。

重点:这是因为,wait的意思,是在notify的时候,能够向下执行逻辑。但在notify的时候,这个wait的条件可能已经是不成立的了,因为在等待的这段时间里条件条件可能发生了变化,需要再进行一次判断,所以写在while循环里是一种简单的写法。

1
2
3
4
5
6
7
8
java复制代码final Object condition = new Object();
public void func(){
synchronized(condition){
while(<条件成立>){
condition.wait();
}
}
}

带if条件的wait和notify要包两层,一层synchronized,一层while,这就是wait等函数的正确用法。

2.4. 不要覆盖锁对象

使用synchronized关键字时,如果是加在普通方法上的,那么锁的就是this对象;如果是加载static方法上的,那锁的就是class。除了用在方法上,synchronized还可以直接指定要锁定的对象,锁代码块,达到细粒度的锁控制。

如果这个锁的对象,被覆盖了会怎么样?比如下面这个。

1
2
3
4
5
6
7
8
9
10
11
java复制代码List listeners = new ArrayList();

void add(Listener listener, boolean upsert){
synchronized(listeners){
List results = new ArrayList();
for(Listener ler:listeners){
...
}
listeners = results;
}
}

上面的代码,由于在逻辑中,强行给锁listeners对象进行了重新赋值,会造成锁的错乱或者失效。

为了保险起见,我们通常把锁对象声明成final类型的。

1
java复制代码final List listeners = new ArrayList();

或者直接声明专用的锁对象,定义成普通的Object对象即可。

1
java复制代码final Object listenersLock = new Object();

2.5. 处理循环中的异常

在异步线程里处理一些定时任务,或者执行时间非常长的批量处理,是经常遇到的需求。我就不止一次看到小伙伴们的程序执行了一部分就停止的情况。

排查到这些中止的根本原因,就是其中的某行数据发生了问题,造成了整个线程的死亡。

我们还是来看一下代码的模板。

1
2
3
4
5
6
7
8
9
java复制代码volatile boolean run = true;
void loop(){
while(run){
for(Task task: taskList){
//do . sth
int a = 1/0;
}
}
}

在loop函数中,执行我们真正的业务逻辑。当执行到某个task的时候,发生了异常。这个时候,线程并不会继续运行下去,而是会抛出异常直接中止。在写普通函数的时候,我们都知道程序的这种行为,但一旦到了多线程,很多同学都会忘了这一环。

值得注意的是,即使是非捕获类型的NullPointerException,也会引起线程的中止。所以,时刻把要执行的逻辑,放在try catch中,是个非常好的习惯。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码volatile boolean run = true;
void loop(){
while(run){
for(Task task: taskList){
try{
//do . sth
int a = 1/0;
}catch(Exception ex){
//log
}
}
}
}

2.6. HashMap正确用法

HashMap在多线程环境下,会产生死循环问题。这个问题已经得到了广泛的普及,因为它会产生非常严重的后果:CPU跑满,代码无法执行,jstack查看时阻塞在get方法上。

至于怎么提高HashMap效率,什么时候转红黑树转列表,这是阳春白雪的八股界话题,我们下里巴人只关注怎么不出问题。

网络上有详细的文章描述死循环问题产生的场景,大体因为HashMap在进行rehash时,会形成环形链。某些get请求会走到这个环上。JDK并不认为这是个bug,虽然它的影响比较恶劣。

如果你判断你的集合类会被多线程使用,那就可以使用线程安全的ConcurrentHashMap来替代它。

HashMap还有一个安全删除的问题,和多线程关系不大,但它抛出的是ConcurrentModificationException,看起来像是多线程的问题。我们一块来看看它。

1
2
3
4
5
6
7
8
9
10
java复制代码Map<String, String> map = new HashMap<>();
map.put("xjjdog0", "狗1");
map.put("xjjdog1", "狗2");

for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
if ("xjjdog0".equals(key)) {
map.remove(key);
}
}

上面的代码会抛出异常,这是由于HashMap的Fail-Fast机制。如果我们想要安全的删除某些元素,应该使用迭代器。

1
2
3
4
5
6
7
8
9
java复制代码
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
String key = entry.getKey();
if ("xjjdog0".equals(key)) {
iterator.remove();
}
}

2.7. 线程安全的保护范围

使用了线程安全的类,写出来的代码就一定是线程安全的么?答案是否定的。

线程安全的类,只负责它内部的方法是线程安全的。如我我们在外面把它包了一层,那么它是否能达到线程安全的效果,就需要重新探讨。

比如下面这种情况,我们使用了线程安全的ConcurrentHashMap来存储计数。虽然ConcurrentHashMap本身是线程安全的,不会再出现死循环的问题。但addCounter函数,明显是不正确的,它需要使用synchronized函数包裹才行。

1
2
3
4
5
6
7
java复制代码private final ConcurrentHashMap<String,Integer> counter;
public int addCounter(String name) {
Integer current = counter.get(name);
int newValue = ++current;
counter.put(name,newValue);
return newValue;
}

这是开发人员常踩的坑之一。要达到线程安全,需要看一下线程安全的作用范围。如果更大维度的逻辑存在同步问题,那么即使使用了线程安全的集合,也达不到想要的效果。

2.8. volatile作用有限

volatile关键字,解决了变量的可见性问题,可以让你的修改,立马让其他线程给读到。

虽然这个东西在面试的时候问的挺多的,包括ConcurrentHashMap中队volatile的那些优化。但在平常的使用中,你真的可能只会接触到boolean变量的值修改。

1
2
3
4
5
java复制代码volatile boolean closed;  

public void shutdown() {
closed = true;
}

千万不要把它用在计数或者线程同步上,比如下面这样。

1
2
3
4
java复制代码volatile count = 0;
void add(){
++count;
}

这段代码在多线程环境下,是不准确的。这是因为volatile只保证可见性,不保证原子性,多线程操作并不能保证其正确性。

直接用Atomic类或者同步关键字多好,你真的在乎这纳秒级别的差异么?

2.9. 日期处理要小心

很多时候,日期处理也会出问题。这是因为使用了全局的Calendar,SimpleDateFormat等。当多个线程同时执行format函数的时候,就会出现数据错乱。

1
2
3
4
5
java复制代码SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

Date getDate(String str){
return format(str);
}

为了改进,我们通常将SimpleDateFormat放在ThreadLocal中,每个线程一份拷贝,这样可以避免一些问题。当然,现在我们可以使用线程安全的DateTimeFormatter了。

1
2
3
4
5
java复制代码static DateTimeFormatter FOMATTER = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss");
public static void main(String[] args) {
ZonedDateTime zdt = ZonedDateTime.now();
System.out.println(FOMATTER.format(zdt));
}

2.10. 不要在构造函数中启动线程

在构造函数,或者static代码块中启动新的线程,并没有什么错误。但是,强烈不推荐你这么做。

因为Java是有继承的,如果你在构造函数中做了这种事,那么子类的行为将变得非常魔幻。另外,this对象可能在构造完毕之前,出递到另外一个地方被使用,造成一些不可预料的行为。

所以把线程的启动,放在一个普通方法,比如start中,是更好的选择。它可以减少bug发生的机率。

End

wait和notify是非常容易出问题的地方,

编码格式要求非常严格。synchronized关键字相对来说比较简单,但同步代码块的时候依然有许多要注意的点。这些经验,在concurrent包所提供的各种API中依然实用。我们还要处理多线程逻辑中遇到的各种异常问题,避免中断,避免死锁。规避了这些坑,基本上多线程代码写起来就算是入门了。

许多java开发,都是刚刚接触多线程开发,在平常的工作中应用也不是很多。如果你做的是crud的业务系统,那么写一些多线程代码的时候就更少了。但总有例外,你的程序变得很慢,或者排查某个问题,你会直接参与到多线程的编码中来。

我们的各种工具软件,也在大量使用多线程。从Tomcat,到各种中间件,再到各种数据库连接池缓存等,每个地方都充斥着多线程的代码。

即使是有经验的开发,也会陷入很多多线程的陷阱。因为异步会造成时序的混乱,必须要通过强制的手段达到数据的同步。多线程运行,首先要保证准确性,使用线程安全的集合进行数据存储;还要保证效率,毕竟使用多线程的目标就是如此。

希望本文中的这些实际案例,让你对多线程的理解,更上一层楼。

本文为掘金社区首发签约文章,未获授权禁止转载

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

还在从零开始搭建项目?推荐一款高颜值的前后端分离脚手架!

发表于 2021-09-08

从零开始搭建项目,没有好用的脚手架怎么行!最近发现一款高颜值的前后端分离脚手架sa-plus,自带代码生成器,可一键生成前端、后端、API文档代码,推荐给大家!

SpringBoot实战电商项目mall(50k+star)地址:github.com/macrozheng/…

sa-plus简介

一款基于SpringBoot的快速开发框架,内置代码生成器。

项目特点:

  • 集成常用开发功能,包括文件上传、角色授权、全局异常处理、Redis控制台、API日志统计等。
  • 内置代码生成器,高自动化代码生成,可一键生成后端、前端和API文档代码。
  • 通过给表添加注释来生成代码,数据库表建好了,项目也就开发一半了。

项目架构

sa-plus前后端使用的技术栈还是非常主流的,下面我们来看下。

使用技术栈

  • 后端技术栈:MySql 5.7、SpringBoot、Mybatis-Plus、Druid、PageHelper、Redis、Sa-Token、Lombok、Hutool、FastJson
  • 前端技术栈:Vue、Element-Ui、WangEditor、Jquery、Layer、Swiper、Echarts

模块介绍

  • sp-server:SpringBoot后端代码。
  • sp-admin:Vue管理系统前端代码。
  • sp-apidoc:Docsify API接口文档代码。
  • sp-generate:代码生成器,可生成后端、前端、API文档。
  • sp-devdoc:sa-plus本地文档。
  • doc:其它文件,存放SQL脚本。

快速开始

sp-server、sp-admin、sp-apidoc为sa-plus的主要项目模块,我们先把它们启动起来。

sp-server

  • 先在MySql中创建sp-dev数据库,导入项目doc目录下的sa-plus.sql脚本,导入成功后将生成如下表;

  • 将sp-server模块导入到IDEA中,导入成功后项目结构如下;

  • 修改项目的配置文件application-dev.yml,将MySql和Redis配置修改为你自己的连接配置;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
yaml复制代码spring: 
# 数据源配置
datasource:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3306/sp-dev?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
username: root
password: root

# redis配置
redis:
# Redis数据库索引(默认为0)
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
# password:
# 连接超时时间(毫秒)
timeout: 5000ms
  • 运行启动类SpServerApplication的main方向,至此后端服务启动成功。
1
2
3
4
5
6
7
8
9
10
11
12
rust复制代码2021-08-09 16:46:00.478   INFO  -->  Initializing ExecutorService 'applicationTaskExecutor'
____ ____ ___ ____ _ _ ____ _ _
[__ |__| __ | | | |_/ |___ |\ |
___] | | | |__| | _ |___ | |
DevDoc:http://sa-token.dev33.cn (v1.24.0)
GitHub:https://github.com/dromara/sa-token
2021-08-09 16:46:00.744 INFO --> Initializing ExecutorService 'taskScheduler'
2021-08-09 16:46:00.778 INFO --> Starting ProtocolHandler ["http-nio-8099"]
2021-08-09 16:46:00.792 INFO --> Tomcat started on port(s): 8099 (http) with context path ''
2021-08-09 16:46:00.802 INFO --> Started SpServerApplication in 3.871 seconds (JVM running for 4.797)

------------- sa-plus (dev) 启动成功 --by 2021-08-09 16:46:00 -------------

sp-admin

  • 将sp-admin模块导入到IDEA中,导入成功后项目结构如下;

  • 打开index.html页面,点击右上角按钮运行到浏览器即可;

  • 使用默认账号密码登录后,即可访问sa-plus的首页,界面还是挺炫酷的;

  • 我们可以稍稍体验下sa-plus的基础功能,比如Redis控制台功能,可以查看Redis状态和管理Redis中的数据;

  • 还有API请求日志功能,可以查看API请求记录和请求耗时;

  • 还有权限管理中的角色管理功能,可以创建角色并给角色分配权限;

  • 还有权限管理中的菜单管理,其实我们可以发现sa-plus中的菜单和权限是绑定在一起的,而菜单是从前端的路由中获取的,给角色分配了菜单即分配了菜单下的权限,这样做的话想做到接口级权限就比较麻烦了;

  • 还有权限管理中的用户管理,可以管理用户信息。

sp-apidoc

  • 将sp-apidoc模块导入到IDEA中,导入成功后项目结构如下;

  • 打开index.html页面,点击右上角按钮运行到浏览器即可,此时我们可以发现API文档中还没有任何内容。

代码生成器

使用代码生成器,可以根据数据库表直接生成前端、后端及API文档代码,让我们来体验下它有何神奇之处。

  • 将sp-generate模块导入到IDEA中,导入成功后项目结构如下;

  • 然后往MySql中导入测试数据,导入项目doc目录下的test-data.sql脚本,导入成功后新增如下表;

  • 接下来修改SpGenerateApplication中的MySql连接配置和代码生成目录配置;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码@SqlFlySetup
@SpringBootApplication
public class SpGenerateApplication {

// 直接运行代码生成器
public static void main(String[] args) {

// 启动springboot
SpringApplication.run(SpGenerateApplication.class, args);


// =================================== 设置连接信息 ===================================
FlyConfig config = new FlyConfig();
config.setDriverClassName("com.mysql.jdbc.Driver");
config.setUrl("jdbc:mysql://127.0.0.1:3306/sp-dev?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC");
config.setUsername("root");
config.setPassword("root");
config.setPrintSql(true); // 是否打印sql
FlyObjects.setConfig(config); // 注入到框架中


// =================================== 一些全局设置 ===================================
GenCfgManager.cfg
.setProjectPath("D:/developer/demo/sa-plus/") // 总项目地址 (生成代码的路径)
.setServerProjectName("sp-server") // 服务端 - 项目名称
// .setServerProjectName("sp-com/sp-core") // 服务端 - 项目名称 (sp-com多模块版填此格式)
.setCodePath("src/main/java/") // 服务端代码 - 存放路径
.setPackagePath("com.pj.project") // 服务端代码 - 总包名
.setPackage_utils("com.pj.utils.sg.*") // 服务端代码 - util类包地址
.setAuthor("macrozheng"); // 服务端代码 - 代码作者
}
}
  • 然后运行启动类SpGenerateApplication的main方法生成代码,运行成功后,sp-server的project包下会生成后端代码;

  • sp-admin的sa-html目录下会生成前端代码,还会在menu-list.js中追加菜单信息;

  • sp-apidoc的project目录也下会生成API文档代码;

  • 重新运行前后端代码后,我们暂时还无法看到新增的菜单,还需要给角色分配权限才可以查看;

  • 之后我们可以看到,对于商品表来说,列表页面和添加页面已经给我们生成好了;

  • 其实sa-plus是通过解析数据库表中的注释来生成代码的,我们可以看下商品表的SQL语句,其中有很多包含[]的注释,sa-plus就是根据这些规则来生成代码的;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码CREATE TABLE `ser_goods` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '记录id [num no-add]',
`name` varchar(200) DEFAULT NULL COMMENT '商品名称 [text j=like]',
`avatar` varchar(512) DEFAULT NULL COMMENT '商品头像 [img]',
`image_list` varchar(2048) DEFAULT NULL COMMENT '轮播图片 [img-list]',
`content` text COMMENT '图文介绍 [f]',
`money` int(11) DEFAULT '0' COMMENT '商品价格 [num]',
`type_id` bigint(20) DEFAULT NULL COMMENT '所属分类 [num]',
`stock_count` int(11) DEFAULT '0' COMMENT '剩余库存 [num]',
`status` int(11) DEFAULT '1' COMMENT '商品状态 (1=上架,2=下架) [j]',
`create_time` datetime DEFAULT NULL COMMENT '创建日期 [date-create]',
`update_time` datetime DEFAULT NULL COMMENT '更新日期 [date-update]',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1005 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='商品表\n[table icon=el-icon-apple]\n[fk-s js=(type_id=sys_type.id), show=name.所属分类, drop]\n';
  • 这里的规则比较多,大家可以自行对照下表查看;

  • 最后我们再来看下已经生成好的API文档,商品表的CRUD接口文档都有了,非常详细;

  • 而且API文档中还提供了接口测试功能,是不是很贴心!

总结

通过上面的一波实践,我们可以发现sa-plus确实是个有意思的框架。不仅提供了项目的基础功能,还提供了代码生成器,可以一键生成前后端及API文档代码,大大提高了开发效率。但是没有一种代码生成器是万能的,复杂的代码还是需要手写。sa-plus的权限功能把菜单和权限绑定在了一起,使用起来不太灵活,还是可以改进下的。

参考资料

官方文档:sa-plus.dev33.cn/

项目地址

gitee.com/click33/sa-…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

springcloud alibaba企业落地实战:一文带你

发表于 2021-09-08

1.为什么使用选择nacos

nacos在springcloud体系中作为注册中心与配置中心使用。相当于eureka与apollo的功能。

一个老生常谈的问题nacos和eureka区别,下图是楼主在网上查找到。

image.png
但是在楼主实际应用中 还有以下有点特别称道:

  1. nacos有配置功能,相对于楼主之前的eureka+apollo 这无疑大大的简化了系统的复杂性。
  2. nacos使用了数据库进行管理数据,使在处理数据时心里更舒服了。
  3. nacos拥有namespace和gourp的概念,可以隔离同名的服务。这样在多人起后端服务时,可以注册到一个nacos服务 隔离开就可以了。

2.Nacos快速开始

这个快速开始手册是帮忙您快速在您的电脑上,下载、安装并使用 Nacos。

1.版本选择

您可以在Nacos的release notes及博客中找到每个版本支持的功能的介绍,当前推荐的稳定版本为1.4.2或2.0.1。

2.预备环境准备

Nacos 依赖 Java 环境来运行。如果您是从代码开始构建并运行Nacos,还需要为此配置 Maven环境,请确保是在以下版本环境中安装使用:

  1. 64 bit OS,支持 Linux/Unix/Mac/Windows,推荐选用 Linux/Unix/Mac。
  2. 64 bit JDK 1.8+;下载 & 配置。
  3. Maven 3.2.x+;下载 & 配置。

3.下载源码或者安装包

你可以通过源码和发行包两种方式来获取 Nacos。

1.从 Github 上下载源码方式

1
2
3
4
5
6
7
bash复制代码git clone https://github.com/alibaba/nacos.git
cd nacos/
mvn -Prelease-nacos -Dmaven.test.skip=true clean install -U
ls -al distribution/target/

// change the $version to your actual path
cd distribution/target/nacos-server-$version/nacos/bin

2.下载编译后压缩包方式

您可以从 最新稳定版本 下载 nacos-server-$version.zip 包。

1
2
bash复制代码  unzip nacos-server-$version.zip 或者 tar -xvf nacos-server-$version.tar.gz
cd nacos/bin

4.启动服务器

1.Linux/Unix/Mac

启动命令(standalone代表着单机模式运行,非集群模式):

1
复制代码sh startup.sh -m standalone

如果您使用的是ubuntu系统,或者运行脚本报错提示[[符号找不到,可尝试如下运行:

1
复制代码bash startup.sh -m standalone

2.Windows

启动命令(standalone代表着单机模式运行,非集群模式):

1
复制代码startup.cmd -m standalone

当idea使用源码单机启动时需要以下配置

)​

image.png

1
ini复制代码-Dnacos.standalone=true

5.单机时使用数据库

当不添加配置时,默认是使用内存保存信息,我们可以添加数据库配置,使数据保存在数据库内.

暂时官方只支持mysql,其他数据库需要自己手动修改。

1.源码启动

使用源码idea启动需要修改以下文件的以下位置
image.png

2.已经打包的jar包

nacos\conf\application.properties修改数据库配置
image.png

6.启动成功

出现以下图案代表启动成功,看出来模式为单机模式,路径为Console输出,访问即可
image.png
image.png

对于官网集群搭建的要求,是要求大企业的,个人认为小公司如果没有条件可以不必满足,也可以正常启动。但是有条件还是建议上集群。

3.springboot注册进入nacos

1.修改pom文件

1
2
3
4
5
6
7
8
9
xml复制代码        <dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- SpringCloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

在pom.xml文件中最好选择合理的版本,否则会报各种各样的错 。

版本可以根据官方文档选择:github.com/alibaba/spr…

同时为了保证版本相同建议再父工程中加入以下配置,意思是子工程groupid为org.springframework.cloud,com.alibaba.cloud,org.springframework.boot的都依赖父工程版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
xml复制代码<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<!--maven不支持多继承,使用import来依赖管理配置-->
<scope>import</scope>
</dependency>
<!-- SpringCloud Alibaba 微服务 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- SpringBoot 依赖配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

2.修改bootstrap.xml

这里一定要使用bootstrap.yml而不是application.yml,因为bootstrap.yml运行先于后者,如果使用application.yml可能会出现即使有注册的地址,还是去连接localhost:8848的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
yaml复制代码spring:
application:
name: systemp
# 数据源配置
cloud:
nacos:
discovery:
# 服务注册地址
server-addr: 192.168.xx.x:8848
#命名空间
namespace: b80f0aa4-3af2-a6e3-c6fda24c2bc0
#分组
group: xxx
config:
# 配置中心地址
server-addr: 192.168.xx.xx:8848
# 配置文件格式
file-extension: yml
#命名空间
namespace: b80f0aa4-3af2-a6e3-c6fda24c2bc0
#分组
group: xxx

可以为服务指定namespace和group,在一个namespace和group中的服务只能获取同namespace和group中的服务,这样可以同时配置dev和pro环境。获取小组内的小伙伴都可以连接一个nacos服务,而不是像eureka 一人启动一个。

1.新建命名空间

配置文件中namespace是需要建立的,方式如图。
image.png

配置文件中是命名空间id

2.新建分组

分组不用在nacos中新建 ,直接编写即可。

3.修改启动类

在spring boot启动类上加入@EnableDiscoveryClient注解即可。

1
2
3
4
5
6
7
8
9
10
11
less复制代码@EnableDiscoveryClient
@SpringBootApplication
public class GetwayApplication {

public static void main(String[] args) {
//去除nacos日志
System.setProperty("nacos.logging.default.config.enabled", "false");
SpringApplication.run(GetwayApplication.class, args);
}

}

注意:如果使用了logback作为日志System.setProperty("nacos.logging.default.config.enabled", "false"); 需要添加 否则会因为日志命名报错

4.启动

然后启动服务,访问ip:8848/nacos可以验证是否注册成功。
image.png
同时也发布到了指定的namespace和groupid。

4.配置中心

配置中心:bootstrap.yml中的配置可以通过nacos配置修改,同时大部门不需要重启服务就可以生效。

由于引入了nacos,楼主不再使用apollo配置中心。变更原因如下

  1. 由于架构变更为springcloud alibaba,nacos可以承担起eureka+apollo的功能。
  2. 可以降低系统的复杂性。方便运维。
  3. 对配置中心的需求仅有动态配置,无更细腻话的权限和灰度发布等功能要求。

1.实战整合springboot

建议启动类使用bootstrap.yml

1.新建nacos配置

image.png

请注意选择合适命名空间
image.png

配置解释如下:

  1. Data ID的命名格式如下:{spring.application.name}-{spring.profiles.active}.{文件类型},也就是系统名称+dev/pro.yaml(一般情况)见下图。
    image.png
    )​

如果没有spring.profiles.active会省略“-”与“spring.profiles.active”

  1. group:其中需要注意namespace与group需要与nacos中配置对应(这两个概念上文有介绍),否则会获取不到配置。
  2. 配置格式:这里因为使用了bootstrap.yml所以选择上述配置。
  3. 配置内容:需要在nacos中更改的配置项。

2.配置成功

如果成功:
image.png

切记namespace与group一定要匹配。
同时可以监控到那台服务使用该配置。
)

3.客户端接口编写

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码@RestController
@RefreshScope
public class DemoController {

@Value("${nacostest.demo}")
private String demo;

@GetMapping("/testConfig")
public String testConfig() {
return demo;
}
}

@RefreshScope不要少写!否则报错!
改变配置多次调用接口,可以发现返回值发生变化。

文末抽奖

首先感谢掘金社区为俺提供了两枚掘金勋章。

勋章能干什么:放在电脑旁,背包上,样式还是蛮漂亮的。主要是全程免费(包邮),更主要中奖概率高,他不香吗。

本文章选评论点赞最多的两位老铁送出,如果评论相同,选择评论时间最早的。希望您的评论与点赞呦。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

还在用MyBatis写CRUD?这款神器帮你5分钟写后台管理

发表于 2021-09-08

一、MyBatis回顾

1.1、回顾MyBatis

1.1.1、建库建表

1
2
3
4
5
6
mysql复制代码CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

1.1.2、引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

1.1.3、application.properties

1
2
3
4
properties复制代码spring.datasource.druid.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql:///db?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
spring.datasource.druid.username=root
spring.datasource.druid.password=123456

1.1.4、编写Mapper接口

1
2
3
java复制代码public interface UserMapper {
List<Employee> selectAll();
}

1.1.5、编写Mapper.xml

1
2
3
4
xml复制代码  <select id="selectAll" resultMap="BaseResultMap">
select id,username,password
from user
</select>

1.1.6、MyBatis存在的缺点

我们可以发现传统的MyBatis存在很致命的问题,每个实体表对应一个实体类,对应一个Mapper.java接口,对应一个Mapper.xml配置文件每个Mapper.java接口都有重复的crud方法,每一个Mapper.xml都有重复的crud的sql配置。如果想解决这个问题,**唯一的办法就是使用MyBatis-Plus**。

二、了解Mybatis-Plus

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

image-20210402092420460

2.1、代码以及文档

文档地址:mybatis.plus/guide/
源码地址:github.com/baomidou/my…

2.2、特性

  1. 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑。
  2. 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作。
  3. 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求。
  4. 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错。
  5. 支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer2005、SQLServer 等多种数据库。
  6. 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题。
  7. 支持 XML 热加载:Mapper 对应的 XML 支持热加载,对于简单的 CRUD 操作,甚至可以无 XML 启动。
  8. 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操
    作。
  9. 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )。
  10. 支持关键词自动转义:支持数据库关键词(order、key……)自动转义,还可自定义关键词。
  11. 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,
    支持模板引擎,更有超多自定义配置等您来使用。
  12. 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List查询。
  13. 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询。
  14. 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作。
  15. 内置 Sql 注入剥离器:支持 Sql 注入剥离,有效预防 Sql 注入攻击。

2.3、快速开始

2.3.1、导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--简化代码的工具包-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--mybatis-plus的springboot支持-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.1</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>

2.3.2、log4j.properties

1
2
3
4
properties复制代码og4j.rootLogger=DEBUG,A1
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=[%t] [%c]-[%p] %m%n

2.3.3、编写实体类

1
2
3
4
5
6
7
8
9
java复制代码@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String username;
private String password;
private String type;
}

2.3.4、编写mapper

1
2
3
java复制代码// 直接继承Myabtis-Plus的BaseMapper即可,泛型表示实体类
public interface UserMapper extends BaseMapper<User> {
}

2.3.5、编写启动类

1
2
3
4
5
6
7
8
java复制代码@SpringBootApplication
// 设置mapper接口扫描包
@MapperScan("cn.linstudy.mapper")
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

2.3.6、测试

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
public void testSelect() {
List<User> userList = userMapper.selectList(null);
for (User user : userList) {
System.out.println(user);
}
}
}

2.4、架构

image-20210406113556258

三、常用注解

3.1、@TableName

MyBatis-Plus中默认表名是跟实体类名一致,当我们实体类的类名和表名不一致的时候,MyBatis-Plus就会报错,但是我们实际上又有这种需求的时候,我们就需要使用`@TableName`这个注解,来指定当前实体类映射哪张数据库表。
1
2
3
4
5
6
7
8
9
10
java复制代码@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("user")
public class User {
private Integer id;
private String username;
private String password;
private String type;
}

3.2、@TableId

我们在使用insert方法的时候会发现一个很奇怪的现象。他生成的ID格外长,这是因为他使用的算法是使用雪花算法生成的ID,我们想要的是自增的ID,所以我们需要设置主键增长的策略。

image-20210406154041215
我们可以使用@TableId这个注解。他的作用是主键注解,标记当前属性映射表的主键,其中type是属性指定的主键类型,他有这几个值:

  1. IdType.AUTO:数据库ID自增。
  2. IdType.NONE:无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)。
  3. IdType.INPUT:insert前自行set主键值。
  4. IdType.ASSIGN_ID:分配ID(主键类型为Number(Long和Integer)或String)(since 3.3.0),使用接口IdentifierGenerator的方法nextId(默认实现类为DefaultIdentifierGenerator雪花算法)。
  5. 分配UUID,主键类型为String(since 3.3.0),使用接口IdentifierGenerator的方法nextUUID(默认default方法)

3.3、@TableField

我们有些时候,数据库的字段名和实体类的名字可能会不一样,或者是说实体类中有的字段而数据库中却没有,我们需要用`@TableField`这个注解。
`@TableField`注解用于标记非主键字段,他的作用是指定当前属性映射数据库表哪一列, 默认是跟属性名一致。常用于解决以下两个问题:
  1. 对象中的属性名和字段名不一致的问题(非驼峰)
  2. 对象中的属性字段在表中不存在的问题

image-20210426183433467
他还有另一种用法,就是指定某个字段不加入查询。

image-20210426183636523

image-20210426183720166

四、通用CRUD

我们之前学过,使用MyBatis-Plus的时候,Mapper接口里面的方法不需要我们再自己写了,只需要继承BaseMapper接口即可获取到各种各样的单表操作。

image-20210426184057266

4.1、插入操作

4.1.1、方法定义

MyBatis-Plus中对于insert的方法定义是:
1
2
3
4
5
6
java复制代码/**
* 插入一条记录
*
* @param entity 实体对象
*/
int insert(T entity);

4.1.2、测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码package cn.linstudy.test

import cn.itcast.mp.mapper.UserMapper;
import cn.itcast.mp.pojo.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserMapperTest {

@Autowired
private UserMapper userMapper;

@Test
public void testInsert() {
User user = new User();
user.setAge(20);
user.setEmail("test@itcast.cn");
user.setName("曹操");
user.setUserName("caocao");
user.setPassword("123456");
int result = this.userMapper.insert(user); //返回的result是受影响的行数,并不是自增后的id
System.out.println("result = " + result);
System.out.println(user.getId()); //自增后的id会回填到对象中
}
}

4.2、更新操作

4.2.1、updateById

4.2.1.1、方法定义

1
2
3
4
5
6
java复制代码/**
* 根据 ID 修改
*
* @param entity 实体对象
*/
int updateById(@Param(Constants.ENTITY) T entity);

4.2.1.2、测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码

// 需求: 将id=1用户名字修改为xiaolin

@Test

public void testUpdateById(){

Employee employee = new Employee();
employee.setId(1L);
employee.setName("xiaolin");
employeeMapper.updateById(employee);

}

// 注意: 拼接sql时,所有非null 字段都进行set 拼接

// UPDATE employee SET name=?, age=?, admin=? WHERE id=?

// 改进的方法是先查,再替换,最后更新

// 需求: 将id=1用户名字修改为xiaolin

@Test

public void testUpdateById2(){

Employee employee = employeeMapper.selectById(1L);

employee.setName("xiaolin");

employeeMapper.updateById(employee);

}

4.2.2、update

4.2.2.1、方法定义

1
2
3
4
5
6
7
8
java复制代码  /**
* 根据 whereEntity 条件,更新记录
*
* @param entity 实体对象 (set 条件值,可以为 null)
* @param updateWrapper 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句)
*/
int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T>
updateWrapper);

4.2.2.2、测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码public class UserMapperTest {

@Autowired
private UserMapper userMapper;

// 方法一:使用QueryWrapper
@Test
public void testUpdate() {
User user = new User();
user.setAge(22); //更新的字段
//更新的条件
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("id", 6);
//执行更新操作
int result = this.userMapper.update(user, wrapper);
System.out.println("result = " + result);
}

//方法二: 通过UpdateWrapper进行更新
@Test
public void testUpdate(){
//更新的条件以及字段
UpdateWrapper<User> wrapper=new UpdateWrapper<>();
wrapper.eq("id",6).set("age",23);
//执行更新操作
int result=this.userMapper.update(null,wrapper);
System.out.println("result = "+result);
}
}

4.2.2.3、使用建议

  1. 知道id,并且所有更新使用updateById
  2. 部分字段更新,使用update

4.3、删除操作

4.3.1、deleteById

4.3.1.1、方法定义

1
2
3
4
5
6
java复制代码/**
* 根据 ID 删除
*
* @param id 主键ID
*/
int deleteById(Serializable id);

4.3.1.2、测试

1
2
3
4
5
6
java复制代码@Test
public void testDeleteById() {
//执行删除操作
int result = this.userMapper.deleteById(6L);
System.out.println("result = " + result);
}

4.3.2、deleteByMap

4.3.2.1、方法定义

1
2
3
4
5
6
java复制代码/**
* 根据 columnMap 条件,删除记录
*
* @param columnMap 表字段 map 对象
*/
int deleteByMap(@Param(Constants.COLUMN_MAP) Map<String, Object> columnMap);

4.3.2.2、测试

1
2
3
4
5
6
7
8
9
java复制代码@Test
public void testDeleteByMap() {
Map<String, Object> columnMap = new HashMap<>();
columnMap.put("age",20);
columnMap.put("name","张三");
//将columnMap中的元素设置为删除的条件,多个之间为and关系
int result = this.userMapper.deleteByMap(columnMap);
System.out.println("result = " + result);
}

4.3.3、delete

4.3.3.1、方法定义

1
2
3
4
5
6
java复制代码/**
* 根据 entity 条件,删除记录
*
* @param wrapper 实体对象封装操作类(可以为 null)
*/
int delete(@Param(Constants.WRAPPER) Wrapper<T> wrapper);

4.3.3.2、测试

1
2
3
4
5
6
7
8
9
10
java复制代码@Test
public void testDeleteByMap() {
User user = new User();
user.setAge(20);
user.setName("张三");
//将实体对象进行包装,包装为操作条件
QueryWrapper<User> wrapper = new QueryWrapper<>(user);
int result = this.userMapper.delete(wrapper);
System.out.println("result = " + result);
}

4.3.4、deleteBatchIds

4.3.4.1、方法定义

1
2
3
4
5
6
7
java复制代码/**
* 删除(根据ID 批量删除)
*
* @param idList 主键ID列表(不能为 null 以及 empty)
*/
int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable>
idList);

4.3.4.2、测试

1
2
3
4
5
6
java复制代码@Test
public void testDeleteByMap() {
//根据id集合批量删除
int result = this.userMapper.deleteBatchIds(Arrays.asList(1L,10L,20L));
System.out.println("result = " + result);
}

4.4、查询操作

MyBatis-Plus提供了多种查询操作,包括根据id查询、批量查询、查询单条数据、查询列表、分页查询等操作。

4.4.1、selectById

4.4.1.1、方法定义

1
2
3
4
5
6
java复制代码/**
* 根据 ID 查询
*
* @param id 主键ID
*/
T selectById(Serializable id);

4.1.1.2、测试

1
2
3
4
5
6
java复制代码@Test
public void testSelectById() {
//根据id查询数据
User user = this.userMapper.selectById(2L);
System.out.println("result = " + user);
}

4.4.2、selectBatchIds

4.4.2.1、方法定义

1
2
3
4
5
6
7
java复制代码/**
* 查询(根据ID 批量查询)
*
* @param idList 主键ID列表(不能为 null 以及 empty)
*/
List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable>
idList);

4.2.2.2、测试

1
2
3
4
5
6
7
8
java复制代码@Test
public void testSelectBatchIds() {
//根据id集合批量查询
List<User> users = this.userMapper.selectBatchIds(Arrays.asList(2L, 3L, 10L));
for (User user : users) {
System.out.println(user);
}
}

4.4.3、selectOne

4.4.3.1、方法定义

1
2
3
4
5
6
java复制代码/**
* 根据 entity 条件,查询一条记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

4.4.3.2、测试

1
2
3
4
5
6
7
8
java复制代码@Test
public void testSelectOne() {
QueryWrapper<User> wrapper = new QueryWrapper<User>();
wrapper.eq("name", "李四");
//根据条件查询一条数据,如果结果超过一条会报错
User user = this.userMapper.selectOne(wrapper);
System.out.println(user);
}

4.4.4、selectCount

4.4.4.1、方法定义

1
2
3
4
5
6
java复制代码/**
* 根据 Wrapper 条件,查询总记录数
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
Integer selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

4.4.4.2、测试

1
2
3
4
5
6
7
java复制代码@Test
public void testSelectCount() {
QueryWrapper<User> wrapper = new QueryWrapper<User>();
wrapper.gt("age", 23); //年龄大于23岁
Integer count = this.userMapper.selectCount(wrapper);
System.out.println("count = " + count);
}

4.4.5、selectList

4.4.5.1、方法定义

1
2
3
4
5
6
java复制代码/**
* 根据 entity 条件,查询全部记录
*
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

4.4.5.2、测试

1
2
3
4
5
6
7
8
9
10
java复制代码@Test
public void testSelectList() {
QueryWrapper<User> wrapper = new QueryWrapper<User>();
wrapper.gt("age", 23); //年龄大于23岁
//根据条件查询数据
List<User> users = this.userMapper.selectList(wrapper);
for (User user : users) {
System.out.println("user = " + user);
}
}

4.4.6、selectPage

4.4.6.1 方法定义

1
2
3
4
5
6
7
java复制代码/**
* 根据 entity 条件,查询全部记录(并翻页)
*
* @param page 分页查询条件(可以为 RowBounds.DEFAULT)
* @param queryWrapper 实体对象封装操作类(可以为 null)
*/
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

4.4.6.2、配置分页插件

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Configuration
@MapperScan("cn.itcast.mp.mapper") //设置mapper接口的扫描包
public class MybatisPlusConfig {
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}

4.4.6.3、测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Test
public void testSelectPage() {
QueryWrapper<User> wrapper = new QueryWrapper<User>();
wrapper.gt("age", 20); //年龄大于20岁
Page<User> page = new Page<>(1,1);
//根据条件查询数据
IPage<User> iPage = this.userMapper.selectPage(page, wrapper);
System.out.println("数据总条数:" + iPage.getTotal());
System.out.println("总页数:" + iPage.getPages());
List<User> users = iPage.getRecords();
for (User user : users) {
System.out.println("user = " + user);
}
}

4.4.7、SQL注入原理

MP在启动后会将BaseMapper中的一系列的方法注册到meppedStatements中,那么究竟是如何注入的呢?流程又是怎么样的?


在MP中,ISqlInjector负责SQL的注入工作,它是一个接口,AbstractSqlInjector是它的实现类,实现关系如下:

image-20210428142720554

在AbstractSqlInjector中,主要是由inspectInject()方法进行注入的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Override
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?>
mapperClass) {
Class<?> modelClass = extractModelClass(mapperClass);
if (modelClass != null) {
String className = mapperClass.toString();
Set<String> mapperRegistryCache =
GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
if (!mapperRegistryCache.contains(className)) {
List<AbstractMethod> methodList = this.getMethodList();
if (CollectionUtils.isNotEmpty(methodList)) {
TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant,
modelClass);
// 循环注入自定义方法
methodList.forEach(m -> m.inject(builderAssistant, mapperClass,
modelClass, tableInfo));
} else {
logger.debug(mapperClass.toString() + ", No effective injection method
was found.");
}
mapperRegistryCache.add(className);
}
}
}

image-20210428164123430

以SelectById为例查看:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class SelectById extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?>
modelClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.LOGIC_SELECT_BY_ID;
SqlSource sqlSource = new RawSqlSource(configuration,
String.format(sqlMethod.getSql(),
sqlSelectColumns(tableInfo, false),
tableInfo.getTableName(), tableInfo.getKeyColumn(),
tableInfo.getKeyProperty(),
tableInfo.getLogicDeleteSql(true, false)), Object.class);
return this.addSelectMappedStatement(mapperClass, sqlMethod.getMethod(),
sqlSource, modelClass, tableInfo);
}
}
可以看到,生成了SqlSource对象,再将SQL通过addSelectMappedStatement方法添加到meppedStatements中。

image-20210428164932955

五、条件构造器

条件构造器可以简单理解为条件拼接对象,用于生成 sql 的 where 条件。

5.1、继承体系

在MyBatis-Plus中,Wrapper接口的实现类关系如下:

image-20210428172222929

  1. AbstractWrapper: 用于查询条件封装,生成 sql 的 where 条件。
  2. QueryWrapper: Entity 对象封装操作类,不是用lambda语法。
  3. UpdateWrapper: Update 条件封装,用于Entity对象更新操作。
  4. AbstractLambdaWrapper: Lambda 语法使用 Wrapper统一处理解析 lambda 获取 column。
  5. LambdaQueryWrapper:看名称也能明白就是用于Lambda语法使用的查询Wrapper。
  6. LambdaUpdateWrapper: Lambda 更新封装Wrapper。

5.2、更新操作

5.2.1、普通更新

1
2
3
4
5
6
7
java复制代码@Test
public void testUpdate(){
Employee employee = new Employee();
employee.setId(1L);
employee.setName("xiaolin");
employeeMapper.updateById(employee);
}
这种更新会导致数据的丢失,因为我只想更新部分的字段。

5.2.2、UpdateWrapper更新

5.2.2.1、set

如果说我们需要更新部分的字段可以选择UpdateWrapper进行更新。他的方法主要是有两个:
  1. set(String column, Object val)
  2. set(boolean condition, String column, Object val)
1
2
3
4
5
6
7
8
9
10
java复制代码// 需求:将id=1的员工name改为xiaolin
@Test
public void testUpdate2(){
UpdateWrapper<Employee> wrapper = new UpdateWrapper<>();
wrapper.eq("id", 1L);
// 相当于sql语句中的set name = xiaolin
wrapper.set("name", "xiaolin");
employeeMapper.update(null, wrapper);

}

5.2.2.2、

MyBatis-Plus还提供了另一种方式来修改,那就是可以利用setSql直接写入sql语句。
1
2
3
4
5
6
7
8
9
java复制代码	// 需求:将id=1的用户name改为xiaolin 
@Test
public void testUpdate3(){
UpdateWrapper<Employee> wrapper = new UpdateWrapper<>();
wrapper.eq("id", 1L);
wrapper.setSql("name='xiaolin'");
employeeMapper.update(null, wrapper);

}

5.2.3、LambdaUpdateWrapper更新

我们还可以利用JDK8的新语法配合LambdaUpdateWrapper来进行操作。
1
2
3
4
5
6
7
8
java复制代码   // 需求:将id=1的用户name改为xiaolin 
@Test
public void testUpdate4(){
LambdaUpdateWrapper<Employee> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(Employee::getId, 1L);
wrapper.set(Employee::getName, "xiaolin");
employeeMapper.update(null, wrapper);
}

5.2.4、开发建议

推荐使用LambdaUpdateWrapper更新。

5.3、查询操作

5.3.1、普通查询

1
2
3
4
5
6
7
8
9
java复制代码  // 需求:查询name=xiaolin, age=18的用户
@Test
public void testQuery1(){
Map<String, Object> map = new HashMap<>();
map.put("name", "xiaolin");
map.put("age", 18);
System.out.println(employeeMapper.selectByMap(map));

}

5.3.2、QueryWrapper查询

1
2
3
4
5
6
7
java复制代码  // 需求:查询name=xiaolin, age=18的用户
@Test
public void testQuery2(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.eq("name", "xiaolin").eq("age", 18);
System.out.println(employeeMapper.selectList(wrapper));
}

5.3.3、LambdaQueryWrapper查询

1
2
3
4
5
6
7
8
java复制代码  //需求:查询name=xiaolin, age=18的用户
@Test
public void testQuery3(){
LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Employee::getName, "xiaolin").eq(Employee::getAge, 18);
System.out.println(employeeMapper.selectList(wrapper));

}

5.3.4、开发建议

推荐使用LambdaUpdateWrapper更新

5.4、高级查询

5.4.1、列投影

所谓的烈投影就是指定查询后返回的列。我们利用的是select方法进行实现的。他有三个重载方法:
  1. select(String… sqlSelect) :参数是指定查询后返回的列。
  2. select(Predicate predicate):参数是Predicate 函数,满足指定判定逻辑列才返回。
  3. select(Class entityClass, Predicate predicate):参数1是通过实体属性映射表中列,参数2是Predicate 函数, 满足指定判定逻辑列才返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码  // 需求:查询所有员工, 返回员工name, age列
@Test
public void testQuery4(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.select("name", "age");
employeeMapper.selectList(wrapper);

}

// 需求:查询所有员工, 返回员工以a字母开头的列
@Test
public void testQuery4(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.select(Employee.class, tableFieldInfo->tableFieldInfo.getProperty().startsWith("a"));
employeeMapper.selectList(wrapper);

}

5.4.2、排序

5.4.2.1、orderByAsc/orderByDesc

排序分为两种,等价SQL: select ..from table **ORDER BY 字段, ... ASC**;
  1. orderByAsc: 正序排序。
  2. orderByDesc:倒序排序。
1
2
3
4
5
6
7
8
java复制代码  // 需求:查询所有员工信息按age正序排, 如果age一样, 按id正序排
@Test
public void testQuery5(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.orderByAsc("age", "id");
employeeMapper.selectList(wrapper);

}

5.4.2.2、orderBy

如果官方写好的排序不适用于我们的话,我们可以使用定制排序-order by。等价SQL:**select ..from table ORDER BY 字段**;


`orderBy(boolean condition, boolean isAsc, R... columns)`:参数1:控制是否进行排序,参数2:控制是不是正序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码  // 需求:查询所有员工信息按age正序排, 如果age一样, 按id正序排
@Test
public void testQuery5(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
// orderBy(true, true, "id", "name")等价于order by id ASC,name ASC
//apper.orderByAsc("age", "id");
//等价于:
wrapper.orderBy(true, true, "age", "id");
employeeMapper.selectList(wrapper);

}

// 需求:查询所有员工信息按age正序排, 如果age一样, 按id倒序排
@Test
public void testQuery7(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.orderByAsc("age");
wrapper.orderByDesc("id");
employeeMapper.selectList(wrapper);

}

5.5、条件查询

5.5.1、allEq(全等匹配)

5.5.1.1、方法

1
2
3
java复制代码allEq(Map<R, V> params) // params : key为数据库字段名,value为字段值	
allEq(Map<R, V> params, boolean null2IsNull) // 为true则在map的value为null时调用 isNull 方法,为false时则忽略value为null的
allEq(boolean condition, Map<R, V> params, boolean null2IsNull)
`null2IsNull`这个参数的意思是为true则在map的value为null时调用 isNull 方法,为false时则忽略value为null的。例如:
  1. allEq({id:1,name:”老王”,age:null})—>id = 1 and name = ‘老王’ and age is null
  2. allEq({id:1,name:”老王”,age:null}, false) —> id = 1 and name = ‘老王’

5.5.1.2、范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码  // 需求:查询name=xiaolin, age=18的员工信息
@Test
public void testQuery8(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
Map<String, Object> map = new HashMap<>();
map.put("name", "xiaolin");
map.put("age", 18);
wrapper.allEq(map);
employeeMapper.selectList(wrapper);
}

@Test
public void testQuery8(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
Map<String, Object> map = new HashMap<>();
map.put("name", "xiaolin");
map.put("age", 18);
map.put("dept_id", null);
wrapper.allEq(map, true);
employeeMapper.selectList(wrapper);
}

5.5.2、allEq(全等匹配带条件过滤的)

5.5.2.1、方法

1
2
3
java复制代码allEq(BiPredicate<R, V> filter, Map<R, V> params)
allEq(BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull)
allEq(boolean condition, BiPredicate<R, V> filter, Map<R, V> params, boolean null2IsNull)
`filter` : 过滤函数,是否允许字段传入比对条件中 params 与 null2IsNull同上,例如:
  1. allEq((k,v) -> k.indexOf(“a”) >= 0, {id:1,name:”老王”,age:null})—>name = ‘老王’ and age is null
  2. allEq((k,v) -> k.indexOf(“a”) >= 0, {id:1,name:”老王”,age:null}, false)—>name = ‘老王’

5.5.2.2、范例

1
2
3
4
5
6
7
8
9
10
java复制代码   // 需求:查询满足条件员工信息, 注意传入的map条件中, 包含a的列才参与条件查询
@Test
public void testQuery9(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
Map<String, Object> map = new HashMap<>();
map.put("name", "xiaolin");
map.put("age", 18);
wrapper.allEq((k, v)-> k.contains("m"), map);
employeeMapper.selectList(wrapper);
}

5.5.3、eq

我们可以使用eq来判断单个参数判断是否相等。eq("name", "老王")等价于name = '老王'。

5.5.3.1、方法

1
2
java复制代码eq(R column, Object val)
eq(boolean condition, R column, Object val) // 相较于上一个方法,多了一个条件,当前面的条件成立时,才拼接后面语句,常用于判断当某个值不为空的时候进行拼接。

5.5.3.2、范例

1
2
3
4
5
6
7
java复制代码  // 需求:查询name=xiaolin员工信息
@Test
public void testQuery10(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.eq("name", "xiaolin");
employeeMapper.selectList(wrapper);
}

5.5.4、ne

我们可以使用ne来判断某个参数是否不相等。 ne("name", "老王")等价于name != '老王'。
1
2
3
4
5
6
7
8
java复制代码  // 需求:查询name !=xiaolin员工信息
@Test
public void testQuery11(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.ne("name", "xiaolin");
employeeMapper.selectList(wrapper);

}

5.5.5、gt

gt表示大于。gt("age", 18)等价于age > 18

5.5.5.1、方法

1
2
java复制代码gt(R column, Object val)
gt(boolean condition, R column, Object val)

5.5.5.2、范例

1
2
3
4
5
6
7
java复制代码  // 需求:查询age > 12 员工信息
@Test
public void testQuery12(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.gt("age", "12");
employeeMapper.selectList(wrapper);
}

5.5.6、ge

ge表示大于等于。ge("age", 18)等价于age > =18

5.5.6.1、方法

1
2
java复制代码ge(R column, Object val)
ge(boolean condition, R column, Object val)

5.5.6.2、范例

1
2
3
4
5
6
7
java复制代码  // 需求:查询age >= 12 员工信息
@Test
public void testQuery12(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.ge("age", "12");
employeeMapper.selectList(wrapper);
}

5.5.7、lt

lt表示小于1。lt("age", 18)等价于age < 18

5.5.7.1、方法

1
2
java复制代码lt(R column, Object val)
lt(boolean condition, R column, Object val

5.5.7.2、范例

1
2
3
4
5
6
7
java复制代码  // 需求:查询age < 12 员工信息
@Test
public void testQuery12(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.lt("age", "12");
employeeMapper.selectList(wrapper);
}

5.5.8、le

le表示小于等于。le("age", 18)等价于age <= 18。
1
2
java复制代码le(R column, Object val)
le(boolean condition, R column, Object val)

5.5.9、between、notBetween

我们使用between/notBetween来表示介于/不介于两者之间。


between("age", 18, 30)等价于age between 18 and 30。


notBetween("age", 18, 30)等价于age not between 18 and 30

5.5.9.1、方法

1
2
3
4
5
6
7
java复制代码// between : BETWEEN 值1 AND 值2
between(R column, Object val1, Object val2)
between(boolean condition, R column, Object val1, Object val2)

// notBetween : NOT BETWEEN 值1 AND 值2
notBetween(R column, Object val1, Object val2)
notBetween(boolean condition, R column, Object val1, Object val2)

5.5.9.2、范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码  // 需求:查询年龄介于18~30岁的员工信息
@Test
public void testQuery13(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.between("age", 18, 30);
employeeMapper.selectList(wrapper);
}

// 需求:查询年龄小于18或者大于30岁的员工信息【用between实现】
@Test
public void testQuery13(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.notBetween("age", 18, 30);
employeeMapper.selectList(wrapper);
}

5.5.10、isNull、isNotNull

我们可以使用isNull/isNotNull来表示为空/不为空。


isNull("name")等价于name is null。


isNotNull("name")等价于name is not null。

5.5.10.1、方法

1
2
3
4
5
6
7
java复制代码// isNull :  字段 IS NULL
isNull(R column)
isNull(boolean condition, R column)

// isNotNull : 字段 IS NOT NULL
isNotNull(R column)
isNotNull(boolean condition, R column)

5.5.10.2、范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码  // 需求: 查询dept_id 为null 员工信息
@Test
public void testQuery16(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.isNull("dept_id");
employeeMapper.selectList(wrapper);
}

// 需求: 查询dept_id 为不为null 员工信息
@Test
public void testQuery16(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.isNotNull("dept_id");
employeeMapper.selectList(wrapper);
}

5.5.11、in、notIn

我们可以使用in/notIn来表示值在/不在这里面。


in("age",{1,2,3})--->age in (1,2,3)


notIn("age",{1,2,3})--->age not in (1,2,3)

5.5.11.1、方法

1
2
3
4
5
6
7
java复制代码// in : 字段 IN (value1, value2, ...)
in(R column, Collection<?> value)
in(boolean condition, R column, Collection<?> value)

// notIn : 字段 NOT IN (value1, value2, ...)
notIn(R column, Object... values)
notIn(boolean condition, R column, Object... values)

5.5.11.2、范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码  // 需求: 查询id为1, 2 的员工信息
@Test
public void testQuery18(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.in("id", 1L, 2L);
employeeMapper.selectList(wrapper);
}

// 需求: 查询id不为1,2 的员工信息
@Test
public void testQuery19(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.notIn("id", 1L, 2L);
employeeMapper.selectList(wrapper);
}

5.5.12、inSql、notInSql

与上一个不同的是,他的格式是:字段 IN ( sql语句 ) / 字段 NOT IN ( sql语句 ),接的是SQL语句片段。

5.5.12.1、方法

inSql("age", "1,2,3,4,5,6")等价于age in (1,2,3,4,5,6)


notInSql("id", "select id from table where id < 3")--->id not in (select id from table where id < 3)
1
2
3
4
5
6
7
java复制代码// inSql : 字段 IN ( sql语句 )
inSql(R column, String inValue)
inSql(boolean condition, R column, String inValue)

// notInSql : 字段 NOT IN ( sql语句 )
notInSql(R column, String inValue)
notInSql(boolean condition, R column, String inValue)

5.5.12.2、范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码  // 需求: 查询id为1, 2 的员工信息
@Test
public void testQuery20(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.inSql("id", "1,2");
employeeMapper.selectList(wrapper);
}

// 需求: 查询id不为1,2 的员工信息
@Test
public void testQuery21(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.notInSql("id", "1,2");
employeeMapper.selectList(wrapper);
}

5.6、模糊查询

5.6.1、like、notLike

5.6.1.1、方法

1
2
3
4
5
6
7
8
9
java复制代码// like: LIKE '%值%':like("name", "王")等价于name like '%王%'
like: LIKE '%值%'
like(R column, Object val)
like(boolean condition, R column, Object val)

// notLike("name", "王")--->name not like '%王%'
notLike : NOT LIKE '%值%'
notLike(R column, Object val)
notLike(boolean condition, R column, Object val)

5.6.1.2、范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码  // 需求: 查询name中含有lin字样的员工
@Test
public void testQuery14(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.like("name", "lin");
employeeMapper.selectList(wrapper);
}

// 需求: 查询name中不含有lin字样的员工
@Test
public void testQuery14(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.notLike("name", "lin");
employeeMapper.selectList(wrapper);
}

5.6.2、likeLeft、likeRight

5.6.2.1、方法

1
2
3
4
5
6
7
8
9
java复制代码// likeLeft("name", "王")--->name like '%王'
likeLeft : LIKE '%值'
likeLeft(R column, Object val)
likeLeft(boolean condition, R column, Object val)

// likeRight("name", "王")--->name like '王%'
likeRight : LIKE '值%'
likeRight(R column, Object val)
likeRight(boolean condition, R column, Object val)

5.6.2.2、范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码  // 需求: 查询name以lin结尾的员工信息
@Test
public void testQuery15(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.likeLeft("name", "lin");
employeeMapper.selectList(wrapper);
}

// 需求: 查询姓王的员工信息
@Test
public void testQuery16(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.likeRight("name", "王");
employeeMapper.selectList(wrapper);
}

5.7、逻辑运算符

5.7.1、or

5.7.1.1、方法

1
2
3
4
5
6
7
8
9
java复制代码// eq("id",1).or().eq("name","老王")--->id = 1 or name = '老王'
or : 拼接 OR
or()
or(boolean condition)

// or还可以嵌套使用
// or(i -> i.eq("name", "李白").ne("status", "活着"))--->or (name = '李白' and status <> '活着')
or(Consumer<Param> consumer)
or(boolean condition, Consumer<Param> consumer)
**主动调用or表示紧接着下一个方法不是用and连接!(不调用or则默认为使用and连接)**

5.7.1.2、范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码  // 需求: 查询age = 18 或者 name=xiaolin 或者 id =1 的用户
@Test
public void testQuery24(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.eq("age", 18)
.or()
.eq("name", "xiaolin")
.or()
.eq("id", 1L);
employeeMapper.selectList(wrapper);
}

// 需求:查询name含有lin字样的,或者 年龄在18到30之间的用户
@Test
public void testQuery25(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.like("name", "lin")
.or(wr -> wr.le("age", 30).ge("age", 18));
employeeMapper.selectList(wrapper);
}

5.7.2、and

5.7.2.1、方法

1
2
3
java复制代码// 嵌套and:
and(Consumer<Param> consumer)
and(boolean condition, Consumer<Param> consumer)

5.7.2.2、范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码  // 需求:查询年龄介于18~30岁的员工信息
@Test
public void testQuery26(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.le("age", 30).ge("age", 18);
employeeMapper.selectList(wrapper);
}

// 需求:查询name含有lin字样的并且 年龄在小于18或者大于30的用户
@Test
public void testQuery27(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.like("name", "lin")
.and(wr -> wr.le("age", 30)
.or()
.ge("age", 18));
employeeMapper.selectList(wrapper);
}

5.8、分组查询

5.8.1、groupBy

5.8.1.1、方法

1
2
3
4
java复制代码// groupBy("id", "name")--->group by id,name
groupBy : 分组:GROUP BY 字段, ...
groupBy(R... columns)
groupBy(boolean condition, R... columns)

5.8.1.2、范例

1
2
3
4
5
6
7
8
java复制代码   // 需求: 以部门id进行分组查询,查每个部门员工个数
@Test
public void testQuery22(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.groupBy("dept_id");
wrapper.select("dept_id", "count(id) count");
employeeMapper.selectMaps(wrapper);
}

5.8.2、having

5.8.2.1、方法

1
2
3
4
5
java复制代码// having("sum(age) > 10")--->having sum(age) > 10
// having("sum(age) > {0}", 11)--->having sum(age) > 11
having : HAVING ( sql语句 )
having(String sqlHaving, Object... params)
having(boolean condition, String sqlHaving, Object... params)

5.8.2.2、范例

1
2
3
4
5
6
7
8
9
10
java复制代码  // 需求: 以部门id进行分组查询,查每个部门员工个数, 将大于3人的部门过滤出来
@Test
public void testQuery23(){
QueryWrapper<Employee> wrapper = new QueryWrapper<>();
wrapper.groupBy("dept_id")
.select("dept_id", "count(id) count")
//.having("count > {0}", 3)
.having("count >3");
employeeMapper.selectMaps(wrapper);
}

六、通用Service接口

6.1、传统方式

在以前的业务层中,我们需要写接口和实现类,其中有不少的接口都是重复的CRUD,没有任何技术含量。

6.1.1、Service接口

1
2
3
4
5
6
7
java复制代码public interface EmployeeService {
void save(Employee employee);
void update(Employee employee);
void delete(Long id);
Employee get(Long id);
List<Employee> list();
}

6.1.2、ServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeMapper mapper;

@Override
public void save(Employee employee) {
mapper.insert(employee);
}

@Override
public void update(Employee employee) {
mapper.updateById(employee); //必须全量更新
}

@Override
public void delete(Long id) {
mapper.deleteById(id);
}

@Override
public Employee get(Long id) {
return mapper.selectById(id);
}

@Override
public List<Employee> list() {
return mapper.selectList(null);
}
}

6.2、MyBatis-Plus的通用Service接口

既然需要重复写那么多没有技术含量的代码,那么肯定MyBatis-Plus会帮我们做好,我们只需要简单两部即可使用MyBatis-Plus给我们写好的CRUD方法,他会自动调用mapper接口方法。
  1. 自定义服务接口继承IService接口,其中泛型是实体类对象
1
2
java复制代码public interface IEmployeeService extends IService<Employee> {
}
  1. 服务接口实现类集成IService接口实现类ServiceImpl同时实现自定义接口,泛型一是实体类的mapper接口,泛型二是实体类。
1
2
3
java复制代码@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements IEmployeeService {
}

6.2.1、常用方法

  1. getBaseMapper():获取引用的XxxxMapper对象。
  2. getOne(wrapper):指定条件查询单个, 结果数据超过1个报错。
  3. list(wrapper):指定条件查询多个。

6.2.2、分页

分页所用的方法是:`page(page, wrapper)`,他也可以配合高级查询一起。

6.2.2.1、配置分页插件

我们需要在配置类中配置分页插件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码//分页

@Bean

public MybatisPlusInterceptor mybatisPlusInterceptor() {

MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);

paginationInnerInterceptor.setOverflow(true); //合理化

interceptor.addInnerInterceptor(paginationInnerInterceptor);

return interceptor;

}

6.2.2.2、编写分页代码

MyBatis-Plus的分页对象是IPage,分页信息封装对象,里面有各种分页相关信息等价于之前的PageInfo。
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码//需求:查询第2页员工信息, 每页显示3条, 按id排序
@Test
public void testPage(){
EmployeeQuery qo = new EmployeeQuery();
qo.setPageSize(3);
qo.setCurrentPage(2);
IPage<Employee> page = employeeService.query(qo);
System.out.println("当前页:" + page.getCurrent());
System.out.println("总页数:" + page.getPages());
System.out.println("每页显示条数:" + page.getSize());
System.out.println("总记录数:" + page.getTotal());
System.out.println("当前页显示记录:" + page.getRecords());
}

七、ActiveRecord

7.1、什么是ActiveRecord

ActiveRecord也属于ORM(对象关系映射)层,由Rails最早提出,遵循标准的ORM模型:表映射到记录,记录映射到对象,字段映射到对象属性。配合遵循的命名和配置惯例,能够很大程度的快速实现模型的操作,而且简洁易懂。


ActiveRecord的主要思想是:
  1. 每一个数据库表对应创建一个类,类的每一个对象实例对应于数据库中表的一行记录;通常表的每个字段
    在类中都有相应的Field。
  2. ActiveRecord同时负责把自己持久化,在ActiveRecord中封装了对数据库的访问,即CURD。
  3. ActiveRecord是一种领域模型(Domain Model),封装了部分业务逻辑;
ActiveRecord(简称AR)一直广受动态语言( PHP 、 Ruby 等)的喜爱,而 Java 作为准静态语言,对于ActiveRecord 往往只能感叹其优雅,所以我们也在 AR 道路上进行了一定的探索,喜欢大家能够喜欢。

7.2、开启AR之旅

在MP中,开启AR非常简单,只需要将实体对象继承Model即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee extends Model<Employee> {

@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String name;
private String password;
private String email;
private Integer age;
private Boolean admin;
private Long deptId;
private Boolean status;
}

7.2.1、查询所有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class Test1 {

@Autowired
EmployeeMapper employeeMapper;

/**
* 用于测试查询所有
*/
@Test
public void test(){
Employee employee = new Employee();
List<Employee> employees = employee.selectAll();
for (Employee employee1 : employees) {
System.out.println(employee1);
}
}
}

7.2.2、根据id查询

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class Test2 {
/**
* 用于测试根据id查询
*/
@Test
public void test2(){
Employee employee = new Employee();
employee.setId(1L);
System.out.println(employee.selectById());
}
}

7.2.3、根据条件查询

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码  /**
* 用于测试根据条件查询
*/
@Test
public void test4(){
Employee employee = new Employee();
QueryWrapper<Employee> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.le("password","123");
List<Employee> employees = employee.selectList(userQueryWrapper);
for (Employee employee1 : employees) {
System.out.println(employee1);
}
}

7.2.4、新增数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class TestAR {

/**
* 用于测试新增数据
*/
@Test
public void test3(){
Employee employee = new Employee();
employee.setId(11L);
employee.setPassword("123");
employee.insert();
}
}

7.2.5、更新数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class TestAR {
/**
* 用于测试更新
*/
@Test
public void test5(){
Employee employee = new Employee();
employee.setId(1L);
employee.setPassword("123456789");
employee.updateById();
}
}

7.2.6、删除数据

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class TestAR {
/**
* 用于测试删除
*/
@Test
public void test6(){
Employee employee = new Employee();
employee.setId(1L);
employee.deleteById();
}
}

八、插件机制

8.1、插件机制简介

MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
  1. Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  2. ParameterHandler (getParameterObject, setParameters)
  3. ResultSetHandler (handleResultSets, handleOutputParameters)
  4. StatementHandler (prepare, parameterize, batch, update, query)
我们看到了可以拦截Executor接口的部分方法,比如update,query,commit,rollback等方法,还有其他接口的一些方法等。总体概括为:
  1. 拦截执行器的方法。
  2. 拦截参数的处理。
  3. 拦截结果集的处理。
  4. 拦截Sql语法构建的处理。

8.2、执行分析插件

在MP中提供了对SQL执行的分析的插件,可用作阻断全表更新、删除的操作,注意:该插件仅适用于开发环境,不适用于生产环境。我们首先要在启动类上配置:
1
2
3
4
5
6
7
8
9
java复制代码  @Bean
public SqlExplainInterceptor sqlExplainInterceptor(){
SqlExplainInterceptor sqlExplainInterceptor = new SqlExplainInterceptor();
List<ISqlParser> sqlParserList = new ArrayList<>();
// 攻击 SQL 阻断解析器、加入解析链
sqlParserList.add(new BlockAttackSqlParser());
sqlExplainInterceptor.setSqlParserList(sqlParserList);
return sqlExplainInterceptor;
}

测试类:

1
2
3
4
5
6
7
java复制代码@Test
public void testUpdate(){
Employee employee = new Employee();
employee.setPassword("123456");
int result = this.employeeMapper.update(employee, null);
System.out.println("result = " + result);
}
执行后会发现控制台报错了,当执行全表更新时,会抛出异常,这样有效防止了一些误操作。

image-20210509210703826.png

8.3、乐观锁插件

8.3.1、适用场景

当要更新一条记录的时候,希望这条记录没有被别人更新。


乐观锁的实现方式:
  1. 取出记录时,获取当前version。
  2. 更新时,带上这个version。
  3. 执行更新时, set version = newVersion where version = oldVersion。
  4. 如果version不对,就更新失败。

8.3.2、插件配置

我们需要在spring.xml中进行配置。
1
xml复制代码<bean class="com.baomidou.mybatisplus.extension.plugins.OptimisticLockerInterceptor"/>
然后在singboot的启动类中配置。
1
2
3
4
java复制代码Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}

8.3.3、注解实体字段

为表添加version字段,并赋初始值为1

1
2
3
mysql复制代码ALTER TABLE `employee`
ADD COLUMN `version` int(10) NULL AFTER `email`;
UPDATE `tb_user` SET `version`='1';

为实体类添加version字段,并且添加@Version注解

1
2
java复制代码@Version
private Integer version;

测试

1
2
3
4
5
6
7
8
9
java复制代码@Test
public void testUpdate(){
Employee employee = new Employee();
user.setPassword("456789");
user.setId(2L);
user.setVersion(1); //设置version为1
int result = this.userMapper.updateById(user);
System.out.println("result = " + result);
}

8.3.4、说明

  1. 支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime。
  2. 整数类型下 newVersion = oldVersion + 1。
  3. newVersion 会回写到 entity 中仅支持 updateById(id) 与 update(entity, wrapper) 方法。
  4. 在 update(entity, wrapper) 方法下, wrapper 不能复用。

文末抽奖福利

最后,因为XiaoLin有幸申请到了 [请查收|你有一次免费申请掘金周边礼物的机会](https://dev.newban.cn/7000643252957216782)活动的名额,具体要求如下👇:


**所有**看到该文章的童鞋都可以在评论区**发表评论**哦!赶快行动起来吧!🤗🤗🤗**截止到 9月10日,如果评论区超过 10 人互动(不含作者本人),作者可以以自己的名义抽奖送出掘金徽章 2 枚(掘金官方承担)。**
  1. 通过评论方式参加。
  2. 评论必须和此文章有关的内容。
  3. 奖品最终会由热评用户获得。
  4. 获奖后我会以评论方式告知你并添加微信留收货地址。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试必备:nginx知识梳理(收藏版)

发表于 2021-09-08

前言

本文已参与【请查收|你有一次免费申请掘金周边礼物的机会】活动。

参与评论,有机会获得掘金官方提供的 2 枚新版徽章,具体的抽奖细节请看文末。

对于初级开发、特别是是小白同学,希望小伙伴们认真看完,我相信你会有收获的。

创作不易,记得点赞、关注、收藏哟。

Nginx概念

Nginx 是一个高性能的 HTTP 和反向代理服务。其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中表现较好。

Nginx 专为性能优化而开发,性能是其最重要的考量指标,实现上非常注重效率,能经受住高负载的考验,有报告表明能支持高达50000个并发连接数。

在连接高并发的情况下,Nginx 是 Apache 服务不错的替代品:Nginx 在美国是做虚拟主机生意的老板们经常选择的软件平台之一。

反向代理

在说反向代理之前,先来说说什么是代理和正向代理。

代理

代理其实就是一个中介,A和B本来可以直连,中间插入一个C,C就是中介。刚开始的时候,代理多数是帮助内网client(局域网)访问外网server用的。
后来出现了反向代理,反向这个词在这儿的意思其实是指方向相反,即代理将来自外网客户端的请求转发到内网服务器,从外到内。

正向代理

正向代理即是客户端代理,代理客户端,服务端不知道实际发起请求的客户端。

正向代理类似一个跳板机,代理访问外部资源。

比如我们国内访问谷歌,直接访问访问不到,我们可以通过一个正向代理服务器,请求发到代理服服务上,代理服务器能够访问谷歌,这样由代理去访问谷歌取到返回数据,再返回给我们,这样我们就能访问谷歌了。

image-20210904171928174

反向代理

反向代理即是服务端代理,代理服务端,客户端不知道实际提供服务的服务端。

客户端是感知不到代理服务器的存在。

是指以代理服务器来接受 Internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。

image-20210904173138672

负载均衡

关于负载均衡,先来举个例子:

地铁大家应该都坐过吧,我们一般在早高峰乘地铁时候,总有那么一个地铁口人最拥挤,这时候,一般会有个地铁工作人员A拿个大喇叭在喊“着急的人员请走B口,B口人少车空”。而这个地铁工作人员A就是负责负载均衡的。

为了提升网站的各方面能力,我们一般会把多台机器组成一个集群对外提供服务。然而,我们的网站对外提供的访问入口都是一个的,比如www.taobao.com。那么当用户在浏览器输入www.taobao.com的时候如何将用户的请求分发到集群中不同的机器上呢,这就是负载均衡在做的事情。

负载均衡(Load Balance),意思是将负载(工作任务,访问请求)进行平衡、分摊到多个操作单元(服务器,组件)上进行执行。是解决高性能,单点故障(高可用),扩展性(水平伸缩)的终极解决方案。

image-20210904175555853

Nginx提供的负载均衡主要有三种方式:轮询,加权轮询,Ip hash。

轮询

nginx默认就是轮询其权重都默认为1,服务器处理请求的顺序:ABCABCABCABC….

1
2
3
4
5
java复制代码upstream mysvr { 
server 192.168.8.1:7070;
server 192.168.8.2:7071;
server 192.168.8.3:7072;
}

加权轮询

根据配置的权重的大小而分发给不同服务器不同数量的请求。如果不设置,则默认为1。下面服务器的请求顺序为:ABBCCCABBCCC….

1
2
3
4
5
java复制代码upstream mysvr { 
server 192.168.8.1:7070 weight=1;
server 192.168.8.2:7071 weight=2;
server 192.168.8.3:7072 weight=3;
}

ip_hash

iphash对客户端请求的ip进行hash操作,然后根据hash结果将同一个客户端ip的请求分发给同一台服务器进行处理,可以解决session不共享的问题。

1
2
3
4
5
6
java复制代码upstream mysvr { 
server 192.168.8.1:7070;
server 192.168.8.2:7071;
server 192.168.8.3:7072;
ip_hash;
}

动静分离

动态与静态页面区别

  • 静态资源: 当用户多次访问这个资源,资源的源代码永远不会改变的资源(如:HTML,JavaScript,CSS,img等文件)。
  • 动态资源:当用户多次访问这个资源,资源的源代码可能会发送改变(如:.jsp、servlet 等)。

什么是动静分离

  • 动静分离是让动态网站里的动态网页根据一定规则把不变的资源和经常变的资源区分开来,动静资源做好了拆分以后,我们就可以根据静态资源的特点将其做缓存操作,这就是网站静态化处理的核心思路。
  • 动静分离简单的概括是:动态文件与静态文件的分离。

为什么要用动静分离

为了加快网站的解析速度,可以把动态资源和静态资源用不同的服务器来解析,加快解析速度。降低单个服务器的压力。

image-20210904204757717

Nginx安装

windows下安装

1、下载nginx

nginx.org/en/download… 下载稳定版本。以nginx/Windows-1.20.1为例,直接下载 nginx-1.20.1.zip。
下载后解压,解压后如下:

image-20210905103735775

2、启动nginx

  • 直接双击nginx.exe,双击后一个黑色的弹窗一闪而过
  • 打开cmd命令窗口,切换到nginx解压目录下,输入命令 nginx.exe ,回车即可

3、检查nginx是否启动成功

直接在浏览器地址栏输入网址 http://localhost:80 回车,出现以下页面说明启动成功!

image-20210905103934702

Docker安装nginx

我之前的文章也讲过Linux下安装的步骤,我采用的是docker安装的,很简单。

相关链接如下:Docker(三):Docker部署Nginx和Tomcat

1、查看所有本地的主机上的镜像,使用命令docker images

image-20210904232433522
2、创建 nginx 容器 并启动容器,使用命令docker run -d --name nginx01 -p 3344:80 nginx

image-20210904233936797

3、查看已启动的容器,使用命令docker ps

image-20210904234231750

浏览器访问服务器ip:3344,如下,说明安装启动成功。

注意:如何连接不上,检查阿里云安全组是否开放端口,或者服务器防火墙是否开放端口!

image-20210905104039595

linux下安装

1、安装gcc

安装 nginx 需要先将官网下载的源码进行编译,编译依赖 gcc 环境,如果没有 gcc 环境,则需要安装:

1
shell复制代码yum install gcc-c++

2、PCRE pcre-devel 安装

PCRE(Perl Compatible Regular Expressions) 是一个Perl库,包括 perl 兼容的正则表达式库。nginx 的 http 模块使用 pcre 来解析正则表达式,所以需要在 linux 上安装 pcre 库,pcre-devel 是使用 pcre 开发的一个二次开发库。nginx也需要此库。命令:

1
shell复制代码yum install -y pcre pcre-devel

3、zlib 安装

zlib 库提供了很多种压缩和解压缩的方式, nginx 使用 zlib 对 http 包的内容进行 gzip ,所以需要在 Centos 上安装 zlib 库。

1
shell复制代码yum install -y zlib zlib-devel

4、OpenSSL 安装

OpenSSL 是一个强大的安全套接字层密码库,囊括主要的密码算法、常用的密钥和证书封装管理功能及 SSL 协议,并提供丰富的应用程序供测试或其它目的使用。
nginx 不仅支持 http 协议,还支持 https(即在ssl协议上传输http),所以需要在 Centos 安装 OpenSSL 库。

1
shell复制代码yum install -y openssl openssl-devel

5、下载安装包

手动下载.tar.gz安装包,地址:nginx.org/en/download…

image-20210905173049111

下载完毕上传到服务器上 /root

6、解压

1
2
shell复制代码tar -zxvf nginx-1.20.1.tar.gz
cd nginx-1.20.1

image-20210905173212111

7、配置

使用默认配置,在nginx根目录下执行

1
2
3
shell复制代码./configue
make
make install

查找安装路径: whereis nginx

image-20210905181408981

8、启动 nginx

1
shell复制代码./nginx

image-20210905181510315

启动成功,访问页面:ip:80

image-20210905181740776

Nginx常用命令

注意:使用Nginx操作命令前提,必须进入到Nginx目录 /usr/local/nginx/sbin

1、查看Nginx版本号:./nginx -v

image-20210905203751070

2、启动 Nginx:./nginx

image-20210905201929397

3、停止 Nginx:./nginx -s stop 或者./nginx -s quit

image-20210905202644529

4、重新加载配置文件:./nginx -s reload

image-20210905202753783

5、查看nginx进程:ps -ef|grep nginx

image-20210905202618893

Nginx配置文件

Nginx配置文件的位置:/usr/local/nginx/conf/nginx.conf

image-20210905204225730

Nginx配置文件有3部分组成:

image-20210906151628317

1、全局块

从配置文件开始到 events 块之间的内容,主要会设置一些影响 nginx 服务器整体运行的配置指令,比如:worker_processes 1。

这是 Nginx 服务器并发处理服务的关键配置,worker_processes 值越大,可以支持的并发处理量也越多,但是会受到硬件、软件等设备的制约。一般设置值和CPU核心数一致。

2、events块

events 块涉及的指令主要影响 Nginx 服务器与用户的网络连接,比如:worker_connections 1024

表示每个 work process 支持的最大连接数为 1024,这部分的配置对 Nginx 的性能影响较大,在实际中应该灵活配置。

3、http块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
shell复制代码http {
include mime.types;

default_type application/octet-stream;

sendfile on;

keepalive_timeout 65;

server {
listen 80;#监听端口
server_name localhost;#域名

location / {
root html;
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;

location = /50x.html {
root html;
}

}

}

这算是 Nginx 服务器配置中最频繁的部分。

演示示例

反向代理/负载均衡

image-20210906150245880

我们在windows下演示,首先我们创建两个springboot项目,端口是9001和9002,如下:

image-20210906142059214

我们要做的就是将localhost:80代理localhost:9001和localhost:9002这两个服务,并且让轮询访问这两个服务。

nginx配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码worker_processes  1;

events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;

sendfile on;
keepalive_timeout 65;

upstream jiangwang {
server 127.0.0.1:9001 weight=1;//轮询其权重都默认为1
server 127.0.0.1:9002 weight=1;
}

server {
listen 80;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root html;
index index.html index.htm;
proxy_pass http://jiangwang;
}
}

}

我们先将项目打成jar包,然后命令行启动项目,然后在浏览器上访问localhost来访问这两个项目,我也在项目中打印了日志,操作一下来看看结果,是不是两个项目轮询被访问。

image-20210906144202974

image-20210906144224135

可以看到,访问localhost,这两个项目轮询被访问。

接下来我们将权重改为如下设置:

1
2
3
4
java复制代码upstream jiangwang {
server 127.0.0.1:9001 weight=1;
server 127.0.0.1:9002 weight=3;
}

重新加载一个nginx的配置文件:nginx -s reload

加载完毕,我们再访问其localhost,观察其访问的比例:

image-20210906145104854

image-20210906145132494

结果显示,9002端口的访问次数与9001访问的次数基本上是3:1。

动静分离

1、将静态资源放入本地新建的文件里面,例如:在D盘新建一个文件data,然后再data文件夹里面在新建两个文件夹,一个img文件夹,存放图片;一个html文件夹,存放html文件;如下图:

image-20210906234145869

2、在html文件夹里面新建一个a.html文件,内容如下:

1
2
3
4
5
6
7
8
9
10
html复制代码<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Html文件</title>
</head>
<body>
<p>Hello World</p>
</body>
</html>

3、在img文件夹里面放入一张照片,如下:

4、配置nginx中nginx.conf文件:

1
2
3
4
5
6
7
8
9
java复制代码location /html/ {
root D:/data/;
index index.html index.htm;
}

location /img/ {
root D:/data/;
autoindex on;#表示列出当前文件夹中的所有内容
}

5、启动nginx,访问其文件路径,在浏览器输入http://localhost/html/a.html,如下:

image-20210906233944234

6、在浏览器输入http://localhost/img/

image-20210906234039557

Nginx工作原理

mater&worker

image-20210907084101801

master接收信号后将任务分配给worker进行执行,worker可有多个。

image-20210906235920093

worker如何工作

客户端发送一个请求到master后,worker获取任务的机制不是直接分配也不是轮询,而是一种争抢的机制,“抢”到任务后再执行任务,即选择目标服务器tomcat等,然后返回结果。

image-20210907104204828

worker_connection

普通的静态访问最大并发数是:worker_connections * worker_processes/ 2 ;若是 HTTP 作为反向代理来说,最大并发数量应该是 worker_connections * worker_processes/ 4 ,因为作为反向代理服务器,每个并发会建立与客户端的连接和后端服务器的连接,会占用两个连接。

当然了,worker数也不是越多越好,worker数和服务器的CPU数相等时最适宜的。

优点

可以使用 nginx –s reload 热部署,利用 nginx 进行热部署操作每个 woker 是独立的进程,若其中一个woker出现问题,其他继续进行争抢,实现请求过程,不会造成服务中断。

总结

关于 Nginx 的基本概念、安装教程、配置、使用实例以及工作原理,本文都做了详细阐述。希望本文对你有所帮助。

抽奖细则

  1. 首先你要参与评论,我希望你能看完文章,不要只打个表情符号之类的。
  2. 记得点赞、关注,动动你的小手指,谢谢!
  3. 至于怎么公平,我也想了一下,我会创建一个抽奖群,我发红包,你们抢,金额最少的获得奖品。或者你们有更好的方式评论区告诉我。如果我觉得更好,我会采纳。
  4. 如果我的评论区热度在Top 1-5 名,获得的 新版徽章1套 或 掘金新版IPT恤1件,我也会送给评论区的小伙伴,前提你关注我的我的公众号:微信搜【初念初恋】。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

习惯了微信聊天,利用WebSocket手动实现个聊天功能怎么

发表于 2021-09-08

1.背景

基于项目需求,最近需要实现一个简单的聊天功能。日常生活中,大家对于聊天也习以为常,微信、QQ等软件也经常用到,其实我们也可以引入一些第三方的sdk包等去实现,也可以利用WebSocket通信协议去手动实现简单的聊天。本文主要讲述下WebSocket实现的具体步骤及实现的效果图。

2.方案选型及优缺点介绍

  • 方案一 利用http接口手动实现三个接口:sengMsg(消息发送)、receiveMsg(消息接收)、getHistoryMsg(获取历史消息) ,然后前端发送消息时调用sendMsg接口,将数据写入数据库以便获取历史消息使用,接收消息时前端声明一个定时器,每一秒钟去刷新消息接收接口,来获取消息内容显示到聊天框中,最后,如果用户需要翻看历史消息,调用getHistoryMsg接口即可。
+ **优点** 后端实现简单,且能将聊天消息持久化到数据库永久保存,可以根据聊天室id随时获取消息内容
+ **缺点** 由于频繁调用接口,服务器和api接口压力比较大,高并发情况下服务器可能会宕机,而且不进行消息发送时,由于定时器的使用,前端频繁请求会造成空跑,显然不太合理
  • 方案二 利用已有的WebSocket服务实现聊天功能
+ **优点** 不用额外自己实现接口,直接按照WebSocket定义的规则直接套用即可
+ **缺点** 消息没有持久化,如果服务宕机,可能无法查看历史消息

3.服务搭建及实现

  • 3.1 引入依赖
1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
  • 3.2 声明socket配置类
1
2
3
4
5
6
7
8
9
typescript复制代码@Configuration
public class WebSocketConfig {

//注入一个ServerEndpointExporter
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
  • 3.3 声明聊天Controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
typescript复制代码/**
* 聊天控制器
* @ServerEndpoint("/chat/{userId}")中的userId是前端创建会话窗口时当前用户的id,即消息发送者的id
*/
@ServerEndpoint("/chat/{userId}")
@Component
public class ChatWebSocketController {

private final Logger logger = Logger.getLogger(ChatWebSocketController.class);

//onlineCount:在线连接数
private static AtomicInteger onlineCount = new AtomicInteger(0);

//webSocketSet:用来存放每个客户端对应的MyWebSocket对象。
public static List<ChatWebSocketController> webSocketSet = new ArrayList<>();

//存放所有连接人信息
public static List<String> userList = new ArrayList<>();

//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;

//用户ID
public String userId = "";

/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId = userId;
this.userList.add(userId) ;
//加入set中
webSocketSet.add(this);
//在线数加1
onlineCount.incrementAndGet();
logger.info("有新连接加入!" + userId + "当前在线用户数为" + onlineCount.get());
JSONObject msg = new JSONObject();
try {
msg.put("msg", "连接成功");
msg.put("status", "SUCCESS");
msg.put("userId", userId);
sendMessage(JSON.toJSONString(msg));
} catch (Exception e) {
logger.debug("IO异常");
}
}

/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(@PathParam("userId") String userId ) {
//从set中删除
webSocketSet.remove(this);
onlineCount.decrementAndGet(); // 在线数减1
logger.info("用户"+ userId +"退出聊天!当前在线用户数为" + onlineCount.get());
}

/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("userId") String userId ) {
//客户端输入的消息message要经过处理后封装成新的message,后端拿到新的消息后进行数据解析,然后判断是群发还是单发,并调用对应的方法
logger.info("来自客户端" + userId + "的消息:" + message);
try {
MyMessage myMessage = JSON.parseObject(message, MyMessage.class);
String messageContent = myMessage.getMessage();//messageContent:真正的消息内容
String messageType = myMessage.getMessageType();
if("1".equals(messageType)){ //单聊
String recUser = myMessage.getUserId();//recUser:消息接收者
sendInfo(messageContent,recUser,userId);//messageContent:输入框实际内容 recUser:消息接收者 userId 消息发送者
}else{ //群聊
sendGroupInfo(messageContent,userId);//messageContent:输入框实际内容 userId 消息发送者
}
} catch (Exception e) {
logger.error("解析失败:{}", e);
}
}

/**
* 发生错误时调用的方法
*
* @OnError
**/
@OnError
public void onError(Throwable error) {
logger.debug("Websocket 发生错误");
error.printStackTrace();
}

public synchronized void sendMessage(String message) {
this.session.getAsyncRemote().sendText(message);
}

/**
* 单聊
* message : 消息内容,输入的实际内容,不是拼接后的内容
* recUser : 消息接收者
* sendUser : 消息发送者
*/
public void sendInfo( String message , String recUser,String sendUser) {
JSONObject msgObject = new JSONObject();//msgObject 包含发送者信息的消息
for (ChatWebSocketController item : webSocketSet) {
if (StringUtil.equals(item.userId, recUser)) {
logger.info("给用户" + recUser + "传递消息:" + message);
//拼接返回的消息,除了输入的实际内容,还要包含发送者信息
msgObject.put("message",message);
msgObject.put("sendUser",sendUser);
item.sendMessage(JSON.toJSONString(msgObject));
}
}
}

/**
* 群聊
* message : 消息内容,输入的实际内容,不是拼接后的内容
* sendUser : 消息发送者
*/
public void sendGroupInfo(String message,String sendUser) {
JSONObject msgObject = new JSONObject();//msgObject 包含发送者信息的消息
if (StringUtil.isNotEmpty(webSocketSet)) {
for (ChatWebSocketController item : webSocketSet) {
if(!StringUtil.equals(item.userId, sendUser)) { //排除给发送者自身回送消息,如果不是自己就回送
logger.info("回送消息:" + message);
//拼接返回的消息,除了输入的实际内容,还要包含发送者信息
msgObject.put("message",message);
msgObject.put("sendUser",sendUser);
item.sendMessage(JSON.toJSONString(msgObject));
}
}
}
}

/**
* Map/Set的key为自定义对象时,必须重写hashCode和equals。
* 关于hashCode和equals的处理,遵循如下规则:
* 1)只要重写equals,就必须重写hashCode。
* 2)因为Set存储的是不重复的对象,依据hashCode和equals进行判断,所以Set存储的对象必须重写这两个方法。
* 3)如果自定义对象做为Map的键,那么必须重写hashCode和equals。
*
* @param o
* @return
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ChatWebSocketController that = (ChatWebSocketController) o;
return Objects.equals(session, that.session);
}

@Override
public int hashCode() {
return Objects.hash(session);
}
}
  • 3.4 声明Controller中的MyMessage实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typescript复制代码public class MyMessage implements Serializable {

private static final long serialVersionUID = 1L;

private String userId;
private String message;//消息内容
private String messageType;//消息类型 1 代表单聊 2 代表群聊

public String getUserId() {
return userId;
}

public void setUserId(String userId) {
this.userId = userId;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

public String getMessageType() {
return messageType;
}

public void setMessageType(String messageType) {
this.messageType = messageType;
}
}
  • 3.5 声明Controller中的StringUtil工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
ini复制代码public final class StringUtil {

/**
* 对象为空
*
* @param object
* @return
*/
public static boolean isEmpty(Object object) {
if (object == null) {
return true;
}
if (object instanceof String && "".equals(((String) object).trim())) {
return true;
}
if (object instanceof List && ((List) object).size() == 0) {
return true;
}
if (object instanceof Map && ((Map) object).isEmpty()) {
return true;
}
if (object instanceof CharSequence && ((CharSequence) object).length() == 0) {
return true;
}
if (object instanceof Arrays && (Array.getLength(object) == 0)) {
return true;
}
return false;
}

/**
* 对象不为空
*
* @param object
* @return
*/
public static boolean isNotEmpty(Object object) {
return !isEmpty(object);
}

/**
* 查询字符串中某个字符首次出现的位置 从1计数
*
* @param string 字符串
* @param c
* @return
*/
public static int strFirstIndex(String c, String string) {
Matcher matcher = Pattern.compile(c).matcher(string);
if (matcher.find()) {
return matcher.start() + 1;
} else {
return -1;
}
}

/**
* 两个对象是否相等
*
* @param obj1
* @param obj2
* @return
*/
public static boolean equals(Object obj1, Object obj2) {
if (obj1 instanceof String && obj2 instanceof String) {
obj1 = ((String) obj1).replace("\\*", "");
obj2 = ((String) obj2).replaceAll("\\*", "");
if (obj1.equals(obj2) || obj1 == obj2) {
return true;
}
}
if (obj1.equals(obj2) || obj1 == obj2) {
return true;
}
return false;
}

/**
* 根据字节截取内容
*
* @param bytes 自定义字节数组
* @param content 需要截取的内容
* @return
*/
public static String[] separatorByBytes(double[] bytes, String content) {
String[] contentArray = new String[bytes.length];
double[] array = new double[bytes.length + 1];
array[0] = 0;
//复制数组
System.arraycopy(bytes, 0, array, 1, bytes.length);
for (int i = 0; i < bytes.length; i++) {
content = content.substring((int) (array[i] * 2));
contentArray[i] = content;
}
String[] strings = new String[bytes.length];
for (int i = 0; i < contentArray.length; i++) {
strings[i] = contentArray[i].substring(0, (int) (bytes[i] * 2));
}
return strings;
}

/**
* 获取指定字符串出现的次数
*
* @param srcText 源字符串
* @param findText 要查找的字符串
* @return
*/
public static int appearNumber(String srcText, String findText) {
int count = 0;
Pattern p = Pattern.compile(findText);
Matcher m = p.matcher(srcText);
while (m.find()) {
count++;
}
return count;
}


/**
* 将字符串str每隔2个分割存入数组
*
* @param str
* @return
*/
public static String[] setStr(String str) {
int m = str.length() / 2;
if (m * 2 < str.length()) {
m++;
}
String[] strings = new String[m];
int j = 0;
for (int i = 0; i < str.length(); i++) {
if (i % 2 == 0) {
//每隔两个
strings[j] = "" + str.charAt(i);
} else {
strings[j] = strings[j] + str.charAt(i);
j++;
}
}
return strings;
}


/**
* 定义一个StringBuffer,利用StringBuffer类中的reverse()方法直接倒序输出
* 倒叙字符串
*
* @param s
*/
public static String reverseString2(String s) {
if (s.length() > 0) {
StringBuffer buffer = new StringBuffer(s);
return buffer.reverse().toString();
} else {
return "";
}
}

/**
* 截取字符串中的所有日期时间
*
* @param str
* @return
*/
public static List<String> dateTimeSubAll(String str) {
try {
List<String> dateTimeStrList = new ArrayList<>();
String regex = "[0-9]{4}[-][0-9]{1,2}[-][0-9]{1,2}[ ][0-9]{1,2}[:][0-9]{1,2}[:][0-9]{1,2}";
Pattern pattern = compile(regex);
Matcher matcher = pattern.matcher(str);
while (matcher.find()) {
String group = matcher.group();
dateTimeStrList.add(group);
}
return dateTimeStrList;
} catch (Exception e) {
e.getMessage();
return null;
}
}


/**
* 截取字符串中的所有日期
*
* @param str
* @return
*/
public static List<String> dateSubAll(String str) {
try {
List<String> dateStrList = new ArrayList<>();
Pattern pattern = compile("[0-9]{4}[-][0-9]{1,2}[-][0-9]{1,2}");
Matcher matcher = pattern.matcher(str);
while (matcher.find()) {
String group = matcher.group();
dateStrList.add(group);
}
return dateStrList;
} catch (Exception e) {
e.getMessage();
return null;
}
}

/**
* 获取随机字符串
*
* @param length
* @return
*/
public static String getRandomString(int length) {
String base = "abcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
}
  • 3.6 后台声明测试的html页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
xml复制代码<!DOCTYPE HTML>
<html>
<head>
<title>WebSocket Chat Demo</title>
</head>

<body>
<input id="inputContent" type="text" style="width:600px;"/>
<button onclick="send()">Send</button>
<button onclick="closeConnection()">Close</button>
<div id="msg"></div>
</body>

<script type="text/javascript">

var websocket = null;

//声明自己搭建的websocket服务
if ('WebSocket' in window) {
var random = parseInt(Math.random() * 1000000) + "";
websocket = new WebSocket("ws://localhost:8005/chat/"+ random);
} else {
alert('Not support websocket')
}

//连接发生错误的回调方法
websocket.onerror = function() {
setMessageInnerHTML("error");
};

//连接成功建立的回调方法
websocket.onopen = function(event) {
//setMessageInnerHTML("open");
}

//接收到消息的回调方法
websocket.onmessage = function(event) {
setMessageInnerHTML(event.data);
}

//连接关闭的回调方法
websocket.onclose = function() {
setMessageInnerHTML("close");
}

//监听窗口关闭事件,当窗口关闭时关闭对应websocket连接
window.onbeforeunload = function() {
websocket.close();
}

//将消息回显在页面上
function setMessageInnerHTML(innerHTML) {
document.getElementById('msg').innerHTML += innerHTML + '<br/>';
}

//关闭连接
function closeConnection() {
websocket.close();
}

//发送消息
function send() {
var msg = document.getElementById('inputContent').value;
websocket.send(msg);
}
</script>
</html>

该类对应的路径如下:

微信图片_20210902214029.jpg

4.启动服务并测试

页面输入ip+端口建立websocket连接并发送一条消息,测试结果如图:

1.png

2.jpg

3.png
注意:

4.jpg

5.png

6.png

7.png

8.jpg

注意

  • 1.正常情况下,输入框中只输入要发送的实际聊天内容即可,比如“在吗老公,急事”,但是为了更容易测试,页面中输入的是拼接后的json消息体,接收者用户id,以及消息类型,实际开发中数据格式让前端处理即可,前端根据输入的内容拼接成如输入框图所示的数据格式即可
  • 2.messageType来区分单聊还是群聊,但是此处的群聊是建立连接的所有websocket服务,没有区分组概念,如果区分的话,后台接口请求路径中要添加上roomId参数,然后建立连接时将进入该聊天室的用户放入一个map集合中,群聊发送消息时,根据不同的roomId,只给该组的用户推送群聊消息即可
  • 3.此外,测试时也可以使用websocket在线测试 链接如下:websocket在线测试链接

文末福利

好了,今天的分享就到这里,如果对你有所帮助的话,记得给小编评论和点赞哦!对于优质评论内容,更有掘金精美礼品等你来领,我会抽取两名用户赠送随机徽章一份,还在等什么吗,赶快参与评论吧,精美礼品不容错过!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【福利,掘金周边】【Spring Boot 快速入门】十一、

发表于 2021-09-08

【福利,本文评论用户将有机会获得掘金新版徽章 1 枚】

前言

  XDM大家好,金秋时节,又与大家见面了。在8月份进行了满勤日更,已经“肝胆欲裂”了。8月底出来新的活动:一次免费申请掘金周边礼物的机会。抱着试试看的想法,提交了申请,争取为广大掘友申请福利。作者有幸获得了首批试行阶段的名额,感谢长期以来各位读者对小阿杰的认可,也感谢掘金提供本次机会,使作者与读者更好的互动。

  活动链接如下:请查收|你有一次免费申请掘金周边礼物的机会

  收到的系统站内信:

图片.png
  官网公布的名单。在收到申请周边活动成功之后的,也进行了充分的技术选题,在能输出技术知识的同时,又能充分调动读者的积极互动性。经过慎重思考之后,选择了写代码生成以及大家对代码生成器在项目中使用的一些思考,针对这些问题展开讨论。
图片.png
  好了开始正题,本次为大家介绍的代码生成器是MyBatis-Plus中的AutoGenerator。

初识 AutoGenerator。

  相信使用过的代码生成器小伙伴对此都感觉很爽,刷刷刷的基础代码就已经开发完成了。在Java开始过程中有一款经常使用的代码生成器AutoGenerator。AutoGenerator是MyBatis-Plus的代码生成器,通过AutoGenerator可以快速生成 Entity、Mapper、Mapper XML、Service、Controller等各个模块的代码,极大的提升了开发效率,减少了基础代码重复编写的工作。

快速开始

添加依赖

  MyBatis-Plus 从 3.0.3 之后移除了代码生成器与模板引擎的默认依赖,需要手动添加相关依赖:本文针对Java开发者,使用Maven引入依赖包信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.0</version>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.29</version>
</dependency>
<!-- mybatis-plus -->

基本配置

  在使用AutoGenerator代码生成器中,需要对相关参数进行配置,以便生成相关代码。主要配置包含:数据源配置、数据库表配置、包名配置、模板配置、全局策略配置、注入配置等。下面具体介绍各个配置的参数及其作用。

数据源配置

  数据源配置DataSourceConfig,其默认值:null,通过该配置,指定需要生成代码的具体数据库。

参数类型 描述 备注
dbQuery 数据库信息查询类 默认由 dbType 类型决定选择对应数据库内置实现
dbType 数据库类型 该类内置了常用的数据库类型【必须】
schemaName 数据库 schema name 例如 PostgreSQL 可指定为 public
typeConvert 类型转换 默认由 dbType 类型决定选择对应数据库内置实现
url 驱动连接的URL 数据库的链接地址
driverName 驱动名称 例如:com.mysql.cj.jdbc.Driver
username 数据库连接用户名 数据库连接用户名
password 数据库连接密码 数据库连接密码

数据库表配置

  数据库表配置StrategyConfig,其默认值:null,通过该配置,可指定需要生成哪些表或者排除哪些表。

参数类型 描述 备注与默认值
isCapitalMode 是否大写命名 false
skipView 是否跳过视图 false
naming 数据库表映射到实体的命名策略
columnNaming 数据库表字段映射到实体的命名策略, 未指定按照 naming 执行 null
tablePrefix 表前缀
fieldPrefix 字段前缀
superEntityClass 自定义继承的Entity类全称,带包名
superEntityColumns 自定义基础的Entity类,公共字段
superMapperClass 自定义继承的Mapper类全称,带包名 String SUPER_MAPPER_CLASS = “com.baomidou.mybatisplus.core.mapper.BaseMapper”;
superServiceClass 自定义继承的Service类全称,带包名 String SUPER_SERVICE_CLASS = “com.baomidou.mybatisplus.extension.service.IService”;
superServiceImplClass 自定义继承的ServiceImpl类全称,带包名
superControllerClass 自定义继承的Controller类全称,带包名
enableSqlFilter 默认激活进行sql模糊表名匹配,关闭之后likeTable与notLikeTable将失,include和exclude将使用内存过滤,如果有sql语法兼容性问题的话,请手动设置为false
include 需要包含的表名,当enableSqlFilter为false时,允许正则表达式(与exclude二选一配置 null
likeTable 自3.3.0起,模糊匹配表名(与notLikeTable二选一配置) likeTable
exclude 需要排除的表名,当enableSqlFilter为false时,允许正则表达式 null
notLikeTable 自3.3.0起,模糊排除表名 null
entityColumnConstant 【实体】是否生成字段常量(默认 false) false
chainMode 【实体】是否为构建者模型(默认 false)3.3.2开始 原来版本是 entityBuilderModel
entityLombokModel 【实体】是否为lombok模型(默认 false) 3.3.2以下版本默认生成了链式模型,3.3.2以后,
entityBooleanColumnRemoveIsPrefix Boolean类型字段是否移除is前缀(默认 false) false
restControllerStyle 生成 @RestController 控制器 false
controllerMappingHyphenStyle 驼峰转连字符 false
entityTableFieldAnnotationEnable 是否生成实体时,生成字段注解 false
versionFieldName 乐观锁属性名称
logicDeleteFieldName 逻辑删除属性名称 is_del
tableFillList 表填充字段

包名配置

  包名配置PackageConfig,其默认值:null,通过该配置,指定生成代码的包路径。

参数类型 描述 默认值
parent 父包名。如果为空,将下面子包名必须写全部, 否则就只需写子包名 com.baomidou
moduleName 父包模块名 null
entity Entity包名 entity
service Service包名 service
serviceImpl Service Impl包名 service.impl
mapper Mapper mapper
xml Mapper XML包名 mapper.xml
controller Controller包名 controller
pathInfo 路径配置信息

模板配置

  模板配置TemplateConfig,其默认值:null,可自定义代码生成的模板,实现个性化操作。

参数类型 描述 备注
entity Java 实体类模板 /templates/entity.java
entityKt Kotin 实体类模板 /templates/entity.kt
service Service 类模板 /templates/service.java
serviceImpl Service impl 实现类模板 /templates/serviceImpl.java
mapper mapper 模板 /templates/mapper.java
xml mapper xml 模板 /templates/mapper.xml
controller controller 控制器模板 /templates/controller.java

全局策略配置

  全局策略配置GlobalConfig,其默认值:null。

参数类型 描述 备注
outputDir 生成文件的输出目录 默认值:D 盘根目录
fileOverride 是否覆盖已有文件 默认值:false
open 是否打开输出目录 默认值:true
enableCache 是否在xml中添加二级缓存配置 默认值:`false
author 开发人员 默认值:null
kotlin 开启 Kotlin 模式 默认值:false
swagger2 开启 swagger2 模式 默认值:false
activeRecord 开启 ActiveRecord 模式 默认值:false
baseResultMap 开启 BaseResultMap 默认值:false
baseColumnList 开启 baseColumnList 默认值:false
dateType 时间类型对应策略 默认值:TIME_PACK
entityName 实体命名方式 默认值:null 例如:%sEntity 生成 UserEntity
mapperName mapper 命名方式 默认值:null 例如:%sDao 生成 UserDao
xmlName Mapper xml 命名方式 默认值:null 例如:%sDao 生成 UserDao.xml
serviceName service 命名方式 默认值:null 例如:%sBusiness 生成 UserBusiness
serviceImplName service impl 命名方式 默认值:null 例如:%sBusinessImpl 生成 UserBusinessImpl
controllerName controller 命名方式 默认值:null 例如:%sAction 生成 UserAction
idType 指定生成的主键的ID类型 默认值:null

注入配置

  注入配置InjectionConfig,其默认值:null,通过该配置,可注入自定义参数等操作以实现个性化操作。

参数类型 描述 备注
map 自定义返回配置 Map 对象 该对象可以传递到模板引擎通过 cfg.xxx 引用
fileOutConfigList 自定义输出文件 配置 FileOutConfig 指定模板文件、输出文件达到自定义文件生成目的
fileCreate 自定义判断是否创建文件 实现 IFileCreate 接口
initMap 注入自定义 Map 对象(注意需要setMap放进去)

  通过上述介绍的配置信息,MyBatis-Plus 的AutoGenerator代码生成器提供了大量的自定义参数,能够满足绝大部分人的使用需求。下面针对上述配置进行编写代码。其实就是针对DataSourceConfig、StrategyConfig、PackageConfig、TemplateConfig、GlobalConfig、InjectionConfig这些实体类赋值即可。

示例

设置常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
js复制代码 /**
* 需要生成的表名
* */
private static final String[] TABLE_NAMES = new String[]{"z_seo"};
/**
* 文件路径
* */
public static final String PROJECT_PATH = "E:\\bootproject\\BootDemo\\";
//
/**
* 项目名
* */
public static final String PROJECT_NAME = "18BootAutoGenerator";
/**
* 模块名称
* */
public static final String MODULE_NAME ="";

/**
* 数据源配置
* */
public static final String DATA_SOURCE_URL ="jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior" +
"=convertToNull";
public static final String DATA_SOURCE_USERNAME ="test";
public static final String DATA_SOURCE_PASSWORD ="123456";
public static final String DATA_SOURCE_DRIVERNAME ="com.mysql.cj.jdbc.Driver";

初始化代码生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码     /**
* @MethodName: main
* @Description: 代码生成器
* @Return:
* @Author: JavaZhan @公众号:Java全栈架构师
* @Date: 2021/9/8
**/
public static void main(String[] args) {
// 代码生成器
AutoGenerator autoGenerator = new AutoGenerator();
autoGenerator.setDataSource(getDataSourceConfigInfo());
autoGenerator.setGlobalConfig(getGlobalConfigInfo());
autoGenerator.setPackageInfo(getPackageConfigInfo());
autoGenerator.setStrategy(getStrategyConfigInfo(TABLE_NAMES));
autoGenerator.setTemplate(new TemplateConfig().setXml(null));
autoGenerator.setTemplateEngine(new FreemarkerTemplateEngine());
autoGenerator.setCfg(getInjectionConfigInfo());
autoGenerator.execute();
}

数据源配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
js复制代码     /**
* @MethodName: getDataSourceConfigInfo
* @Description: 数据源配置
* @Return: DataSourceConfig
* @Author: JavaZhan @公众号:Java全栈架构师
* @Date: 2021/9/8
**/
public static DataSourceConfig getDataSourceConfigInfo() {
DataSourceConfig dataSourceConfig = new DataSourceConfig();
dataSourceConfig.setUrl("jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull");
dataSourceConfig.setUsername("test");
dataSourceConfig.setPassword("123456");
dataSourceConfig.setDbType(DbType.MYSQL);
dataSourceConfig.setDriverName("com.mysql.cj.jdbc.Driver");
return dataSourceConfig;
}

全局配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
js复制代码     /**
* @MethodName: getGlobalConfigInfo
* @Description: 全局配置
* @Return: GlobalConfig
* @Author: JavaZhan @公众号:Java全栈架构师
* @Date: 2021/9/8
**/
public static GlobalConfig getGlobalConfigInfo() {
// 全局配置
GlobalConfig globalConfig = new GlobalConfig();
// String projectPath = System.getProperty("user.dir");
globalConfig.setOutputDir(PROJECT_PATH + PROJECT_NAME + "/src/main/java/");
globalConfig.setAuthor("JavaZhan");
globalConfig.setOpen(false);
globalConfig.setSwagger2(true);
globalConfig.setBaseColumnList(true);
globalConfig.setBaseResultMap(true);
globalConfig.setActiveRecord(false);
globalConfig.setFileOverride(true);
globalConfig.setServiceName("%sService");
return globalConfig;
}

包配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
js复制代码     /**
* @MethodName: getPackageConfigInfo
* @Description: 包配置
* @Return: PackageConfig
* @Author: JavaZhan @公众号:Java全栈架构师
* @Date: 2021/9/8
**/
public static PackageConfig getPackageConfigInfo() {
PackageConfig packageConfig = new PackageConfig();
packageConfig.setParent("com.example.demo");
packageConfig.setModuleName("test");
packageConfig.setEntity("module");
return packageConfig;
}

策略配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
js复制代码    /**
* @MethodName: getStrategyConfigInfo
* @Description: 策略配置
* @Return: StrategyConfig
* @Author: JavaZhan @公众号:Java全栈架构师
* @Date: 2021/9/8
**/
public static StrategyConfig getStrategyConfigInfo(String... tableNames) {
StrategyConfig strategyConfigInfo = new StrategyConfig();
strategyConfigInfo.setCapitalMode(true);
strategyConfigInfo.setNaming(NamingStrategy.underline_to_camel);
//下划线转驼峰命名
strategyConfigInfo.setColumnNaming(NamingStrategy.underline_to_camel);
//需要生成的的表名,多个表名传数组
strategyConfigInfo.setInclude(tableNames);
//设置逻辑删除字段
strategyConfigInfo.setLogicDeleteFieldName("data_state");
//使用lombok
strategyConfigInfo.setEntityLombokModel(true);
//设置表格前缀
strategyConfigInfo.setTablePrefix("");
//rest风格
strategyConfigInfo.setRestControllerStyle(true);

return strategyConfigInfo;
}

抽象的对外接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
js复制代码     /**
* @MethodName: getInjectionConfigInfo
* @Description: 抽象的对外接口
* @Return: InjectionConfig
* @Author: JavaZhan @公众号:Java全栈架构师
* @Date: 2021/9/8
**/
public static InjectionConfig getInjectionConfigInfo(){
InjectionConfig injectionConfig = new InjectionConfig() {
@Override
public void initMap() {
}
};
List<FileOutConfig> focList = new ArrayList<FileOutConfig>();
focList.add(new FileOutConfig("/templates/mapper.xml.ftl") {
@Override
public String outputFile(TableInfo tableInfo) {
// 输出xml
return PROJECT_PATH + PROJECT_NAME + "/src/main/resources/mapper/" + MODULE_NAME
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
injectionConfig.setFileOutConfigList(focList);
return injectionConfig;
}

执行

  在项目中执行初始化main方法,出现如下图中日志,包含module、service、impl、mapper、xml等相关文件。
图片.png

  如下图,生成的自动生成的文件相关文件目录结构,把我们需要的基本文件都已经生成了。
图片.png
  如下图中是我们生成的实体对象信息,其中包含了lombok和swagger2的相关注解,如果不需要这些,可以在GlobalConfig全局配置文件中进行配置。

图片.png
  好了,本文基于Spring Boot集成AutoGenerator代码生成器的相关功能已经介绍完了,并针对配置进行了Demo演示。大家可以根据项目中具体的需要,进行更加详实的参数配置,以满足个性化的需求。

思考(欢迎留言评论交流)

1、使用AutoGenerator代码生成器有哪些弊端?

2、大家还有哪些常用的代码生成器推荐,分享给大家?

3、关于使用代码生成器,在使用过程中都遇到过哪些奇葩的事情?

4、针对代码生成器你持什么态度?

5、在使用AutoGenerator代码生成器中遇到哪些问题,大家一起讨论学习。

欢迎大家积极交流评论。

福利

  本文参加的是:请查收|你有一次免费申请掘金周边礼物的机会 ,在所有评论用户中,将产生2位幸运的评论用户,将获得掘金官方提供的掘金新版徽章 2 枚。

评论区抽奖要求(官方要求):

  • 截止到 9月10日,如果评论区超过 10 人互动(不含作者本人),作者可以以自己的名义抽奖送出掘金新版徽章 2 枚(掘金官方承担)。
  • 截止到 9月10日,如果评论区超过 10 人互动(不含作者本人),评论数超过 20 条(含作者本人),作者本人额外获得一份掘金周边礼物。
  • 评论区热度最高(评论人数+条数综合数据)TOP 1-5:新版徽章1套 或 掘金新版IPT恤1件

获奖条件

  热评幸运用户
如果本文评论达到掘金活动的要求,将从热评区用户中抽取一位幸运读者赠送掘金新版徽章 1 枚。如无热评用户,将转为评论幸运用户。

  评论幸运用户
如果本文评论达到掘金活动的要求,将所有评论区用户中抽取一位幸运读者赠送掘金新版徽章 1 枚。

开奖规则

  将于9月11日(星期六下午|晚上开奖),随机方式抽取。

结语

  本次基于Spring Boot集成AutoGenerator代码生成器的项目就完成了,粗枝大叶的建立了一个快速代码生成的框架,当然还有更深入的配置参数去满足个性化的需求。本文主要针对新手入门练习使用,也作为基础查询手册使用,希望本文可以帮助到你。感谢阅读。

  作者介绍:【小阿杰】一个爱鼓捣的程序猿,JAVA开发者和爱好者。公众号【Java全栈架构师】维护者,欢迎关注阅读交流。

  好了,感谢您的阅读,希望您喜欢,如对您有帮助,欢迎点赞收藏。如有不足之处,欢迎评论指正。下次见。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

MySQL-SQL join(多表查询) 多表查询

发表于 2021-09-07

多表查询

多表查询的结果是一个笛卡尔乘积(一个集合中的每一条数据和另一个集合的每一条集合结合叫笛卡尔乘积)
详解SQL join

SQL join

image.png

学习准备

  1. 创建products表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sql复制代码CREATE TABLE `products` (

  `id` int NOT NULL AUTO_INCREMENT,

  `name` varchar(20) DEFAULT NULL,

  `price` int DEFAULT NULL,

  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,

  `modify_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  `brand_id` int DEFAULT NULL,

  PRIMARY KEY (`id`),

  KEY `brand_id` (`brand_id`),

  CONSTRAINT `products_ibfk_1` FOREIGN KEY (`brand_id`) REFERENCES `brand` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE

)
  1. 创建brand表
1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码CREATE TABLE `brand` (

  `id` int NOT NULL AUTO_INCREMENT,

  `name` varchar(20) DEFAULT NULL,

  `url` varchar(100) DEFAULT NULL,

  `modify_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

  PRIMARY KEY (`id`)

)

products表通过brand_id引用brand表

多表查询SQL学习-SQL join

left join

image.png

  1. 查询所有手机(包括没有品牌信息的手机)以及对应的品牌信息:
    SELECT * FROM products LEFT JOIN brand ON products.brand_id = brand.id;

image.png

  1. 查询没有对应品牌数据的产品:
    SELECT * FROM products LEFT JOIN brand ON products.brand_id = brand.id where brand.id IS NULL;

right join

image.png

  1. 查询所有的品牌以及品牌对应的产品信息:
    SELECT * FROM products RIGHT JOIN brand ON products.brand_id = brand.id;

image.png

  1. 查询没有对应产品的品牌信息
    SELECT * FROM products RIGHT JOIN brand ON products.brand_id = brand.id WHERE products.brand_id IS NULL;

inner join

image.png

  1. 查询产品信息并且该产品有对应的品牌信息:
    SELECT * FROM products JOIN brand ON products.brand_id = brand.id;

等价于:

SELECT * FROM products, brand WHERE products.brand_id = brand.id;

full outer join

MySQL不支持FULL JOIN关键字,full out join 相当于 left join union right join

image.png

  1. 查询产品及品牌信息
1
2
sql复制代码(SELECT * FROM products LEFT JOIN brand ON products.brand_id = brand.id) UNION
(SELECT * FROM products RIGHT JOIN brand ON products.brand_id = brand.id);

full outer join excluding inner join

1
2
sql复制代码(SELECT * FROM products LEFT JOIN brand ON products.brand_id = brand.id WHERE brand.id IS NULL) UNION
(SELECT * FROM products RIGHT JOIN brand ON products.brand_id = brand.id WHERE products.brand_id IS NULL);

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

配置Mybatis-plus分页插件,返回统一结果集 一、M

发表于 2021-09-07

一、MyBatisPlusConfig中配置分页插件

1
2
3
4
5
6
7
8
9
10
11
java复制代码   /**
* 配置分页插件
* @return page
*/
@Bean
public PaginationInterceptor paginationInterceptor(){
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 开启 count 的 join 优化,只针对部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
  1. 分页实现的原理

Mybatis-plus分页插件使用的是IPage进行分页。IPage内部原理是基于拦截器,拦截的是方法以及方法中的参数。判断是否是查询操作,如果是查询操作,才会进入分页的处理逻辑。 进入分页逻辑处理后,拦截器会通过反射获取该方法的参数进行判断是否存在IPage对象的实现类。如果不存在则不进行分页,存在则将该参数赋值给IPage对象,然后进行拼接sql的处理完成分页操作。

在这里插入图片描述

二、统一结果集

  1. 创建返回码定义类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码public class ResultCode {

/**
* 成功
**/
public final static int OK = 20000;
/**
* 失败
**/
public final static int ERROR = 20001;
/**
* 用户名或密码错误
**/
public final static int LOGIN_ERROR = 20002;
/**
* 权限不足
**/
public final static int ACCESS_ERROR = 20003;
/**
* 远程调用失败
**/
public final static int REMOTE_ERROR = 20004;
/**
* 重复操作
**/
public final static int REPEAT_ERROR = 20005;
}
  1. 创建结果集类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
java复制代码@Data
@ApiModel(value = "全局统一返回结果")
public class R implements Serializable {

public final static String OK_MSG = "请求成功";
public final static String FAIL_MSG = "请求失败";

@ApiModelProperty(value = "是否成功")
private boolean success;

@ApiModelProperty(value = "返回码")
private Integer code;

@ApiModelProperty(value = "返回消息")
private String message;

@ApiModelProperty(value = "返回数据")
private Object data;

@ApiModelProperty(value = "总条数")
private Long total;

@ApiModelProperty(value = "分页信息")
private PageInfo pageInfo;

@Data
public static class PageInfo {

@ApiModelProperty("当前页")
protected int currentPage;
@ApiModelProperty("页大小")
protected int pageSize;
@ApiModelProperty("总记录数")
protected long totalCount;
@ApiModelProperty("总页数")
protected long totalPage;

public PageInfo() {
}

@ConstructorProperties({"currentPage", "pageSize", "totalCount", "totalPage"})
public PageInfo(int currentPage, int pageSize, long totalCount, long totalPage) {
this.currentPage = currentPage;
this.pageSize = pageSize;
this.totalCount = totalCount;
this.totalPage = totalPage;
}
}

private R(){}

private R(int code, String msg, Object data) {
this.code = code;
this.message = msg;
if (data instanceof Page<?>) {
Page<?> page = (Page<?>) data;
this.total = page.getTotal();
this.data = page.getRecords();
this.pageInfo = new PageInfo((int)page.getCurrent(), (int)page.getSize(), page.getTotal(), page.getPages());
} else {
this.data = data;
}
}

public static R ok(){
R r = new R();
r.setSuccess(true);
r.setCode(ResultCode.OK);
r.setMessage("成功");
return r;
}
public static R ok(Object data) {
return new R(ResultCode.OK, OK_MSG, data);
}

public static R ok(String msg, Object data) {
return new R(ResultCode.OK, msg, data);
}

public static R error(){
R r = new R();
r.setSuccess(false);
r.setCode(ResultCode.ERROR);
r.setMessage("失败");
return r;
}

public static R error(String msg) {
return new R(ResultCode.ERROR, msg, null);
}

public static R error(int errorCode, String msg) {
return new R(errorCode, msg, null);
}


public R message(String message){
this.setMessage(message);
return this;
}

public R code(Integer code){
this.setCode(code);
return this;
}

public R data(Object data){
this.setData(data);
return this;
}

}

三、编写分页接口

  1. 先编写查询类

代码如下:

1
2
3
4
5
6
java复制代码@Data
public class MemberQueryVo extends BasePageEntity{

@ApiModelProperty(value = "用户名")
private String userName;
}
  1. service层

先定义一个查询分页的接口,在实现类里做相关处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Service
public class MemberServiceImpl extends ServiceImpl<MemberMapper, Member> implements MemberService {

@Override
public IPage<Member> listMemberPage(MemberQueryVo queryVo) {
IPage<Member> page = new Page<>(queryVo.getCurrentPage(),queryVo.getCurrentPage());
//条件查询
LambdaQueryWrapper<Member> queryWrapper = new LambdaQueryWrapper<Member>();

if (StringUtils.isNotBlank(queryVo.getUserName())) {
queryWrapper.like(Member::getUserName, queryVo.getUserName());
}

return baseMapper.selectPage(page,queryWrapper);
}
}
  1. controller层

编写分页接口,代码如下:

1
2
3
4
5
6
java复制代码    @ApiOperation(value = "分页用户列表")
@GetMapping(value = "/getPage")
public R listPage(MemberQueryVo queryVo){
IPage<Member> page = memberService.listMemberPage(queryVo);
return R.ok(page);
}
  1. 接口测试

直接通过swagger生成的api接口页面进行测试,当前页、每页参数传1时,返回的分页信息里,总数是两条,只返回了一条数据。说明分页成功。
在这里插入图片描述
再进行条件查询的时候,也成功查询对应数据。
在这里插入图片描述


总结

感谢大家的阅读,上就是今天要讲的内容,本文简单介绍了如何配置分页插件、以及分页的原理。如有不足之处,纯属能力有限,还请多多包涵。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…536537538…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%