简

人生短暂,学海无边,而大道至简。


  • 首页

  • 归档

  • 分类

  • 标签

windows docker访问dockerVM

发表于 2019-05-09 | 分类于 docker

先执行:

1
docker run --privileged -it -v /var/run/docker.sock:/var/run/docker.sock jongallant/ubuntu-docker-client

在执行:

1
docker run --net=host --ipc=host --uts=host --pid=host -it --security-opt=seccomp=unconfined --privileged --rm -v /:/host alpine /bin/sh

最后登陆到宿主host:

1
chroot /host

koa框架

发表于 2019-05-09 | 分类于 Node

一、Koa介绍

Koa是一个类似于Express的Web开发框架,创始人也是同一个人。它的主要特点是,使用了ES6的Generator函数,进行了架构的重新设计。也就是说,Koa的原理和内部结构很像Express,但是语法和内部结构进行了升级。

官方faq有这样一个问题:”为什么koa不是Express 4.0?“,回答是这样的:”Koa与Express有很大差异,整个设计都是不同的,所以如果将Express 0按照这种写法升级到4.0,就意味着重写整个程序。所以,我们觉得创造一个新的库,是更合适的做法。“

1、示例

安装好nodejs,创建项目文件,按照如下操作引入koa。

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
#初始化项目
D:\mydoc\NodeJs\koa-study-demo\Demo-01>npm init -y
Wrote to D:\mydoc\NodeJs\koa-study-demo\Demo-01\package.json:

{
"name": "Demo-01",
"version": "1.0.0",
"description": "",
"main": "my-koa-node.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
# 安装koa
D:\mydoc\NodeJs\koa-study-demo\Demo-01>npm install koa
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN Demo-01@1.0.0 No description
npm WARN Demo-01@1.0.0 No repository field.

+ koa@2.11.0
added 43 packages from 23 contributors and audited 55 packages in 8.114s
found 0 vulnerabilities

创建一个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
// 导入koa模块
const Koa = require('koa');
// 创建koa的实例app
const app = new Koa();

/*app.use(async ctx => {
ctx.body = "Hello World"
})*/
app.use(()=>{
this.body = 'Hello World';
});
// 监听端口
app.listen(3000, () => {
console.log("服务器已启动,http://localhost:3000");
})
/*
// 导入koa,和koa 1.x不同,在koa2中,我们导入的是一个class,因此用大写的Koa表示:
const Koa = require('koa');

// 创建一个Koa对象表示web app本身:
const app = new Koa();

// 对于任何请求,app将调用该异步函数处理请求:
app.use(async (ctx, next) => {
await next();
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Hello, koa2!</h1>';
});

// 在端口3000监听:
app.listen(3000);
console.log('app started at port 3000...');
*/

运行文件,浏览器即可访问。

app.use方法用于向middleware数组添加Generator函数。

listen方法指定监听端口,并启动当前应用。它实际上等同于下面的代码。

1
2
3
4
ar http = require('http');
var koa = require('koa');
var app = new koa();
http.createServer(app.callback()).listen(3000);

参数ctx是由koa传入的封装了request和response的变量,next是koa传入的将要处理的下一个异步函数,然后,设置response的Content-Type和内容。

由async标记的函数称为异步函数,在异步函数中,可以用await调用另一个异步函数,这两个关键字将在ES7中引入。

引入包方式:

命令 npm install koa@2.0.0;

项目的package.json中的 dependencies引入,在使用命令 npm install即会吧项目的包都安装好!

直接用命令node app.js在命令行启动程序,或者用npm start启动。npm start命令会让npm执行定义在package.json文件中的start对应命令:

1
2
3
"scripts": {
"start": "node app.js"
}

2、级联

在上述的代码中,node每收到一个http请求,koa就会调用通过app.use()注册的async函数,并传入ctx和next参数。

这里的await next()的作用是:处理函数的执行顺序。

koa把很多中间件组成一个处理链,每个async函数做自己任务,然后用await next()来调用下一个async函数。

就是停止该中间件,继续执行下一个符合请求的中间件(‘downstrem’),然后控制权再逐级返回给上层中间件(‘upstream’)。

例如,4个中间件组成处理链,依次请求,打印日志,记录处理时间,输出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
const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
console.log("请求数据");
await next();
});

app.use(async (ctx, next) => {
console.log(`${ctx.request.method} ${ctx.request.url}`);
await next();
});

app.use(async (ctx, next) => {
const start = new Date().getTime();
await next();
const ms = new Date().getTime() - start;
console.log(`Time: ${ms}ms`);
});

app.use(async (ctx, next) => {
await next();
ctx.response.type = 'text/html';
ctx.response.body = '<h1>Hello, koa!</h1>';
});

app.listen(3000, () => {
console.log("服务器已启动,http://localhost:3000");
})

运行后访问,后台打印出结果。

1
2
3
4
5
6
请求数据
GET /
Time: 0ms
请求数据
GET /favicon.ico
Time: 0ms

如果一个没有调用await next(),后续的代码将不再执行了。

3、配置

应用配置是 app 实例属性,目前支持的配置项如下:

1
2
3
app.env 默认为 NODE_ENV or "development"
app.proxy 如果为 true,则解析 "Host" 的 header 域,并支持 X-Forwarded-Host
app.subdomainOffset 默认为2,表示 .subdomains 所忽略的字符偏移量。

app.listen(…)

Koa 应用并非是一个 1-to-1 表征关系的 HTTP 服务器。 一个或多个Koa应用可以被挂载到一起组成一个包含单一 HTTP 服务器的大型应用群。

如下为一个绑定3000端口的简单 Koa 应用,其创建并返回了一个 HTTP 服务器,为 Server#listen() 传递指定参数(参数的详细文档请查看nodejs.org)。

1
2
3
const Koa = require('koa');
const app = new Koa();
app.listen(3000);

同时支持 HTTPS 和 HTTPS,或者在多个端口监听同一个应用。

1
2
3
4
5
6
const http = require('http');
const https = require('https');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
https.createServer(app.callback()).listen(3001);

app.callback()

返回一个适合 http.createServer() 方法的回调函数用来处理请求。

app.use(function)

为应用添加指定的中间件,详情请看 Middleware

app.keys=

设置签名cookie密钥。

该密钥会被传递给KeyGrip, 当然,您也可以自己生成 KeyGrip. 例如:

1
2
app.keys = ['im a newer secret', 'i like turtle'];
app.keys = new KeyGrip(['im a newer secret', 'i like turtle'], 'sha256');

在进行cookie签名时,只有设置 signed 为 true 的时候,才会使用密钥进行加密:

1
ctx.cookies.set('name', 'tobi', { signed: true });

app.context

app.context是从中创建ctx的原型。 可以通过编辑app.context向ctx添加其他属性。当需要将ctx添加到整个应用程序中使用的属性或方法时,这将会非常有用。这可能会更加有效(不需要中间件)和/或更简单(更少的require()),而不必担心更多的依赖于ctx,这可以被看作是一种反向模式。

例如,从ctx中添加对数据库的引用:

1
2
3
4
5
app.context.db = db();

app.use(async ctx => {
console.log(ctx.db);
});

注:ctx上的很多属性是被限制的,在app.context只能通过使用Object.defineProperty()来编辑这些属性(不推荐)。可以在 https://github.com/koajs/koa/issues/652上查阅
已安装的APP沿用父级的ctx和配置。因此,安装的应用程序只是一组中间件。

4、错误处理

默认情况下Koa会将所有错误信息输出到 stderr, 除非 app.silent 是true.当err.status是404或者err.expose时,默认错误处理程序也不会输出错误。要执行自定义错误处理逻辑,如集中式日志记录,您可以添加一个”错误”事件侦听器:

1
2
3
app.on('error', err => {
log.error('server error', err)
});

如果错误发生在 请求/响应 环节,并且其不能够响应客户端时,Contenxt 实例也会被传递到 error 事件监听器的回调函数里。

1
2
3
app.on('error', (err, ctx) => {
log.error('server error', err, ctx)
});

当发生错误但仍能够响应客户端时(比如没有数据写到socket中),Koa会返回一个500错误(Internal Server Error)。 无论哪种情况,Koa都会生成一个应用级别的错误信息,以便实现日志记录等目的。

二、Context(上下文)

Koa Context 将 node 的 request 和 response 对象封装在一个单独的对象里面,其为编写 web 应用和 API 提供了很多有用的方法。

context 在每个 request 请求中被创建,在中间件中作为接收器(receiver)来引用,或者通过 this 标识符来引用:

1
2
3
4
5
app.use(async ctx => {
ctx; // is the Context
ctx.request; // is a koa Request
ctx.response; // is a koa Response
});

许多 context 的访问器和方法为了便于访问和调用,简单的委托给他们的 ctx.request 和 ctx.response 所对应的等价方法, 比如说 ctx.type 和 ctx.length 代理了 response 对象中对应的方法,ctx.path 和 ctx.method 代理了 request 对象中对应的方法。

具体API见https://www.koajs.com.cn/

三、Request和Response

Koa Request 对象是对 node 的 request、response进一步抽象和封装,提供了日常 HTTP 服务器开发中一些有用的功能。

详细api见https://www.koajs.com.cn/

四、处理URL

通常情况下需要对不同的URL调用不同的处理函数,这样才能返回不同的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.use(async (ctx, next) => {
if (ctx.request.path === '/') {
ctx.response.body = 'index page';
} else {
await next();
}
});

app.use(async (ctx, next) => {
if (ctx.request.path === '/test') {
ctx.response.body = 'TEST page';
} else {
await next();
}
});

但是我们可以借助中间件来处理不同的URL调用不同的处理函数。

koa-router

koa-router这个middleware负责处理URL映射。

在中增加,之后执行npm install执行安装模块。

创建文件,写入代码

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
const Koa = require('koa');
// 注意require('koa-router')返回的是函数:相当于
/*const fn_router = require('koa-router');
const router = fn_router();
*/
const router = require('koa-router')();

const app = new Koa();

app.use(async (ctx, next) => {
console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
await next();
});

// add url-route:
router.get('/hello/:name', async (ctx, next) => {
var name = ctx.params.name;
ctx.response.body = `<h1>Hello, ${name}!</h1>`;
});

router.get('/', async (ctx, next) => {
ctx.response.body = '<h1>Index</h1>';
});

// add router middleware:
app.use(router.routes());

app.listen(3000);
console.log('app started at port 3000...');

处理post请求

router.get(‘/path’, async fn)处理的是get请求;router.post(‘/path’, async fn)处理post请求。

post请求通常会发送一个表单,或者JSON,它作为request的body发送,但无论是Node.js提供的原始request对象,还是koa提供的request对象,都不提供解析request的body的功能!所以,需要引入另一个middleware来解析原始request请求,然后,把解析后的参数,绑定到ctx.request.body中。

步骤:

1
2
3
1、package.json中添加依赖项:"koa-bodyparser": "3.2.0"然后使用npm install安装。
2、修改app.js,引入koa-bodyparser:const bodyParser = require('koa-bodyparser');在合适的位置加上:app.use(bodyParser());
3、因为middleware有顺序,这个koa-bodyparser必须在router之前被注册到app对象上。
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
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const router = require('koa-router')();
const app = new Koa();

app.use(bodyParser());

app.use(async (ctx, next) => {
console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
await next();
});
router.get('/', async (ctx, next) => {
ctx.response.body = `<h1>Index</h1>
<form action="/admin" method="post">
<p>Name: <input name="name" value="admin"></p>
<p>Password: <input name="password" type="password"></p>
<p><input type="submit" value="Submit"></p>
</form>`;
});

router.post('/admin', async (ctx, next) => {
var name = ctx.request.body.name || '',
password = ctx.request.body.password || '';
console.log(`admin with name: ${name}, password: ${password}`);
if (name === 'admin' && password === '111111') {
ctx.response.body = `<h1>Welcome, ${name}!</h1>`;
} else {
ctx.response.body = `<h1>Login failed!</h1>
<p><a href="/">Try again</a></p>`;
}
});
app.use(router.routes());
app.listen(3000);
console.log('app started at port 3000...');

在之前的示例中都是全部写在app.js里,每加一个URL,就需要修改app.js。随着URL越来越多,app.js就会越来越长。如果把URL处理函数集中分类写到其他js文件,让app.js自动导入所有处理URL的函数,代码一分离比较清楚:

新建目录结构:

把路由信息分别写在不同的文件:

hello.js

1
2
3
4
5
6
7
8
var fn_hello = async (ctx, next) => {
var name = ctx.params.name;
ctx.response.body = `<h1>Hello, ${name}!</h1>`;
};

module.exports = {
'GET /hello/:name': fn_hello
};

index.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

var fn_index = async (ctx, next) => {
ctx.response.body = `<h1>Index</h1>
<form action="/admin" method="post">
<p>Name: <input name="name" value="admin"></p>
<p>Password: <input name="password" type="password"></p>
<p><input type="submit" value="Submit"></p>
</form>`;
};

var fn_admin = async (ctx, next) => {
var name = ctx.request.body.name || '',
password = ctx.request.body.password || '';
console.log(`admin with name: ${name}, password: ${password}`);
if (name === 'admin' && password === '11111') {
ctx.response.body = `<h1>Welcome, ${name}!</h1>`;
} else {
ctx.response.body = `<h1>Login failed!</h1>
<p><a href="/">Try again</a></p>`;
}
};

module.exports = {
'GET /': fn_index,
'POST /admin': fn_admin
};

app.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
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const router = require('koa-router')();
const app = new Koa();

app.use(async (ctx, next) => {
console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
await next();
});
app.use(bodyParser());

// 先导入fs模块,然后用readdirSync列出文件
const fs = require('fs');
// 这里可以用sync是因为启动时只运行一次,不存在性能问题:
var files = fs.readdirSync(__dirname + '/controllers');

// 过滤出.js文件:
var js_files = files.filter((f)=>{
return f.endsWith('.js');
});

// 处理每个js文件:
for (var f of js_files) {
// 处理每个js文件
for (var url in mapping) {
if (url.startsWith('GET ')) {
// 如果url类似"GET xxx":
var path = url.substring(4);
router.get(path, mapping[url]);
} else if (url.startsWith('POST ')) {
// 如果url类似"POST xxx":
var path = url.substring(5);
router.post(path, mapping[url]);
} else {
// 无效的URL:
console.log(`invalid URL: ${url}`);
}
}
}

app.use(router.routes());
app.listen(3000);
console.log('app started at port 3000...');

启动后测试没问题,这里app.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
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const router = require('koa-router')();
const app = new Koa();
const fs = require('fs');

app.use(async (ctx, next) => {
console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
await next();
});

function addMapping(router, mapping) {
for (var url in mapping) {
if (url.startsWith('GET ')) {
var path = url.substring(4);
router.get(path, mapping[url]);
} else if (url.startsWith('POST ')) {
var path = url.substring(5);
router.post(path, mapping[url]);
} else {
console.log(`invalid URL: ${url}`);
}
}
}

function addControllers(router) {
var files = fs.readdirSync(__dirname + '/controllers');
var js_files = files.filter((f) => {
return f.endsWith('.js');
});

for (var f of js_files) {
let mapping = require(__dirname + '/controllers/' + f);
addMapping(router, mapping);
}
}
addControllers(router);

app.use(bodyParser());
app.use(router.routes());
app.listen(3000);
console.log('app started at port 3000...');

Controller Middleware

我们把扫描controllers目录和创建router的代码从app.js中提取出来,作为一个简单的middleware使用,命名为controller.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
const fs = require('fs');
function addMapping(router, mapping) {
for (var url in mapping) {
if (url.startsWith('GET ')) {
var path = url.substring(4);
router.get(path, mapping[url]);
console.log(`register URL mapping: GET ${path}`);
} else if (url.startsWith('POST ')) {
var path = url.substring(5);
router.post(path, mapping[url]);
console.log(`register URL mapping: POST ${path}`);
} else if (url.startsWith('PUT ')) {
var path = url.substring(4);
router.put(path, mapping[url]);
console.log(`register URL mapping: PUT ${path}`);
} else if (url.startsWith('DELETE ')) {
var path = url.substring(7);
router.del(path, mapping[url]);
console.log(`register URL mapping: DELETE ${path}`);
} else {
console.log(`invalid URL: ${url}`);
}
}
}

function addControllers(router, dir) {
fs.readdirSync(__dirname + '/' + dir).filter((f) => {
return f.endsWith('.js');
}).forEach((f) => {
console.log(`process controller: ${f}...`);
let mapping = require(__dirname + '/' + dir + '/' + f);
addMapping(router, mapping);
});
}

module.exports = function (dir) {
let controllers_dir = dir || 'controllers',
router = require('koa-router')();
addControllers(router, controllers_dir);
return router.routes();
};

app中简化为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const controller = require('./controller');
const app = new Koa();

app.use(async (ctx, next) => {
console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
await next();
});

app.use(bodyParser());

// add controllers:
app.use(controller());

app.listen(3000);
console.log('app started at port 3000...');

经过重新整理后的代码具有非常好的模块化,所有处理URL的函数按功能组存放在controllers目录,只需要不断往这个目录下加东西就可以了,app.js保持不变。

五、Nunjucks

模板引擎

模板引擎就是基于模板配合数据构造出字符串输出的一个组件。比如下面的函数就是一个模板引擎:

1
2
3
function examResult (data) {
return `${data.name}同学一年级期末考试语文${data.chinese}分,数学${data.math}分,位于年级第${data.ranking}名。`
}

如果我们输入数据如下:

1
2
3
4
5
6
examResult({
name: '小明',
chinese: 78,
math: 87,
ranking: 999
});

该模板引擎把模板字符串里面对应的变量替换以后,就可以得到以下输出:

小明同学一年级期末考试语文78分,数学87分,位于年级第999名。

模板引擎最常见的输出就是输出网页,也就是HTML文本。当然,也可以输出任意格式的文本,比如Text,XML,Markdown等等。

有同学要问了:既然JavaScript的模板字符串可以实现模板功能,那为什么我们还需要另外的模板引擎?

因为JavaScript的模板字符串必须写在JavaScript代码中,要想写出新浪首页这样复杂的页面,是非常困难的。

输出HTML有几个特别重要的问题需要考虑:

转义

对特殊字符要转义,避免受到XSS攻击。比如,如果变量name的值不是小明,而是小明,模板引擎输出的HTML到了浏览器,就会自动执行恶意JavaScript代码。

格式化

对不同类型的变量要格式化,比如,货币需要变成12,345.00这样的格式,日期需要变成2016-01-01这样的格式。

简单逻辑

模板还需要能执行一些简单逻辑,比如,要按条件输出内容,需要if实现如下输出:

1
2
3
4
5
6
7
8
{{ name }}同学,
{% if score >= 90 %}
成绩优秀,应该奖励
{% elif score >=60 %}
成绩良好,继续努力
{% else %}
不及格,建议回家打屁股
{% endif %}

所以,我们需要一个功能强大的模板引擎,来完成页面输出的功能。

Nunjucks

Nunjucks是Mozilla开发的一个纯JavaScript编写的模板引擎,既可以用在Node环境下,又可以运行在浏览器端。但是,主要还是运行在Node环境下,因为浏览器端有更好的模板解决方案,例如MVVM框架。

如果你使用过Python的模板引擎jinja2,那么使用Nunjucks就非常简单,两者的语法几乎是一模一样的,因为Nunjucks就是用JavaScript重新实现了jinjia2。

从上面的例子我们可以看到,虽然模板引擎内部可能非常复杂,但是使用一个模板引擎是非常简单的,因为本质上我们只需要构造这样一个函数:

1
2
3
function render(view, model) {
// TODO:...
}

其中,view是模板的名称(又称为视图),因为可能存在多个模板,需要选择其中一个。model就是数据,在JavaScript中,它就是一个简单的Object。render函数返回一个字符串,就是模板的输出。

下面我们来使用Nunjucks这个模板引擎来编写几个HTML模板,并且用实际数据来渲染模板并获得最终的HTML输出。

创建项目:

package.json中引入:”nunjucks”: “2.4.2”

模板引擎是可以独立使用的,并不需要依赖koa。

在app.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
const nunjucks = require('nunjucks');

function createEnv(path, opts) {
var autoescape = opts.autoescape === undefined ? true : opts.autoescape,
noCache = opts.noCache || false,
watch = opts.watch || false,
throwOnUndefined = opts.throwOnUndefined || false,
env = new nunjucks.Environment(
new nunjucks.FileSystemLoader('views', {
noCache: noCache,
watch: watch,
}), {
autoescape: autoescape,
throwOnUndefined: throwOnUndefined
});
if (opts.filters) {
for (var f in opts.filters) {
env.addFilter(f, opts.filters[f]);
}
}
return env;
}
//变量env就表示Nunjucks模板引擎对象,它有一个render(view, model)方法,正好传入view和model两个参数,并返回字符串。
var env = createEnv('views', {
watch: true,
filters: {
hex: function (n) {
return '0x' + n.toString(16);
}
}
});

创建env需要的参数可以查看文档获知。我们用autoescape = opts.autoescape && true这样的代码给每个参数加上默认值,最后使用new nunjucks.FileSystemLoader(‘views’)创建一个文件系统加载器,从views目录读取模板。

我们编写一个hello.html模板文件,放到views目录下,内容如下:

Hello

然后,我们就可以用下面的代码来渲染这个模板:

1
2
var s = env.render('hello.html', { name: '小明' });
console.log(s);

运行获得输出如下:

1
2
"D:\Program Files\nodejs\node.exe" D:\mydoc\NodeJs\koa-study-demo\Demo-03\app.js
<h1>Hello 小明</h1>

咋一看,这和使用JavaScript模板字符串没啥区别嘛。不过,试试:

1
2
var s = env.render('hello.html', { name: '<script>alert("小明")</script>' });
console.log(s);

获得输出如下:

1
<h1>Hello <script>alert("小明")</script></h1>

这样就避免了输出恶意脚本。

此外,可以使用Nunjucks提供的功能强大的tag,编写条件判断、循环等功能,例如:

1
2
3
4
5
6
7
<!-- 循环输出名字 -->
<body>
<h3>Fruits List</h3>
{% for f in fruits %}
<p>{{ f }}</p>
{% endfor %}
</body>

Nunjucks模板引擎最强大的功能在于模板的继承。

仔细观察各种网站可以发现,网站的结构实际上是类似的,头部、尾部都是固定格式,只有中间页面部分内容不同。如果每个模板都重复头尾,一旦要修改头部或尾部,那就需要改动所有模板。

更好的方式是使用继承。先定义一个基本的网页框架base.html:

1
2
3
4
5
6
7
8
9
10
11
12
<html>
<body>
{% block header %}
<h3>Unnamed</h3>
{% endblock %}
{% block body %}
<div>No body</div>
{% endblock %}
{% block footer %}
<div>copyright</div>
{% endblock %}
</body>

base.html定义了三个可编辑的块,分别命名为header、body和footer。子模板可以有选择地对块进行重新定义:

1
2
3
{% extends 'base.html' %}
{% block header %}<h1>{{ header }}</h1>{% endblock %}
{% block body %}<p>{{ body }}</p>{% endblock %}

然后,我们对子模板进行渲染:

1
2


输出HTML如下:

1
2
3
4
5
6
7
8
<html>
<body>
<h1>Hello</h1>
<p>哈哈哈哈...</p>

<div>copyright</div>

</body>

性能

对于模板渲染本身来说,速度是非常非常快的,因为就是拼字符串嘛,纯CPU操作。

性能问题主要出现在从文件读取模板内容这一步。这是一个IO操作,在Node.js环境中,我们知道,单线程的JavaScript最不能忍受的就是同步IO,但Nunjucks默认就使用同步IO读取模板文件。

但是Nunjucks会缓存已读取的文件内容,也就是说,模板文件最多读取一次,就会放在内存中,后面的请求是不会再次读取文件的,只要我们指定了noCache: false这个参数。

在开发环境下,可以关闭cache,这样每次重新加载模板,便于实时修改模板。在生产环境下,一定要打开cache,这样就不会有性能问题。

Nunjucks也提供了异步读取的方式,但是这样写起来很麻烦,有简单的写法我们就不会考虑复杂的写法。保持代码简单是可维护性的关键。

实现简单编写View实现

处理静态文件

我们把所有静态资源文件全部放入/static目录,目的就是能统一处理静态文件。在koa中,我们需要编写一个middleware,处理以/static/开头的URL。

编写middleware

处理静态文件的middleware,static-files.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
const path = require('path');
const mime = require('mime');
const fs = require('mz/fs');

// url: 类似 '/static/'
// dir: 类似 __dirname + '/static'
function staticFiles(url, dir) {
return async (ctx, next) => {
let rpath = ctx.request.path;
// 判断是否以指定的url开头:
if (rpath.startsWith(url)) {
// 获取文件完整路径:
let fp = path.join(dir, rpath.substring(url.length));
// 判断文件是否存在:
if (await fs.exists(fp)) {
// 查找文件的mime:
ctx.response.type = mime.lookup(rpath);
// 读取文件内容并赋值给response.body:
ctx.response.body = await fs.readFile(fp);
} else {
// 文件不存在:
ctx.response.status = 404;
}
} else {
// 不是指定前缀的URL,继续处理下一个middleware:
await next();
}
};
}

module.exports = staticFiles;

staticFiles是一个普通函数,它接收两个参数:URL前缀和一个目录,然后返回一个async函数。这个async函数会判断当前的URL是否以指定前缀开头,如果是,就把URL的路径视为文件,并发送文件内容。如果不是,这个async函数就不做任何事情,而是简单地调用await next()让下一个middleware去处理请求。

mz提供的API和Node.js的fs模块完全相同,但fs模块使用回调,而mz封装了fs对应的函数,并改为Promise。这样,我们就可以非常简单的用await调用mz的函数,而不需要任何回调。

所有的第三方包都可以通过npm官网搜索并查看其文档:https://www.npmjs.com/

最后,这个middleware使用起来也很简单,在app.js里加一行代码:

1
2
let staticFiles = require('./static-files');
app.use(staticFiles('/static/', __dirname + '/static'));

注意:也可以去npm搜索能用于koa2的处理静态文件的包并直接使用。

集成Nunjucks

集成Nunjucks实际上也是编写一个middleware,这个middleware的作用是给ctx对象绑定一个render(view, model)的方法,这样,后面的Controller就可以调用这个方法来渲染模板了。

我们创建一个templating.js来实现这个middleware:

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
const nunjucks = require('nunjucks');

function createEnv(path, opts) {
var autoescape = opts.autoescape === undefined ? true : opts.autoescape,
noCache = opts.noCache || false,
watch = opts.watch || false,
throwOnUndefined = opts.throwOnUndefined || false,
env = new nunjucks.Environment(
new nunjucks.FileSystemLoader(path || 'views', {
noCache: noCache,
watch: watch,
}), {
autoescape: autoescape,
throwOnUndefined: throwOnUndefined
});
if (opts.filters) {
for (var f in opts.filters) {
env.addFilter(f, opts.filters[f]);
}
}
return env;
}

function templating(path, opts) {
// 创建Nunjucks的env对象:
var env = createEnv(path, opts);
return async (ctx, next) => {
// 给ctx绑定render函数:
ctx.render = function (view, model) {
// 把render后的内容赋值给response.body:
ctx.response.body = env.render(view, Object.assign({}, ctx.state || {}, model || {}));
// 设置Content-Type:
ctx.response.type = 'text/html';
};
// 继续处理请求:
await next();
};
}

module.exports = templating;

在这个middleware中,主要的是templating,给ctx绑定render函数,就继续调用下一个middleware。

使用的时候,我们在app.js添加如下代码:

1
2
3
4
5
6
const isProduction = process.env.NODE_ENV === 'production';

app.use(templating('views', {
noCache: !isProduction,
watch: !isProduction
}));

isProduction,它判断当前环境是否是production环境。如果是,就使用缓存,如果不是,就关闭缓存。在开发环境下,关闭缓存后,我们修改View,可以直接刷新浏览器看到效果,否则,每次修改都必须重启Node程序,会极大地降低开发效率。

全局变量process中定义了一个环境变量env.NODE_ENV,开发的时候,环境变量应该设置为’development’,而部署到服务器时,环境变量应该设置为’production’。

注意:生产环境上必须配置环境变量NODE_ENV = ‘production’,而开发环境不需要配置,实际上NODE_ENV可能是undefined,所以判断的时候,不要用NODE_ENV === ‘development’。

类似的,我们在使用上面编写的处理静态文件的middleware时,也可以根据环境变量判断:

1
2
3
4
if (! isProduction) {
let staticFiles = require('./static-files');
app.use(staticFiles('/static/', __dirname + '/static'));
}

在生产环境下,静态文件是由部署在最前面的反向代理服务器(如Nginx)处理的,Node程序不需要处理静态文件。而在开发环境下,我们希望koa能顺带处理静态文件,否则,就必须手动配置一个反向代理服务器,这样会导致开发环境非常复杂。

编写View
运行

再检查一下app.js里的middleware的顺序:

第一个middleware是记录URL以及页面执行时间:

1
2
3
4
5
6
7
8
9
app.use(async (ctx, next) => {
console.log(`Process ${ctx.request.method} ${ctx.request.url}...`);
var
start = new Date().getTime(),
execTime;
await next();
execTime = new Date().getTime() - start;
ctx.response.set('X-Response-Time', `${execTime}ms`);
});

第二个middleware处理静态文件:

1
2
3
4
if (! isProduction) {
let staticFiles = require('./static-files');
app.use(staticFiles('/static/', __dirname + '/static'));
}

第三个middleware解析POST请求:

1
app.use(bodyParser());

第四个middleware负责给ctx加上render()来使用Nunjucks:

1
2
3
4
app.use(templating('view', {
noCache: !isProduction,
watch: !isProduction
}));

最后一个middleware处理URL路由:

1
app.use(controller());

现在,在VS Code中运行代码,不出意外的话,在浏览器输入localhost:3000/,可以看到首页内容:

koa-index

直接在首页登录,如果输入正确的Email和Password,进入登录成功的页面:

koa-admin-ok

如果输入的Email和Password不正确,进入登录失败的页面:

koa-admin-failed

怎么判断正确的name和Password?目前我们在admin.js中是这么判断的:

1
2
3
if (name === 'admin' && password === '111111') {
...
}
扩展

注意到ctx.render内部渲染模板时,Model对象并不是传入的model变量,而是:

Object.assign({}, ctx.state || {}, model || {})

首先,model || {}确保了即使传入undefined,model也会变为默认值{}。Object.assign()会把除第一个参数外的其他参数的所有属性复制到第一个参数中。第二个参数是ctx.state || {},这个目的是为了能把一些公共的变量放入ctx.state并传给View。

例如,某个middleware负责检查用户权限,它可以把当前用户放入ctx.state中:

1
2
3
4
5
6
7
8
9
app.use(async (ctx, next) => {
var user = tryGetUserFromCookie(ctx.request);
if (user) {
ctx.state.user = user;
await next();
} else {
ctx.response.status = 403;
}
});

这样就没有必要在每个Controller的async函数中都把user变量放入model中。

docker从容器里面拷文件到宿主机或从宿主机拷文件到docker容器里面

发表于 2019-05-08 | 分类于 docker

docker从容器里面拷文件到宿主机或从宿主机拷文件到docker容器里面

1、从容器里面拷文件到宿主机

docker cp 容器名:要拷贝的文件在容器里面的路径       要拷贝到宿主机的相应路径 

docker cp ae46b6f81c6a:/usr/share/elasticsearch/plugins/ ik

2、从宿主机拷文件到容器里面

          docker cp 要拷贝的文件路径 容器名:要拷贝到容器里面对应的路径

docker cp ik ae46b6f81c6a:/usr/share/elasticsearch/plugins/

synchronized原理分析

发表于 2019-05-03 | 分类于 java

Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:

  • 原子性:确保线程互斥的访问同步代码;
  • 可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
  • 有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;

Synchronized总共有三种用法:

Synchronized修饰静态方法,对类对象进行加锁,是类锁。

Synchronized修饰实例方法,对方法所属对象进行加锁,是对象锁。

Synchronized修饰代码块时,对一段代码块进行加锁,是对象锁。

注意,synchronized 内置锁 是一种 对象锁(锁的是对象而非引用变量),作用粒度是对象 ,可以用来实现对 临界资源的同步互斥访问 ,是可重入 的。其可重入最大的作用是避免死锁,如:

子类同步方法调用了父类同步方法,如没有可重入的特性,则会发生死锁;

Synchronized底层实现原理

当一个线程访问同步代码块时,首先是需要得到锁才能执行同步代码,当退出或者抛出异常时必须要释放锁,那么它是如何来实现这个机制的呢?

在 Java 中,每个对象都会有一个 monitor 对象,监视器。
1)某一线程占有这个对象的时候,先monitor 的计数器是不是0,如果是0还没有线程占有,这个时候线程占有这个对象,并且对这个对象的monitor+1;如果不为0,表示这个线程已经被其他线程占有,这个线程等待。当线程释放占有权的时候,monitor-1;
2)同一线程可以对同一对象进行多次加锁,+1,+1,重入性

1、线程堆栈分析(互斥)

可以通过Jconsole工具看对象的堆栈情况,

还有就是通过 JVM指令分析

Javap -V 反编译

20200503173621

monitorenter: 每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者;
如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;
如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权;

monitorexit: 执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

monitorexit指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁;

Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

对方法的加锁

ACC_SYNCHRONIZED

20200503173949

方法的同步并没有通过指令 monitorenter 和 monitorexit 来完成(理论上其实也可以通过这两条指令来实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的:

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切换,对性能有较大影响。

同步原理

锁是加在对象上的,无论是类对象还是实例对象。每个对象主要由一个对象头、实例变量、填充数据三部分组成,结构如图:

20200503174148

Synchronized用的锁就是存在Java对象头里的,那么什么是Java对象头呢?Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。其中 Class Pointer是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。,其结构说明如下:

20200503174437

Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构:

20200503174636

由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:

20200503174737

monitor

Synchronized属于结构中的重量级锁,锁标识位为10,其中指针指向的是monitor对象的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是 基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。

MonitorEnter指令:插入在同步代码块的开始位置,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁;
MonitorExit指令:插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit;

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:

首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);

对象头中Mark Word与线程中Lock Record

在线程进入同步代码块的时候,如果此同步对象没有被锁定,即它的锁标志位是01,则虚拟机首先在当前线程的栈中创建我们称之为“锁记录(Lock Record)”的空间,用于存储锁对象的Mark Word的拷贝,官方把这个拷贝称为Displaced Mark Word。整个Mark Word及其拷贝至关重要。

Lock Record是线程私有的数据结构,每一个线程都有一个可用Lock Record列表,同时还有一个全局的可用列表。每一个被锁住的对象Mark Word都会和一个Lock Record关联(对象头的MarkWord中的Lock Word指向Lock Record的起始地址),同时Lock Record中有一个Owner字段存放拥有该锁的线程的唯一标识(或者object mark word),表示该锁被这个线程占用。如下图所示为Lock Record的内部结构:

Lock Record 描述
Owner 初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
EntryQ 关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程;
RcThis 表示blocked或waiting在该monitor record上的所有线程的个数;
Nest 用来实现 重入锁的计数;
HashCode 保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate 用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

监视器Monitor有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。

什么时候需要协作? 比如:

一个线程向缓冲区写数据,另一个线程从缓冲区读数据,如果读线程发现缓冲区为空就会等待,当写线程向缓冲区写入数据,就会唤醒读线程,这里读线程和写线程就是一个合作关系。JVM通过Object类的wait方法来使自己等待,在调用wait方法后,该线程会释放它持有的监视器,直到其他线程通知它才有执行的机会。一个线程调用notify方法通知在等待的线程,这个等待的线程并不会马上执行,而是要通知线程释放监视器后,它重新获取监视器才有执行的机会。如果刚好唤醒的这个线程需要的监视器被其他线程抢占,那么这个线程会继续等待。Object类中的notifyAll方法可以解决这个问题,它可以唤醒所有等待的线程,总有一个线程执行。

20200503175734

如上图所示,一个线程通过1号门进入Entry Set(入口区),如果在入口区没有线程等待,那么这个线程就会获取监视器成为监视器的Owner,然后执行监视区域的代码。如果在入口区中有其它线程在等待,那么新来的线程也会和这些线程一起等待。线程在持有监视器的过程中,有两个选择,一个是正常执行监视器区域的代码,释放监视器,通过5号门退出监视器;还有可能等待某个条件的出现,于是它会通过3号门到Wait Set(等待区)休息,直到相应的条件满足后再通过4号门进入重新获取监视器再执行。

当一个线程释放监视器时,在入口区和等待区的等待线程都会去竞争监视器,如果入口区的线程赢了,会从2号门进入;如果等待区的线程赢了会从4号门进入。只有通过3号门才能进入等待区,在等待区中的线程只有通过4号门才能退出等待区,也就是说一个线程只有在持有监视器时才能执行wait操作,处于等待的线程只有再次获得监视器才能退出等待状态。

锁的优化

从JDK5引入了现代操作系统新增加的CAS原子操作( JDK5中并没有对synchronized关键字做优化,而是体现在J.U.C中,所以在该版本concurrent包有更好的性能 ),从JDK6开始,就对synchronized的实现机制进行了较大调整,包括使用JDK5引进的CAS自旋之外,还增加了自适应的CAS自旋、锁消除、锁粗化、偏向锁、轻量级锁这些优化策略。

自旋锁

所谓自旋锁,就是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。

自旋等待的时间(自旋的次数)必须要有一个限度,如果自旋超过了定义的时间仍然没有获取到锁,则应该被挂起。在JDK1.6中默认开启。同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整。

自旋次数,不一定能设置合理,有时候多线程都是等你刚刚退出的时候就释放了锁。于是JDK1.6引入自适应的自旋锁,让虚拟机会变得越来越聪明。

适应性自旋锁

所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

锁消除:为了保证数据的完整性,在进行操作时需要对这部分操作进行同步控制,但是在有些情况下,JVM检测到不可能存在共享数据竞争,这是JVM会对这些同步锁进行锁消除。
锁粗化:在使用同步锁的时候,需要让同步块的作用范围尽可能小—仅在共享数据的实际作用域中才进行同步,这样做的目的是 为了使需要同步的操作数量尽可能缩小,如果存在锁竞争,那么等待锁的线程也能尽快拿到锁。

偏向锁

偏向锁是JDK6中的重要引进,因为HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。
轻量级锁是为了在线程交替执行同步块时提高性能,而偏向锁则是在只有一个线程执行同步块时进一步提高性能。

现在几乎所有的锁都是可重入的,即已经获得锁的线程可以多次锁住/解锁监视对象,按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是 一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。

轻量级锁

引入轻量级锁的主要目的是 在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁。

重量级锁

Synchronized是通过对象内部的一个叫做 监视器锁(Monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。

重量级锁、轻量级锁和偏向锁之间转换

20200503182042

每种锁是只能升级,不能降级,即由偏向锁->轻量级锁->重量级锁,而这个过程就是开销逐渐加大的过程。

如果是单线程使用,那偏向锁毫无疑问代价最小,并且它就能解决问题,连CAS都不用做,仅仅在内存中比较下对象头就可以了;
如果出现了其他线程竞争,则偏向锁就会升级为轻量级锁;
如果其他线程通过一定次数的CAS尝试没有成功,则进入重量级锁;

20200503182223

https://www.cnblogs.com/aspirant/p/11470858.html

查看docker容器日志

发表于 2019-05-03 | 分类于 docker

命令格式:

1
2
3
4
5
6
7
8
$ docker logs [OPTIONS] CONTAINER
Options:
--details 显示更多的信息
-f, --follow 跟踪实时日志
--since string 显示自某个timestamp之后的日志,或相对时间,如42m(即42分钟)
--tail string 从日志末尾显示多少行日志, 默认是all
-t, --timestamps 显示时间戳
--until string 显示自某个timestamp之前的日志,或相对时间,如42m(即42分钟)

查看指定时间后的日志,只显示最后100行:

$ docker logs -f -t –since=”2019-08-08” –tail=100 CONTAINER_ID

查看最近20分钟的日志:

$ docker logs –since 20m CONTAINER_ID

查看某时间之后的日志:

$ docker logs -t –since=”2019-08-08T13:23:37” CONTAINER_ID

查看某时间段日志:

$ docker logs -t –since=”2019-08-08T13:23:37” –until “2019-08-09T12:23:37” CONTAINER_ID

CentOS Docker 安装

发表于 2019-05-01 | 分类于 docker
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
1、卸载旧版本
命令:yum remove -y docker docker-common docker-selinux dockerengine
2、设置仓库 REPOSITORY
命令:yum install -y yum-utils device-mapper-persistent-data lvm2
设置 docker 稳定的源
命令:yum-config-manager --add-repo \https://download.docker.com/linux/centos/docker-ce.repo
更新 yum 的安装索引
命令:yum makecache fast
3、安装 docker 版本
yum install -y docker-ce
4、启动 docker 服务
systemctl start docker.service
docker 服务开机自启动
systemctl enable docker.service
5、测试
[root@localhost redata]# docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
1b930d010525: Pull complete
Digest: sha256:2557e3c07ed1e38f26e389462d03ed943586f744621577a99efb77324b0fe535
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

由于本地没有hello-world这个镜像,所以会下载一个hello-world的镜像,并在容器内运行。

常用Docker命令
镜像管理:
docker images:列出本地所有镜像
docker search <IMAGE_ID/NAME>:查找image
docker pull <IMAGE_ID>:下载image
docker push <IMAGE_ID>:上传image
docker rmi <IMAGE_ID>:删除image
容器管理:
docker run -i -t <IMAGE_ID> /bin/bash:-i:标准输入给容器 -t:分配一个虚拟终端 /bin/bash:执行bash脚本
-d:以守护进程方式运行(后台)
-P:默认匹配docker容器的5000端口号到宿主机的49153 to 65535端口
-p <HOT_PORT>:<CONTAINER_PORT>:指定端口号
- -name: 指定容器的名称
- -rm:退出时删除容器
docker stop:停止container
docker start:重新启动container
docker ps - Lists containers.
-l:显示最后启动的容器
-a:同时显示停止的容器,默认只显示启动状态
docker attach <CONTAINER_ID> 连接到启动的容器
docker logs <CONTAINER_ID> 输出容器日志
-f:实时输出
docker cp <CONTAINER_ID>:path hostpath:复制容器内的文件到宿主机目录上
docker rm:删除container
docker rm `docker ps -a -q`:删除所有容器
docker kill `docker ps -q`
docker rmi `docker images -q -a`
docker wait <CONTAINER_ID>:阻塞对容器的其他调用方法,直到容器停止后退出
docker top <CONTAINER_ID>:查看容器中运行的进程
docker diff:查看容器中的变化
docker inspect:查看容器详细信息(输出为Json)
-f:查找特定信息,如docker inspect{ .NetworkSettings.IPAddress }'
docker commit -m "comment" -a "author" <CONTAINER_ID>
docker extc -it <CONTAINER> <COMMAND>:在容器里执行命令,并输出结果

mybatis架构和源码分析释

发表于 2019-04-24 | 分类于 mybatis

mybatis架构

20200506220853

三层:

(1)API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。

(2)数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。

(3)基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。

主要的构建及其关系

20200506221308

层次结构

20200506221448

SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要数据库增删改查功能
Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
StatementHandler 封装了JDBC Statement操作,负责对JDBCstatement的操作,如设置参数、将Statement结果集转换成List集合。
ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所需要的参数
ResultSetHandler *负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
TypeHandler 负责java数据类型和jdbc数据类型之间的映射和转换
MappedStatement MappedStatement维护了一条<select|update|delete|insert>节点的封
SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
BoundSql 表示动态生成的SQL语句以及相应的参数信息
Configuration MyBatis所有的配置信息都维持在Configuration对象之中

处理流程

MyBatis的初始化,会从mybatis-config.xml配置文件,解析构造成Configuration这个类。

(1)加载配置:配置来源于两个地方,一处是配置文件,一处是Java代码的注解,将SQL的配置信息加载成为一个个MappedStatement对象(包括了传入参数映射配置、执行的SQL语句、结果映射配置),存储在内存中。

(2)SQL解析:当API接口层接收到调用请求时,会接收到传入SQL的ID和传入对象(可以是Map、JavaBean或者基本数据类型),Mybatis会根据SQL的ID找到对应的MappedStatement,然后根据传入参数对象对MappedStatement进行解析,解析后可以得到最终要执行的SQL语句和参数。

(3)SQL执行:将最终得到的SQL和参数拿到数据库进行执行,得到操作数据库的结果。

(4)结果映射:将操作数据库的结果按照映射的配置进行转换,可以转换成HashMap、JavaBean或者基本数据类型,并将最终结果返回。

核心类

1、SqlSessionFactoryBuilder

每一个MyBatis的应用程序的入口是SqlSessionFactoryBuilder。

它的作用是通过XML配置文件创建Configuration对象,然后通过build方法创建SqlSessionFactory对象。

1
2
3
4
5
6
7
8
9
private static SqlSessionFactoryBuilder sqlSessionFactoryBuilder;
private static SqlSessionFactory sqlSessionFactory;

private static void init() throws IOException {
String resource = "mybatis-config.xml";
Reader reader = Resources.getResourceAsReader(resource);
sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
sqlSessionFactory = sqlSessionFactoryBuilder.build(reader);
}

org.apache.ibatis.session.Configuration 是mybatis初始化的核心。

mybatis-config.xml中的配置,最后会解析xml成Configuration这个类。

SqlSessionFactoryBuilder根据传入的数据流(XML)生成Configuration对象,然后根据Configuration对象创建默认的SqlSessionFactory实例。

2、SqlSessionFactory对象由SqlSessionFactoryBuilder创建:

它的主要功能是创建SqlSession对象,和SqlSessionFactoryBuilder对象一样,全局的对象。SqlSessionFactory对象一个必要的属性是Configuration对象,它是保存Mybatis全局配置的一个配置对象,通常由SqlSessionFactoryBuilder从XML配置文件创建。

3、SqlSession

SqlSession对象的主要功能是完成一次数据库的访问和结果的映射,它类似于数据库的session概念,由于不是线程安全的,所以SqlSession对象的作用域需限制方法内。SqlSession的默认实现类是DefaultSqlSession,它有两个必须配置的属性:Configuration和Executor。SqlSession对数据库的操作都是通过Executor来完成的。

SqlSession :默认创建DefaultSqlSession 并且开启一级缓存,创建执行器 、赋值。

SqlSession有一个重要的方法getMapper,顾名思义,这个方式是用来获取Mapper对象的。什么是Mapper对象?根据Mybatis的官方手册,应用程序除了要初始并启动Mybatis之外,还需要定义一些接口,接口里定义访问数据库的方法,存放接口的包路径下需要放置同名的XML配置文件。

应用程序访问getMapper时,Mybatis会根据传入的接口类型和对应的XML配置文件生成一个代理对象,应用程序获得Mapper对象后,就应该通过这个Mapper对象来访问Mybatis的SqlSession对象。

4、Executor

Executor对象在创建Configuration对象的时候创建,并且缓存在Configuration对象里。Executor对象的主要功能是调用StatementHandler访问数据库,并将查询结果存入缓存中(如果配置了缓存的话)。

5、StatementHandler

StatementHandler是真正访问数据库的地方,并调用ResultSetHandler处理查询结果。

6、ResultSetHandler

处理查询结果。

源码分析释

初始化配置信息

1
2
3
4
5
6
7
8
9
private static SqlSessionFactoryBuilder sqlSessionFactoryBuilder;
private static SqlSessionFactory sqlSessionFactory;

private static void init() throws IOException {
String resource = "mybatis-config.xml";
Reader reader = Resources.getResourceAsReader(resource);
sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
sqlSessionFactory = sqlSessionFactoryBuilder.build(reader);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
//解析配置文件的对象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}

解析器

例如配置dataSource的时候使用了 ${driver} 这种表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class PropertyParser {

public static String parse(String string, Properties variables) {
VariableTokenHandler handler = new VariableTokenHandler(variables);
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}

private static class VariableTokenHandler implements TokenHandler {
private Properties variables;

public VariableTokenHandler(Properties variables) {
this.variables = variables;
}

public String handleToken(String content) {
if (variables != null && variables.containsKey(content)) {
return variables.getProperty(content);
}
return "${" + content + "}";
}
}
}

解析别名

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
/**
* 解析typeAliases节点
*/
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
//如果子节点是package, 那么就获取package节点的name属性, mybatis会扫描指定的package
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
//TypeAliasRegistry 负责管理别名, 这儿就是通过TypeAliasRegistry 进行别名注册, 下面就会看看TypeAliasRegistry源码
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
//如果子节点是typeAlias节点,那么就获取alias属性和type的属性值
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type);
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}

public class TypeAliasRegistry {

// 别名就仅仅通过一个HashMap来实现, key为别名, value就是别名对应的类型(class对象)
private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();

/**
* 以下就是mybatis默认为我们注册的别名
*/
public TypeAliasRegistry() {
registerAlias("string", String.class);

registerAlias("byte", Byte.class);
registerAlias("long", Long.class);
registerAlias("short", Short.class);
registerAlias("int", Integer.class);
registerAlias("integer", Integer.class);
registerAlias("double", Double.class);
registerAlias("float", Float.class);
registerAlias("boolean", Boolean.class);

registerAlias("byte[]", Byte[].class);
registerAlias("long[]", Long[].class);
registerAlias("short[]", Short[].class);
registerAlias("int[]", Integer[].class);
registerAlias("integer[]", Integer[].class);
registerAlias("double[]", Double[].class);
registerAlias("float[]", Float[].class);
registerAlias("boolean[]", Boolean[].class);

registerAlias("_byte", byte.class);
registerAlias("_long", long.class);
registerAlias("_short", short.class);
registerAlias("_int", int.class);
registerAlias("_integer", int.class);
registerAlias("_double", double.class);
registerAlias("_float", float.class);
registerAlias("_boolean", boolean.class);

registerAlias("_byte[]", byte[].class);
registerAlias("_long[]", long[].class);
registerAlias("_short[]", short[].class);
registerAlias("_int[]", int[].class);
registerAlias("_integer[]", int[].class);
registerAlias("_double[]", double[].class);
registerAlias("_float[]", float[].class);
registerAlias("_boolean[]", boolean[].class);

registerAlias("date", Date.class);
registerAlias("decimal", BigDecimal.class);
registerAlias("bigdecimal", BigDecimal.class);
registerAlias("biginteger", BigInteger.class);
registerAlias("object", Object.class);

registerAlias("date[]", Date[].class);
registerAlias("decimal[]", BigDecimal[].class);
registerAlias("bigdecimal[]", BigDecimal[].class);
registerAlias("biginteger[]", BigInteger[].class);
registerAlias("object[]", Object[].class);

registerAlias("map", Map.class);
registerAlias("hashmap", HashMap.class);
registerAlias("list", List.class);
registerAlias("arraylist", ArrayList.class);
registerAlias("collection", Collection.class);
registerAlias("iterator", Iterator.class);

registerAlias("ResultSet", ResultSet.class);
}


/**
* 处理别名, 直接从保存有别名的hashMap中取出即可
*/
@SuppressWarnings("unchecked")
public <T> Class<T> resolveAlias(String string) {
try {
if (string == null) return null;
String key = string.toLowerCase(Locale.ENGLISH); // issue #748
Class<T> value;
if (TYPE_ALIASES.containsKey(key)) {
value = (Class<T>) TYPE_ALIASES.get(key);
} else {
value = (Class<T>) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}

/**
* 配置文件中配置为package的时候, 会调用此方法,根据配置的报名去扫描javabean ,然后自动注册别名
* 默认会使用 Bean 的首字母小写的非限定类名来作为它的别名
* 也可在javabean 加上注解@Alias 来自定义别名, 例如: @Alias(user)
*/
public void registerAliases(String packageName){
registerAliases(packageName, Object.class);
}

public void registerAliases(String packageName, Class<?> superType){
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
for(Class<?> type : typeSet){
// Ignore inner classes and interfaces (including package-info.java)
// Skip also inner classes. See issue #6
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
registerAlias(type);
}
}
}

public void registerAlias(Class<?> type) {
String alias = type.getSimpleName();
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
registerAlias(alias, type);
}

//这就是注册别名的本质方法, 其实就是向保存别名的hashMap新增值而已, 呵呵, 别名的实现太简单了,对吧
public void registerAlias(String alias, Class<?> value) {
if (alias == null) throw new TypeException("The parameter alias cannot be null");
String key = alias.toLowerCase(Locale.ENGLISH); // issue #748
if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
}
TYPE_ALIASES.put(key, value);
}

public void registerAlias(String alias, String value) {
try {
registerAlias(alias, Resources.classForName(value));
} catch (ClassNotFoundException e) {
throw new TypeException("Error registering type alias "+alias+" for "+value+". Cause: " + e, e);
}
}

/**
* 获取保存别名的HashMap, Configuration对象持有对TypeAliasRegistry的引用,因此,如果需要,我们可以通过Configuration对象获取
*/
public Map<String, Class<?>> getTypeAliases() {
return Collections.unmodifiableMap(TYPE_ALIASES);
}

}

TypeHandler配置

无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成 Java 类型。Mybatis默认为我们实现了许多TypeHandler, 当我们没有配置指定TypeHandler时,Mybatis会根据参数或者返回结果的不同,默认为我们选择合适的TypeHandler处理。

1
2
3
4
5
6
7
8
9
10
11
12
<configuration>
<typeHandlers>
<!-- handler属性直接配置我们要指定的TypeHandler -->
<typeHandler handler=""/>
<!-- javaType 配置java类型,例如String, 如果配上javaType, 那么指定的typeHandler就只作用于指定的类型 -->
<typeHandler javaType="" handler=""/>
<!-- jdbcType 配置数据库基本数据类型,例如varchar, 如果配上jdbcType, 那么指定的typeHandler就只作用于指定的类型 -->
<typeHandler jdbcType="" handler=""/>
<!-- 也可两者都配置 -->
<typeHandler javaType="" jdbcType="" handler=""/>
</typeHandlers>
</configuration>

首先解析,再TypeHandler的管理注册

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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
/**
* typeHandler注册管理类
*/
public final class TypeHandlerRegistry {

//源码一上来,二话不说,几个大大的HashMap就出现,这不又跟上次讲的typeAliases的注册类似么

//基本数据类型与其包装类
private static final Map<Class<?>, Class<?>> reversePrimitiveMap = new HashMap<Class<?>, Class<?>>() {
private static final long serialVersionUID = 1L;
{
put(Byte.class, byte.class);
put(Short.class, short.class);
put(Integer.class, int.class);
put(Long.class, long.class);
put(Float.class, float.class);
put(Double.class, double.class);
put(Boolean.class, boolean.class);
put(Character.class, char.class);
}
};

//这几个MAP不用说就知道存的是什么东西吧,命名的好处
private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<JdbcType, TypeHandler<?>>(JdbcType.class);
private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new HashMap<Type, Map<JdbcType, TypeHandler<?>>>();
private final TypeHandler<Object> UNKNOWN_TYPE_HANDLER = new UnknownTypeHandler(this);
private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<Class<?>, TypeHandler<?>>();

//就像上篇文章讲的typeAliases一样,mybatis也默认给我们注册了不少的typeHandler
//具体如下
public TypeHandlerRegistry() {
register(Boolean.class, new BooleanTypeHandler());
register(boolean.class, new BooleanTypeHandler());
register(JdbcType.BOOLEAN, new BooleanTypeHandler());
register(JdbcType.BIT, new BooleanTypeHandler());

register(Byte.class, new ByteTypeHandler());
register(byte.class, new ByteTypeHandler());
register(JdbcType.TINYINT, new ByteTypeHandler());

register(Short.class, new ShortTypeHandler());
register(short.class, new ShortTypeHandler());
register(JdbcType.SMALLINT, new ShortTypeHandler());

register(Integer.class, new IntegerTypeHandler());
register(int.class, new IntegerTypeHandler());
register(JdbcType.INTEGER, new IntegerTypeHandler());

register(Long.class, new LongTypeHandler());
register(long.class, new LongTypeHandler());

register(Float.class, new FloatTypeHandler());
register(float.class, new FloatTypeHandler());
register(JdbcType.FLOAT, new FloatTypeHandler());

register(Double.class, new DoubleTypeHandler());
register(double.class, new DoubleTypeHandler());
register(JdbcType.DOUBLE, new DoubleTypeHandler());

register(String.class, new StringTypeHandler());
register(String.class, JdbcType.CHAR, new StringTypeHandler());
register(String.class, JdbcType.CLOB, new ClobTypeHandler());
register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
register(String.class, JdbcType.LONGVARCHAR, new ClobTypeHandler());
register(String.class, JdbcType.NVARCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCHAR, new NStringTypeHandler());
register(String.class, JdbcType.NCLOB, new NClobTypeHandler());
register(JdbcType.CHAR, new StringTypeHandler());
register(JdbcType.VARCHAR, new StringTypeHandler());
register(JdbcType.CLOB, new ClobTypeHandler());
register(JdbcType.LONGVARCHAR, new ClobTypeHandler());
register(JdbcType.NVARCHAR, new NStringTypeHandler());
register(JdbcType.NCHAR, new NStringTypeHandler());
register(JdbcType.NCLOB, new NClobTypeHandler());

register(Object.class, JdbcType.ARRAY, new ArrayTypeHandler());
register(JdbcType.ARRAY, new ArrayTypeHandler());

register(BigInteger.class, new BigIntegerTypeHandler());
register(JdbcType.BIGINT, new LongTypeHandler());

register(BigDecimal.class, new BigDecimalTypeHandler());
register(JdbcType.REAL, new BigDecimalTypeHandler());
register(JdbcType.DECIMAL, new BigDecimalTypeHandler());
register(JdbcType.NUMERIC, new BigDecimalTypeHandler());

register(Byte[].class, new ByteObjectArrayTypeHandler());
register(Byte[].class, JdbcType.BLOB, new BlobByteObjectArrayTypeHandler());
register(Byte[].class, JdbcType.LONGVARBINARY, new BlobByteObjectArrayTypeHandler());
register(byte[].class, new ByteArrayTypeHandler());
register(byte[].class, JdbcType.BLOB, new BlobTypeHandler());
register(byte[].class, JdbcType.LONGVARBINARY, new BlobTypeHandler());
register(JdbcType.LONGVARBINARY, new BlobTypeHandler());
register(JdbcType.BLOB, new BlobTypeHandler());

register(Object.class, UNKNOWN_TYPE_HANDLER);
register(Object.class, JdbcType.OTHER, UNKNOWN_TYPE_HANDLER);
register(JdbcType.OTHER, UNKNOWN_TYPE_HANDLER);

register(Date.class, new DateTypeHandler());
register(Date.class, JdbcType.DATE, new DateOnlyTypeHandler());
register(Date.class, JdbcType.TIME, new TimeOnlyTypeHandler());
register(JdbcType.TIMESTAMP, new DateTypeHandler());
register(JdbcType.DATE, new DateOnlyTypeHandler());
register(JdbcType.TIME, new TimeOnlyTypeHandler());

register(java.sql.Date.class, new SqlDateTypeHandler());
register(java.sql.Time.class, new SqlTimeTypeHandler());
register(java.sql.Timestamp.class, new SqlTimestampTypeHandler());

// issue #273
register(Character.class, new CharacterTypeHandler());
register(char.class, new CharacterTypeHandler());
}

public boolean hasTypeHandler(Class<?> javaType) {
return hasTypeHandler(javaType, null);
}

public boolean hasTypeHandler(TypeReference<?> javaTypeReference) {
return hasTypeHandler(javaTypeReference, null);
}

public boolean hasTypeHandler(Class<?> javaType, JdbcType jdbcType) {
return javaType != null && getTypeHandler((Type) javaType, jdbcType) != null;
}

public boolean hasTypeHandler(TypeReference<?> javaTypeReference, JdbcType jdbcType) {
return javaTypeReference != null && getTypeHandler(javaTypeReference, jdbcType) != null;
}

public TypeHandler<?> getMappingTypeHandler(Class<? extends TypeHandler<?>> handlerType) {
return ALL_TYPE_HANDLERS_MAP.get(handlerType);
}

public <T> TypeHandler<T> getTypeHandler(Class<T> type) {
return getTypeHandler((Type) type, null);
}

public <T> TypeHandler<T> getTypeHandler(TypeReference<T> javaTypeReference) {
return getTypeHandler(javaTypeReference, null);
}

public TypeHandler<?> getTypeHandler(JdbcType jdbcType) {
return JDBC_TYPE_HANDLER_MAP.get(jdbcType);
}

public <T> TypeHandler<T> getTypeHandler(Class<T> type, JdbcType jdbcType) {
return getTypeHandler((Type) type, jdbcType);
}

public <T> TypeHandler<T> getTypeHandler(TypeReference<T> javaTypeReference, JdbcType jdbcType) {
return getTypeHandler(javaTypeReference.getRawType(), jdbcType);
}

private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(type);
TypeHandler<?> handler = null;
if (jdbcHandlerMap != null) {
handler = jdbcHandlerMap.get(jdbcType);
if (handler == null) {
handler = jdbcHandlerMap.get(null);
}
}
if (handler == null && type != null && type instanceof Class && Enum.class.isAssignableFrom((Class<?>) type)) {
handler = new EnumTypeHandler((Class<?>) type);
}
@SuppressWarnings("unchecked")
// type drives generics here
TypeHandler<T> returned = (TypeHandler<T>) handler;
return returned;
}

public TypeHandler<Object> getUnknownTypeHandler() {
return UNKNOWN_TYPE_HANDLER;
}

public void register(JdbcType jdbcType, TypeHandler<?> handler) {
JDBC_TYPE_HANDLER_MAP.put(jdbcType, handler);
}

//
// REGISTER INSTANCE
//

/**
* 只配置了typeHandler, 没有配置jdbcType 或者javaType
*/
@SuppressWarnings("unchecked")
public <T> void register(TypeHandler<T> typeHandler) {
boolean mappedTypeFound = false;
//在自定义typeHandler的时候,可以加上注解MappedTypes 去指定关联的javaType
//因此,此处需要扫描MappedTypes注解
MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
for (Class<?> handledType : mappedTypes.value()) {
register(handledType, typeHandler);
mappedTypeFound = true;
}
}
// @since 3.1.0 - try to auto-discover the mapped type
if (!mappedTypeFound && typeHandler instanceof TypeReference) {
try {
TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
register(typeReference.getRawType(), typeHandler);
mappedTypeFound = true;
} catch (Throwable t) {
// maybe users define the TypeReference with a different type and are not assignable, so just ignore it
}
}
if (!mappedTypeFound) {
register((Class<T>) null, typeHandler);
}
}

/**
* 配置了typeHandlerhe和javaType
*/
public <T> void register(Class<T> javaType, TypeHandler<? extends T> typeHandler) {
register((Type) javaType, typeHandler);
}

private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
//扫描注解MappedJdbcTypes
MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
if (mappedJdbcTypes != null) {
for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
register(javaType, handledJdbcType, typeHandler);
}
if (mappedJdbcTypes.includeNullJdbcType()) {
register(javaType, null, typeHandler);
}
} else {
register(javaType, null, typeHandler);
}
}

public <T> void register(TypeReference<T> javaTypeReference, TypeHandler<? extends T> handler) {
register(javaTypeReference.getRawType(), handler);
}

/**
* typeHandlerhe、javaType、jdbcType都配置了
*/
public <T> void register(Class<T> type, JdbcType jdbcType, TypeHandler<? extends T> handler) {
register((Type) type, jdbcType, handler);
}

/**
* 注册typeHandler的核心方法
* 就是向Map新增数据而已
*/
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (javaType != null) {
Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
if (map == null) {
map = new HashMap<JdbcType, TypeHandler<?>>();
TYPE_HANDLER_MAP.put(javaType, map);
}
map.put(jdbcType, handler);
if (reversePrimitiveMap.containsKey(javaType)) {
register(reversePrimitiveMap.get(javaType), jdbcType, handler);
}
}
ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}

//
// REGISTER CLASS
//

// Only handler type

public void register(Class<?> typeHandlerClass) {
boolean mappedTypeFound = false;
MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
for (Class<?> javaTypeClass : mappedTypes.value()) {
register(javaTypeClass, typeHandlerClass);
mappedTypeFound = true;
}
}
if (!mappedTypeFound) {
register(getInstance(null, typeHandlerClass));
}
}

// java type + handler type

public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
}

// java type + jdbc type + handler type

public void register(Class<?> javaTypeClass, JdbcType jdbcType, Class<?> typeHandlerClass) {
register(javaTypeClass, jdbcType, getInstance(javaTypeClass, typeHandlerClass));
}

// Construct a handler (used also from Builders)

@SuppressWarnings("unchecked")
public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
if (javaTypeClass != null) {
try {
Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
return (TypeHandler<T>) c.newInstance(javaTypeClass);
} catch (NoSuchMethodException ignored) {
// ignored
} catch (Exception e) {
throw new TypeException("Failed invoking constructor for handler " + typeHandlerClass, e);
}
}
try {
Constructor<?> c = typeHandlerClass.getConstructor();
return (TypeHandler<T>) c.newInstance();
} catch (Exception e) {
throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e);
}
}


/**
* 根据指定的pacakge去扫描自定义的typeHander,然后注册
*/
public void register(String packageName) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(TypeHandler.class), packageName);
Set<Class<? extends Class<?>>> handlerSet = resolverUtil.getClasses();
for (Class<?> type : handlerSet) {
//Ignore inner classes and interfaces (including package-info.java) and abstract classes
if (!type.isAnonymousClass() && !type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
register(type);
}
}
}
/**
* 通过configuration对象可以获取已注册的所有typeHandler
*/
public Collection<TypeHandler<?>> getTypeHandlers() {
return Collections.unmodifiableCollection(ALL_TYPE_HANDLERS_MAP.values());
}

}

执行流分析

SqlSessionFactory 与 SqlSession

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
(1)首先,SqlSessionFactoryBuilder去读取mybatis的配置文件,然后build一个DefaultSqlSessionFactory。源码如下:
```java
/**
* 一系列的构造方法最终都会调用本方法(配置文件为Reader时会调用本方法,还有一个InputStream方法与此对应)
* @param reader
* @param environment
* @param properties
* @return
*/
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
//通过XMLConfigBuilder解析配置文件,解析的配置相关信息都会封装为一个Configuration对象
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
//这儿创建DefaultSessionFactory对象
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}

public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}

(2)当我们获取到SqlSessionFactory之后,就可以通过SqlSessionFactory去获取SqlSession对象。源码如下:

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
/**
* 通常一系列openSession方法最终都会调用本方法
* @param execType
* @param level
* @param autoCommit
* @return
*/
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
//通过Confuguration对象去获取Mybatis相关配置信息, Environment对象包含了数据源和事务的配置
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//之前说了,从表面上来看,咱们是用sqlSession在执行sql语句, 实际呢,其实是通过excutor执行, excutor是对于Statement的封装
final Executor executor = configuration.newExecutor(tx, execType);
//关键看这儿,创建了一个DefaultSqlSession对象
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}

MapperProxy

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
@SuppressWarnings("unchecked")
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 交给MapperProxyFactory去做
final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}

@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
//动态代理我们写的dao接口
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}

public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}


/**
* MapperProxy在执行时会触发此方法
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (Object.class.equals(method.getDeclaringClass())) {
try {
return method.invoke(this, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}

//先判断CRUD类型,然后根据类型去选择到底执行sqlSession中的哪个方法
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
if (SqlCommandType.INSERT == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
} else if (SqlCommandType.UPDATE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
} else if (SqlCommandType.DELETE == command.getType()) {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
} else if (SqlCommandType.SELECT == command.getType()) {
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
}
} else {
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}

//最终执行
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
//查看SimpleExecutor中,最终执行
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
//StatementHandler封装了Statement, 让 StatementHandler 去处理
return handler.<E>query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}

https://www.cnblogs.com/dongying/p/4142476.html

使用的设计模式

20200506224912

jpa中批量更新和插入实现及复杂查询.md

发表于 2019-04-23 | 分类于 springboot

批量更新和插入

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
@NoRepositoryBean
public interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
<S extends T> Iterable<S> batchSave(Iterable<S> var1);
<S extends T> Iterable<S> batchUpdate(Iterable<S> var1);
}

public class BaseRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements BaseRepository<T, ID> {
//500一批
private static final int BATCH_SIZE = 500;

private EntityManager em;

public BaseRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager em) {
super(entityInformation, em);
this.em = em;

}

public BaseRepositoryImpl(Class<T> domainClass, EntityManager em) {
super(domainClass, em);
this.em = em;
}

@Override
@Transactional
public <S extends T> Iterable<S> batchSave(Iterable<S> var1) {
Iterator<S> iterator = var1.iterator();
int index = 0;
while (iterator.hasNext()) {
em.persist(iterator.next());
index++;
if (index % BATCH_SIZE == 0) {
em.flush();
em.clear();
}
}
if (index % BATCH_SIZE != 0) {
em.flush();
em.clear();
}
return var1;
}

@Override
@Transactional
public <S extends T> Iterable<S> batchUpdate(Iterable<S> var1) {
Iterator<S> iterator = var1.iterator();
int index = 0;
while (iterator.hasNext()) {
em.merge(iterator.next());
index++;
if (index % BATCH_SIZE == 0) {
em.flush();
em.clear();
}
}
if (index % BATCH_SIZE != 0) {
em.flush();
em.clear();
}
return var1;
}
}

//业务中使用
public interface QuestionRepository extends BaseRepository<Question, Integer> {

Question findQuestionByQuestionIdAndDeleteState(String questionId, Short deleteState) throws Exception;

@Modifying
@Query("update Question set deleteState=1,deleteTime=:deleteTime where categoryId=:categoryId and createUserId=:userId")
int deleteAllByCategoryId(String categoryId, Integer userId, Long deleteTime) throws Exception;

}

jpa复杂查询

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
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
     //查询结果所需要的类型(Entity相对应)
CriteriaQuery<Entity> criteriaQuery = criteriaBuilder.createQuery(Entity.class);
     //查询所需要的主体类(Entity0相对应)
Root<Entity0> root = criteriaQuery.from(Entity0.class);
     //查询结果-select(此处查询所有符合条件的主体类)
criteriaQuery.select(root);
     //过滤条件用Predicate方法拼接
Predicate restrictions = criteriaBuilder.conjunction();
     //过滤条件——equal(当Entity0关联member类时,Entity0:member=m:1)
restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.equal(root.get("member"), member));
//过滤条件——like
restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.like(root.<String>get("str"), "%" + str + "%"));
//用户名查询(member里面的username匹配) ———— 多层查询 ———— 子查询的一种:适用于m:1或1:1(即多对一或一对一)
restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.like(root.get("member").<String>get("username"), "%" + username + "%"));
//子查询(规范写法,先判断查询内容是否存在)(适用于1:m)(即一对多)
if (searchType != null || searchValue != null || hasExpired != null || status != null || type != null || isPendingReceive != null
|| isPendingRefunds != null || isAllocatedStock != null || businessType != null) {
//建立子查询
      Subquery<Order> orderSubquery = criteriaQuery.subquery(Order.class);
Root<Order> orderSubqueryRoot = orderSubquery.from(Order.class);
orderSubquery.select(orderSubqueryRoot);
       //子查询和父查询相关联
Predicate orderRestrictions = criteriaBuilder.equal(orderSubqueryRoot.<MergeOrder>get("mergeOrder"), root);
//子查询过滤条件拼接
if (searchType != null && searchValue != null) {
if ("phone".equals(searchType)) {
orderRestrictions = criteriaBuilder.and(orderRestrictions, criteriaBuilder.like(orderSubqueryRoot.<String>get("phone"), "%" + searchValue + "%"));
}
}
if (type != null) {
CriteriaBuilder.In<Order.Type> in = criteriaBuilder.in(orderSubqueryRoot.<Order.Type>get("type"));
in.value(type);
orderRestrictions = criteriaBuilder.and(orderRestrictions, in);
}
//and、or以及判断是否为null,比较(>)的使用(比较可以用于日期比较)
if (hasExpired != null) {
orderRestrictions = criteriaBuilder.and(orderRestrictions, criteriaBuilder.or(orderSubqueryRoot.get("expire").isNull(), criteriaBuilder.greaterThan(orderSubqueryRoot.<Date>get("expire"), new Date())));
}
// not的使用方法(不符合上述过滤条件),notEqual的使用,<(小于)的使用
if (isPendingReceive != null) {
restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.equal(root.get("paymentMethodType"), PaymentMethod.Type.cashOnDelivery));
Predicate predicate = criteriaBuilder.and(criteriaBuilder.or(orderSubqueryRoot.get("expire").isNull()
, criteriaBuilder.greaterThan(orderSubqueryRoot.<Date>get("expire"), new Date()))
, criteriaBuilder.notEqual(orderSubqueryRoot.get("status"), Order.Status.completed)
, criteriaBuilder.lessThan(orderSubqueryRoot.<BigDecimal>get("amountPaid"), orderSubqueryRoot.<BigDecimal>get("amount")));
if (isPendingReceive) {
orderRestrictions = criteriaBuilder.and(orderRestrictions, criteriaBuilder.not(predicate));
}
}
// 多层查询使用
if (businessType != null) {
orderRestrictions = criteriaBuilder.and(orderRestrictions, criteriaBuilder.equal(orderSubqueryRoot.get("store").get("business").get("businessType"), businessType));
}
    // 拼接过滤条件
orderSubquery.where(orderRestrictions);
// 和总条件拼接(exists的使用)
restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.exists(orderSubquery));
}
criteriaQuery.where(restrictions);
    
TypedQuery<Entity> query = entityManager.createQuery(criteriaQuery);
    
Entity singleResult = query.getSingleResult();

跨域问题整理

发表于 2019-04-23 | 分类于 spring

跨域介绍

1、同源策略简介

同源策略[same origin policy]是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。 同源策略是浏览器安全的基石。

什么是源:源[origin]就是协议、域名和端口号。例如:http://www.baidu.com:80这个URL。

什么是同源:若地址里面的协议、域名和端口号均相同则属于同源。

哪些操作不受同源策略限制

  • 页面中的链接,重定向以及表单提交是不会受到同源策略限制的;
  • 跨域资源的引入是可以的。

2、跨域

受前面所讲的浏览器同源策略的影响,不是同源的脚本不能操作其他源下面的对象。想要操作另一个源下的对象就需要跨域。 在同源策略的限制下,非同源的网站之间不能发送 AJAX 请求。

如何跨域?
降域:可以通过设置 document.damain=’a.com’,浏览器就会认为它们都是同一个源。想要实现以上任意两个页面之间的通信,两个页面必须都设置documen.damain=’a.com’。

JSONP跨域
CORS 跨域

CORS 简介

为了解决浏览器同源问题,W3C 提出了跨源资源共享,即 CORS(Cross-Origin Resource Sharing)。

CORS 做到了两点:不破坏即有规则和服务器实现了 CORS 接口,就可以跨源通信。

基于这两点,CORS 将请求分为两类:简单请求和非简单请求。

1、简单请求

在CORS出现前,发送HTTP请求时在头信息中不能包含任何自定义字段,且 HTTP 头信息不超过以下几个字段:

1
2
3
4
5
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type 只限于 [application/x-www-form-urlencoded 、multipart/form-data、text/plain ] 类型

一个简单的请求例子:

1
2
3
4
5
6
GET /test HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, sdch, br
Origin: http://www.examples.com
Host: www.examples.com
对于简单请求,CORS的策略是请求时在请求头中增加一个Origin字段,服务器收到请求后,根据该字段判断是否允许该请求访问。

如果允许,则在 HTTP 头信息中添加 Access-Control-Allow-Origin 字段,并返回正确的结果 ;
如果不允许,则不在 HTTP 头信息中添加 Access-Control-Allow-Origin 字段 。
除了上面提到的 Access-Control-Allow-Origin ,还有几个字段用于描述 CORS 返回结果 :

Access-Control-Allow-Credentials: 可选,用户是否可以发送、处理 cookie;
Access-Control-Expose-Headers:可选,可以让用户拿到的字段。有几个字段无论设置与否都可以拿到的,包括:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma 。

2、非简单请求

对于非简单请求的跨源请求,浏览器会在真实请求发出前,增加一次OPTION请求,称为预检请求(preflight request)。预检请求将真实请求的信息,包括请求方法、自定义头字段、源信息添加到 HTTP 头信息字段中,询问服务器是否允许这样的操作。

例如一个DELETE请求:

1
2
3
4
5
OPTIONS /test HTTP/1.1
Origin: http://www.examples.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: X-Custom-Header
Host: www.examples.com

与 CORS 相关的字段有:

请求使用的 HTTP 方法 Access-Control-Request-Method ;
请求中包含的自定义头字段 Access-Control-Request-Headers 。
服务器收到请求时,需要分别对 Origin、Access-Control-Request-Method、Access-Control-Request-Headers 进行验证,验证通过后,会在返回 HTTP头信息中添加 :

1
2
3
4
5
6
7
8
9
10
11
12
Access-Control-Allow-Origin: http://www.examples.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
他们的含义分别是:

Access-Control-Allow-Methods: 真实请求允许的方法
Access-Control-Allow-Headers: 服务器允许使用的字段
Access-Control-Allow-Credentials: 是否允许用户发送、处理 cookie
Access-Control-Max-Age: 预检请求的有效期,单位为秒。有效期内,不会重复发送预检请求
当预检请求通过后,浏览器会发送真实请求到服务器。这就实现了跨源请求。

跨域配置实现

1、使用@CrossOrigin 注解实现,可以加在方法或者类上。

  • origins : 允许可访问的域列表
  • maxAge:准备响应前的缓存持续的最大时间(以秒为单位)。
    @CrossOrigin(origins = “http://domain2.com", maxAge = 3600)

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
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/front/edu/study/notes/getNote.php*");
//registry.addMapping("*");
}
/*
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedHeaders("Content-Type","X-Requested-With","accept,Origin","Access-Control-Request-Method","Access-Control-Request-Headers","token")
.allowedMethods("*")
.allowedOrigins("*")
.allowCredentials(true);
}
*/
}

//过期,不建议使用,1.5版本为继承WebMvcConfigurerAdapter 类实现抽象方法
public class WebMvcConfig extends WebMvcConfigurerAdapter {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
.maxAge(3600)
.allowCredentials(true);
}
}

3、拦截器配置

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
@Configuration
public class CorsConfig {

@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();


config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowCredentials(true);
//config.addAllowedOrigin("http://localhost:9000");//允许请求方
config.addAllowedOrigin("*");

source.registerCorsConfiguration("/front/edu/study/notes/getNote.php*", config);
//source.registerCorsConfiguration("/**", config); // CORS 配置对所有接口都有效

/*FilterRegistrationBean bean = newFilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);*/
return new CorsFilter(source);
}

}
//普通拦截器在HttpServletResponse 加上也可以
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.addHeader("Access-Control-Allow-Origin", "*");
response.addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.addHeader("Access-Control-Allow-Headers",
"Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers,token");
return true;
}

无论是通过哪种方式配置 CORS,都是在构造 CorsConfiguration。 一个 CORS 配置用一个 CorsConfiguration类来表示,它的定义如下:

1
2
3
4
5
6
7
8
public class CorsConfiguration {
private List<String> allowedOrigins;
private List<String> allowedMethods;
private List<String> allowedHeaders;
private List<String> exposedHeaders;
private Boolean allowCredentials;
private Long maxAge;
}

Spring 中对 CORS 规则的校验,都是通过委托给 DefaultCorsProcessor实现的。

DefaultCorsProcessor 处理过程如下:

1
2
3
4
5
6
7
8
9
10
①判断依据是 Header中是否包含 Origin。如果包含则说明为 CORS请求,转到 2;否则,说明不是 CORS 请求,不作任何处理。
②判断 response 的 Header 是否已经包含 Access-Control-Allow-Origin,如果包含,证明已经被处理过了, 转到 3,否则不再处理。
③判断是否同源,如果是则转交给负责该请求的类处理
④是否配置了 CORS 规则,如果没有配置,且是预检请求,则拒绝该请求,如果没有配置,且不是预检请求,则交给负责该请求的类处理。如果配置了,则对该请求进行校验。
⑤校验就是根据 CorsConfiguration 这个类的配置进行判断:

判断 origin 是否合法
判断 method 是否合法
判断 header是否合法
如果全部合法,则在 response header中添加响应的字段,并交给负责该请求的类处理,如果不合法,则拒绝该请求。

4、Servlet方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//设置允许跨域的配置
// 这里填写你允许进行跨域的主机
rep.setHeader("Access-Control-Allow-Origin", "*");
//rep.setHeader("Access-Control-Allow-Origin", "*");
rep.setHeader("Access-Control-Expose-Headers", jwtProperties.getHeader());
// 允许的访问方法
rep.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE, PATCH");
// Access-Control-Max-Age 用于 CORS 相关配置的缓存
rep.setHeader("Access-Control-Max-Age", "3600");
rep.setHeader("Access-Control-Allow-Headers", "token, Origin, X-Requested-With, Content-Type, Accept");
//若要返回cookie、携带seesion等信息则将此项设置我true
rep.setHeader("Access-Control-Allow-Credentials", "true");
// 把获取的Session返回个前端Cookie
//rep.addCookie(new Cookie("JSSESIONID", session.getId()));
chain.doFilter(req, rep);

到此,完成。
如果是其他语言,解决方式都是类似,这篇文章包含了多种语言的处理方式。
https://segmentfault.com/a/1190000012469713

Hibernate Validator

发表于 2019-04-23 | 分类于 hibernate

在日常开发中,Hibernate Validator经常用来验证bean的字段,基于注解,方便快捷高效。

1、Bean Validation 中内置的 constraint

@Valid        被注释的元素是一个对象,需要检查此对象的所有字段值
@Null        被注释的元素必须为 null
@NotNull        被注释的元素必须不为 null
@AssertTrue        被注释的元素必须为 true
@AssertFalse        被注释的元素必须为 false
@Min(value)        被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value)        被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value)    被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value)    被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min)    被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction)    被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past    被注释的元素必须是一个过去的日期
@Future    被注释的元素必须是一个将来的日期
@Pattern(value)    被注释的元素必须符合指定的正则表达式

2、Hibernate Validator 附加的 constraint

@Email    被注释的元素必须是电子邮箱地址
@Length(min=, max=)    被注释的字符串的大小必须在指定的范围内
@NotEmpty    被注释的字符串的必须非空
@Range(min=, max=)    被注释的元素必须在合适的范围内
@NotBlank    被注释的字符串的必须非空
@URL(protocol=,host=,    port=, regexp=, flags=)    被注释的字符串必须是一个有效的url
@CreditCardNumber    被注释的字符串必须通过Luhn校验算法,银行卡,信用卡等号码一般都用Luhn计算合法性
@ScriptAssert(lang=, script=, alias=)    要有Java Scripting API 即JSR 223 ("Scripting for the JavaTM Platform")的实现
@SafeHtml(whitelistType=, additionalTags=)    classpath中要有jsoup包

主要区分下@NotNull @NotEmpty @NotBlank 3个注解的区别:

@NotNull           任何对象的value不能为null,通常用在基本类型上
@NotEmpty       用在集合类上面 集合对象的元素不为0,即集合不为空,也可以用于字符串不为null
@NotBlank        用在String上面 只能用于字符串不为null,并且字符串trim()以后length要大于0

使用

在实体类的属性中添加注解。
之后在统一异常处理类中,进一步解析异常,返回前台信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 数据校验
*/
@ExceptionHandler(BindException.class)
@ResponseBody
public RetViewModel methodArgumentNotValidHandler(BindException e) {
logger.error(Constants.BAD_REQUEST, e);
BindingResult bindingResult = e.getBindingResult();
Map<String, Object> map = getErrors(bindingResult);
RetViewModel responseResult = new RetViewModel(HttpStatus.BAD_REQUEST.value(),map.values().toString(),null);
return responseResult;
}
private Map<String, Object> getErrors(BindingResult result) {
Map<String, Object> map = new HashMap<>();
List<FieldError> list = result.getFieldErrors();
for (FieldError error : list) {
map.put(error.getField(), error.getDefaultMessage());
}
return map;
}

控制器中,直接在对应bean上增加注解@Valid

具体分析如下:

1、简单的demo 配置校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class ValidatorConfig {
@Bean
public LocalValidatorFactoryBean localValidatorFactoryBean(){
LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
localValidatorFactoryBean.setProviderClass(org.hibernate.validator.HibernateValidator.class);
localValidatorFactoryBean.setValidationMessageSource(reloadableResourceBundleMessageSource());
return localValidatorFactoryBean;
}
public ReloadableResourceBundleMessageSource reloadableResourceBundleMessageSource(){
ReloadableResourceBundleMessageSource r = new ReloadableResourceBundleMessageSource();
r.setBasename("classpath:validationMessages.properties");//资源文件
//r.setFileEncodings();
r.setDefaultEncoding("UTF-8");
r.setCacheSeconds(120);//资源内容缓存时间
return r;
}
}

资源文件内容如:

notnull=该字段不能为空

验证的model里面对字段进行验证

@NotBlank(message = "{notnull}")
private String name;

使用,加入注解即可@Valid

1
2
3
4
5
6
7
public void demo(@Valid Demo demo, BindingResult result){
if(result.hasErrors()){
for (ObjectError error : result.getAllErrors()) {
System.out.println(error.getDefaultMessage());
}
}
}

通常不在控制器方法中写BindingResult,统一放到类去处理,如上面的统一异常处理类。

2、hibernate的校验模式

普通模式(默认是这个模式):会校验完所有的属性,然后返回所有的验证失败信息;快速失败返回模式:只要有一个验证失败,则返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

/*ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.failFast( true )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();*/

return validator;
}

3、hibernate的两种校验

请求参数校验和GET参数校验

请求参数校验,在控制器方法中加注解 @Valid,然后后面加BindindResult,有多个参数对应加多个注解和BindindResult
GET参数校验(@RequestParam参数校验)

如果是参数较少的情况下:

public void demo(@RequestParam int grade,@RequestParam int classroom) {}

使用@Valid注解,对RequestParam对应的参数进行注解,是无效的,需要使用@Validated注解来使得验证生效。

需要使用MethodValidationPostProcessor 的Bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
//MethodValidationPostProcessor默认是普通模式,配置的validator不会生效,通过配置Validator使用validator的快速方式
MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
postProcessor.setValidator(validator());
return postProcessor;
}

@Bean
public Validator validator(){
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();

return validator;
}

使用:在控制器上加上注解,方法参数直接使用校验规则

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@Validated
public class DemoController {
/**
* 直接在方法参数上使用
*/
@PostMapping("/student")
public String validate1(
@Size(min = 1,max = 10,message = "姓名长度必须为1到10")@RequestParam("name") String name,
@Min(value = 10,message = "年龄最小为10")@Max(value = 100,message = "年龄最大为100") @RequestParam("age") Integer age,
@Future @RequestParam("birth")@DateTimeFormat(pattern = "yyyy-MM-dd hh:mm:ss") Date birth){
return name;
}

统一处理验证不通过的信息

1
2
3
4
5
6
7
8
9
10
11
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String handle(ConstraintViolationException e) {
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
for (ConstraintViolation<?> item : violations) {
item.getMessage();//验证不通过信息
}

return "bad request, " ;
}

4、实体对象校验

实体类中的属性增加对应的规则

方法中可以通过代码直接校验对象

1
2
3
4
Set<ConstraintViolation<Demo>> violationSet = validator.validate(demo);
for (ConstraintViolation<Demo> model : violationSet) {
System.out.println(model.getMessage());
}

5、对象联级校验

对象内部包含另一个对象作为属性,属性上加@Valid,可以验证作为属性的对象内部的验证:

@Valid
private Demo demo;

校验方式和上面一致。

6、分组校验

分组顺序校验时,按指定的分组先后顺序进行验证,前面的验证不通过,后面的分组就不行验证。

设置validator为普通验证模式(”hibernate.validator.fail_fast”, “false”)

两个组GroupA、GroupB:

public interface GroupA {}

public interface GroupB {
}

实体类:

1
2
3
4
5
6
7
8
@NotBlank
@Range(min = 1,max = Integer.MAX_VALUE,message = "必须大于0",groups = {GroupA.class})
private Integer userId;
@NotBlank
@Length(min = 4,max = 20,message = "必须在[4,20]",groups = {GroupB.class})
private String userName;
@NotBlank
@Range(min = 0,max = 100,message = "年龄必须在[0,100]",groups={Default.class})
GroupA验证字段userId;
GroupB验证字段userName、sex;
Default验证字段age(Default是Validator自带的默认分组)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void demo5(){
Student s = new Student();
//GroupA验证不通过
s.setUserId(-12);
//GroupA验证通过
//p.setUserId(12);
s.setUserName("a");
s.setAge(110);
s.setSex(5);
Set<ConstraintViolation<Student>> validate = validator.validate(s, GroupA.class, GroupB.class);
for (ConstraintViolation<Student> item : validate) {
System.out.println(item);
}
}

如果在参数中使用bean,加入注解即可:@Validated({GroupA.class, GroupB.class})

如果使用组验证顺序:

@GroupSequence({GroupA.class, GroupB.class, Default.class})
    public interface GroupOrder {
}

定义好,使用时,直接用GroupOrder

7、自定义验证器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 自定义一个注解用来校验
*/
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {ValidatorDemo.class})
public @interface ValidateDemo {

String message() default "Not Null";

String name();

Class<?>[] groups() default {};

Class<? extends Payload>[] payload() default {};
}

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ValidatorDemo implements ConstraintValidator<ValidateDemo, Student> {

private String name;

/**
* 用于初始化注解上的值到这个validator
*/
@Override
public void initialize(ValidateDemo constraintAnnotation) {
name =constraintAnnotation.name();
}

/**
* 具体的校验逻辑
*/
@Override
public boolean isValid(Student value, ConstraintValidatorContext context) {
return name ==null || name.equals(value.getName());
}
}

这样就可以直接使用@ValidateDemo在属性或者参数上校验,类似@NotBlank,@Max一样的使用方式。

参考:http://docs.jboss.org/hibernate/validator/4.2/reference/zh-CN/html_single/#validator-gettingstarted

以上是Hibernate Validator校验方式,一般还可以用拦截器实现,比如创建一个校验注解来实现(针对单一参数)

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
@Target({ElementType.PARAMETER})//参数级别
@Retention(RetentionPolicy.RUNTIME) //注解保留到运行阶段
public @interface ParamsNotNull {

}

/**
* 拦截器,直接在参数上就可以校验参数
*/
public class CheckParamsInterceptor extends HandlerInterceptorAdapter {
private static Logger logger = LoggerFactory.getLogger(CheckParamsInterceptor.class);

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
logger.warn("UnSupport handler");
return true;
}
List<String> list = getParamsName((HandlerMethod) handler);
for (String s : list) {
String parameter = request.getParameter(s);
if (StringUtils.isEmpty(parameter)) {
//可以增加更多的规则,还可以吧规则交给注解去定义
JSONObject jsonObject = new JSONObject();
jsonObject.put("status", 202);
jsonObject.put("msg", "缺少必要参数");
response.setHeader("Content-type", "application/json;charset=UTF-8");
response.setHeader("Access-Control-Allow-Origin", "*");//跨域
response.getWriter().write(jsonObject.toJSONString());
return false;
}
}
return true;
}

/**
* 获取使用了该注解的参数名称
*/
private List getParamsName(HandlerMethod handlerMethod) {
Parameter[] parameters = handlerMethod.getMethod().getParameters();
List<String> list = new ArrayList<>();
for (Parameter parameter : parameters) {
if (parameter.isAnnotationPresent(ParamsNotNull.class)) {
list.add(parameter.getName());
}
}
return list;
}
}

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
CheckParamsInterceptor checkSourceInterceptor = new CheckParamsInterceptor();
//增加校验拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(checkSourceInterceptor).addPathPatterns("/**");
}
//跨域
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**");
}
}

这样就可以在控制器方法上对应参数加上注解即可。

还有一些spring mvc一定定义好的一些异常,通过统一处理异常的方式去校验参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//参数类型不匹配
//getPropertyName()获取数据类型不匹配参数名称
//getRequiredType()实际要求客户端传递的数据类型
@ExceptionHandler({TypeMismatchException.class})
@ResponseBody
public String requestTypeMismatch(TypeMismatchException ex){
ex.printStackTrace();
return outputJson(400, "参数类型不匹配,参数" + ex.getPropertyName() + "类型应该为" + ex.getRequiredType());
}
//缺少参数异常
//getParameterName() 缺少的参数名称
@ExceptionHandler({MissingServletRequestParameterException.class})
@ResponseBody
public String requestMissingServletRequest(MissingServletRequestParameterException ex){
ex.printStackTrace();
return outputJson(400, "缺少必要参数,参数名称为" + ex.getParameterName());
}

代码见:
https://github.com/huingsn/tech-point-record.git中的springboot-validation-demo

上一页1…345…25下一页
初晨

初晨

永远不要说你知道本质,更别说真相了。

249 日志
46 分类
109 标签
近期文章
  • WebSocket、Socket、TCP、HTTP区别
  • Springboot项目的接口防刷
  • 深入理解Volatile关键字及其实现原理
  • 使用vscode搭建个人笔记环境
  • HBase介绍安装与操作
© 2018 — 2020 Copyright
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4