Python

一、基础

1
2
3
4
5
6
7
# 变量,直接赋值不用声明
message = "this is message"

# 类型转换
# str -> int
ageStr = '12'
age = int(ageStr)

字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
name = "ifangcy"

# 常用方法,不会修改字符串本身而是返回一个新字符串
name.title()
name.upper()
name.lower()
name.rstrip()
name.lstrip()
name.strip()

# Python 中拼接字符串时不会自动把其他类型转成字符串,需要手动转
age = 30
message = "I'm " + str(age)

数字

1
3**2  # 3的2次方

列表 list

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
bicycles = ['trek', 'cannondale', 'redline', 'specialized']

# 索引使用负数从尾部取元素,注意负值从 -1 开始
bicycles[-1]

# 判断列表中是否有指定值
if 'trek' in bicycles:
print('in')
# 判断列表中是否没有指定值
if 'trek' not in bicycles:
print('not in')

# 判断列表是否为空
if bicycles:
print("非空")
else:
print('空')

# 添加
bicycles.append('ducati')
bicycles.insert(0, 'ducati')

# 删除
del bicycles[0]
# 删除最后一个元素并拿到该元素以便后续使用
pop_value = bicycles.pop()
# pop 也可以删除指定位置元素
pop_value2 = bicycles.pop(0)
# 删除指定值的元素,注意这个方法只能删除第一个匹配的元素,如果列表中有多个该值的元素只会删除第一个
bicycles.remove('ducati')
while 'ducati' in bicycles:
bicycles.remove('ducati')

# 排序,永久修改列表自身
# 按字母顺序正序排序
bicycles.sort()
# 逆序
bicycles.sort(reverse=True)

# 排序,临时排序也就是返回一个新排序后的列表
sorted(bicycles)
sorted(bicycles, reverse=True)

# 逆序,永久修改列表
bicycles.reverse()
reversed(bicycles)

# 列表长度
len(bicycles)

# 遍历列表
for item in bicycles:
print(item)

# range 生成列表
# [1, 2, 3, 4, 5]
numbers = list(range(1, 6))
numbers = list(range(30))
# 使用步长,[1, 3, 5]
list(range(1, 6, 2))

# 数字列表有几个特殊的函数
min(numbers)
max(numbers)
sum(numbers)

# 列表解析:使用一行代码包含 for 循环列表和生成新元素的操作
squares = [value**2 for value in range(1, 11)]

# 切片,从指定的第一个位置到第二个位置前一个
players = ['charles', 'mertina', 'michael', 'florence', 'eli']
players[1:4]
players[:4] # 从第一个元素到第三个元素
players[1:] # 从第二个元素到最后一个元素
players[:] # 所有元素
players[-3:] # 最后三个元素

# 注意,Python 中的切片和 Go 中的不同,Python 中的切片是真正的深拷贝,切片和原来的列表不共享内存

元组 tuple

1
2
# 元组和列表的区别就是元组的值不能修改
dimensions = (200, 500)

字典

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
alien = { "color": "green", "points": 5 }

# 使用
alien['color']

# 添加
alien['x_position'] = 10
alien['y_position'] = 20

# 删除
del alien['color']

# 遍历
# 遍历索引、值
for key, value in alien.items():
print("key: " + key + ", value: " + value)
# 遍历索引
for key in alien.keys():
print("key: " + key)
# 遍历值
for value in alien.values():
print("value: " + value)

# 使用 sorted 排序
for key in sorted(alien.keys()):
print("key: " + key)

# 使用 set 去除重复项
for value in set(alien.values):
print("value: " + value)

条件

1
2
3
age = 30
age > 30 and age < 50
age > 20 or age < 10

函数

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
# 定义
def describe_pet(animal_type, pet_name):
print("animal_type: " + animal_type + ", pet_name: " + pet_name)

# 参数默认值,有默认值的参数是可选参数,Python 中的默认参数没有要求一定放在最后
def describe_pet(animal_type, pet_name="harry"):
pass

# 数量不定参数,可接受任意个数参数,函数内部使用元组接收,必须作为最后一个参数
def make_pizza(size, *toppings):
pass
make_pizza(12, 'green peppers', 'extra cheese')

# 数量不定字典参数,可接受任意个键值对参数,函数内部使用字典接收,必须作为最后一个参数
def make_pizza(size, **toppings):
pass
make_pizza(12, location="shanghai", field="physics")

# 使用
describe_pet("hamster", "harry")
describe_pet(animal_type="hamster", pet_name="harry")

# 函数中修改列表参数会影响实参值
unprinted_designs = ["iphone case", "robot pendant"]
completed_models = []

def change(unprinted_designs, completed_models):
while unprinted_designs:
value = unprinted_designs.pop()
completed_models.append(value)

change(unprinted_designs, completed_models)

# 如果想让函数中修改列表不影响外层实参,可以传递列表切片
change(unprinted_designs[:], completed_models)

类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 定义类 
class Car():
# __init__ 是一个特殊方法,创建实例会自动调用,相当于其他语言中的构造函数,第一个参数必须是 self
def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
# Python 方法中的 self 都是手动传进去的
def get_descriptive_name(self):
pass

# 继承
# 子类在括号中指定父类
class ElectricCar(Car):
# 子类中的 __init__ 第一句必须调用父类 __init__ 初始化
def __init__(self, make, model, year):
super().__init__(self, make, model, year)

模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Python 中不需要显示指定,文件名即是模块名
# 文件夹中的 py 文件,完整路径作为模块名,比如 /a/b/demo.py 模块名为 a.b.demo

# 导入模块
import pizza
# 使用别名导入模块
import pizza as newPizza

# 导入模块中指定方法
from pizza import func1, func2
# 使用别名导入方法
from pizza import func1 as newFunc

# 导入模块中的指定类
from pizza import Class1, Class2

# 导入模块中所有方法和类
from pizza import *

文件操作

1
2
3
4
5
6
7
8
9
10
11
12
13
# 读文件,注意这里的文件路径在 windows 中应该使用 \
with open('path/demo.txt') as file_object:
# 一次读取所有内容
contents = file_object.read()
print(contents)
# 逐行读取
for line in file_object:
print(line.rstrip())


# 写文件,写入文件是需要指定读取模式:r 只读(默认),w 写入,a 追加,r+ 读写
with open('path/demo.txt', 'w') as file_object:
file_object.write("This is content")

异常

1
2
3
4
5
6
7
8
9
try:
# 可能出错的代码
answer = int(first_number) / int(second_number)
except ZeroDivisionError:
# 捕获处理异常
print('You cant divide by 0!')
else:
# 只有 try 中执行成功之后才会执行的内容放到这里
print(answer)

存储数据

1
2
3
4
5
6
7
8
9
10
players = ['charles', 'mertina', 'michael', 'florence', 'eli']

# 保存数据到文件中
with open('demo.txt', 'w') as file_object:
json.dump(players, file_object)

# 从文件中读取数据
with open('demo.txt') as file_object:
newPlayers = json.load(file_object)
print(newPlayers)

二、Django

在环境变量中配置参数

为不同环境使用不同 settings.py

写测试,测试覆盖度

三、PyCharm 远程开发

1、SSH 连接服务器

新增 SFTP 方式服务器:

3 处的 Root Path 是服务器上的项目上一级目录,记住是上一级目录。

这时候点击 Test Connection 如果提示连接失败,查看服务器是否开启了 ssh 服务:

1
2
3
4
5
6
# ubuntu
# 查看 ssh 状态
sudo service ssh status
# 启动 ssh
sudo service ssh start
sudo service ssh restart

如果确定 ssh 启动还是连接失败,提示:

1
sshd: no hostkeys available — exiting

修改配置

1
2
3
4
5
6
# 1
ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key

# 修改 /etc/ssh/sshd_config
PasswordAuthentication yes

1 处的 Local Path 是代码拉取到本机时存放的项目目录。

2 处的 Deployment Path 是服务器上的项目目录名称,这个地址会和上一步的 Root Path 合并一起用来查找服务器上的项目目录 ${Root Path}/${Deployment Path}。

选择刚刚新建的服务器。

1 处 Host 即服务器 IP 地址

2 处 Run Browser 即服务器地址加端口

3 处 Python Interpreter 即上一步设置的 Python Interpreter

4 处 Working Directory 即本机项目目录

Vue3

一、容易忽略的小技巧

Vue 中可以动态的决定绑定属性名、事件名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vue.createApp({
data() {
return {
key: 'title',
value: 'value',
event: 'mouseenter'
}
},
template: `
<div
@[event]="handleClick"
:[key]=value
>
{{ message }}
</div>
`
}).mount('#root')

Vue 中有更简单的 preventDefault 方法:

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
  Vue.createApp({
data() {
return {
key: 'title',
value: 'value',
event: 'mouseenter'
}
},
methods: {
handleClick(e) {
e.preventDefault()
},
handleClick2(e) {
// $event 接收原生事件
},
handleClick3() {
// 一次点击同时掉多个方法
},
handleClick4() {
// 一次点击同时掉多个方法
}
},
template: `
<button @click="handleClick">提交</button>
<button @click.prevent="handleClick">提交</button>
<button @click.prevent="handleClick2($event)">提交</button>
<button @click.prevent="handleClick3(), handleClick4()">提交</button>
`
}).mount('#root')

Vue 事件修饰符:

1
2
3
4
5
6
7
@click.stop
@click.self
@click.prevent
@click.capture
@click.once
@click.passive
@click.ctrl // 组合键

Vue 按键修饰符:

1
2
3
4
5
6
@keydown.enter
@keydown.tab
@keydown.delete
@keydown.esc
@keydown.up
@keydown.down

Vue 鼠标修饰符:

1
2
3
@click.left
@click.right
@click.middle

Vue 精确修饰符:

1
@click.ctrl.exact  // ctrl + 点击,不能是 ctrl + 其他按键 + 点击

v-model 修饰符:

1
2
3
v-model.lazy
v-model.number
v-model.trim

二、组件

全局组件:

1
2
3
4
5
6
7
8
9
10
11
const app = Vue.createApp({
template: `<div><hello/><world/></div>`
})
// 全局组件,一直挂在 app 上
app.component(`hello`, {
template: `<div>Hello</div>`
})
app.component(`world`, {
template: `<div>world</div>`
})
app.mount('#root')

局部组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 const counter = {
data() {
return {
count: 1
}
},
template: `<div @click="count += 1">{{ count }}</div>`
}
const app = Vue.createApp({
// 局部组件
components: { counter },
template: `<div><counter/></div>`
})
app.mount('#root')

Go

一、基础

1、类型

基础类型

Go 自带基础类型有如下:

类型 长度 默认值 说明
bool 1 false
byte 1 0 uint8 别名
int, uint 4, 8 0 默认整数类型,不同平台使用不同长度32或64位
int8, uint8 1 0 -128127, 0255
int16, uint16 2 0 -3276832767, 065535
int32, uint32 4 0 -21亿21亿, 042亿
int64, uint64 8 0
float32 4 0.0
float64 8 0.0 默认浮点类型
complex64 8
complex128 16
rune 4 0 int32 别名
uintptr 4, 8 0 存储指针的 uint
string “” 默认空字符串而不是 NULL
array
struct
function nil
interface nil
map nil 引用类型
slice nil 引用类型
channel nil 引用类型
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
// strconv 包用来在不同类型间转换:
strconv.ParseInt()
strconv.ParseFloat()

// Go 中没有隐式转换,所有的类型转换都必须显示转换,注意如果要转成指针、通道、方法时类型也要加上括号
a := 10
b = byte(a)
c = (*int)(a)
(<-chan int)(a)
(func())(a)

// 未指定类型赋值时并不一定总是由赋值决定类型,还可能由其后的操作决定
var v = 20 // 未指定类型,按理说应该被推断为 int 类型
var a byte = 10
b = v + a // 但被用来和 byte 类型一起计算,所以 v 被推断为 byte 类型


// 多变量赋值,注意:先计算出右边的所有值才会执行赋值
x, y := 1, 2
x, y = y+3, x+2 // out: x = 5, y = 3

// Go 中没有 enum,通过一组自增常量实现,iota 自增类型默认为 int
const (
a = iota // 0
b // 1
c = 100 // 100 打断了 iota,必须显示恢复不会默认恢复
d // 100 没有使用 iota 时,未指定时默认与上一行类型、值相同
e = iota // 4 显示恢复 iota,而且恢复后是按照行序递增
f // 5
)

// iota 默认类型是 int,可以自定义类型
type color byte
const (
black color = iota
red
blue
)

// 常量和变量的区别:
// 变量在运行期分配内存地址,运行中可以获取变量内存地址
// 常量经过编译器预处理之后直接使用值,在运行期不存在这个常量了所以不能取到常量内存地址
var x = 0x100
const y = 0x200
&x // ok
&y // error


// Go 中有两个函数用来分配内存:new、make
// 对于非引用类型可以使用 new 来分配需要的内存并分配零值,最后返回指针
new(int)
new(float32)
// 对于三种引用类型:slice、map、channel 则必须使用 make 来分配内存并返回该类型(注意不是返回指针)
make([]int, 0, 10)
make(map[string]int)

自定义类型

Go 中自定义类型有二种方式:

  • 基于现有的基础类型创建,也就是别名。别名只能继承基础类型的操作符,其他方法等都不能继承。
  • 自定义结构体、方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 别名,别名只能继承基础类型的操作符,其他方法等都不能继承
type flags byte

// 结构体,注意:结构体的 tag 也是类型的一部分,tag 不同的 struct 肯定不是一个类型
type person struct {
name string `name`
}

type (
user struct {
name string `name`
age int `age`
}

admin struct {
user
level int
}

// 方法
event func(string) bool
)

2、表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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
// Go 中自增自减只能后置不能前置,而且必须放在单独的一行语句中而不能放在表达式中
a := 1
++a // error: ++ 不能前置
a++ // ok
if (a++) > 1 { // error:不能用于表达式中
}

// 如果两个指针指向同一个地址或都为 nil 时,两个指针相等

// 对于数组、切片、字典、结构体的字面量初始化时,每行必须以逗号或花括号结尾
type data struct {
x int
y string
}
var a data = data{1, "abc"} // ok
var b data = data{
1,
"abc", // 这里的逗号是必须的,必须以逗号或花括号结尾
}

// Go 中没有类似如下的三目条件运算符
a > b ? a : b

// if switch 这两个条件语句都支持初始化语句
// 初始化了 err 然后判断
if err := check(s); err != nil {

}
// 初始化了 x 然后判断
switch x := 5; x {
case 5, 6: // 多个值时表示或者
x += 10
default
x += 100
}

// switch 不会自动贯穿,需要手动 fallthrough 贯穿,使用 fallthrough 之后按照源码顺序直接执行下一个 case 而不会在判断是否满足条件
switch x := 5; x {
case 5:
x += 10
fallthrough // 贯穿
case 6:
x += 20 // 按照源码顺序执行这里,并在这里停止
default:
x += 30
}

// Go 中的循环只有 for 一种方式,for-range 可以用于字符串、数组、数组指针、切片、字典、通道
data := [3]int{"a", "b", "c"}
for i, v := range data {

}

// 仅迭代不取值
for range data {

}

// for-range 遍历有个注意点:遍历时会复制一次遍历对象,对于数组、切片等类型可能表现不同
data := [3]int{10, 20, 30}
for i, x := range data {
if i == 0 {
data[0] = 100
data[1] = 200
data[2] = 300
}
fmt.Printf("x = %d, data: %d\n", x, data[i])
}

// out:
// x = 10, data: 100
// x = 20, data: 200
// x = 30, data: 300

data := [3]int{10, 20, 30}
for i, x := range data[:] {
if i == 0 {
data[0] = 100
data[1] = 200
data[2] = 300
}
fmt.Printf("x = %d, data: %d\n", x, data[i])
}

// out:
// x = 10, data: 100
// x = 200, data: 200
// x = 300, data: 300

3、函数

Go 中函数有一些限制:不支持默认参数、不支持函数重载。

函数在 Go 中是第一类对象,具有相同参数和返回类型的函数是同一类型。

1
2
3
4
5
6
// 以下三个方法是同一类型
func func1(str string) int {}
func func2(str string) int {}
var f = func(str string) int {

}

二、GO 商城

1、RabbitMQ

RabbitMQ 主要使用场景:流量削峰、异步处理、应用解耦。

RabbitMQ 安装依赖 Erlang。

三、Go 容器微服务

Java

一、基础

1、基本数据类型

整型

类型 存储 说明
byte 1 byte
short 2 byte
int 4 byte 默认整型
long 8 byte

浮点型

类型 存储 说明
float 4 byte
double 8 byte 默认浮点型

字符型

类型 存储 说明
char

布尔型

类型 存储 说明
boolean

Java 中没有无符号整型。

Java 中整型和布尔型之间不能互相转换。

1
2
3
4
5
// 声明变量
double varDouble = 12.4d;

// 声明常量,Java 中使用 final 声明常量,常量一般使用全大写
final double CONST_DOUBLE = 12.4d;

2、字符串

Java 中 String 不是基本类型而是一个类。

String 是不可变字符串,每次对字符串的修改都会返回一个新的字符串。

String 的判等只能使用 equals 而不能使用 ==,equals 判断的是字符串语义上是否相等,== 判断的是内存地址是否相同。

String 常用方法:

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
int compareTo(String other); // 按字典顺序排序,在 other 前返回小于0;在 other 之后返回大于0;相等返回0;
boolean equals(String other); // 两个字符串是否相等,字符串的判等必须使用 equals 而不能使用 ==
boolean equalsIgnoreCase(String other); // 忽略大小写判等

boolean startsWith(String prefix); // 是否以 prefix 开头
boolean endsWith(String suffix); // 是否以 suffix 结尾
int indexOf(String str); // 从索引0开始检索第一个与 str 匹配的子串位置
int indexOf(String str, int fromIndex); // 从索引fromIndex开始检索第一个与 str 匹配的子串位置
int indexOf(int cp); // 从索引0开始检索第一个与代码点 cp 匹配的子串位置
int indexOf(int cp, int fromIndex); // 从索引fromIndex开始检索第一个与代码点 cp 匹配的子串位置
int lastIndexOf(String str); // 从字符串尾部开始检索第一个与代码点 cp 匹配的子串位置
int lastIndexOf(String str, int fromIndex); // 从字符串尾部fromIndex开始检索第一个与代码点 cp 匹配的子串位置
int lastIndexOf(int cp);
int lastIndexOf(int cp, int fromIndex);

int length();

String replace(CharSequence oldString, CharSequence newString); // 把字符串中所有 oldString 替换为 newString
String subString(int beginIndex); // 从指定位置截取字符串
String subString(int beginIndex, int endIndex);

String toLowerCase();
String toUpperCase();
String trim(); // 去掉字符串中头部和尾部空格

String join(CharSequence delimiter, CharSequence... elements); // 使用指定分隔符连接所有字符串

因为字符串是不可变的,所以每次拼接字符串都会产生一个新的对象。可以使用 StringBuilder 和 StringBuffer 拼接字符串。StringBuilder 线程不安全性能更好,StringBuffer 线程安全但性能略差。

1
2
3
4
5
StringBuilder builder = new StringBuilder();
builder.append("1");
builder.append("2");
...
builder.toString();

3、控制流程

switch-case 中可以放三种类型:

  • 整型(byte、short、int、long)
  • 枚举类型
  • 字符串

4、数组

数组创建之后不能在修改大小,如果要使用能动态修改大小的数组可以使用 ArrayList。

1
2
3
4
5
6
// 初始化
int[] a = { 1, 2, 3, 4 };
int[] a = new int[]{ 2, 3, 4, 5 };

// 数组转字符串
Arrays.toString(a);

java.util.Arrays 是数组的伴随类,提供了数组的常见操作,基本都是静态方法:

1
2
Arrays.toString();
Arrays.sort();

5、枚举

所有的枚举类型都是 Enum 类的子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum Size {
SMALL("S"),
MEDIUM("M"),
LARGE("L"),
EXTRA_LARGE("E");

private String description;

private Size(String description) {
this.description = description;
}

private String getDescription() {
return description;
}
}

定义了一个 Size 类,该类有四个实例。

枚举的判等必须使用 == 而不能使用 equals。

枚举常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 
Size size = Size.SMALL;

// 返回枚举常量名
size.toString()

// 通过枚举常量名返回枚举值
Size s = Enum.valueOf(Size.class, "SMALL");

// 返回枚举类型中所有枚举值
Size[] values = Size.values();

// 返回枚举值在枚举类型中的位置
Size.SMALL.ordinal();

6、面向对象

私有域 + 公开的访问器与修改器方法。

方法重载:多个方法有相同的名字,不同的参数就叫方法重载。

方法覆盖:子类重写父类的方法叫方法覆盖,方法覆盖时子类必须比父类更开放。

方法签名:包括方法名称、参数类型,但不包括返回类型。

默认构造器:如果类没有提供构造器则会自动添加一个默认无参构造器,如果提供了则不会有默认无参构造器。

初始化一个类中的域可以有三个地方:

  • 域声明中
  • 初始化块(对象初始化块、静态域初始化块)
  • 构造器

执行顺序是加载该类是就执行静态初始化块,构造对象时先执行域声明语句,然后执行对象初始化块,最后执行构造器:

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 Employee {
// 声明域
private int id;
private String name;
private static int level;

// 静态域初始化块
static {
Employee.level = 1000;
}

// 对象初始化块
{
id = 100;
name = "ifangcy";
}

// 构造器
public Employee(int id, String name) {
this.id = id;
this.name = name;
}
}

子类必须在构造器第一行调用父类的构造器,如果没有主动调用就会自动调用父类的默认无参构造器,如果父类没有默认无参构造器则报错。

多态:一个对象变量可能指向多种实际的类型叫多态。(变量可以引用一个类型,也可以引用一个类型的子类)

动态绑定:在运行时自动决定使用哪个对象的方法叫动态绑定。

Java 中的方法调用默认是动态绑定的,使用 static、final、private 可以消除动态绑定使用静态绑定。

访问控制符:

  • public :所以类都可见
  • protected:本包及所有子类可见
  • 默认:本包可见
  • private:只有本类可见

超类 Object 常用方法:

1
2
3
4
5
6
7
8
// 默认判断两个对象是否具有相同的引用
equals()

// 默认返回对象的内存地址,如果覆盖 equals 方法就必须同时覆盖 hashCode 方法
hashCode()

//
toString()

7、抽象类

抽象类不一定有抽象方法,但有抽象方法就一定是抽象类。

抽象类中可以包含具体数据和方法:

1
2
3
4
5
6
7
8
9
10
11
12
abstract class Person {
private String name;
public Person(String name) {
this.name = name;
}

public String getName() {
return this.name;
}

public abstract String getDescription();
}

8、反射

9、接口

接口中所有方法默认都是 public 的不用显式声明,但是实现接口类中必须显示声明 public。

接口不能包含实例域,但可以包含常量,所有的常量默认是 public static final。

1
2
3
4
5
6
7
8
9
public interface Comparable {
int compareTo(Object other);
double CAN_COMPARABLE = 10; // public static final
}

// 扩展接口
public interface Moveable extends Comparable {
void move(double x, double y);
}

接口中方法可以使用 default 默认实现方法,实现类不必一定实现默认实现方法:

1
2
3
4
5
6
public interface Comparable<T> {
// 方法默认实现,实现类不用一定实现
default int compareTo(T other) {
return 0;
}
}

如果一个类中有很多静态方法,以前的解决办法是实现一个伴随类把这些静态方法都放到伴随类中,比如 Collection/Collections、Path/Paths等。现在接口中可以有静态方法,这是一个更好的解决办法:

1
2
3
4
5
6
public interface Path {
// 静态方法,可代替伴随类
public static Path get(String first, String... more) {
return FileSystems.getDefault().getPath(first, more);
}
}

如果实现类的父类和它实现的接口中有方法冲突,则父类方法优先,叫类优先。

在没有 lambda 和内部类之前,回调的实现只能使用接口实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义回调中需要使用的接口
public interface ActionListener {
void actionPerformed(ActionEven evnet);
}

// 实现接口
class TimePrinter implements ActionListener {
public void actionPerformed(ActionEvent event) {
System.out.print("action event");
}
}

// 使用回调接口
ActionListener listener = new TimePrinter();
Timer t = new Time(1000, listener);
t.start();

10、lambda

lambda 不需要指定返回类型。

lambda 可以指定参数类型,如果可以推导出参数类型也可以省略参数类型。

lambda 如果没有参数也不能省略小括号。

lambda 如果只有一个参数才可以省略小括号。

函数式接口:对于只有一个抽象方法(注意是只有一个抽象方法,可以有多个静态方法、多个默认实现方法)的接口叫函数式接口。

1
2
3
4
5
6
7
8
// Timer 需要一个接口 ActionListener 参数
// ActionListener 接口只有一个抽象方法 actionPerformed
// 所以 ActionListener 接口是函数式接口
// 函数式接口可以直接使用 lambda
Timer t = new Timer(1000, event -> {
System.out.println(event);
});
t.start();

java.util.function 中定义了很多常用的函数是接口,方便实用。

方法引用:如果已有方法就满足了函数式接口的需求,可以使用方法引用的方式把该方法直接作为 lambda 传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {

public static void main(String[] args) {
// 方法引用
Timer t = new Timer(1000, new Test()::print);
t.start();
}

// 该方法接收一个 ActionEvent 参数,可直接作为 lambda 方法直接方法引用
public void print(ActionEvent event) {
System.out.println(event);
}
}

方法引用有三种方式:

  • 对象::实例方法
  • 类::静态方法
  • 类::实例方法

前两个形式中所有参数都作为引用方法的参数。

第三种方法中必须有多个参数,第一个参数作为该实例方法的调用者,其他参数作为实例方法参数。

1
2
3
4
5
String::compareToIgnoreCase

// 等同于

(x, y) -> x.compareToIgnoreCase(y);

构造器也可以用在方法引用中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Person {
private String name;

// 构造器
public Person(String name) {
this.name = name;
}

public static void main(String[] args) {
String[] names = { "Jack", "Tom"};
// 构造器用作方法引用
Stream<Person> persons = Arrays.stream(names).map(Person::new);
}
}

lambda 中使用的 this 指的是创建该 lambda 表达式的方法中的 this。

1
2
3
4
5
6
7
8
public class Person {
public void print() {
// 这里 this 指的是当前 Person 对象
ActionListener listener = event -> {
System.out.println(this.toString());
};
}
}

lambda 会捕获外围变量,注意被捕获的变量不能被修改(无论 lambda 内部修改还是外部修改都不行)。

11、内部类

1
2
3
4
5
public class Outer {
class Inner {

}
}

内部类所有的静态域都必须是 final。

内部类不能有 static 方法。

在虚拟机中并没有内部类,内部类会被编译器编译成用 $ 分隔的外部类与内部类:

1
2
3
4
5
6
7
public class Outer {
class Inner {

}
}

// 编译后内部类名:Outer$Inner.class

内部类也可以定义在方法中,这时候称为局部内部类,局部内部类不能使用 private 或 public 修饰:

1
2
3
4
5
6
7
8
public class Outer {
public void sayHello() {
// 局部内部类
class Inner {

}
}
}

和 lambda 一样,内部类中捕获的外部变量必须是不可变的 final 。

如果局部内部类只需要创建一次对象,可以直接 匿名内部类 直接创建对象而不用命名类:

1
2
3
4
5
6
7
8
ActionListener listener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {

}
};
Timer t = new Timer(1000, listener);
t.start();

如果内部类不需要使用外部类的变量,可以将内部类设置为 static 以便取消内部类对外部类的引用:

1
2
3
4
5
6
public class Outer {
// 静态内部类
static class Inner {

}
}

12、异常

所有异常都派生自 Throwable:

  • Error:系统内部错误,发生时应该停止程序。
  • Exception:分为 RuntimeException、IOException。程序本身错误是 RuntimeException,IO 错误是 IOException。

常见 RuntimeException:

  • 错误类型转换
  • 数组越界访问
  • 访问 null 指针

常见 IOException:

  • 试图在文件尾部读取数据
  • 试图打开一个不存在的文件
  • 给定字符串查找类时类不存在

Error 和 RuntimeException 是非受查异常,IOException 是受查异常,只有受查异常才能抛出。

异常的处理有两种方法:抛出异常、捕获异常。

try-catch 中可以 catch 多个异常:

1
2
3
4
5
try {

}catch (FileNotFoundException | UnknownHostException) {
// 同时捕获多个异常
}

finally 在不论是否捕获异常都会执行,一般用来释放资源,但如果资源实现了 AutoCloseable 接口,可以使用更好的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 传统的 finally 中释放资源
try {
// 打开资源,使用资源
}catch (Exception e) {
// 捕获异常
}finally {
// 释放资源
}

// 资源实现了 AutoCloseable 接口时更好的释放资源方式
// 在 try 中打开资源,可以同时打开多个资源,当 try 块退出时会自动释放资源
try (Scanner in = new Scanner(new FileInputStream("demo.txt"))) {
// 使用资源
}catch (Exception e) {
// 捕获异常
}

13、泛型

1
2
3
4
5
6
7
// 泛型类
public class Pair<T> {
// 泛型方法,泛型标志放在修饰符后面,返回类型前面
public static <T> T getMiddle(T t) {

}
}

限定泛型类型:

1
2
3
4
// 限定列表中,类应该放在第一个,接口应该放在后面
public class Son<T extends Person & Comparable & Serialzable> {

}

类型擦除:虚拟机中没有泛型,泛型类在编译之后会被去除类型,使用限定类型替换,如果没有限定类型则使用 Object 类型替换:

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
// 没有限定泛型类型时
public clas Pair<T> {
private T lower;

public T getLower(T t) {

}
}

// ------> 编译之后

// 编译之后,擦除类型,因为没有限定类型所以使用 Object 替换
public clas Pair {
private Object lower;

public Object getLower(Object t) {

}
}

// 有限定泛型类型时
public clas Pair<T extends Comparable & Serializable> {
private T lower;

public T getLower(T t) {

}
}

// ------> 编译之后

// 编译之后,擦除类型,因为有对泛型进行限制所以使用第一个限定类型替换泛型
public clas Pair {
private Comparable lower;

public Comparable getLower(Comparable t) {

}
}

因为类型擦除,应该将没有方法的接口放到限定列表的后面。

桥方法。

几个注意点:

泛型的类型不能是基本类型,必须使用包装器:

1
2
3
4
5
6
7
8
9
// error
public class Pair<double> {

}

// ok
public class Pair<Double> {

}

不能创建泛型类型的数组:

1
2
// error
Pair<String>[] table = new Pair<String>[10];

注意,无论 S 和 T 有什么关系,Pair<S> 和 Pair<T> 都没关系:

1
2
3
4
5
6
7
8
class Pair<T> {}

class SuperClass {}

class SonClass extends SuperClass {}

// error
Pair<SuperClass> pair = new Pair<SonClass>();

但是 Pair<SonClass> 一定是 Pair 的子类型:

1
2
air<Manager> managerPair = new Pair<>();
Pair pair = managerPair;

通配符类型 ???

14、集合

for-each 循环可以用在任何实现了 Iterable 接口的类上。

1
2
3
4
5
6
7
Iterator<String> it = c.iterator();

// 调用 remove 之前必须先调用 next
if (it.hasNext()) {
it.next();
it.remove();
}

15、并发

16、打包

传统的 Java 会打包成一个 applet 程序,可以通过网络下载在浏览器中运行,后来出现了打包成 jar 包。

applet 程序已经基本废弃不用,如今基本使用 jar 打包代码。

jar

jar 其实就是一个使用 zip 格式压缩的文件,可以包含代码及资源文件。

打包成 jar 包:

1
2
3
4
5
# 打包
jar cvf 打包后名称.jar *.class 资源文件名.gif

# 打包一个包含清单文件的包
jar cfm 打包后名称.jar manifest.mf *.class 资源名.gif

一般 jar 包中有一个描述文档特征的清单文件 META-INF/MANIFEST.MF,文件分为多个节,开头主节作用于整个 jar 包,下面的命名条目(指定文件、包、URL 都可以作为命名条目)作用于指定条目:

1
2
3
4
5
6
7
8
9
10
# 主节
Manifest-Version: 1.0
Sealed: true

# 命名条目
Name: Demo.class

# 命名条目
Name: com/ifangcy/demo/util
Sealed: true

注意:清单文件最后一行必须以换行符结束。

要想直接启动 jar 包,在打包时必须指定程序入口或者在清单文件中指定主类:

1
2
3
4
5
# 打包时指定入口
jar cvfe 打包后名称.jar com.ifangcy.demo.MyMainClass

# 清单中指定主类
Main-Class: com.ifangcy.demo.MyMainClass

启动 jar 包:

1
java -jar 包名.jar

jar 包中的资源如何定位?通过可以访问的 class 文件来定位资源文件,一般可以把资源文件放到类文件文件夹中:

1
2
3
4
5
6
7
8
9
// 通过 ResourceTest 的位置来定位资源 about.gif
// 这里会在找到 ResourceTest 类的地方查找 about.gif
URL url = ResourceTest.class.getResource("about.gif");
Image img = new ImageIcon(url).getImage();

// 这里会在找到 ResourceTest 类的子文件夹中查找 about.gif
// 注意,不论那种操作系统都使用 / 分隔符
URL url = ResourceTest.class.getResource("data/about.gif");
Image img = new ImageIcon(url).getImage();

Properties、Preferences 常用作配置功能,web 开发一般用不到就不详写了。

applet

applet 是包含在 HTML 页面中的 Java 程序,基本不再使用。

Java Web Start

二、JDBC

三、JavaWeb

JSP 与 Servlet 是一致的,JSP 最终会被web 服务器编译为 Servlet,web 服务器上实际运行的是 Servlet。第一次访问 JSP 文件时会比较慢,因为要等待 JSP 编译成 Servlet 文件。

一个标准的 Web 应用结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
webapp
|
-- META-INF
|
-- WEB-INF
| |
| -- web.xml # web 项目配置文件
| |
| -- classes # 项目的 .class 文件
| |
| -- lib # 项目用到的 jar 包
|
-- index.jsp # jsp 文件在最外层
|
-- demo.html
|
__ index.js
|
-- css
|
__ img

Servlet 3.0 之前 web.xml 是必须的,之后 web.xml 不再是必需文件,因为可以使用注解。

项目中的这个 web.xml 文件是项目级的配置,只会影响该项目,web 服务器还会提供一个服务器级的 web.xml (Tomcat 配置文件位于 conf/web.xml)配置文件,作为所有项目共有配置。

web.xml 及注解主要可配置有:

  • JSP
  • Servlet
  • Listener
  • Filter

1、JSP

2、Servlet

Servlet 生命周期由 web 服务器(比如 Tomcat)来管理,创建 Servlet 实例有两个时机:

  • 第一次请求到该 Servlet 时
  • 通过 load-on-startup 配置让启动 web 服务器时就创建该 Servlet 实例,load-on-startup 值越小越先创建

已创建的 Servelet 会放到容器中,后续再请求到该 Servlet 时不需要重新创建实例。

Servlet 3.0 增加了很多新特性,比如注解配置、web 模块支持、Servlet 异步处理。

注解配置

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
package com.ifangcy.servlets;

@WebServlet(
loadOnStartup = 1,
name = "helloServlet",
urlPatterns = { "/hello" },
initParams = {
@WebInitParam(name="driver", value = "com.mysql.cj.jdbc.Driver"),
@WebInitParam(name = "url", value = "jdbc:mysql://localhost:3306?database")
}
)
public class HelloServlet extends HttpServlet {

@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html");
PrintStream out = new PrintStream(resp.getOutputStream());
out.println("<html>");
out.println("<body>");
out.println("<div>Hello world</div>");
out.println("</body>");
out.println("</html>");
}
}

web.xml 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<servlet>
<servlet-name>helloServlet</servlet-name>
<servlet-class>com.ifangcy.servlets.HelloServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>driver</param-name>
<param-value>com.mysql.cj.jdbc.Driver</param-value>
</init-param>
<init-param>
<param-name>url</param-name>
<param-value>jdbc:mysql://localhost:3306?database</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>helloServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>

3、Filter

Filter 是一种加强版的 Servlet,用来对请求进行预处理、对响应进行后处理。Filter 一般用来先对请求预处理,然后把请求交给 Servlet,最后拿到 Servlet 的响应在进行后处理,最后返回响应。

Filter 通常可以用来处理多个 Servlet 中的共有部分,比如记录日志、权限控制等。

Filter 可拦截多个请求或响应,一个请求或响应也可以被多个 Filter 拦截。

注解配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@WebFilter(
filterName = "hello",
urlPatterns = { "/*" }
)
public class HelloFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 这里拦截请求,处理请求
// 注意这个方法必须调用,否则请求无法放行到 Servlet 中
filterChain.doFilter(servletRequest, servletResponse);
// 这里拦截响应,处理响应
}

@Override
public void destroy() {

}
}

web.xml 配置

1
2
3
4
5
6
7
8
<filter>
<filter-name>hello</filter-name>
<filter-class>com.ifangcy.filters.HelloFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>hello</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

4、Listener

Listener 用来监听 web 容器内部的事件,比如 web 应用被启动、web 应用停止、用户 session 开始、用户 session 结束、用户请求到达等,这些内容本来对开发者是不可见的。

常用的 web 事件监听器接口有如下:

  • ServletContextListener:监听 web 容器的启动和关闭
  • ServletContextAttributeListener:监听 ServletContext 范围内属性改变
  • ServletRequestListener:监听用户请求,可以用来实现系统日志
  • ServletRequestAttributeListener:监听 ServletRequest 范围内属性改变
  • HttpSessionListener:监听用户 session 开始和结束,可以用来实现监听在线用户数
  • HttpSessionAttributeListner:监听 HttpSession 范围内属性改变

Listener 配置无需参数。

注解配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@WebListener
public class HelloListener implements ServletContextListener {

// web 容器启动
@Override
public void contextInitialized(ServletContextEvent sce) {

}

// web 容器关闭
@Override
public void contextDestroyed(ServletContextEvent sce) {

}
}

web.xml 配置

1
2
3
<listener>
<listener-class>com.ifangcy.listeners.HelloListener</listener-class>
</listener>

四、Maven

在 Maven 打包时不会把 src/test 目录下的测试代码打包进最终包。

Maven 默认打包成 jar 包,默认打包成的 jar 并不能直接运行,因为打包出的 MATA-INF/MANIFEST.MF 文件中没有指定 Main-Class,如果想要直接运行该包需要配置在配置中使用 maven-shade-plugin 配置主类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.MainfestResourceTransformer">
<mainClass>com.ifangcy.demo.HelloWorld</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>

1、基础

安装

Maven 安装目录:

1
2
3
4
5
6
|---bin	 # maven 命令脚本
|---boot # maven 自带的类加载器
|---conf
|---settings.xml # maven 全局配置文件
|---lib # maven 依赖包

安装之后,把 ${M2_HOME}/conf 下的 settings.xml 配置文件复制一份到用户目录下 ~/.m2/settings.xml,作为用户级的配置文件。

1
2
~/.m2/---repository	  # 下载包存放
|---settings.xml # 配置文件

在 settings.xml 添加阿里云镜像:

1
2
3
4
5
6
7
8
<mirrors>
<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
</mirror>
</mirrors>

常用命令

1
2
3
mvn dependency:list
mvn dependency:tree
mvn dependency:analyze

当插件的 groupId 不是 org.apache.maven.plugins 和 org.codehaus.mojo 时,必须在 ~/.m2/settings.xml 中配置上插件的 groupId 才能通过命令行使用插件:

1
2
3
4
5
<settings>
<pluginGroups>
<pluginGroup>插件的 groupId</pluginGroup>
</pluginGroups>
</settings>

Maven 项目结构

Maven 项目必须遵守固定的结构才能被 Maven 编译打包:

1
2
3
4
5
6
7
8
9
.---src
|---main # 项目主要代码,资源目录
|---java # 项目源码
|---resources # 项目资源
|---webapp # web项目固定
|---WEB-INF # web项目固定
|---web.xml # web项目配置
|---test # 项目测试代码
|---pom.xml # maven 配置文件
  • 源码目录:src/main/java/
  • 包输出目录:target/
  • 编译输出目录:target/classes/
  • 默认打包方式:jar

一般不会手动去创建这样的项目结构,可以使用 Archetype 自动生成项目骨架,即使是 IDEA 也是通过 Archetype 命令生成的:

1
mvn archetype:generate

坐标

项目必须定义坐标,引入的第三方包也是通过坐标引入。

项目坐标包括:

  • groupId:对应到项目,比如 com.ifangcy.mall
  • artifactId:对应到项目的模块,比如 com.ifangcy.mall.account
  • version:项目版本
  • packaging:项目打包方式,默认(不填时)jar,可选 war、pom
  • classifier:生成打包时的一些附属构件,需要配合插件

打包生成的包名为 artifactId-version[-classifier].packaging。

引入的第三方包坐标包括:

  • groupId:基本坐标,必填
  • artifactId:基本坐标,必填
  • version:基本坐标,非必填,如果不填则使用依赖最新发布版本
  • type:对应 packaging,项目类型,默认 jar,非必填
  • scope:依赖范围,非必填
  • optional:是否可选,非必填
  • exclusions:排除依赖,非必填

依赖

classpath 与依赖范围

classpath 可以看做运行环境,Maven 使用三套环境/classpath隔离依赖:

  • 测试 classpath
  • 编译 classpath
  • 运行 classpath

依赖通过指定 scope 来决定放到哪个 classpath 环境中:

  • compile:编译依赖范围,默认值,三种 classpath 环境中都会放入依赖
  • test:测试依赖范围,只会放入测试 classpath 环境中,例如 JUnit
  • provided:依赖范围,编译、测试 classpath 环境中会放入依赖,例如 servlet-api
  • runtime:运行时依赖范围,测试、运行 classpath 环境中会放入依赖,例如 JDBC 驱动
  • system:系统依赖范围,与 provided 以来范围一致,区别是 system 需要指定 systemPath 从本地获取依赖而不是 Maven 仓库
  • import:不影响三种 classpath 环境,在使用 dependencyManagement 时可能用到
传递依赖

直接依赖如果还有其他依赖,这时候依赖范围怎么处理?

最左边一列为第一直接依赖,第一行为第二直接依赖。

compile test provided runtime
compile compile - - runtime
test test - - test
provided provided - provided provided
runtime runtime - - runtime

如果依赖范围不包括运行 classpath,则该依赖不会被放到最终的打包中。

依赖调解

对于多个依赖最终依赖了一个包的不同版本时怎么处理?

两个原则:

  • 路劲最近优先:A->B->C(1.0) 与 A->C(2.0) 因为第二个路径更近,所以 C 导入 2.0 版本
  • 第一声明优先:同样长度的依赖,在 POM 中先声明的优先

2、仓库

依赖下载到本地的仓库下 ~/.m2/repository/,路径为 groupId/artifactId/version/artifactId-version.packaging。

仓库分为本地仓库、远程仓库,私服也是一种远程仓库。

国内一般需要配置远程仓库的镜像,在 ~/.m2/settings.xml 添加镜像:

1
2
3
4
5
6
7
8
<mirrors>
<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
</mirror>
</mirrors>

这个镜像对当前用户所有项目都生效。

一般来说,有的项目需要特定的远程仓库,可以在项目 pom.xml 中配置项目范围的远程仓库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<project>
<repositories>
<repository>
<id>jboss</id>
<name>JBoss Repository</name>
<url>http://repository.jboss.com/maven2/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
<layout>default</layout>
</repository>
</repositories>
</project>

这种配置只对当前项目有效。

有的仓库为了安全需要认证才能访问,认证信息的配置必须放到 ~/.m2/settings.xml 中,不能放到项目 pom.xml 中(放在项目中会被提交到版本控制中,不安全):

1
2
3
4
5
6
7
<servers>
<server>
<id>JBoss Respository</id>
<username>username</username>
<password>password</password>
</server>
</servers>

快照版本:指的是 version 指定为 1.1-SNAPSHOT 这样形式的包。

1
2
3
4
5
6
7
8
<project>
<groupId>com.ifangcy.mavendemo</groupId>
<artifactId>mavendemo</artifactId>

<!-- 快照版本 -->
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
</project>

快照版本在每次打包时 Maven 会自动为项目版本加上时间戳,生成形如 1.1-20201212.221212-12 这样的版本号。

快照版本的依赖每次在编译时都会自动检查是否有最新快照版本可下载,确保使用最新快照版。

3、生命周期与插件

Maven 把构建过程抽象成一整套抽象的生命周期,每个生命周期都可以完成一定的任务,这些任务都交给插件来完成,Maven 已经自带了每个生命周期的默认插件,当然这些插件是可以随时替换的。

对于整个构建过程,Maven 抽象出了三套生命周期:clean、default、site。每套生命周期内部又分为多个阶段,在一套生命周期内部的阶段是有顺序的,后面的阶段依赖前面的阶段,但是三套生命周期是互相独立的,在不同生命周期的阶段是没有依赖关系。

clean 生命周期是负责清理项目,主要阶段:

  • pre-clean
  • clean
  • post-clean

default 生命周期是真正的构建,主要阶段:

  • validate
  • initialize
  • generate-sources
  • process-sources
  • generate-resources
  • process-resources
  • compile
  • process-classes
  • generate-test-sources
  • process-test-sources
  • generate-test-resources
  • process-test-resources
  • test-compile
  • process-test-classes
  • test
  • prepare-package
  • package
  • pre-integration-test
  • integration-test
  • post-integration-test
  • verify
  • install
  • deploy

site 生命周期是用于建立和发布站点,主要阶段:

  • pre-site
  • site
  • post-site
  • site-deploy

执行指定任务的方式就是调用特定的 Maven 生命周期阶段,比如 mvn clean deploy 调用 clean 生命周期的 clean 阶段、default 生命周期的 deploy 阶段。

如上所述,生命周期的阶段的任务由插件来完成,但阶段与插件并不是直接对应的,而是阶段与插件目标直接对应,而且一个阶段可能对应多个目标,多个目标的执行顺序是按照声明顺序执行。

所谓的插件目标就是插件可以完成的工作,通常来说一个插件可以完成多种工作,每种工作都叫做一个目标,如下:

1
2
3
maven-dependency-plugin:analyze
maven-dependency-plugin:tree
maven-dependency-plugin:list

表示 maven-dependency-plugin 有三个目标:analyze、tree、list。

只有 org.apache.maven.plugins 和 org.codehaus.mojo 两个 groupId 下的插件才支持命令行调用,如果不是这两个 groupId 下的插件想要使用命令,需要修改 ~/.m2/settings.xml 配置:

1
2
3
4
5
6
<settings>
<pluginGroups>
<!-- 这里配置插件的 groupId -->
<pluginGroup>org.mortbay.jetty</pluginGroup>
</pluginGroups>
</settings>

自定义阶段对应的目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<plugin>
<!-- 插件 -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.1.1</version>
<executions>
<execution>
<id>attach-sources</id>
<!-- 绑定阶段 -->
<phase>verify</phase>
<goals>
<!-- 对应目标 -->
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>

phase 并不是必须的,一般插件会默认指定绑定阶段,所以不显式指定也可以。

每个插件都可以通过 pom.xml 配置参数,有的插件还可以通过命令行配置参数,插件的每个目标也可以单独配置参数:

1
2
3
4
5
6
7
8
9
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<!-- 配置参数 -->
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>

依赖的远程仓库和插件的远程仓库是不同的,在 pom.xml 中配置插件的远程仓库,一般不需要自己配置插件仓库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<pluginRepositories>
<pluginRepository>
<id>central</id>
<name>Maven Plugin Repository</name>
<url>https://repo1.maven.org/maven2/</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
<releases>
<updatePolicy>never</updatePolicy>
</releases>
</pluginRepository>
</pluginRepositories>

Maven 有一个超级 pom.xml 文件,所有项目的 pom.xml 都继承自该超级 pom.xml 文件,该文件路径 ${M2_HOME}/lib/maven-model-builder-x.x.x.jar/org/apache/maven/model/pom-4.0.0.xml。这个超级 pom.xml 中指定了每个默认核心插件的版本号,所以即使项目中没有指定核心插件版本也可以,当然其他第三方插件是不行的。

4、聚合与继承

聚合:指的是把一个项目分成多个模块,把这些模块聚合到一起。

继承:指的是把多个模块相同的依赖、插件抽取到一起。

聚合模块仅帮助聚合其他模块,本身不包含实质性内容。

聚合模块的 packaging 必须使用 pom。

有的设计中聚合模块、所有模块的父模块是分开成两个模块的,有的设计中聚合模块、所有模块的父模块作为一个模块,我们采取第二种方式。

子模块会自动继承父模块的 groupId 和 version。

父模块/聚合模块 pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
<project>
<groupId>com.ifangcy.mavendemo</groupId>
<artifactId>mavendemo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>

<!-- 子模块 -->
<modules>
<module>module1</module>
<module>module2</module>
</modules>
</project>

模块1 pom.xml:

1
2
3
4
5
6
7
8
9
10
11
<project>
<!-- 父模块 -->
<parent>
<groupId>com.ifangcy.mavendemo</groupId>
<artifactId>mavendemo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<!-- 只需指定 artifactId,会自动继承父模块的 groupId、version -->
<artifactId>module1</artifactId>
</project>

父模块可以被子模块继承的元素有:

  • groupId
  • version
  • description
  • organization
  • inceptionYear
  • url
  • developers
  • contributors
  • distributionManagement
  • issueManagement
  • ciManagement
  • scm
  • mailingLists
  • properties
  • dependencies
  • dependencyManagement
  • pluginManagement
  • repositories
  • build
  • reporting

依赖管理

所有模块的依赖都可以放到父模块的 dependencies 中,但是放到 dependencies 中的依赖所有模块都会全部继承,即使该模块不需要这个依赖。问题在于如果即让父模块统一管理依赖,又能让子模块按需导入依赖。

dependencyManagement 就是为了解决这个问题而存在,它 不会给父模块和子模块导入依赖,但子模块却可以继承这些依赖。

父模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jcl</artifactId>
<version>5.2.11.RELEASE</version>
</dependency>
</dependencies>
</dependencyManagement>

子模块:

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>

父模块指定两个依赖 junit、spring-jcl,子模块只需要一个 junit 而不需要 spring-jcl,子模块指定依赖时不需要指定版本,版本由父模块统一管理。

父模块实际不会导入任何依赖,子模块只会导入 junit 一个依赖。

插件管理

插件的管理和依赖一样,使用 pluginManagement,而且插件的配置参数等也会一并继承。

父模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
</plugins>
</pluginManagement>
</build>

子模块:

1
2
3
4
5
6
7
8
9
10
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>

5、使用 Maven 测试代码

Maven 运行测试代码主要通过 maven-surefire-plugin 与 JUnit/TestNG 集成,Maven 本身不提供测试功能,通过执行生命周期的测试阶段来执行 JUnit 或 TestNG 测试用例。

运行 mvn test 时 maven-surefire-plugin 插件自动执行测试源码 src/test/java 路径下所以符合如下命名的测试代码:

  • **/Test*.java
  • **/*Test.java
  • **/*TestCase.java

跳过运行测试代码:

1
mvn package -DskipTests

或在 pom.xml 中配置:

1
2
3
4
5
6
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>

要想不运行测试代码,同时也不编译测试代码:

1
mvn package -Dmaven.test.skip=true

或 pom.xml 中配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>

指定运行特定测试代码,可以使用明确指定、逗号分隔、匹配符等多种方式:

1
2
3
mvn test -Dtest=RandomGeneratorTest
mvn test -Dtest=RandomGeneratorTest,AccountCaptchaServiceTest
mvn tset -Dtest=Random*Test

maven-surefire-plugin 会自动在 target/surefire-reports 下生成错误报告。

如果想生成测试覆盖率报告,可以使用 Cobertura 实现,加入插件 cobertura-maven-plugin 使用命令:

1
mvn cobertura:cobertura

6、Maven 构建 war 包

Web 应用目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
war
|-META-INF
|-WEB-INF
| |
| |-web.xml # web 配置
| |-classes # 源码编译 class
| |-lib # 依赖包
|
|-index.html
|-login.jsp
|-img
|-css
|-js

在 Web 容器中运行时,clases 和 lib 目录会被加入到 classpath 中。

Web 项目的打包必须为 war,项目目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
project
|
|-pom.xml
|-src
| |-main
| | |-java # 源码目录
| | |-resources # 资源目录
| | |-webapp # web 项目特有目录
| | | |-WEB-INF
| | | | |-web.xml # web 配置
| | | |-img
| | | |-css
| | | |-js
| | | |-index.html
| | | |-login.jsp
| |-test

Web 项目比起普通 jar 项目特殊在多了一个 webapp 目录,对应打包后的 Web 应用。Maven 在打包时会把依赖 jar 文件复制到打包的 war/WEB-INF/lib 中。

一般 Web 项目 pom.xml 中会特别配置 finalName 来指定最终打包名,替代默认的 artifactId-version.packaging 格式的打包名。

1
2
3
<build>
<finalName>finalName</finalName>
</build>

常见的,Web 项目中 src/main/resources 和 src/main/webapp 中都可能有资源文件,但是打包后这些资源文件在 war 包中的位置是不同的。src/main/resources 下的资源文件打包后位于 war/WEB-INF/classes 目录下(也就是会放到应用的 classpath 中),src/main/webapp 下的资源文件打包后位于 war/ 根目录下(这些资源不会被放到 classpath 中,一般称为 web 资源比如 css、js、img 等)。

内嵌热部署

Web 开发中,每次修改都需要重新打包,部署到容器中查看效果。

可以使用内嵌容器的方式直接在项目中启动容器查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<plugins>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<version>7.6.13.v20130916</version>
<configuration>
<!-- 每隔10秒扫描一次项目,如果发生更改自动热部署 -->
<scanIntervalSeconds>10</scanIntervalSeconds>
<webAppConfig>
<!-- 访问路径 -->
<contextPath>/api</contextPath>
</webAppConfig>
</configuration>
</plugin>
</plugins>

因为该插件 groupId 是 org.mortbay.jetty ,为了在命令行中使用该插件,需要修改下 ~/.m2/settings.xml:

1
2
3
4
5
<settings>
<pluginGroups>
<pluginGroup>org.mortbay.jetty</pluginGroup>
</pluginGroups>
</settings>

在命令中启动容器:

1
2
mvn jetty:run  # 默认使用 8080 端口
mvn jetty:run -Djetty.port=9999 # 指定 9999 端口

自动部署

可以使用 Cargo 将项目部署到本地或远程容器。

Cargo 有两种部署方式:

  • standalone:把本地容器内容复制一份到指定目录下,然后把项目部署到该目录下
  • existing:把项目部署到本地容器目录下,推荐
standalone 模式部署
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
<plugins>
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId>
<version>1.0</version>
<configuration>
<container>
<!-- 容器类型 -->
<containerId>tomcat6x</containerId>
<!-- 容器安装目录 -->
<home>/etc/tomcat6</home>
</container>
<configuration>
<!-- 部署模式 -->
<type>standalone</type>
<!-- 把 tomcat 目录复制一份到 target/tomcat6x 下 -->
<home>${project.build.directory}/tomcat6x</home>
<properties>
<!-- 端口 -->
<cargo.servlet.port>8080</cargo.servlet.port>
</properties>
</configuration>
</configuration>
</plugin>
</plugins>

同样的,需要修改 ~/.m2/settings.xml 以便使用命令。

使用:

1
mvn cargo:start
existing 模式部署
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
<plugins>
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId>
<version>1.0</version>
<configuration>
<container>
<!-- 容器类型 -->
<containerId>tomcat6x</containerId>
<!-- 容器安装目录 -->
<home>/etc/tomcat6</home>
</container>
<configuration>
<!-- 部署模式 -->
<type>existing</type>
<!-- 容器安装目录 -->
<home>/etc/tomcat6</home>
<properties>
<!-- 端口 -->
<cargo.servlet.port>8080</cargo.servlet.port>
</properties>
</configuration>
</configuration>
</plugin>
</plugins>
部署到远程容器
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
<plugins>
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven2-plugin</artifactId>
<version>1.0</version>
<configuration>
<container>
<!-- 容器类型 -->
<containerId>tomcat6x</containerId>
<!-- 远程容器 -->
<type>remote</type>
</container>
<configuration>
<!-- 远程运行中的容器 -->
<type>runtime</type>
<properties>
<!-- 配置 -->
<cargo.remote.username>root</cargo.remote.username>
<cargo.remote.password>123456</cargo.remote.password>
<cargo.tomcat.manager.url>http://localhost:8080/manager</cargo.tomcat.manager.url>
</properties>
</configuration>
</configuration>
</plugin>
</plugins>

部署:

1
mvn cargo:deploy

7、版本管理

版本约定:

1
<主版本>.<次要版本>.<增量版本>-<里程碑版本>
  • 主版本:架构重大变化
  • 次要版本:较大范围功能增加、变化,bug修复
  • 增量版本:bug修复
  • 里程碑版本:非稳定版,需要测试

使用 maven-release-plugin 管理版本

8、灵活构建

属性

Maven 中可使用6中属性:

  • 内置属性:主要就是两个,${basedir} 表示项目根目录(也就是 pom.xml 所在目录),${version} 项目版本
  • POM 属性:pom.xml 文件中每个元素都可以作为属性使用,比如 <project><artifactId> 可以通过 ${project.artifactId} 使用
  • Settings 属性:和 POM 属性类似,settings.xml 中每个元素也可以作为属性使用,如 ${settings.localRepository}
  • 自定义属性:在 pom.xml 的 <properties> 中定义属性
  • Java 系统属性:Java 系统属性可以通过属性使用,比如 ${user.home}
  • 环境变量属性:所有环境变量也可以通过 env. 开头属性使用,比如 ${env.JAVA_HOME}

常用 POM 属性:

  • ${project.build.sourceDirectory}
  • ${project.build.testSourceDirectory}
  • ${project.build.directory}
  • ${project.outputDirectory}
  • ${project.testOutputDirectory}
  • ${project.groupId}
  • ${project.artifactId}
  • ${project.version}
  • ${project.build.finalName}

环境隔离

资源文件过滤

Maven 对资源文件的处理知识在打包后把资源文件复制到编译输出目录中,这个过程是由插件 maven-resources-plugin 实现的,并不会主动对资源文件中使用到的 Maven 属性进行处理。

要想让资源文件中的 Maven 属性生效,需要修改插件配置开启资源过滤,这个配置在超级 POM 中(${M2_HOME}/lib/maven-model-builder.jar/org/apache/maven/model/pom.xml):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<resources>
<resource>
<directory>${project.basedir}/src/main/resources</directory>
<!-- 对该资源进行过滤,默认没有开启 -->
<filtering>true</filtering>
</resource>
</resources>
<testResources>
<testResource>
<directory>${project.basedir}/src/test/resources</directory>
<!-- 对该资源进行过滤,默认没有开启 -->
<filtering>true</filtering>
</testResource>
</testResources>

进行资源过滤之后,资源文件中就可以使用 Maven 属性,

web 资源过滤

对于 web 资源(位于 src/main/webapp 下,一般为 css、js、img 等),有时候可能也需要不同环境使用不同资源,可以通过配置 maven-war-plugin 实现过滤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<webResources>
<resource>
<filtering>true</filtering>
<directory>src/main/webapp</directory>
<includes>
<include>**/*.css</include>
<include>**/*.js</include>
</includes>
</resource>
</webResources>
</configuration>
</plugin>

Maven 对不同环境使用不同配置通过 profile 实现,一个 profile 就是一个环境。

profile 可以配置的位置有:

  • pom.xml:只对当前项目生效,推荐
  • ~/.m2/settings.xml:对所有项目生效
  • ${M2_HOME}/conf/settings.xml:对所有项目生效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<profiles>
<profile>
<id>dev</id>
<activation>
<!-- 默认激活 -->
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<css.ref>blue.css</css.ref>
<db.username>root</db.username>
<db.password>123456</db.password>
<db.url>http://localhost:3306?db</db.url>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<css.ref>red.css</css.ref>
<db.username>root</db.username>
<db.password>123456</db.password>
<db.url>http://199.119.111.11:3306?db</db.url>
</properties>
</profile>
</profiles>

激活 profile 方式有多种:

  • 命令行中激活:比如 mvn install -Pdev 激活 dev profile
  • profile 配置中激活:使用 activeByDefault 默认激活
  • 系统属性激活:’’
  • 操作系统环境激活:
  • 文件是否存在激活:

Maven 的 maven-site-plugin 插件默认会使用两个自定义属性配置读取源码和文档的编码格式及生成站点的编码格式:

1
2
3
4
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>

Maven 私服??

持续集成??

实现插件??

自定义 archetype???

五、Mybatis

1、基础

一般的 ORM 框架将 Java 对象和数据库表关联,而 Mybatis 是将 Java 方法和 SQL 语句关联。

Java 方法和 SQL 语句的关联是通过称为 mapper 的 xml 文件和以 Mapper 为后缀的接口文件实现的。

Mybatis 自带基于 HashMap 的高速缓存,第一次执行一条 SQL 时会从数据库中取数据,之后再次执行就会从高速缓存中取数据。