0%

视屏教程

Day 1

安装

TODO

版本查询:node --version

Hello Word

新建测试文件 hello.js,写入内容如下

1
2
var foo = 'bar';
console.log(foo);

执行文件 node hello.js, 可以看到 log 输出

读文件

输出文件内容

1
2
3
4
5
var fs = require('fs')

fs.readFile('./helloworld.js', function(error, data) {
console.log(data.toString())
})

写文件和错误处理

1
2
3
4
5
var fs = require('fs')

fs.writeFile('./hello.md', 'writing to file', function(error) {
console.log('write success...')
})

简单 http 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var http = require('http')

var server = http.createServer()

server.on('request', function(request, response){
console.log('accept request, path: ' + request.url)

// 中文需要自定义 header
response.setHeader('Content-type', 'text/plain; charset=utf-8')
response.write('reponse given...')
response.write('你好')
// 必须用 end 结尾
response.end()
})

server.listen(3000, function() {
console.log('server started...')
})

核心模块

Node 为 JS 提供了很多服务器级别的 API,包装到具名的核心模块中。例如 fs/http/path/os 等,通过 require('xx') 使用。

模块系统

分三类:

  • 具名核心模块
  • 自己编写的文件模块 require('/path/to/b.js')

js 文件为顺序执行,包括 require 中的内容。

Node 中可以通过 exports.foo = ‘hello’ 暴露文件中的变量,通过 var bExports = require('./b') 得到

Content-type

fs.readFile() 之后可以通过指定 Content-type 来指定返回数据,专业名称叫做 mime 类型

无分号代码风格

代码以 (, [, ` 开头的,补分号确保语法解析正确。 ES6 中使用 反引号 凭借自负,支持换行。反引号中可以使用 ${} 做替换操作。

模版引擎

安装:npm install art-template

浏览器中使用 art-tempalte, 新建 html 文本,写入内容. 打开 browser 可以看到 console 中有对应的 ‘hello Jack’ log.

{{}} 被称为 mustach 语法

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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title> Test art template </title>
</head>

<body>
<script src="node_modules/art-template/lib/template-web.js"></script>


<script type="text/template" id="tpl">
我叫 {{ name }}
今年 {{ age }}
喜欢 {{ each hobbies }} {{ $value }} {{ /each }}
</script>

<script>
var ret = template('tpl', {
name: 'Jack',
age: 11,
hobbies: [
'唱',
'跳',
'rap'
]
})

console.log(ret)
</script>
</body>
</html>

Node 中使用 模版引擎

1
2
3
4
5
var template = require('art-template')

var ret = template.render('hello {{ name }}', { name: "Jack"})
console.log(ret)
// hello Jack

查看源代码能找到的都是服务器端渲染的,商品列表一般用服务器端渲染,方便 SEO 搜索引擎查找。

终端直接输入 node 可以得到自带的交互界面(REPL)

Node 中的模块系统

程序主要在:

  • EcmaScript 语言
    • 和浏览器不一样,没有 BOM,DOM
  • 核心模块
    • 文件操作 fs
    • http
    • url
  • 第三方模块
    • art-template
  • 自己写的模块

CommonJS 模块规范

  • 模块作用于
  • 使用 require 加载模块
  • 使用 exports 带出模块中成员

require 语法:var name = require('module'), 作用:

  • 执行被夹在模块中的代码
  • 得到被夹在模块中 exports 到处接口对象

exports 作用:

  • Node 中是模块作用域,默认文件中所有的成员只在当前文件模块有效
  • 对于希望可以被其他模块访问的成员,我们需要把这个公开的成员挂在到 exports 接口对象上

导出多个成员:

1
2
exports.a = 123
exports.b = 'hello'

导出单个成员:

1
2
3
4
5
6
7
8
module.exports = add

module.exports = {
add: function() {
return x + u
},
str: 'hello'
}

声明多个,后者覆盖前者

如果模块想要直接到处成员,而非挂在的方式,可以使用 module.exports=add

加载规则:

  • 优先从缓存加载
  • 判断模块标识符
    • 核心模块,核心模块文件已被编译成二进制文件,直接使用名字即可
    • 第三方模块,通过 npm 下载,通过 require(‘包名’) 引用
    • 自己写的模块

package.json

建议每个项目都要有 package.json, 管理包依赖。 –save 选项可以将依赖加进去 e.g. npm install jquery --save

这个文件可以通过 npm init 向导生成

npm 常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
npm --version

npm install --gloabl npm # 自己升级

npm init
npm init -y 跳过向导,快速生成

npm install packege_name
npm install packege_name --save
npm i -S package_name # 简写

npm uninstall package_name
# 新版的都不需要 --save 了

npm 加速

1
2
npm install --gloabl cnpm
cnpm install xxx

Express

原生的 http 在某些方面不足以满足开发需求,使用框架加快开发,代码高度统一。Express 是 node 的一个 web 框架。

hello world 代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var express = require('express')

var app = express()

app.get('/', function(req, res){
res.send('hello express...')
})

// 中文自动处理了
app.get('/about', function(req, res){
res.send('你好 express...')
})

app.listen(3000, function() {
console.log('app is running at 3000')
})

// 方便的公开指定目录
app.use('/public', express.static('./public/'))

热部署

nodemon, 代码修改完立刻生效 npm install --global nodemon, 使用时使用 nodemon app.js 即可

基本路由

1
2
3
4
5
6
7
app.get('/', function(req, res){
res.send('..')
})

app.post('/', function(req, res){
res.send('..')
})

静态文件

通过 app.use('/public', express.static('./public/')) 的方式公开资源访问。第一个参数为别名,可以为任何表达式,同文件夹名更容易辨识

Express 中配置使用 art-template 模版引擎

1
2
npm install --save art-template
npm install --save express-art-template

配置

1
app.engine('art', require('express-art-template'))

使用

1
2
3
4
5
6
app.get('/', function(req, res){
// express 默认回去项目中的 views 目录找 index.html
res.render('index.html', {
title: 'hello world'
})
})

如果要改默认 views 试图渲染存储目录,可以

1
app.set('views', 目录路径)

express 获取表单 post 请求

安装:npm install --save body-parser, 貌似新版的已经自动集成了,不需要自己下载

使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var express = require('express')
var bodyParser = require('body-parser')

var app = express()

// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }))

// parse application/json
app.use(bodyParser.json())

app.use(function (req, res) {
res.setHeader('Content-Type', 'text/plain')
res.write('you posted:\n')
res.end(JSON.stringify(req.body, null, 2))
})

PS: 对于 get 请求,内置了 req.query 对象作为 body 的容器

crud demo

1
2
3
npm init -y
npm i -S express
npm i -S bootstrap@3

MongoDB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mongo # 启动终端

exit # 退出

show dbs # 显示数据库

db # 当前数据库

use xxx # 切换指定数据库

db.students.insertOne({ "name": "Jack" }) # 插入数据

db.students.find() # 查询

show collections # 显示表

基本概念

  • 数据库
  • 集合 - 表
  • 文档 - 一条记录
  • 文档结构没有任何限制
  • 灵活,不需要建表,直接使用

Mongoose

基于官方包的再封装

1
2
npm init -y
npm i mongoose

跟着官方文档跑了一个 demo

Promise

为了解决 回调地狱 引入 Promise 语法

例子中用的 json-server, hp-server 工具挺有意思

Node 中的其他成员

每个模块中除了 require, exports 外,还默认带有两个字打的变量

  • __dirname
  • __filename

文件相对路径是针对 node 执行命令来说的。同时结合 path.join() 拼接路径

模块加载路径不受影响

看 Spring 的 BeanFactoryProcessor 部分的时候,看到 sort processor 方法中用了 Comparator 做比较依据, 已经忘的差不多了,复习一下

创建一个简单的对象,有 name 和 age 属性,分别新建两个比较器,一个是 name 字母顺序,一个是 age 大小逆序。自定义的比较器实现 Comparator 接口并重写 compare 方法即可

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
public class ComparatorTest {

public static void main(String[] args) {
A a1 = new A("a", 1);
A a2 = new A("b", 2);
List<A> list = new ArrayList<>();
list.add(a1);
list.add(a2);

System.out.println("before sort: " + list);
list.sort(new AgeSort());
System.out.println("after sort age: " + list);
list.sort(new NameSort());
System.out.println("after sort name: " + list);
}
}
// before sort: [A(name=a, age=1), A(name=b, age=2)]
// after sort age: [A(name=b, age=2), A(name=a, age=1)]
// after sort name: [A(name=a, age=1), A(name=b, age=2)]

/**
* age 逆序
*/
class AgeSort implements Comparator<A> {

@Override
public int compare(A o1, A o2) {
return o2.getAge() - o1.getAge();
}
}

/**
* name 顺序
*/
class NameSort implements Comparator<A> {

@Override
public int compare(A o1, A o2) {
return o1.getName().compareTo(o2.getName());
}
}

@Data
@AllArgsConstructor
class A {
private String name;
private int age;
}

看了视屏,过了一个周末,只剩一个大概的映像,其他都忘了,果然还是要实际操作一下印象才会深刻。mysql 使用 docker 版本的,参考 mysql 安装那篇教程

数据准别

视屏下方留言区有对应的 SQL 文件可以下载,按照上面的教程,安装完 docker 版本的 mysql 之后,默认会在 tmp 文件夹下创建共享文件。将 sql 文件复制到共享文件中,然后运行一下命令倒入数据

1
2
3
4
5
6
docker exec -it mysql-test /bin/bash
# 登陆 docker 中的 mysql 终端
mysql -h localhost -P 2999 -u root -p
# 导入数据
source /etc/mysql/conf.d/employees.sql
source /etc/mysql/conf.d/girls.sql

变量声明

  • 系统变量
    • 全局变量 - 系统级别的改动,跨会话有效,重启重置
    • 回话变量 - 新建回话(链接)会重置
  • 自定义变量
    • 用户变量
    • 局部变量

系统变量

1
2
3
4
5
6
7
8
9
10
11
12
-- 查看所有的系统变量
SHOW GLOBAL | [SESSION] VARIABLES;

-- 查看满足条件的部分系统变量
show global | [session] variables like '%char%';

-- 查看置顶的某个系统变量的值
select @@global | [session].系统变量名;

-- 赋值
set global | [session] 系统变量名 = 值;
set @@global | [session] .系统变量名 = 值;

自定义变量

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
-- 使用方式:声明,赋值,使用
-- 用户变量:
-- 作用域: 针对于当前会话有效,同会话变量作用域。可以放在任何地方,begin/end 内部或外部

-- 声明并初始化
SET @VARIABLE_NAME=VALUE;
SET @VARIABLE_NAME:=VALUE;
SELECT @VARIABLE_NAME:=VALUE;

-- 赋值
-- 方式一,同声明
-- 方式二, select into, 要求必须是**一个**值,不能是一组
select 字段 into 变量名 from 表;

-- 使用(查看用户变量值)
select @VARIABLE_NAME;

-- e.g.
set @count=1;
select count(*) into @count from employees;
SELECT @count;

-- 局部变量,作用域:仅仅在定义他的 begin/end 内部, 并且必须为第一句话

-- 声明
declear 变量名 类型;
declear 变量名 类型 default 值;
-- 赋值
-- 方式一
set 局部变量名=值;
set 局部变量名:=值;
select @局部变量名:=值;
-- 方式二:select into
select 字段 into 局部变量 from 表;

-- 使用
select 局部变量名;
类型 作用域 定义和使用位置 语法
用户变量 当前回话 会话中的任意位置 必须加@,不限定类型
局部变量 begin/end中 必须在 begin/end中,且为第一句 一般不用@,除了 select 语句,需要限定类型

Store Procedure

存储过程优点:

  • 代码重用
  • 简化操作
  • 预编译,减少联接次数
1
2
3
4
CREATE PROCEDURE 存储过程(参数列表)
BEGIN
存储过程体(一组合法的 SQL 语句)
END

参数列表包含三部分:参数模式,参数名,参数类型, e.g. IN stuname VARCHAR(20)

参数模式:

  • IN: 该参数作为输入
  • OUT: 该参数作为输出
  • INOUT: 该参数即作为输入又可以作为输出

存储过程体如果仅含有一句话,BEGIN END 可以省略

存储过程体中的每条 SQL 语句的结尾要求必须加分好

存储过程的结尾可以使用 DELIMITER 重新设置,DELIMITER 结束标志: e.g. DELIMITER $

调用:CALL 存储过程名(实参列表);

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
-- 1. 空参列表
-- 貌似只能在终端执行
DELIMITER $
CREATE PROCEDURE myp1()
BEGIN
INSERT INTO ADMIN (USERNAME, `PASSWORD`)
VALUES ('john1', 0000), ('rose', 0001), ('jack', 0002), ('tom', 0003), ('lin', 0004);
END $

CALL myp1()$

-- 2. 创建带 in 模式参数的存储过程
-- 根据女神名查询对应的男神信息
CREATE PROCEDURE myp2(IN beautyName VARCHAR(20))
BEGIN
SELECT bo.*
FROM boys bo
RIGHT JOIN beauty b ON bo.id = b.boyfriend_id
WHERE b.name = beautyName;
END $

CALL myp2('小昭')$

-- 传入双参数,验证登陆
CREATE PROCEDURE myp4(IN username VARCHAR(20), IN PASSWORD VARCHAR(20))
BEGIN
DECLARE result VARCHAR(20) DEFAULT '';

SELECT COUNT(*) INTO result
FROM admin
WHERE admin.username = username
AND admin.password = PASSWORD;

-- SELECT result;
SELECT IF(result>0, 'success', 'failed');
END $

CALL myp4('lin', 4)$

-- 3. 创建带 out 模式的存储过程
-- 根据女神名返回男神名
CREATE PROCEDURE myp5(IN beautyName VARCHAR(20), OUT boyName VARCHAR(20))
BEGIN
SELECT bo.boyname INTO boyName
FROM boys bo
INNER JOIN beauty b ON bo.id = b.boyfriend_id
WHERE b.name=beautyName;
END $

# 调用
CALL myp5('小昭', @bName)$
SELECT @bName$

-- 根据女神名返回对应男神名和魅力值
CREATE PROCEDURE myp6(IN beautyName VARCHAR(20), OUT boyName VARCHAR(20), OUT userCP INT)
BEGIN
SELECT bo.boyname, bo.userCP INTO boyName, userCP
FROM boys bo
INNER JOIN beauty b ON bo.id = b.boyfriend_id
WHERE b.name=beautyName;
END $

# 调用
CALL myp6('小昭', @bName, @usercp)$
SELECT @bName$, @userCP$

-- 4. 带 inout 模式的存储过程
-- 传入 a,b 返回对应的翻倍值
CREATE PROCEDURE myp7(INOUT a INT, INOUT b INT)
BEGIN
SET a=a*2;
SET b=b*2;
END $

SET @m=10$
SET @n=20$
call myp7(@m, @n)$

-- 删除
DROP PROCEDURE 存储过程名称;
-- 查看
SHOW CREATE PROCEDURE myp2;

-- 传入日期,返回日期字符串
CREATE PROCEDURE test_pro4(IN mydata DATETIME, OUT strDate VARCHAR(20))
BEGIN
SELECT DATE_FORMAT(mydata, '%y-%m-%d') INTO strDate;
END $

CALL test_pro4(now(), @strDate)$
SELECT @strDate$

Function

区别:存储过程可以有任意个返回,函数有且仅有一个返回

创建语法:

CREATE FUNCTION 函数名(参数列表) RETURENS 返回类型
BEGIN
函数体
END

参数列表:参数名 参数类型

函数体:肯定有 return,没有报错,可以不放在最后,但是不建议

函数体为空,可以省略 begin end

使用 delimiter 语句设置结束标记

调用语法: SELECT 函数名(参数列表)

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
-- 1. 无参有返回
-- 返回公司员工个数
CREATE FUNCTION myf1() RETURNS INT
BEGIN
DECLARE c INT DEFAULT 0;
SELECT COUNT(*) INTO c
FROM employees;
RETURN c;
END$
SELECT myf1()$

-- 报错:ERROR 1418 (HY000): This function has none of DETERMINISTIC, NO SQL
-- 设置:SET global log_bin_trust_function_creators=TRUE;

-- 2. 有参数返回
-- 根据员工名返回工资
CREATE FUNCTION myf2(empName VARCHAR(20)) RETURNS DOUBLE
BEGIN
SET @sal=0;
SELECT salary INTO @sal
FROM employees
WHERE last_name = empName;

RETURN @sal;
END$
select myf2('Kochhar') $

-- 根据部门名返回平均工资
CREATE FUNCTION myf3(deptName VARCHAR(20)) RETURNS DOUBLE
BEGIN
DECLARE sal DOUBLE;

SELECT AVG(salary) INTO sal
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE d.department_name=deptName;

RETURN sal;
END$
select myf2('Kochhar') $

-- 查看和删除,同存储过程
SHOW CREATE FUNCTION myf3();
DROP FUNCTION myf3;

流程控制

分支结构:

if 函数

select if(expr1, expr2, expr3). expr1 成立则返回 expr2 否则 expr3

case 结构

方式一:

CASE 变量|表达式|字段
WHEN 要判断的值 THEN 返回值 1 或语句 1;
WHEN 要判断的值 THEN 返回值 2 或语句 2;

ELSE 要返回的值 n 或语句 2;
END CASE;

方式二:

CASE
WHEN 要判断的条件1 THEN 返回值 1
WHEN 要判断的条件2 THEN 返回值 2

ELSE 要返回的值 n
END

可以作为表达式,嵌套在其他语句中使用,比如 BEGIN END 中/外

可以作为独立的语句去使用,只能放在 BEGIN/END 中

如果 WHEN 中条件成立,则执行 THEN 然后结束,如果都不满足,执行 ELSE。ELSE 可以省略。如果没有 ELSE 并且 WHEN 都不满足,返回 NULL

1
2
3
4
5
6
7
8
9
10
11
12
-- 存储过程,显示成绩
CREATE PROCEDURE test_case(IN score INT)
BEGIN
CASE
WHEN score >= 90 AND score <= 100 THEN SELECT 'A';
WHEN score >=80 THEN SELECT 'B';
WHEN score >=60 THEN SELECT 'C';
ELSE SELECT 'D';
END CASE;
END$

CALL test_case(95)$

if 结构

if 条件1 then 语句1;
elseif 条件2 then 语句2;

[else 语句n;]
end if;

只能放在 begin/end 中

1
2
3
4
5
6
7
8
9
-- 根据传入成绩返回等级
CREATE FUNCTION test_if(score INT) RETURNS CHAR
BEGIN
IF score >= 90 AND score <=100 THEN RETURN 'A';
ELSEIF score >= 80 THEN RETURN 'B';
ELSEIF score >= 60 THEN RETURN 'C';
ELSE RETURN 'D';
END IF;
END $

循环

分类:while, loop, repeat

循环控制:iterate, 对应 continue; leave 对应 break;

[标签:] loop 循环条件 do
循环体
end while [标签];

[标签:] loop
循环体
end loop [标签];

可用于模拟死循环

[标签:] repeat
循环体
until 结束循环条件 [标签];

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
-- 批量插入 admin
CREATE PROCEDURE pro_while2(IN insertCount INT)
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i<insertCount DO
INSERT INTO admin (username, `password`) VALUES (CONCAT('jjjj', i), '666');
SET i=i+1;
END WHILE;
END $

-- 添加 leave 控制
-- 清空并重置 index
TRUNCATE TABLE admin$

CREATE PROCEDURE pro_while3(IN insertCount INT)
BEGIN
DECLARE i INT DEFAULT 1;
a: WHILE i<insertCount DO
INSERT INTO admin (username, `password`) VALUES (CONCAT('jjjj', i), '666');
IF
i>= 20 THEN LEAVE a;
END IF;
SET i=i+1;
END WHILE a;
END $
call pro_while3(100)$

-- iterate, 只插入偶数次
TRUNCATE TABLE admin$
CREATE PROCEDURE pro_while4(IN insertCount INT)
BEGIN
DECLARE i INT DEFAULT 1;
a: WHILE i<insertCount DO
SET i=i+1;
IF
MOD(i, 2) != 0 THEN ITERATE a;
END IF;
INSERT INTO admin (username, `password`) VALUES (CONCAT('jjjj', i), '666');
END WHILE a;
END $
call pro_while4(100)$

其他 SQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- 创建备份表
CREATE TABLE <schema>.USER_ACCOUNT_BAK LIKE <schema>.USER_ACCOUNT;
-- 复制行
INSERT
INTO
<schema>.USER_ACCOUNT_BAK uab (uab.ACCOUNT_ID ,
uab.ACCOUNT_STATUS,
uab.PERSON_ID ,
uab.USERNAME ,
uab.ACCOUNT_TYPE,
uab.VISIBILITY)
SELECT
ua.ACCOUNT_ID ,
ua.ACCOUNT_STATUS,
ua.PERSON_ID ,
ua.USERNAME ,
ua.ACCOUNT_TYPE,
ua.VISIBILITY
FROM
<schema>.USER_ACCOUNT ua;

gradle, maven 都提供了现成的打 jar 功能

Maven

这里以 maven 为例,创建工程如下

1
2
3
4
5
6
.
├── java
│ └── com
│ └── jk
│ └── Main.java
└── resources
1
2
3
4
5
6
7
8
9
package com.jk;

public class Main {
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println("Index: " + i + ", Value: " + args[i]);
}
}
}

在 pom 文件中添加配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<build>
<plugins>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.5.5</version>
<configuration>
<archive>
<manifest>
<mainClass>com.jk.Main</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>

</plugins>
</build>

终端输入命令:mvn package assembly:single, 运行后在 src 同级目录下会生产 target 文件夹,其中包含了对应的 jar 包, 选择带 with-dependencies 的那个

1
2
3
java -jar app-jar-1.0-SNAPSHOT-jar-with-dependencies.jar hello world 
Index: 0, Value: hello
Index: 1, Value: world

PS: 直接选 plugins -> assembly -> assembly:single 不 work,不是很了解为啥,需要系统学一下 maven 才知道

Spring 中 BeanFactoryPostProcessor 相关代码的源码解析,涉及到的主要 class 整理如下

  • BeanFactory: bean 工厂,生产单个 bean, 典型方法 getBean/containsBean/isSingleton/getType etc
  • ListableBeanFactory: 继承自 BeanFactory,额外拥有枚举 bean 的能力,即批量返回
  • HierarchicalBeanFactory: 继承自 BeanFactory,额外拥父子容器的概念
  • AutowireCapableBeanFactory: 继承自 BeanFactory, 额外提供管理非自动装配 bean 的能力
  • SingletonBeanRegistry: 保证单例的注册器
  • ConfigurableBeanFactory: 继承自 HierarchicalBeanFactory + SingletonBeanRegistry, 额外的配置能力
  • ConfigurableListableBeanFactory: 整合 listale, hierarch 和 autowire 的能力,额外提供分析修改 bean definition 的能力,还有 pre-instantiate singletons 的能力
  • AliasRegistry: alias 管理接口,典型方法 registerAlias/removeAlias etc
  • BeanDefinitionRegistry: 继承自 AliasRegistry,增加了持有 bean definition 的功能,是 Spring factory 包下唯一一个有这种能力的接口
  • SimpleAliasRegistry: AliasRegistry 的简单实现,内部通过 map 存储 name 和 alias 的对应关系
  • DefaultSingletonBeanRegistry: 类实现,强调 registry 和 singleton 属性,不包含任何 bean definition 的概念
  • FactoryBean: 不是普通的 bean, 这个 bean 的作用类似工厂,用来生产其他 bean 的. 通常用在 infrastructure code 中
  • FactoryBeanRegistrySupport: FactoryBeanRegistrySupport 派生类, 专门处理 FactoryBean 的 instance
  • AbstractBeanFactory: BeanFactory 抽象实现,并不具备 Listable 的功能,具备从 resource 中提取 bean definition 的功能,核心方法 getBeanDefinition
  • AbstractAutowireCapableBeanFactory: 实现了默认的 bean 创建逻辑,没有 bean definition 注册能力

Spring 中有两种 post processor,一种是 BeanFactoryPostProcessor, 另一种是 BeanPostProcessor. BeanFactoryPostProcessor 执行时机为:bean definition 加载完成之后,bean 实例化之前。而 BeanPostProcessor 则是 bean 初始化的后置处理器,包含两个方法,可以分别在初始化之前和之后执行。

总结各个扩展点的执行顺序:@PostConstruct -> InitializingBean -> initMethod -> @PreDestory -> DisposableBean -> destoryMethod

BeanFactoryPostProcessor 使用案例

Xml 配置的方式

不需要添加额外的注解,新建测试 bean 和 BeanFactoryPostProcessor 之后,通过 xml 关联,并在测试代码中通过 ClassPathXmlApplicationContext 加载配置即可。示例说明,测试 bean 中包含 name, age 属性,我们通过 BeanFactoryPostProcessor 在 bean definition 加载完之后修改 age 的值并将 scope type 修改为 prototype

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
System.out.println("------- BeanFactoryPostProcessor::postProcessBeanFactory");
BeanDefinition bd = beanFactory.getBeanDefinition("postProcessorTestBean");

MutablePropertyValues propertyValues = bd.getPropertyValues();
if (propertyValues.contains("age")) {
propertyValues.addPropertyValue("age", 24);
}
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
}
}

public class PostProcessorTestBean {
private String name;
private Integer age;

// getter + setter + toString
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="postProcessorTestBean" class="com.bin.postprocessor.PostProcessorTestBean">
<property name="name" value="Tom"/>
<property name="age" value="12" />
</bean>

<bean id="myBeanFactoryPostProcessor" class="com.bin.postprocessor.MyBeanFactoryPostProcessor"/>
</beans>

测试用例

1
2
3
4
5
6
7
8
9
10
@Test
public void test_config_with_xml() {
ApplicationContext ctx2 = new ClassPathXmlApplicationContext("postprocessor/bean_with_bean_factory.xml");
PostProcessorTestBean bean2 = (PostProcessorTestBean) ctx2.getBean("postProcessorTestBean");
System.out.println(bean2);
System.out.println("------- Is singleton: " + ctx2.isSingleton("postProcessorTestBean"));
}
// ------- BeanFactoryPostProcessor::postProcessBeanFactory
// PostProcessorTestBean{name='Tom', age=24}
// ------- Is singleton: false

Annotation 配置的方式

沿用之前的 bean 和 processor 代码,分别为他们添加 @Component 和 @Value 注解并设置值,通过 AnnotationConfigApplicationContext 加载配置执行测试

1
2
3
4
5
6
7
8
9
10
@Test
public void test_config_with_annotation() {
ApplicationContext ctx = new AnnotationConfigApplicationContext("com.bin.postprocessor");
PostProcessorTestBean bean = (PostProcessorTestBean) ctx.getBean("postProcessorTestBean");
System.out.println(bean);
System.out.println("------- Is singleton; " + ctx.isSingleton("postProcessorTestBean"));
}
// ------- BeanFactoryPostProcessor::postProcessBeanFactory
// PostProcessorTestBean{name='Jack', age=22}
// ------- Is singleton; false

PS: 这里需要注意的是,value 并没有改变,因为完成 bean definition 加载的时候,@Value 并没有完成解析,所以修改时无效的。这一点可以看看对应的源码,后面再完善一下。从 BeanFactoryPostProcessor 的定义来说,这种用法才正确,之前的用法反而有点邪道的意思了。

BeanPostProcessor 使用案例

和之前的 BeanFactoryPostProcessor 使用基本是一样的套路

Xml 配置的方式

bean 沿用之前的, BeanPostProcess 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MyBeanPostProcessor implements BeanPostProcessor {

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("------- BeanPostProcessor::postProcessBeforeInitialization");
if (beanName.endsWith("postProcessorTestBean")) {
((PostProcessorTestBean) bean).setAge(100);
}
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("------- BeanPostProcessor::postProcessAfterInitialization");
if (beanName.endsWith("postProcessorTestBean")) {
((PostProcessorTestBean) bean).setName("Updated");
}
return bean;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="postProcessorTestBean" class="com.bin.postprocessor.PostProcessorTestBean">
<property name="name" value="Tom"/>
<property name="age" value="12" />
</bean>

<bean id="myBeanPostProcessor" class="com.bin.postprocessor.MyBeanPostProcessor"/>
</beans>

测试用例

1
2
3
4
5
6
7
8
9
@Test
public void test_bean_post_processor_xml() {
ApplicationContext ctx = new ClassPathXmlApplicationContext("postprocessor/bean_post_processor.xml");
System.out.println(ctx.getBean("postProcessorTestBean"));
}
// ------- PostProcessorTestBean::constructor
// ------- BeanPostProcessor::postProcessBeforeInitialization
// ------- BeanPostProcessor::postProcessAfterInitialization
// ------- print in test: PostProcessorTestBean{name='Updated', age=100}

Annotation 配置的方式

为前面的 processor 类添加 Component 注解并通过 AnnotationConfigApplicationContext 加载即可

1
2
3
4
5
6
7
8
9
@Test
public void test_bean_post_processor_annotation() {
ApplicationContext ctx = new AnnotationConfigApplicationContext("com.bin.postprocessor");
System.out.println(ctx.getBean("postProcessorTestBean"));
}
// ------- PostProcessorTestBean::constructor
// ------- BeanPostProcessor::postProcessBeforeInitialization
// ------- BeanPostProcessor::postProcessAfterInitialization
// PostProcessorTestBean{name='Updated', age=100}

由此可见 initialization 和属性设置是两个概念,属性设置应该是在实例化之后,BeanPostProcessor 之前的操作,不然 age 就会改变了

顺便加介绍一下 initialization 的方法

Xml 配置的方式

之前说过 BeanPostProcessor 是初始化阶段的后置处理器,初始化可以通过在 xml 中配置 init-method 实现,对应的还有销毁方法 destory-method

在之前的测试 bean 中新加方法

1
2
3
4
5
6
7
public void init() {
System.out.println("------- initMethod");
}

public void cleanUp() {
System.out.println("------- destroyMethod");
}

在 xml 中 bean 声明部分指定对应的方法,再结合 processor 查看执行顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="postProcessorTestBean" class="com.bin.postprocessor.PostProcessorTestBean">
<property name="name" value="Tom"/>
<property name="age" value="12" />
</bean>

<bean id="myBeanPostProcessor" class="com.bin.postprocessor.MyBeanPostProcessor"/>
</beans>

测试如下

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void test_init_cleanup() {
ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("postprocessor/bean_init_cleanup.xml");
System.out.println(ctx.getBean("postProcessorTestBean"));
ctx.close();
}
// ------- PostProcessorTestBean::constructor
// ------- BeanPostProcessor::postProcessBeforeInitialization
// ------- initMethod
// ------- BeanPostProcessor::postProcessAfterInitialization
// PostProcessorTestBean{name='Updated', age=100}
// ------- destroyMethod

PS: close() 是 ConfigurableApplicationContext 中添加的接口,再上层就不具备这个能力了

Annotation 配置的方式

上面的功能我们可以通过创建一个 @Configuration 类达到同样的效果。 这时,原来 bean 上的 @Component 标签需要去掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
public class LifecycleConfig {
@Bean(initMethod = "init", destroyMethod = "cleanUp")
public PostProcessorTestBean getBean() {
System.out.println("------- init LifecycleConfig");
return new PostProcessorTestBean();
}
}

@Test
public void test_config() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext("com.bin.postprocessor");
System.out.println(ctx.getBean("postProcessorTestBean"));
ctx.close();
}

// ------- PostProcessorTestBean::constructor
// ------- BeanPostProcessor::postProcessBeforeInitialization
// ------- initMethod
// ------- BeanPostProcessor::postProcessAfterInitialization
// ------- print in test: PostProcessorTestBean{name='Updated', age=100}
// ------- destroyMethod

类似的还有 @PostConstruct/@PreDestory,我们为之前的测试 bean 新增两个测试方法并在此运行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@PostConstruct
public void postConstruct() {
System.out.println("------- invoke postConstruct");
}

@PreDestroy
public void PreDestroy() {
System.out.println("------- invoke PreDestroy");
}

// ------- PostProcessorTestBean::constructor
// ------- BeanPostProcessor::postProcessBeforeInitialization
// ------- @PostConstruct
// ------- initMethod
// ------- BeanPostProcessor::postProcessAfterInitialization
// ------- print in test: PostProcessorTestBean{name='Updated', age=100}
// ------- @PreDestroy
// ------- destroyMethod

可以看到 postConstruct 和 PreDestroy 是包在 initialization 最外层的

接口方式

除此之外还有一种通过实现接口来扩展的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PostProcessorTestBean implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("------- InitializingBean::afterPropertiesSet");
}

@Override
public void destroy() throws Exception {
System.out.println("------- DisposableBean::destroy");
}
}

// ------- PostProcessorTestBean::constructor
// ------- BeanPostProcessor::postProcessBeforeInitialization
// ------- @PostConstruct
// ------- InitializingBean::afterPropertiesSet
// ------- initMethod
// ------- BeanPostProcessor::postProcessAfterInitialization
// ------- print in test: PostProcessorTestBean{name='Updated', age=100}
// ------- @PreDestroy
// ------- DisposableBean::destroy
// ------- destroyMethod

最近看的 Spring 课程上面,那些讲师都会把 Spring 源码下载到本地然后在上面写 demo, 做笔记什么的,感觉这个方式很棒,实践一下。

spring-framework 有自己的官方 git 地址的,而且 readme 上也将本地编译的步骤写的很清楚了,再参考一下其他人的博客,难度应该不大。

实践

  1. 下载 v5.2.x 源码进行编译(看 Supported Versions 里的信息,这个版本是支持 JDK8 的)
  2. cd spring-framework 修改 gradle.build 文件配置,默认用的官方源会很慢
  3. ./gradlew build 构建,这个命令会下载 gradle.properties 中指定的 gradle 版本用于构建
  4. 文章的末尾有说怎么导入 Idea, 先 ./gradlew :spring-oxm:compileTestJava
  5. 在 Idea 中新建项目,从 existing 的 code 中导入,选择 Gradle project,其他就依此点下去就行了,本地导入成功
  6. 新建一个 gralde module: spring-debug 写一个简单的 Spring demo, 测试项目是否构建成功
  7. 在 module 的 gradle 文件中 dependencies 下添加 spring-context 的依赖
  8. 测试通过,删掉 .git 文件,重新上传到自己账户下做为笔记源文件
1
compile(project(":spring-context"))
1
2
3
4
5
6
7
8
9
; -- gradle.build 修改 repo 信息如下 --

repositories {
mavenCentral()
maven { url "https://repo.spring.io/libs-spring-framework-build" }
maven { url "https://repo.spring.io/snapshot" } // Reactor
maven {url 'https://maven.aliyun.com/nexus/content/groups/public/'} //阿里云
maven {url 'https://maven.aliyun.com/nexus/content/repositories/jcenter'}
}

Issues

  1. 使用 JDK8 编译 v5.3.12 的 code 有三个 module 编译失败,”Execution failed for task ‘:spring-instrument:compileJava’. > invalid source release: 17”。将源码切换到 v5.2.18 成功,但是有几个 UT 挂了,直接删掉
  2. lombok 不 work, 添加 @Data 之后对应的方法并没有生产,手写 getter/setter 可以 work

参考

从广义上讲,不管 Spring 框架自发布到现在经过多少次迭代,其本质是始终不变的,都是为了提供各种服务,以帮助我们简化基于 POJO 的 Java 应用程序开发。

Spring 框架为 POJO 提供的各种服务组成的生命树如下

spring tree

Spring Core 是基础,提供了 IoC 容器的实现,帮助我们通过依赖注入方式管理对象之间的依赖关系。AOP 采用 Proxy 模式构建,结合 IoC, 增强 POJO 能力。

在 Core 和 AOP 的基础上,提供数据库和事物的服务, Spring 中的事务管理抽象层是 AOP 的最佳实践。

为了简化 Java EE 的各种服务,Spring 还提供了对应的简化服务,怎对 Web 开发,提供了对应的 Web MVC。

IoC

IoC(Inverse of Control) 控制反转,也有人叫做依赖注入(DI - Dependency Injection). 不过 Spring 的创始人说这两个是不同的概念, DI 是 IoC 的一种表现形式,这里不纠结这么多。为了便于理解,从书上抄一段代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class FXNewsProvider {
private IFXNewsListener newsListener;
private IFXNewsPersister newsPersister;

public void getAndPersistNews() {
String[] newsIds = newsListener.getAvailableNewsIds();
if (newsIds.length == 0) {
return;
}

for (String newsId : newsIds) {
FXNewsBean newsBean = newsListener.getNewsByPK(newsId);
newsPersister.persistNews(newsBean);
newsListener.postProcessIfNecessary(newsId);
}
}
}

上面的类中为了提供 getAndPersistNews() 这个功能,需要调用内部两个接口的方法。传统做法中,为了拿到接口的实例我们会写类似如下的代码

1
2
3
4
public FXNewsProvider () {
this.newsListener = new DowJonesNewsListener();
this.newsPersister = new DowJonesNewsPersister();
}

这种方式中,我们通过新建接口示例拿到对象并提供服务。但是细想一下,其实我们并不需要知道接口的具体实现,我们想要的只是,当我们想要用借口的服务时,有对应的实例能调用方法即可,至于示例的表现形式我们根本不 care。

PS: 这种做法的中二表现形式 - 神说,要有光!然后他就有了。

Spring 提供了 IoC Service Provider, 充当你的管家,他可以帮你管理实例,你只需要调用方法即可,不需要你管理 bean。将这个 bean 的控制权托管给 IoC Service, 这就是控制反转。

但是 IoC Service Provider 并不会读心术,当你给出上面的代码的时候,他并不知道去那里帮你找来接口的实例。这里就引入了一些规范,我们可以通过三种方式达到依赖注入的效果

  • 构造方式注入 - constructor injection
  • setter 方法注入 - setter injection
  • 接口注入 - interface injection,过时了,了解即可

一句话概括 IoC 可以带给我们什么:IoC 是一种可以帮助我们解偶各业务对象间依赖关系的对象绑定方式。

IoC 是一种策略,而 IoC Service Provider 就是这个策略的实施者。Spring 的 IoC 容器就是一个提供以来注入服务的 IoC Service Provider。

IoC Service Provider 职责就两个:

  • 业务对象的构建管理:将构建逻辑从客户端剥离,避免污染业务逻辑
  • 业务对象间的依赖绑定:最艰巨也是最重要的任务,正确的匹配对象之间的依赖

IoC Service Provider 产品使用的注册对象管理信息方式主要有

  • 直接编码方式
  • 配置文件方式:properies, xml 等
  • 元数据方式:注解

Spring 的 IoC 容器是一个超集,IoC Service Provider 只是其中的一部分,除此之外,他还提供了对象生命周期管理,API 等很多功能。

Spring IoC

Spring 提供了两种容器类型

  • BeanFactory: 基础类型的 IoC 容器,提供完整的 IoC 服务支持,采用 lazy-load。
  • ApplicationContext: 构建与 BeanFactory 之上,提供其他一些高级特性,比如 event,i18n 等。

ioc container relationship

BeanFactory 顾名思义就是一个工厂,你提供原料然后他给你成品,至于中间过程,作为用户,你并不需要知道,这是框架的职责范围。

问题

Q: Spring 的 容器 怎么体现的

A: 容器即装东西的地方,项目中的 Bean 都是通过他保管的(存在 map 中),所以还是很贴切的

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:

输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

提示:

你可以假设 nums 中的所有元素是不重复的。
n 将在 [1, 10000]之间。
nums 的每个元素都将在 [-9999, 9999]之间。

题解

出错的点:

  1. while 条件少了 ‘=’
  2. mid 取值没有考虑越界
  3. start, end 没有 +/-1 优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class S704 {
public static void main(String[] args) {
int[] arr = new int[]{-1, 0, 3, 5, 9, 12};
System.out.println(new S704().search(arr, 9));
}

public int search(int[] nums, int target) {
int start=0, end=nums.length - 1;
while (start <= end) {
int mid = start + (end - start) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
start = mid + 1;
} else {
end = mid - 1;
}
}
return -1;
}
}

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例 1:

输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3

解题 1

通过 Set 的特性解题,将元素一次存入 set, 如果 add 失败,则为重复元素,返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {

public static void main(String[] args) {
int[] arr = new int[]{ 2, 3, 1, 0, 2, 5, 3 };
System.out.println(new Solution().findRepeatNumber(arr));
}

public int findRepeatNumber(int[] nums) {
Set<Integer> set = new HashSet<>();
for (int num : nums) {
if (!set.add(num)) {
return num;
}
}
return -1;
}
}

我很喜欢这个解法,很直接了当,时间/空间 复杂度都是 O(n)

解题 2

活用题中另一个条: 件数字范围 0-(n-1), 如果将数组中的值一次排开,重复数字会在相同的下标下有冲突这一特性解题,时间复杂度 O(n), 空间复杂度 O(1)

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

public static void main(String[] args) {
int[] arr = new int[]{2, 3, 1, 0, 2, 5, 3};
System.out.println(new Solution2().findRepeatNumber(arr));
}

public int findRepeatNumber(int[] nums) {
int i = 0;
while(i < nums.length) {
if (i == nums[i]) {
i ++;
continue;
}
if (nums[i] == nums[nums[i]]) return nums[i];

int tmp = nums[i];
nums[i] = nums[tmp]; // 使用 tmp 做索引,nums[i] 会改变
nums[tmp] = tmp;
}
return -1;
}
}