0%

内部类

内部类的基本使用

内部类概念:在一个类中定义一个类

比如,在一个类 A 的内部定义一个类 B,类 B 就被称为内部类。内部类定义格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
class 外部类名{
修饰符 class 内部类名{

}
}
*/

class Outer {
//此处的Inner就是所说的内部类
public class Inner {

}
}

内部类的访问特点

  • 内部类可以直接访问外部类的成员,包括私有
  • 外部类要访问内部类的成员,必须创建对象

1、示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test1Inner {
public static void main(String[] args) {
//此处编译器报错
Inner i = new Inner();
}
}

class Outer {
private int a = 10;
//内部类
class Inner {
int num = 10;

public void show(){
System.out.println("Inner..show");
}
}
}

以上代码中,为什么这里编译器会报错呢:

1
Inner i = new Inner();

因为类名没有写全 –> 为什么呢?举例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test1Inner {
public static void main(String[] args) {
Inner i = new Inner();
}
}

class Outer {
private int a = 10;
//内部类
class Inner {

}
}

class Outer2{
class Inner{

}
}

此时创建 Inner 对象时,编译器都懵掉了,你到底是想要创建那个类中的 Inner 类 –> 需要我们正确书写格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test1Inner {
public static void main(String[] args) {
//创建内部类的格式
//外部类名.内部类类名 对象名 = new 外部类对象().new 内部类对象;
Outer.Inner i = new Outer().new Inner();
}
}

class Outer {
private int a = 10;
//内部类
class Inner {

}
}

emm,创建内部类为什么格式那么复杂:-)。如下是演示内部类变量、内部类方法的访问方式:

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 Test1Inner {  
public static void main(String[] args) {
//创建内部类的格式
//外部类名.内部类类名 对象名 = new 外部类对象().new 内部类对象;
Outer.Inner i = new Outer().new Inner();
//访问内部类变量
System.out.println(i.num);
//访问内部类方法
i.show();
}

}

class Outer {
private int a = 10;
//内部类
class Inner {
int num = 10;
public void show(){
System.out.println("Inner...show");
}
}
}

内部类是可以直接使用外部类中的成员变量的,代码如下:

可以看到,调用内部类方法时,可以访问到外部类中的 a 变量(包括私有的成员变量)

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
public class Test1Inner {  
public static void main(String[] args) {
//创建内部类的格式
//外部类名.内部类类名 对象名 = new 外部类对象().new 内部类对象;
Outer.Inner i = new Outer().new Inner();
//访问内部类变量
System.out.println(i.num);
//访问内部类方法
i.show();
}

}

class Outer {
//外部类中私有的成员变量
private int a = 10;
//内部类
class Inner {
int num = 10;
public void show(){
System.out.println("Inner...show");
//内部类可以直接访问外部类的成员变量(包括私有的成员变量)
System.out.println("外部类中的成员变量" + a);
}
}
}

程序输出

1
2
3
10
Inner...show
外部类中的成员变量10

成员内部类

按照内部类在类中定义的位置不同,可以分为如下两种形式

  • 在类的成员位置:成员内部类
  • 在类的局部位置:局部内部类

外界创建成员内部类格式

1
2
//外部类名.内部类名 对象名 = 外部类对象.内部类对象;
Outer.Inner oi = new Outer().new Inner();

成员内部类,也属于(成员),既然是成员就可以被一些修饰符所修饰,比如 private 和 static

私有成员内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Test2Innerclass {  
public static void main(String[] args) {
//此处编译错误
Outer.Inner oi = new Outer().new Inner();
}
}

class Outer {
//私有成员内部类
private class Inner {
public void show(){
System.out.println("inner..show");
}
}
}

此处,创建成员内部类对象时编译错误
因为 private 是同一类中可见 –> 也就是说只在 Outer 类中是可见的。那么外界如何创建这个内部类对象呢:

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 Test2Innerclass {  
public static void main(String[] args) {
Outer outer = new Outer();
//调用外部类中的方法,利用类中的方法来间接访问内部类
outer.method();
}
}

class Outer {
//私有成员内部类
private class Inner {
public void show(){
System.out.println("inner..show");
}
}

public void method(){
//因为和Inner类都属于Outer类,所以可以直接这样创建
Inner i = new Inner();
//访问类中的方法
i.show();
}
}

小结

私有成员内部类访问:在自己所在的外部类中创建对象访问

静态成员内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test3Innerclass {  
public static void main(String[] args) {
// 外部类名.内部类名 对象名 = new 外部类名.内部类名();
Outer.Inner oi = new Outer.Inner();
oi.show();
}
}

class Outer {
//静态成员内部类
static class Inner {
public void show(){
System.out.println("inner..show");
}
}
}
1
2
Outer.Inner oi = new Outer().new Inner(); //错误,这是没有加static修饰符时的方式
Outer.Inner oi = new Outer.Inner(); //正确
  • 因为 Inner 不是 private 修饰的,所以外部可以访问
  • 因为 Inner 是 static 修饰的,可以不用创建类的对象『new Outer ()』来访问,通过类『Outer』直接就可以访问了

对于 Staic 修饰的其实很好调用,比如静态内部类中还有一个静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test3Innerclass {  
public static void main(String[] args) {
// 外部类名.内部类名 对象名 = new 外部类名.内部类名();
Outer.Inner oi = new Outer.Inner();
//类名一路调用即可
Outer.Inner.method();
}
}

class Outer {
//静态成员内部类
static class Inner {
public static void method(){
System.out.println("inner..method");
}
}
}
1
2
//类名一路调用即可,都是Static的,无需创建对象,直接类名调用
Outer.Inner.method();

小结

静态成员内部类访问:

1
外部类名.内部类名 对象名 = new 外部类名.内部类名(); 

静态成员内部类中的静态方法:

1
外部类名.内部类名.方法名();

局部内部类

局部内部类定义位置:局部内部类是在方法中定义的类,所以外界是无法直接访问的,需要在方法内部创建对象并使用

该类可以直接访问外部类的成员,也可以访问方法内地局部变量

1、代码展示:调用局部内部类中的方法

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 Test4Innerclass {  
public static void main(String[] args) {
Outer o = new Outer();
o.method();
}
}

class Outer {

public void method(){

//写在method当中的局部内部类
class Inner {
public void show(){
System.out.println("show...");
}
}

//方法内部可以访问Inner类
Inner inner = new Inner();
inner.show();
}
}

因为局部内部类的访问范围仅仅是方法体中的范围,范围如下图所示

01-局部内部类的访问范围.png

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
public class Test4Innerclass {
public static void main(String[] args) {
Outer o = new Outer();
o.method();
}
}

class Outer {
// 类中的成员变量
int a = 10;

public void method(){
// 方法中的局部变量
int b = 20;

//写在method当中的局部内部类
class Inner {
public void show(){
System.out.println("show...");
// 可以访问(嵌在里边的可以访问外边的)
System.out.println(a);
// 可以访问方法中的局部变量
System.out.println(b);
}
}

//方法内部可以访问Inner类
Inner inner = new Inner();
inner.show();
}
}

小结

  • 局部内部类,外界是无法直接使用,需要在方法内部创建对象并使用
  • 该类可以直接访问外部类的成员,也可以访问方法内的局部变量
  • 局部内部类我们平时是很少编写的,因为局部内部类实在是太受限了;在看源码的过程中也很少会见到局部内部类,讲他的作用是为后面的匿名内部类打基础。因为匿名内部类属于一种特殊的局部内部类

匿名内部类

匿名内部类本质上说一个特殊的局部内部类(定义在方法内部)
前提:需要存在一个接口或类

格式如下:

1
2
3
4
5
6
7
8
9
10
/*
new 类名或接口名(){
重写方法;
}
*/

new Inter(){
@Override
public void method(){}
}

1、代码演示:正常使用接口中的方法需要几步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test5Innerclass {
public static void main(String[] args) {
InterImpl ii = new InterImpl();
ii.show();
}
}

interface Inner{
void show();
}

class InterImpl implements Inner{
@Override
public void show() {
System.out.println("InterImpl 重写的show方法");
}
}

1)创建实现类,通过 implements 关键字去实现接口
2)重写方法
3)创建实现类对象
4)调用重写后的方法

可以看到,想要使用接口中的方法还是比较复杂的。而如果是通过匿名内部类,就可以化简为 1 步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test5Innerclass {  
public static void main(String[] args) {
- InterImpl ii = new InterImpl();
- ii.show();
//匿名内部类
+ new Inner(){
+ @Override
+ public void show() {
+ System.out.println("匿名内部类中的show方法");
+ }
+ }.show();
}
}

interface Inner{
void show();
}

-class InterImpl implements Inner{
- @Override
- public void show() {
- System.out.println("InterImpl 重写的show方法");
- }
-}

我们重点分析这几行代码

1
2
3
new Inner(){
//『实现接口的类』中需要重写的方法
}.show();

可以认为

02-正常方式访问接口中的方法VS匿名内部类方式.png

匿名内部类的理解:将继承(或实现)、方法重写、创建对象这三步放在了一步都当完成

1
2
new Inner(){}; --> 相当于创建了一个实现了接口的实现类对象
new Inner(){}.show(); --> 实现类对象调用方法

2、代码演示:如果接口中有多个方法,匿名内部类最多只能调用其中的一个方法

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 Test5Innerclass {
public static void main(String[] args) {

//匿名内部类
new Inner(){
@Override
public void show1() {
System.out.println("匿名内部类中的show1方法");
}

@Override
public void show2() {
System.out.println("匿名内部类中的show2方法");
}
//此处只能调用一次方法,就相当于"对象.方法",而不能"对象.方法1.方法2"
}.show1();
}
}

interface Inner{
void show1();
void show2();
}

此时,我就想调用其中的多个方法呢,怎么办

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
public class Test5Innerclass {  
public static void main(String[] args) {

//Inner i 接口的引用;new Inner(){} 实现类对象 --> 父类的引用指向了一个子类的对象(多态)
Inner i = new Inner(){
@Override
public void show1() {
System.out.println("匿名内部类中的show1方法");
}

@Override
public void show2() {
System.out.println("匿名内部类中的show2方法");
}
};

i.show1();
i.show2();
}
}

interface Inner{
void show1();
void show2();
}

案例小结:如果说一个接口当中有多个方法,如果使用匿名内部类的方式将其中的多个方法都进行调用,可以在匿名内部类前面通过一个父类或父接口的引用去接收一下,这样就能以多态的形式将匿名内部类接受过来。通过引用就可以调其中的方法了

匿名内部类在开发中的使用

当方法的形式参数是接口或者抽象类时,可以将匿名内部类作为实际参数进行传递

1、代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestSwimming {  
public static void main(String[] args) {

//调用方法(参数要求为实现类对象,这里我们使用匿名内部类的方式)
goSwimming(new Swimming(){
@Override
public void swim() {
System.out.println("GoGoGo!");
}
});
}

//实际参数为接口的方法
public static void goSwimming(Swimming swimming){
swimming.swim();
}
}

interface Swimming {
void swim();
}

此处蓝色标记的就是创建了一个匿名内部类,将其作为 goSwimming () 方法的参数

03-匿名内部类作为方法参数.png

既然匿名内部类作为方法参数,其格式比较固定,编译器也为我们提供了代码提示:只要输入 new 父接口名,根据提示回车即可自动生成代码段

04-匿名内部类作为方法参数代码快速生成.gif

Lambda 表达式

Lambda 表达式可以认为是对匿名内部类的优化,在了解这一块知识前,请先熟悉匿名内部类

匿名内部类和 Lambda 表达式

1、代码演示:匿名内部类和 Lambda 表达式的使用方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TestSwimming {
public static void main(String[] args) {

//1、使用匿名内部类的方式
goSwimming(new Swimming() {
@Override
public void swim() {
System.out.println("铁汁, 我们去游泳吧");
}
});

//2、使用Lambda表达式,此处可以理解为是对匿名内部类的优化
goSwimming(() -> System.out.println("铁汁, 我们去游泳吧"));
}

//使用接口的方法
public static void goSwimming(Swimming swimming) {
swimming.swim();
}
}

interface Swimming {
void swim();
}

小结:Lambda 表达式可以使关注点更加明确,为什么这么说呢,请看一下分析:

1、对于匿名内部类的方式(面向对象思想,以什么形式去做)
1)方法要一个接口,我得给个接口的实现类对象
2)创建匿名内部类对象,重新方法
3)方法要干嘛呢,其实就是打印一句话

而其实,我们想要做的仅仅是打印一句话,也就是想要完成第 3 步,而第 1 步和第 2 步都是附加的操作,是 “不得不” 才这样写的

2、对于 Lambda 表达式(函数式编程思想,更多关注做什么)

Lambda 表达式的标准格式

05-匿名内部类与Lambda表达式.png

组成 Lambda 表达式的三要素:形式参数、箭头、代码块

1
(形式参数) -> {代码块}
  • 形式参数:如果有多个参数,参数之间用逗号隔开;如果没有参数,留空即可
  • ->:表示将小括号中的形式参数传到大括号的代码块中进行处理
  • 代码块:是我们具体要做到事情,也就是方法体中的内容

Lambda 表达式的使用前提:

  • 有一个接口(也就表明 Lambda 只能操作接口,不能操作类)
  • 接口中有且仅有一个抽象方法

Lambda 带参数无返回值

1、代码示例:

  • 接口中有且只有一个方法,方法有一个参数,无返回值
  • 采用匿名内部类和 Lambda 表达式分别进行实现
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
public class StringHandlerDemo {  

public static void main(String[] args) {
// 匿名内部类的实现方式
useStringHandler(new StringHandler() {
@Override
public void printMessage(String msg) {
System.out.println("我是匿名内部类" + msg);
}
});

// Lambda实现方式
useStringHandler((String msg)->{System.out.println("我是Lambda表达式" + msg);});
useStringHandler( (msg) -> System.out.println("我是Lambda表达式" + msg));
useStringHandler( msg -> System.out.println("我是Lambda表达式" + msg));
}

public static void useStringHandler(StringHandler stringHandler){
stringHandler.printMessage("coffeelize");
}
}

interface StringHandler {
//带参数无返回值
void printMessage(String msg);
}

Lambda 无参数有返回值

如果 Lambda 所操作的接口中的方法,有返回值,一定要通过 return 语句,否则会出现编译错误

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

public static void main(String[] args) {
//匿名内部类实现方式
useRandomNumHandler(new RandomNumHandler() {
@Override
public int getNumber() {
Random r = new Random();
//产生一个1-10的随机数
int num = r.nextInt(10) + 1;
return num;
}
});

//Lambda表达式实现方式
useRandomNumHandler( () -> {
Random r = new Random();
int num = r.nextInt(10) + 1;
// 注意: 如果lambda所操作的接口中的方法, 有返回值, 一定要通过return语句,否则会出现编译错误
return num;
} );
}

public static void useRandomNumHandler(RandomNumHandler randomNumHandler){
int result = randomNumHandler.getNumber();
System.out.println(result);
}
}

interface RandomNumHandler {
//无参数有返回值
int getNumber();
}

Lambda 带参数带返回值

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

public static void main(String[] args) {
//匿名内部类的实现方式
useCalculator(new Calculator() {
@Override
public int calc(int a, int b) {
return a + b;
}
});

//Lambda表达式实现方式
useCalculator((int a, int b)->{
return a + b;
});

useCalculator( (a,b) ->
a + b
);
}

public static void useCalculator(Calculator calculator){
int result = calculator.calc(10,20);
System.out.println(result);
}
}

interface Calculator {
//带参数,带返回值
int calc(int a, int b);
}

我们可以看到,以上代码中,第二种 Lambda 的书写更加简练,关于省略规则请往下看

1
2
3
4
5
6
7
8
//Lambda表达式实现方式  
useCalculator((int a, int b)->{
return a + b;
});

useCalculator( (a,b) ->
a + b
);

省略规则

  • 参数类型可以省略,但是有多个参数的情况下,不能只省略一个
    • 为什么可以省略呢?因为在接口中的方法中已经定义了参数类型,可以推导出来参数类型,所以可以省略
  • 如果参数有且仅有一个,那么小括号可以省略
  • 如果代码块的语句只有一条,可以省略大括号和分号,甚至是 return
    • 若有返回参数,要省略,大括号、分号和 return 一起省略

初识 Docker

项目部署的问题

微服务虽然具备各种各样的优势,但服务的拆分通用给部署带来了很大的麻烦

  • 分布式系统中,依赖的组件非常多,不同组件之间部署时往往会产生一些冲突
    • 依赖关系复杂,容易出现兼容性问题
  • 在数百上千台服务中重复部署,环境不一定一致,会遇到各种问题
    • 开发、测试、生产环境有差异

Docker 解决依赖兼容问题

Docker 如何解决依赖的兼容问题的呢,采用了两个手段:

  • 将应用的 Libs(函数库)、Deps(依赖)、配置与应用一起 打包
  • 将每个应用放到一个 隔离 容器去运行,避免互相干扰

01-应用放在隔离带容器中运行.png

这样打包好的应用包中,既包含应用本身,也保护应用所需要的 Libs、Deps,无需再操作系统上安装这些,自然就不存在不同应用之间的兼容问题了

Docker 解决操作系统环境差异

虽然解决了不同应用的兼容问题,但是开发、测试等环境会存在差异,操作系统版本也会有差异,怎么解决这些问题呢?

02-操作系统结构.png

  • 计算机硬件:例如 CPU、内存、磁盘等
  • 系统内核(内核与硬件交互,提供操作硬件指令):所有 Linux 发行版的内核都是 Linux,例如 CentOS、Ubuntu、Fedora 等。内核可以与计算机硬件交互,对外提供 内核指令,用于操作计算机硬件
  • 系统应用(系统应用封装内核指令为函数,便于程序员调用):操作系统本身提供的应用、函数库。这些函数库是对内核指令的封装,使用更加方便

Ubuntu 和 CentOS 都是基于 Linux 内核,无非是系统应用不同,提供的函数库有差异。此时,如果将一个 Ubuntu 版本的 MySQL 应用安装到 CentOS 系统,MySQL 在调用 Ubuntu 函数库时,会发现找不到或者不匹配,就会报错了,那 Docker 如何解决不同系统环境问题的呢

  • Docker 将用户程序与所需要调用的系统 (比如 Ubuntu) 函数库一起打包
  • Docker 运行到不同操作系统时,直接基于打包的函数库,借助于操作系统的 Linux 内核来运行

那么就可以认为 Docer 打包好的程序包可以应用在任何 Linux 内核的操作系统上

03-Docker打包好的程序包可以运行在任一Linux内核的系统上.png

总结

Docker 是一个快速交付应用、运行应用的技术,具备下列优势:

  • 可以将程序及其依赖、运行环境一起打包为一个镜像,可以迁移到任意 Linux 操作系统
  • 运行时利用沙箱机制形成隔离容器,各个应用互不干扰
  • 启动、移除都可以通过一行命令完成,方便快捷

Docker 架构

镜像和容器

  • 镜像(Image,硬盘中的文件):Docker 将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像
  • 容器(Container,相当于进程):镜像中的应用程序运行后形成的进程就是容器,只是 Docker 会给容器进程做隔离,对外不可见

镜像都是只读的,这样可以防止容器对镜像数据的写入,造成数据污染;如果容器需要写数据,可以从镜像中拷贝一份数据到自己的空间中,在本空间中进行读写操作

04-镜像和容器.png

DockerHub

DockerHub 是一个官方的 Docker 镜像的托管平台。这样的平台称为 Docker Registry,国内也有类似于 DockerHub 的公开服务,比如 网易云镜像服务阿里云镜像库

我们一方面可以将自己的镜像共享到 DockerHub,另一方面也可以从 DockerHub 拉取镜像
05-DockerHub.png

Docker 架构

Docker 是一个 CS 架构的程序,由两部分组成

  • 服务端 (server):Docker 守护进程,负责处理 Docker 指令,管理镜像、容器等
  • 客户端 (client):通过命令或 RestAPI 向 Docker 服务端发送指令。可以在本地或远程向服务端发送指令

06-Docker架构.png

安装 Docker

Docker 分为 CE 和 EE 两大版本。CE 即社区版(免费,支持周期 7 个月),EE 即企业版,强调安全,付费使用,支持周期 24 个月。这里主要介绍 CentOS 安装 Docker

1.1、卸载(可选)

如果之前安装过旧版本的 Docker,可以使用下面命令卸载

1
2
3
4
5
6
7
8
9
10
11
yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-selinux \
docker-engine-selinux \
docker-engine \
docker-ce

\ 表示命令没有结束还需要继续往下读(换行),命令太长可以通过 \ 提高可读性

1.2、安装 yum-utils 工具

1
2
3
yum install -y yum-utils \
device-mapper-persistent-data \
lvm2 --skip-broken

1.3、设置下载的镜像源

1
2
3
4
5
6
7
# 设置docker镜像源
yum-config-manager \
--add-repo \
https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo
yum makecache fast

1.4、安装 docker

1
yum install -y docker-ce

至此 docker 安装完毕

启动 docker

Docker 应用需要用到各种端口,逐一去修改防火墙设置。非常麻烦,因此建议大家直接关闭防火墙。启动 docker 前,一定要关闭防火墙!

1、关闭防火墙

1
2
3
4
5
6
# 关闭防火墙
systemctl stop firewalld
# 禁止开机启动防火墙
systemctl disable firewalld
# 查看防火墙状态
systemctl status firewalld

2、启动 Docker

1
2
3
4
5
systemctl start docker  # 启动docker服务

systemctl stop docker # 停止docker服务

systemctl restart docker # 重启docker服务

3、可通过查看 Docker 状态或 Docker 版本查看是否已经启动

1
2
3
systemctl status docker
//或者
docker -v

配置镜像

docker 官方镜像仓库网速较差,我们需要设置国内镜像服务,可参考阿里云的镜像加速文档:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors

1
2
3
4
5
6
7
8
9
10
11
sudo mkdir -p /etc/docker

//将内容写入json文件中
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://5vycoa8o.mirror.aliyuncs.com"]
}
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker

Docker 基本操作

镜像操作

镜像名称

镜名称一般分两部分组成:[repository]:[tag],在没有指定 tag 时,默认是 latest,代表最新版本的镜像。如这里的 mysql 就是 repository,5.7 就是 tag,合一起就是镜像名称,代表 5.7 版本的 MySQL 镜像

07-镜像名称的组成.png

镜像命令

常见的镜像操作命令如图

08-常用镜像指令.png

  • 从镜像服务器拉去镜像:docker pull
  • 从本地文件构建镜像:docker build
  • 查看本地存在哪些镜像:docker images
  • 删除本地镜像:docker rmi
  • 推送镜像到镜像服务器:docker push
  • 将镜像打包成一个压缩包:docker save
  • 加载压缩包为镜像:docker load

查看 docker 帮助文档

1
2
3
4
//查看docker命令及简介
docker --help
//查看具体的某一命令,比如这里详细查看images命令的功能
docker images --help

案例:从 DockerHub 中拉取镜像

需求:从 DockerHub 中拉取一个 nginx 镜像并查看

1、首先去镜像仓库搜索 nginx 镜像,比如 Docker Hub Container Image Library | App Containerization

09-DockerHub拉取镜像.png

1
2
//这里官方示例中没有指定版本,那么默认就是最新版
docker pull nginx

10-DockerHub搜索镜像.png

2、查看镜像

1
docker images

可以查看到本地中已经有 Nginx 镜像了

1
2
3
[root@localhost ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest 605c77e624dd 12 months ago 141MB

案例:通过压缩包导出导入镜像

需求:利用 docker save 将 nginx 镜像导出磁盘,然后再通过 load 加载回来

1、利用 docker xx –help 命令查看 docker save 和 docker load 的语法

1
docker save --help

通过帮助文档可得知 save 的命令格式为:

1
docker save -o [保存的目标文件名称] [镜像名称]

2、使用 docker save 导出镜像到磁盘

1
docker save -o nginx.tar nginx:latest

-o 表示选项,注意此时镜像在本地还是有的

3、使用 docker load 加载镜像

3.1)先删除本地的 nginx 镜像

1
docker rmi nginx:latest

3.2)加载压缩包镜像

1
docker load -i nginx.tar

容器操作

容器相关命令

11-容器常用指令.png

容器有三个状态:

  • 运行:进程正常运行
  • 暂停:进程暂停,CPU 不再运行,并不释放内存
  • 停止:进程终止,回收进程占用的内存、CPU 等资源

容器操作的命令:

  • docker run:创建并运行一个容器,处于运行状态
  • docker pause:让一个运行的容器暂停
  • docker unpause:让一个容器从暂停状态恢复运行
  • docker stop:停止一个运行的容器
  • docker start:让一个停止的容器再次运行
  • docker rm:删除一个容器
  • docker ps:查看所有运行的容器及状态
  • docker logs:查看容器运行日志
  • docker exec:进入容器执行命令

案例:创建运行一个容器

需求:创建并运行 nginx 容器的命令

可以去官网搜索 Nginx,并查看其文档:nginx - Official Image | Docker Hub,比如官网中给了如下运行命令示例:

1
docker run --name some-nginx -d -p 8080:80 some-content-nginx

这里以如下命令进行命令解读:

1
docker run --name containerName -p 80:80 -d nginx
  • docker run :创建并运行一个容器
  • –name : 给容器起一个名字,比如叫做 mn
  • -p :将宿主机端口与容器端口映射,冒号左侧是宿主机端口,右侧是容器端口;此处宿主机端口不做要求,但容器端口基本上取决于容器本身(软件监听的端口可能就是某一个端口)
  • -d:后台运行容器
  • nginx:镜像名称,例如 nginx,没有写标签 tag 说明是最新版 latest

![[Pasted image 20230110202450.png]]

因为容器是隔离的,所以用户无法直接通过 80 端口来访问到容器,需要将容器的端口与宿主机端口映射。端口映射就相当于将原本隔离的容器暴露出一个小窗口,通过这个小窗口来对容器进行访问

容器创建完成后,会生成一个唯一 ID

1
2
[root@localhost ~]# docker run --name mn -p 80:80 -d nginx
b8ae9bcbdde97a1ef9b055e44470427cd937571c4f2fdb5cb7a710c3d9a828e7

通过访问宿主机 80 端口,就可访问 docker 中的 Nginx 服务了

1
192.168.119.128:80

通过 logs 命令可以查看容器日志

1
2
3
4
5
6
7
8
//命令格式
docker logs [OPTIONS] CONTAINER

//docker logs 容器名
docker logs mn

//持续跟踪日志,通过Ctrl+C可以停止跟踪
docker logs -f mn

案例:操作容器

需求:进入 Nginx 容器,修改 HTML 文件内容,添加 “coffeelize 欢迎您”

1、进入容器(容器是运行的)

1
docker exec -it mn bash

命令解读:

  • docker exec :进入容器内部,执行一个命令
  • -it : 给当前进入的容器创建一个标准输入、输出终端,允许我们与容器交互
  • mn :要进入的容器的名称
  • bash:进入容器后执行的命令,bash 是一个 linux 终端交互命令

注意:exec 命令可以进入容器修改文件,但是在容器内修改文件是不推荐的,修改了是没有记录(日志的),之后都不知道进行了哪些修改操作

2、进入 nginx 的 HTML 所在目录 /usr/share/nginx/html

容器内部会模拟一个独立的 Linux 文件系统,看起来如同一个 linux 服务器一样

1
2
3
root@b8ae9bcbdde9:/# ls
bin dev docker-entrypoint.sh home lib64 mnt proc run srv tmp var
boot docker-entrypoint.d etc lib media opt root sbin sys usr

我们进入 Nginx 的目录(至于如何找到这个目录的可能需要在 DockerHub 查看 Nginx 的文档了),可以发现目录下包含 index.html

1
cd /usr/share/nginx/html

3、修改 index.html 的内容

容器内没有 vi 命令,无法直接修改,我们用下面的命令来修改

1
sed -i -e 's#Welcome to nginx#coffeelize欢迎您#g' -e 's#<head>#<head><meta charset="utf-8">#g' index.html

4、验证

访问虚拟机 80 端口,输出页面如下,说明修改成功

12-修改成功.png

5、退出容器

1
exit

6、停止容器

1
2
3
4
5
6
docker stop mn

//查看运行中的docker
docker ps
//查看所有容器(包括停止的)
docker ps -a

7、启动容器

1
docker start mn

8、删除容器

1
2
//停掉容器之后删除容器 或者 强制删除运行中的程序
docker rm -f mn

数据卷

在之前的 nginx 案例中,修改 nginx 的 html 页面时,需要进入 nginx 内部。并且因为没有编辑器,修改文件也很麻烦,这就是因为容器与数据(容器内文件)耦合带来的后果。要解决这个问题,必须将 数据与容器解耦,这就要用到数据卷了

13-容器与数据耦合度高.png

数据卷(volume)是一个虚拟目录,指向宿主机文件系统中的某个目录

14-数据卷示意图.png

一旦完成数据卷挂载,对容器的一切操作都会作用在数据卷对应的宿主机目录了,这样,我们操作宿主机的 /var/lib/docker/volumes/html 目录,就等于操作容器内的 /usr/share/nginx/html 目录了;多个容器可以挂在同一个卷,就可以 “共享” 修改操作了;如果哪一天将容器删除了,没关系,数据卷还在,将新容器在挂载到这个数据卷上就可以了访问之前的数据了

数据卷操作命令

数据卷操作的基本语法如下

1
docker volume [COMMAND]
  • create:创建一个 volume
  • inspect:显示一个或多个 volume 的信息
  • ls:列出所有的 volume
  • prune:删除未使用的 volume
  • rm:删除一个或多个指定的 volume

案例:创建和查看数据卷

需求:创建一个数据卷,并查看数据卷在宿主机的目录位置

1、创建数据卷

1
docker volume create html

2、查看所有数据卷

1
docker volume ls

3、查看数据卷详细信息卷

1
docker volume inspect html

返回信息如下,其中重点关注 Mountpoint 挂载点

1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost ~]# docker volume inspect html
[
{
"CreatedAt": "2023-01-10T21:32:34+08:00",
"Driver": "local",
"Labels": {},
"Mountpoint": "/var/lib/docker/volumes/html/_data",
"Name": "html",
"Options": {},
"Scope": "local"
}
]

4、小结

数据卷的作用:将容器与数据分离,解耦合,方便操作容器内数据,保证数据安全

挂载数据卷

我们在创建容器时,可以通过 -v 参数来挂载一个数据卷到某个容器内目录,命令格式如下:

1
2
3
4
5
docker run \
--name mn \
-v html:/root/html \
-p 8080:80
nginx \

这里的 -v 就是挂载数据卷的命令:

  • docker run:创建并运行容器
  • --name mn:给容器起个名字叫 mn
  • -v html:/root/html :把 html 数据卷挂载到容器内的 /root/html 这个目录中
  • -p 8080:80:吧宿主机的 8080 端口映射到容器内的 80 端口
  • nginx:镜像名称

案例:给 nginx 挂载数据卷

需求:创建一个 nginx 容器,修改容器内的 html 目录内的 index.html 内容
分析:上个案例中,我们进入 nginx 容器内部,已经知道 nginx 的 html 目录所在位置 /usr/share/nginx/html ,我们需要把这个目录挂载到 html 这个数据卷上,方便操作其中的内容

0、查看容器是否在运行,并且已经提前创建好了 html 数据卷

1
2
# 检查容器是否在运行
docker ps -a

1、创建容器并挂载数据卷到容器内的 HTML 目录

1
docker run --name mn -v html:/usr/share/nginx/html -p 80:80 -d nginx

2、进入 html 数据卷所在位置,并修改 HTML 内容

1
2
3
4
5
6
7
8
# 查看html数据卷的位置
docker volume inspect html
# 通过查看挂载点可知如下目录,进入该目录
cd /var/lib/docker/volumes/html/_data
# 查看目录下有哪些文件
ls
# 修改文件,此处可通过FinalShell使用本地的高级编辑工具来打开编辑
vi index.html

3、在做数据卷挂在时,如果要创建数据卷不存在,docker 会为我们自动创建数据卷

比如在我们使用如下命令前,docker 中是没有 html 数据卷的,一样可以正常使用如下命令,因为 docker 会为我们自动创建 html 数据卷

1
docker run --name mn -v html:/usr/share/nginx/html -p 80:80 -d nginx

案例:给 MySQL 挂载本地目录

容器不仅仅可以挂载数据卷,也可以直接挂载到宿主机目录上。关联关系如下

  • 带数据卷模式:宿主机目录 –> 数据卷 –> 容器内目录
  • 直接挂载模式:宿主机目录 –> 容器内目录

15-容器直接挂在到宿主机.png

目录挂载与数据卷挂载的语法是类似的:

  • -v [宿主机目录]:[容器内目录]
  • -v [宿主机文件]:[容器内文件]

案例需求:创建并运行一个 MySQL 容器,将宿主机目录直接挂载到容器

1、将课前资料中的 mysql.tar 文件上传到虚拟机的 tmp 目录,通过 load 命令加载为镜像

1
2
3
4
5
6
7
8
cd /
cd tmp/

//rz或FinalShell上传mysql.tar

docker load -i mysql.tar
//查看镜像是否导入,mysql的版本为5.7.25
docker images

2、创建目录 /tmp/mysql/data

1
2
//-p表示多级目录创建
mkdir -p /tmp/mysql/data

3、创建目录 /tmp/mysql/conf,将课前资料提供的 hmy.cnf 文件上传到 /tmp/mysql/conf

1
2
3
4
//-p表示多级目录创建
mkdir -p /tmp/mysql/conf

//rz或FinalShell上传hmy.cnf

hmy.cnf 的文件内容为

1
2
3
4
5
[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000

4、去 DockerHub 查阅资料 mysql | Docker Hub,创建并运行 MySQL 容器,要求:

1)挂载 /tmp/mysql/data 到 mysql 容器内数据存储目录
2)挂载 /tmp/mysql/conf/hmy.cnf 到 mysql 容器的配置文件
3)设置 MySQL 密码

官网上给定的运行示例如下

1
docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag

其中 -e 表示运行环境,后面可以直接设置 mysql 密码;-d 表示后台运行;tag 为版本号,其中还缺少了端口号的设置,我们对这个命令进行修改

1
2
3
4
5
6
7
8
docker run \
--name mysql \
-e MYSQL_ROOT_PASSWORD=123 \
-p 3306:3306 \
-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \
-v /tmp/mysql/data:/var/lib/mysql \
-d \
mysql:5.7.25

但是此时有个报错 (bind: address already in use),因为之前我们已经在虚拟机中运行 MySQL 了,也就是已经占用了宿主机的 3306 端口,这里我们改成 3305 试一下

1
2
3
4
5
6
7
8
9
10
11
12
//删除刚才创建的mysql容器
docker rm mysql

//再次执行如下命令,注意端口改为了3305
docker run \
--name mysql \
-e MYSQL_ROOT_PASSWORD=123 \
-p 3305:3306 \
-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \
-v /tmp/mysql/data:/var/lib/mysql \
-d \
mysql:5.7.25

注意:此处的 /etc/mysql/conf.d 目录,可以合并添加我们创建的 hmy.cnf 配置,而不是将 MySQL 默认的配置文件完全覆盖掉(因为我们创建的配置文件只包含了默认配置的少数配置信息,替换掉默认配置的话配置就不全了)

5、测试 MySQL 连接

通过 Navicat 测试可正常连接
16-Navicat连接成功.png

6、小节

数据卷挂载与目录直接挂载的比较:

  • 数据卷挂载耦合度低,由 docker 来管理目录,但是目录较深,不好找
  • 目录挂载耦合度高,需要我们自己管理目录,不过目录容易寻找查看

Dockerfile 自定义镜像

镜像结构

常见的镜像在 DockerHub 就能找到,但是我们自己写的项目就必须自己构建镜像了,而要自定义镜像,就必须先了解镜像的结构才行

镜像是将应用程序及其需要的系统函数库、环境、配置、依赖打包而成。我们以 MySQL 为例,来看看镜像的组成结构

17-镜像结构.png

  • 基础镜像(BaseImage):应用依赖的系统函数库、环境、配置、文件等
  • 入口(Entrypoint):镜像运行入口,一般是程序启动的脚本和参数
  • 层(Layer):在 BaseImage 基础上添加安装包、依赖、配置等,每次操作都形成新的层

Dockerfile

Dockerfile 是一个文本文件,其中包含一个个的 ** 指令 (Instruction)**,用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层 Layer。更新详细语法说明,可参考官网文档: https://docs.docker.com/engine/reference/builder

18-Dockerfile指令.png

构建 Java 项目

基于 Ubuntu 构建 Java 项目

需求:基于 Ubuntu 镜像构建一个新镜像,运行一个 java 项目

1、新建一个空文件夹 docker-demo

1
2
cd /tmp/
mkdir docker-demo

2、拷贝课前资料中的 docker-demo.jar 文件到 docker-demo 这个目录
3、拷贝课前资料中的 jdk8.tar.gz 文件到 docker-demo 这个目录
4、拷贝课前资料提供的 Dockerfile 到 docker-demo 这个目录

Dockerfile 中的内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local

# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar

# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8

# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin

# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar

5、进入 docker-demo

1
cd docker-demo

6、运行命令

1
docker build -t javaweb:1.0 .

-t 表示 tag;javaweb 为镜像名称;注意命令后面还有个 .,表示 dockerfile 所在的目录(构建时告知 dockerfile 在哪)

可以看到 dockerfile 共有 9 个指令,也就分为了 9 个 step,每个指令执行都会创建出一个层

7、通过命令查看构建好的镜像

1
2
3
4
5
6
7
[root@localhost docker-demo]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
javaweb 1.0 c94aff541e94 42 seconds ago 722MB
nginx latest 605c77e624dd 12 months ago 141MB
redis latest 7614ae9453d1 12 months ago 113MB
ubuntu 16.04 b6f507652425 16 months ago 135MB
mysql 5.7.25 98455b9624a9 3 years ago 372MB

可以看到我们基于 ubuntu 构建的(配置好 java 环境的)javaweb 项目的镜像已经构建好了

可以通过命令来运行镜像(8090 端口在 dockerfile 中已经声明暴露了端口)

1
docker run --name web -p 8090:8090 -d javaweb:1.0

浏览器访问如下地址,可以发现我们的项目(之前我们的 docker-demo.jar 项目)正常跑起来了

1
http://192.168.119.128:8090/hello/count

19-项目运行成功.png

虚拟机中运行 docker –> docker 中运行 ubuntu –> ubuntu 中运行 docker-demo java 项目😂,虚拟机内存开始吃紧了

20-虚拟机内存吃紧.png

小结

分析:其实我们的 java 项目真正只用到了如下一行

1
COPY ./docker-demo.jar /tmp/app.jar

dockerfile 文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local

# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar

# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8

# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin

# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar

那么我们之后在构建 Java 项目镜像时,可以先构建如下不会改变的层做一个镜像,然后在基于这个镜像来构建。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local

# 拷贝jdk
COPY ./jdk8.tar.gz $JAVA_DIR/

# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8

# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin

而实际上,有人也已经构建好了这个镜像了,我们直接拿来用就行,镜像名为 java:8-alpine

1
2
3
4
5
6
7
8
# 指定基础镜像
FROM java:8-alpine
# 拷贝java项目的包
COPY ./app.jar /tmp/app.jar
# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar

DockerCompose

Docker Compose 可以基于 Compose 文件帮我们快速部署分布式应用,而无需手动一个个创建和运行容器

初识 DockerCompose

Compose 文件是一个文本文件,通过指令定义集群中的每个容器如何运行。格式如下(相当于把 docker run 中的所有指令转换为了 Compose 指令了):

1
2
3
4
5
6
7
8
9
10
11
12
13
version: "3.8"
services:
  mysql:
    image: mysql:5.7.25
environment:
MYSQL_ROOT_PASSWORD: 123
    volumes:
     - "/tmp/mysql/data:/var/lib/mysql"
     - "/tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf"
  web:
    build: .
    ports:
     - "8090:8090"

上面的 Compose 文件就描述一个项目,其中包含两个容器

  • mysql:一个基于 mysql:5.7.25 镜像构建的容器,并且挂载了两个目录
    • 为什么没有定义端口呢:因为 MySQL 运行在微服务当中,供内部使用无需对外开放
    • 无需定义后台运行,默认就是后台运行
  • web:一个基于 docker build 临时构建的镜像容器,映射端口时 8090
    • 为什么没有指定镜像:因为通过 build 就可以构建镜像

安装 DockerCompose

1、下载

1
2
# 安装
curl -L https://github.com/docker/compose/releases/download/1.23.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose

或者通过本地文件准备好的文件直接上传,上传至 /usr/local/bin/ 目录

2、修改文件权限

1
2
# 修改权限
chmod +x /usr/local/bin/docker-compose

3、Base 自动补全命令

之后使用 Docker Compose 时就会有补全提示

1
2
# 补全命令
curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose

此时可能会报:拒绝连接的错误,需要执行如下命令修改 hosts 文件

1
echo "199.232.68.133 raw.githubusercontent.com" >> /etc/hosts

案例:利用 DockerCompose 部署

需求:将之前学习的 cloud-demo 微服务集群利用 DockerCompose 部署

1、查看课前资料提供的 cloud-demo 文件夹,里面已经编写好了 docker-compose 文件

课前资料提供的 cloud-demo 文件夹,里面已经编写好了 docker-compose 文件,而且每个微服务都准备了一个独立的目录。对于每一个微服务目录,其中都包含一个 Dockerfile 文件和对于的微服务 jar 包。最外层包含 docker-compose.yml 配置文件,文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
version: "3.2"

services:
nacos:
image: nacos/nacos-server
environment:
MODE: standalone
ports:
- "8848:8848"
mysql:
image: mysql:5.7.25
environment:
MYSQL_ROOT_PASSWORD: 123
volumes:
- "$PWD/mysql/data:/var/lib/mysql"
- "$PWD/mysql/conf:/etc/mysql/conf.d/"
userservice:
build: ./user-service
orderservice:
build: ./order-service
gateway:
build: ./gateway
ports:
- "10010:10010"

这几个微服务(mysql、userservice、orderservice 以及 gateway)中,只有网关暴露了端口,因为网关是外部访问微服务的入口。其他微服务都需要注册到 Nacos 服务中

MySQL 微服务中需要的表和数据课程资料也已经为我们准备好了

2、修改自己的 cloud-demo 项目,将数据库、nacos 地址都命名为 docker-compose 中的服务名

因为微服务将来要部署为 docker 容器,而容器之间互联不是通过 IP 地址,而是通过容器名。这里我们将 order-service、user-service、gateway 服务的 mysql、nacos 地址都修改为基于容器名的访问

比如 user-service 中的 bootstrap 配置文件

1
2
3
4
5
6
7
8
9
10
11
spring:  
application:
name: userservice # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
- server-addr: localhost:8848 # Nacos地址
+ server-addr: nacos:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名

application.yml 配置文件中

1
2
- url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false
+ url: jdbc:mysql://mysql:3306/cloud_user?useSSL=false

同理,order-service 和 gateway 微服务的配置文件也这样修改

3、使用 maven 打包工具,将项目中的每个微服务都打包为 app.jar

为什么都打包成 app.jar 呢 –> 因为我们在微服务目录下的 Dockerfile 文件里是这样配置的,我们配置的名称都是 app.jar

1
2
3
FROM java:8-alpine
COPY ./app.jar /tmp/app.jar
ENTRYPOINT java -jar /tmp/app.jar

那么,既然各个微服务打包完成都需要叫这个 app.jar 名字,我们是否可以修改配置文件实现项目打包自动叫这个名字呢 –> 可以的,在各个微服务的 pom 文件中添加如下配置

1
2
3
4
5
6
7
8
9
<build>
<finalName>app</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

通过 Maven 的 Package 来打包

4、将打包好的 app.jar 拷贝到 cloud-demo 中的每一个对应的微服务子目录中

21-项目部署前准备.gif

5、将 cloud-demo 上传至虚拟机 (tmp 目录),利用 docker-compose up -d 来部署

1
2
3
4
5
6
7
cd /
cd /tmp/

//上传cloud-demo文件夹

cd cloud-demo/
docker-compose up -d
  • up:表示创建并执行容器
  • down:停止并删除容器
  • 其他命令可以通过 help 命令查看

6、查看打包好的镜像和运行的容器

1
2
docker images
docker ps
1
2
3
4
5
6
7
8
9
10
11
12
[root@localhost cloud-demo]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
cloud-demo_gateway latest 3ce691e26939 About a minute ago 185MB
cloud-demo_orderservice latest b81195944331 About a minute ago 187MB
cloud-demo_userservice latest 2dc6d8c88bdc About a minute ago 184MB
javaweb 1.0 c94aff541e94 3 hours ago 722MB
nginx latest 605c77e624dd 12 months ago 141MB
redis latest 7614ae9453d1 12 months ago 113MB
ubuntu 16.04 b6f507652425 16 months ago 135MB
nacos/nacos-server latest bdf60dc2ada3 17 months ago 1.05GB
mysql 5.7.25 98455b9624a9 3 years ago 372MB
java 8-alpine 3fd9dd82815c 5 years ago 145MB

虚拟机 2G 内存快要炸了😳,开始借用交换内存了

7、通过查看日志可发现 order-service 有报错

1
2
3
4
5
6
7
docker-compose logs -f

//退出日志
Ctrl+C

//重启微服务(微服务启动前Nacos已经启动完成)
docker-compose restart gateway userservice orderservice

原因是因为 Nacos 微服务启动晚于 order-service,导致报错。关键是报错之后没有进行重新启动 –> 因此,我们最好是先启动 Nacos 微服务,之后再启动 order-service 等系列微服务

浏览器访问如下,均可正常接收到数据

1
2
http://192.168.119.128:10010/user/2?authorization=admin
http://192.168.119.128:10010/order/102?authorization=admin

至此,DockerCompose 部署微服务完成

8、删除掉这些容器吧,虚拟机要炸了

1
2
//删除通过docker-compose部署的容器,同时删除镜像
docker-compose down --rmi all
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@localhost cloud-demo]# docker-compose down --rmi all
Stopping cloud-demo_nacos_1 ... done
Stopping cloud-demo_userservice_1 ... done
Stopping cloud-demo_gateway_1 ... done
Stopping cloud-demo_mysql_1 ... done
Stopping cloud-demo_orderservice_1 ... done
Removing cloud-demo_nacos_1 ... done
Removing cloud-demo_userservice_1 ... done
Removing cloud-demo_gateway_1 ... done
Removing cloud-demo_mysql_1 ... done
Removing cloud-demo_orderservice_1 ... done
Removing network cloud-demo_default
Removing image nacos/nacos-server
Removing image mysql:5.7.25
Removing image cloud-demo_userservice
Removing image cloud-demo_orderservice
Removing image cloud-demo_gateway

Docker 镜像仓库

简化版镜像仓库

Docker 官方的 Docker Registry 是一个基础版本的 Docker 镜像仓库,具备仓库管理的完整功能,但是没有图形化界面

1
2
3
4
5
6
docker run -d \
--restart=always \
--name registry \
-p 5000:5000 \
-v registry-data:/var/lib/registry \
registry

命令中挂载了一个数据卷 registry-data 到容器内的 /var/lib/registry 目录,这是私有镜像库存放数据的目录,访问如下链接可以查看当前私有镜像服务中包含的镜像

1
http://yourip:5000/v2/_catalog

带有图形化界面版本

操作此步骤前,需要先完成 Docker 信任地址配置

使用 DockerCompose 部署带有图象界面的 DockerRegistry,命令如下

1
2
3
4
5
6
7
8
9
cd /
cd /tmp/
mkdir registry-ui
cd registry-ui
touch docker-compose.yml
//修改这个yml文件,内容如下代码块

//修改yml文件完成后执行docker-compose
docker-compose up -d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
version: '3.0'
services:
registry:
image: registry
volumes:
- ./registry-data:/var/lib/registry
ui:
image: joxit/docker-registry-ui:static
ports:
- 8080:80
environment:
- REGISTRY_TITLE=coffeelize私有仓库
- REGISTRY_URL=http://registry:5000
depends_on:
- registry

通过访问如下地址,即可访问我们创建的带有图形界面的 Docker 镜像仓库了

1
192.168.119.128:8080

22-建立私有仓库.png

配置 Docker 信任地址

我们的私服采用的是 http 协议,默认不被 Docker 信任,所以需要做一个配置

1
2
3
4
5
6
7
8
# 打开要修改的文件
vi /etc/docker/daemon.json
# 添加内容:
"insecure-registries":["http://192.168.119.128:8080"]
# 重加载
systemctl daemon-reload
# 重启docker
systemctl restart docker

在添加内容时,注意多个配置之间别把逗号忘加了

1
2
3
4
{
"registry-mirrors": ["https://5vycoa8o.mirror.aliyuncs.com"],
+ "insecure-registries":["http://192.168.119.128:8080"]
}

在私有镜像仓库中推送或拉去镜像

推送镜像到私有镜像服务必须先 tag,步骤如下:

1、重新 tag 本地镜像(重命名镜像,并且以镜像仓库地址为前缀),名称前缀为私有仓库的地址:192.168.119.128:8080/

1
docker tag nginx:latest 192.168.119.128:8080/nginx:1.0 

利用 tag 命令,可以将一个镜像重命名,这里我们对之前下载的最新版 Nginx 镜像进行操作

23-推送镜像前重命名镜像.png

此时,查看本地的镜像,就可以找到我们打包并且重命名后的镜像了,可以发现这两个镜像的 ID(605c77e624dd)其实是一样的

1
2
192.168.119.128:8080/nginx   1.0        605c77e624dd   12 months ago   141MB
nginx latest 605c77e624dd 12 months ago 141MB

2、推送镜像

1
docker push 192.168.119.128:8080/nginx:1.0 

24-推送镜像到私有仓库.png

3、拉取镜像

1
docker pull 192.168.150.101:8080/nginx:1.0 

单元测试

JUnit 是一个 Java 编程语言的单元测试工具。JUnit 是一个非常重要的测试工具

JUnit 在一个条中显示进度。如果运行良好则是绿色;如果运行失败,则变成红色

使用步骤

  1. 将 junit 的 jar 包导入到工程中 junit-4.9.jar
  2. 编写测试方法该测试方法必须是公共的无参数无返回值的非静态方法
  3. 在测试方法上使用 @Test 注解标注该方法是一个测试方法
  4. 选中测试方法右键通过 junit 运行该方法

代码示例

1
2
3
4
5
6
7
8
9
10
public class JunitDemo1 {
@Test
public void add() {
System.out.println(2 / 0);
int a = 10;
int b = 20;
int sum = a + b;
System.out.println(sum);
}
}

相关注解

  • @Test:表示测试该方法
  • @Before:在测试的方法前运行
  • @After:在测试的方法后运行

日志

程序中的日志可以用来记录程序在运行的时候点点滴滴,并可以进行永久存储

日志与输出语句的区别

输出语句 日志技术
取消日志 需要修改代码,灵活性比较差 不需要修改代码,灵活性比较好
输出位置 只能是控制台 可以将日志信息写入到文件或者数据库中
多线程 和业务代码处于一个线程中 多线程方式记录日志,不影响业务代码的性能

日志体系结构

01-日志的体系结构.png

Log4J

Log4j 是 Apache 的一个开源项目;通过使用 Log4j,我们可以控制日志信息输送的目的地是控制台、文件等位置,我们也可以控制每一条日志的输出格式,通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程

最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码

入门案例

  1. 导入 log4j 的相关 jar 包
  2. 编写 log4j 配置文件
  3. 在代码中获取日志的对象
  4. 按照级别设置记录日志信息

1、properties 文件

注意:配置文件的文件名必须是 log4j.properties,放在 src 目录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// log4j的配置文件,名字为log4j.properties, 放在src根目录下
log4j.rootLogger=debug,my,fileAppender

### direct log messages to my ###
log4j.appender.my=org.apache.log4j.ConsoleAppender
log4j.appender.my.ImmediateFlush = true
log4j.appender.my.Target=System.out
log4j.appender.my.layout=org.apache.log4j.PatternLayout
log4j.appender.my.layout.ConversionPattern=%d %t %5p %c{1}:%L - %m%n

# fileAppender演示
log4j.appender.fileAppender=org.apache.log4j.FileAppender
log4j.appender.fileAppender.ImmediateFlush = true
log4j.appender.fileAppender.Append=true
log4j.appender.fileAppender.File=D:/log4j-log.log
log4j.appender.fileAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.fileAppender.layout.ConversionPattern=%d %5p %c{1}:%L - %m%n

2、测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Log4JTest01 {
//方式1:使用log4j的api来获取日志的对象
//弊端:如果以后我们更换日志的实现类,那么下面的代码就需要跟着改
//不推荐使用
//private static final Logger LOGGER = Logger.getLogger(Log4JTest01.class);

//方式2:使用slf4j里面的api来获取日志的对象
//好处:如果以后我们更换日志的实现类,那么下面的代码不需要跟着修改
//推荐使用(在代码中获取日志的对象)
private static final Logger LOGGER = LoggerFactory.getLogger(Log4JTest01.class);

public static void main(String[] args) {
//1.导入jar包
//2.编写配置文件
//3.在代码中获取日志的对象
//4.按照日志级别设置日志信息
LOGGER.debug("debug级别的日志");
LOGGER.info("info级别的日志");
LOGGER.warn("warn级别的日志");
LOGGER.error("error级别的日志");
}
}

配置文件详解

三个核心

  • Loggers (记录器) :日志的级别
    • Loggers 组件在此系统中常见的五个级别:DEBUG、INFO、WARN、ERROR 和 FATAL
    • DEBUG < INFO < WARN < ERROR < FATAL
    • Log4j 有一个规则:只输出级别不低于设定级别的日志信息
  • Appenders (输出源):日志要输出的地方
    • 把日志输出到不同的地方,如控制台(Console)、文件(Files)等
    • org.apache.log4j.ConsoleAppender(控制台)
    • org.apache.log4j.FileAppender(文件)
  • Layouts (布局):日志输出的格式
    • org.apache.log4j.PatternLayout(可以灵活地指定布局模式,这个比较常用)
    • org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串)
    • org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// log4j的配置文件,名字为log4j.properties, 放在src根目录下
log4j.rootLogger=debug,my,fileAppender

### direct log messages to my ###
log4j.appender.my=org.apache.log4j.ConsoleAppender
log4j.appender.my.ImmediateFlush = true
log4j.appender.my.Target=System.out
log4j.appender.my.layout=org.apache.log4j.PatternLayout
log4j.appender.my.layout.ConversionPattern=%d %t %5p %c{1}:%L - %m%n

# fileAppender演示
log4j.appender.fileAppender=org.apache.log4j.FileAppender
log4j.appender.fileAppender.ImmediateFlush = true
log4j.appender.fileAppender.Append=true
log4j.appender.fileAppender.File=D:/log4j-log.log
log4j.appender.fileAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.fileAppender.layout.ConversionPattern=%d %5p %c{1}:%L - %m%n

1、配置根 Logger

1
2
3
4
log4j.rootLogger=日志级别,appenderName1,appenderName2,…

# 日志级别:OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL或者自定义的级别
# appenderName1:就是指定日志信息要输出到哪里。可以同时指定多个输出目的地,用逗号隔开

2、ConsoleAppender 常用的选项

1
2
3
log4j.appender.my=org.apache.log4j.ConsoleAppender
log4j.appender.my.ImmediateFlush = true
log4j.appender.my.Target=System.out
  • 第一句表示往控制台输出
  • 第二句表示所有消息都会被立即输出,设为 false 则不输出,默认值是 true
  • 第三句默认值是 System.out
    • 比如 System.err 打印出来是红色的

3、FileAppender 常用的选项

1
2
3
4
log4j.appender.fileAppender=org.apache.log4j.FileAppender
log4j.appender.fileAppender.ImmediateFlush = true
log4j.appender.fileAppender.Append=true
log4j.appender.fileAppender.File=D:/log4j-log.log
  • 第一句:往文件中输出
  • 第二句:表示所有消息都会被立即输出。设为 false 则不输出,默认值是 true
  • 第三句:true 表示将消息添加到指定文件中,原来的消息不覆盖,默认值是 true
  • 第四句:指定消息输出到某个文件中

4、PatternLayout 常用的选项

  • org.apache.log4j.PatternLayout(可以灵活地指定布局模式,最常用)
  • org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串)
  • org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息)

这里我们只讲第一种:PatternLayout 的常用选项

02-日志中PatternLayout的常用选项.png

在项目中的应用

  1. 导入相关的依赖
  2. 将资料中的 properties 配置文件复制到 src 目录下
  3. 在代码中获取日志的对象
  4. 按照级别设置记录日志信息

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@WebServlet(urlPatterns = "/servlet/loginservlet")
public class LoginServlet implements HttpServlet{

//获取日志的对象
private static final Logger LOGGER = LoggerFactory.getLogger(LoginServlet.class);

@Override
public void service(HttpRequest httpRequest, HttpResponse httpResponse) {
//处理
System.out.println("LoginServlet处理了登录请求");

LOGGER.info("现在已经处理了登录请求,准备给浏览器响应");

//响应
httpResponse.setContentTpye("text/html;charset=UTF-8");
httpResponse.write("登录成功");
}
}

注意:Logger 对象是 slf4j (org.slf4jh) 中的,不要导错包了

反射的概述

反射机制

  • 是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;
  • 对于任意一个对象,都能够调用它的任意属性和方法;
  • 这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。

01-用子类创建对象与常规创建对象的比较.png

总结下来

  • 利用反射可以无视修饰符获取类里面所有的属性和方法
  • 先获取配置文件中的信息,动态获取(如从配置文件中的读取)信息并创建对象和调用方法

获取 Class 对象

调用一个类中的方法(如上图所示)

  1. 创建这个类的对象
  2. 用对象调用方法

反射去调用一个类中的方法(如上图所示)

  1. 反射方式:创建对象
  2. 反射方式:调用方法
    02-利用反射调用类中的方法.png

那么现在的问题就是如何获取这个 Class 对象
03-获取Class对象的三种方式.png

数据准备

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
public class Student {
private String name;
private int age;

public Student() {
}

public Student(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public void study(){
System.out.println("学生在学习");
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

三种获取 Class 对象方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ReflectDemo1 {
public static void main(String[] args) throws ClassNotFoundException {
//1.Class类中的静态方法forName("全类名")
//全类名:包名 + 类名
Class clazz = Class.forName("com.itheima.myreflect2.Student");
System.out.println(clazz);

//2.通过class属性来获取
Class clazz2 = Student.class;
System.out.println(clazz2);

//3.利用对象的getClass方法来获取class对象
//getClass方法是定义在Object类中.
Student s = new Student();
Class clazz3 = s.getClass();
System.out.println(clazz3);

//class对象是唯一的
System.out.println(clazz == clazz2);
System.out.println(clazz2 == clazz3);
}
}

反射获取 Class 类的对象

Class 类的对象包括:成员变量对象、构造器对象及成员方法对象

04-Class对象中的3类对象.png

数据准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Student {
private String name;
private int age;

//私有的有参构造方法
private Student(String name) {
System.out.println("name的值为:" + name);
System.out.println("private...Student...有参构造方法");
}

//公共的无参构造方法
public Student() {
System.out.println("public...Student...无参构造方法");
}

//公共的有参构造方法
public Student(String name, int age) {
System.out.println("name的值为:" + name + "age的值为:" + age);
System.out.println("public...Student...有参构造方法");
}
}

获取 Constructor 对象

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
public class ReflectDemo1 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
//method1();
//method2();
//method3();
//method4();
}

private static void method4() throws ClassNotFoundException, NoSuchMethodException {
//返回单个构造方法对象
//1.获取Class对象
Class clazz = Class.forName("com.itheima.myreflect3.Student");
//获取单个构造方法对象
Constructor constructor = clazz.getDeclaredConstructor(String.class);
System.out.println(constructor);
}

private static void method3() throws ClassNotFoundException, NoSuchMethodException {
//返回单个公共构造方法对象
//1.获取Class对象
Class clazz = Class.forName("com.itheima.myreflect3.Student");
//小括号中,一定要跟构造方法的形参保持一致
//获取单个公共构造方法对象
Constructor constructor1 = clazz.getConstructor();
System.out.println(constructor1);
//这里获取有参构造方法
Constructor constructor2 = clazz.getConstructor(String.class, int.class);
System.out.println(constructor2);

//因为Student类中,没有只有一个int的构造,所以这里会报错.
Constructor constructor3 = clazz.getConstructor(int.class);
System.out.println(constructor3);
}

private static void method2() throws ClassNotFoundException {
//返回所有构造方法对象的数组
//1.获取Class对象
Class clazz = Class.forName("com.itheima.myreflect3.Student");
//2.获取所有构造方法对象的数组
Constructor[] constructors = clazz.getDeclaredConstructors();
for (Constructor constructor : constructors) {
System.out.println(constructor);
}
}

private static void method1() throws ClassNotFoundException {
//返回所有公共构造方法对象的数组
//1.获取Class对象
Class clazz = Class.forName("com.itheima.myreflect3.Student");
//2.获取所有公共构造方法对象的数组
Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
System.out.println(constructor);
}
}
}

Constructor 创建对象

那么我们获取到的 Class 对象中的构造方法,不就是为了使用它来创建对象嘛,方法如下:

  • T newInstance(Object…initargs):根据指定的构造方法创建对象(T 表示返回值类型,此处为创建对来的对象)
  • setAccessible(boolean flag):设置为 true,表示取消访问检查(通过获取私有的构造方法来创建对象,如果用反射强行获取并使用,需要临时取消访问检查)

也就是说,如果构造方法是 public 的,直接使用 newInstance 即可创建对象;如果构造方法是 private 的,那么需要在创建对象之前临时取消访问检查,也就是 暴力反射

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
// Student类同上一个示例,这里就不在重复提供了
public class ReflectDemo2 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//T newInstance(Object... initargs):根据指定的构造方法创建对象
//method1();
//method2();
//method3();
//method4();

}
private static void method1() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
//1.获取class对象
Class clazz = Class.forName("com.itheima.myreflect3.Student");

//2.获取构造方法对象
Constructor constructor = clazz.getConstructor(String.class, int.class);

//3.利用newInstance创建Student的对象
Student student = (Student) constructor.newInstance("zhangsan", 23);

System.out.println(student);
}

private static void method2() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
//1.获取class对象
Class clazz = Class.forName("com.itheima.myreflect3.Student");

//2.获取构造方法对象
Constructor constructor = clazz.getConstructor();

//3.利用空参来创建Student的对象
Student student = (Student) constructor.newInstance();

System.out.println(student);
}

private static void method3() throws ClassNotFoundException, InstantiationException, IllegalAccessException {
//method2的简写格式
//1.获取class对象
Class clazz = Class.forName("com.itheima.myreflect3.Student");

//2.在Class类中,有一个newInstance方法,可以利用空参直接创建一个对象
Student student = (Student) clazz.newInstance();//这个方法现在已经过时了,了解一下

System.out.println(student);
}

private static void method4() throws ClassNotFoundException, NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
//获取一个私有的构造方法并创建对象
//1.获取class对象
Class clazz = Class.forName("com.itheima.myreflect3.Student");

//2.获取一个私有化的构造方法.
Constructor constructor = clazz.getDeclaredConstructor(String.class);

//被private修饰的成员,不能直接使用的
//如果用反射强行获取并使用,需要临时取消访问检查
constructor.setAccessible(true);

//3.直接创建对象
Student student = (Student) constructor.newInstance("zhangsan");

System.out.println(student);
}

}

反射获取成员变量

  1. 获取 Class 对象
  2. 获取 Field 对象
  3. 赋值或者取值

数据准备

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

public String name;
public int age;
public String gender;
private int money = 300;

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
", money=" + money +
'}';
}
}

获取成员变量

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
public class ReflectDemo1 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
// method1();
//method2();
//method3();
//method4();
}

private static void method1() throws ClassNotFoundException {
//返回所有公共成员变量对象的数组

//1.获取class对象
Class clazz = Class.forName("com.itheima.myreflect4.Student");

//2.获取Field对象.
Field[] fields = clazz.getFields();

//3.遍历
for (Field field : fields) {
System.out.println(field);
}
}

private static void method2() throws ClassNotFoundException {
//返回所有成员变量对象的数组
//1.获取class对象
Class clazz = Class.forName("com.itheima.myreflect4.Student");

//2.获取所有的Field对象
Field[] fields = clazz.getDeclaredFields();

//3.遍历
for (Field field : fields) {
System.out.println(field);
}
}

private static void method3() throws ClassNotFoundException, NoSuchFieldException {
//返回单个公共成员变量对象
//想要获取的成员变量必须是真实存在的
//且必须是public修饰的.
//1.获取class对象
Class clazz = Class.forName("com.itheima.myreflect4.Student");

//2.获取name这个成员变量
//Field field = clazz.getField("name");
//Field field = clazz.getField("name1");
Field field = clazz.getField("money");

//3.打印一下
System.out.println(field);
}

private static void method4() throws ClassNotFoundException, NoSuchFieldException {
//返回单个成员变量对象
//1.获取class对象
Class clazz = Class.forName("com.itheima.myreflect4.Student");

//2.获取money成员变量
Field field = clazz.getDeclaredField("money");

//3.打印一下
System.out.println(field);
}
}

Field 对象取值或赋值

既然获取到 Field 对象了,那么就赋值或取值吧

赋值:void set(Object obj, Object value),给指定对象的成员变量赋值
取值:Object get(Object obj),返回指定对象的 Field 值

为什么中间需要有一个 Object 类呢?比如遇到如下情形,set 方法怎么知道需要将值赋给谁呢?
05-为成员变量赋值.png

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
public class ReflectDemo2 {  
public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InstantiationException {
//返回由该 Field表示的字段在指定对象上的值。
//method1();
//method2(); }

private static void method1() throws ClassNotFoundException, NoSuchFieldException, InstantiationException, IllegalAccessException {
//给obj对象的成员变量赋值为value
//1.获取class对象
Class clazz = Class.forName("com.itheima.myreflect4.Student");

//2.获取name这个Field对象
Field field = clazz.getField("name");

//3.利用set方法进行赋值.
//3.1先创建一个Student对象
Student student = (Student) clazz.newInstance();
//3.2有了对象才可以给指定对象进行赋值
field.set(student,"zhangsan");

System.out.println(student);
}

private static void method2() throws ClassNotFoundException, NoSuchFieldException, InstantiationException, IllegalAccessException {
//1.获取class对象
Class clazz = Class.forName("com.itheima.myreflect4.Student");

//2.获取成员变量Field的对象
Field field = clazz.getDeclaredField("money");

//3.取消一下访问检查
field.setAccessible(true);

//4.调用get方法来获取值
//4.1创建一个对象
Student student = (Student) clazz.newInstance();
//4.2获取指定对象的money的值
Object o = field.get(student);

//5.打印一下
System.out.println(o);
}
}

反射获取成员方法

  1. 获取 Class 对象
  2. 获取 Method 对象
  3. 运行方法
  • Method[] getMethods():返回所有公共成员方法对象的数组,包括继承的
  • Method[] getDeclaredMethods():返回所有成员方法对象的数组,不包括继承的
  • 返回单个公共成员方法对象
  • 返回单个成员方法对象

数据准备

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

//私有的,无参无返回值
private void show() {
System.out.println("私有的show方法,无参无返回值");
}

//公共的,无参无返回值
public void function1() {
System.out.println("function1方法,无参无返回值");
}

//公共的,有参无返回值
public void function2(String name) {
System.out.println("function2方法,有参无返回值,参数为" + name);
}

//公共的,无参有返回值
public String function3() {
System.out.println("function3方法,无参有返回值");
return "aaa";
}

//公共的,有参有返回值
public String function4(String name) {
System.out.println("function4方法,有参有返回值,参数为" + name);
return "aaa";
}
}

获取成员方法

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
public class ReflectDemo1 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {
//method1();
//method2();
//method3();
//method4();
//method5();
}

private static void method1() throws ClassNotFoundException {
//返回所有公共成员方法对象的数组,包括继承的
//1.获取class对象
Class clazz = Class.forName("com.itheima.myreflect5.Student");
//2.获取成员方法对象
Method[] methods = clazz.getMethods();
//3.遍历
for (Method method : methods) {
System.out.println(method);
}
}

private static void method2() throws ClassNotFoundException {

//返回所有成员方法对象的数组,不包括继承的
//1.获取class对象
Class clazz = Class.forName("com.itheima.myreflect5.Student");

//2.获取Method对象
Method[] methods = clazz.getDeclaredMethods();
//3.遍历一下数组
for (Method method : methods) {
System.out.println(method);
}
}

private static void method3() throws ClassNotFoundException, NoSuchMethodException {
//返回单个公共成员方法对象
//1.获取class对象
Class clazz = Class.forName("com.itheima.myreflect5.Student");
//2.获取成员方法function1
Method method1 = clazz.getMethod("function1");
//3.打印一下
System.out.println(method1);
}

private static void method4() throws ClassNotFoundException, NoSuchMethodException {
//1.获取class对象
Class clazz = Class.forName("com.itheima.myreflect5.Student");
//2.获取一个有形参的方法function2
Method method = clazz.getMethod("function2", String.class);
//3.打印一下
System.out.println(method);
}

private static void method5() throws ClassNotFoundException, NoSuchMethodException {
//返回单个成员方法对象
//1.获取class对象
Class clazz = Class.forName("com.itheima.myreflect5.Student");
//2.获取一个成员方法show
Method method = clazz.getDeclaredMethod("show");
//3.打印一下
System.out.println(method);
}
}

运行成员方法

  • Object invoke(Object obj, Object… args):运行方法
    • 参数 1:用 obj 对象调用该方法
    • 参数 2:调用方法的传递的参数(如果没有就不写)
    • 返回值:方法的返回值(如果没有就不写)

比如说,我们现在想通过反射来获取 Class 中的 function4 方法,并运行它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ReflectDemo2 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
//Object invoke(Object obj, Object... args):运行方法
//参数一:用obj对象调用该方法
//参数二:调用方法的传递的参数(如果没有就不写)
//返回值:方法的返回值(如果没有就不写)

//1.获取class对象
Class clazz = Class.forName("com.itheima.myreflect5.Student");
//2.获取里面的Method对象 function4
Method method = clazz.getMethod("function4", String.class);
//3.运行function4方法就可以了
//3.1创建一个Student对象,当做方法的调用者
Student student = (Student) clazz.newInstance();
//3.2运行方法
Object result = method.invoke(student, "zhangsan");
//4.打印一下返回值
System.out.println(result);
}
}

coffeelize

这两天收到阿里云域名续费消息,同时也收到了网站 SSL 证书到期的邮件,是的,域名买了一年,到今天刚好一年。

博客在中途更换过一次域名,也就是现在的域名:coffeelize.top。之所以叫 coffeelize,不仅仅是因为 #咖啡日常 #,更多的其实是想强调 lizelize 在英语单词中通常为动词的后缀,很多情况下 名词 + lize 后就变成了动词,一是想表达:就像喝了一杯咖啡一样,满满的驱动力;二是期望:自己能够像 “coffeee” 那样,能够带给周围人更多的积极向上的 “动力”。

01-博客运行时间.png

02-域名续费消息.png

03-证书到期.png

去年的这个时候,还在复习着数值分析的期末考试呢😂,搭建这个博客花了我很长时间,记得那个 SSL 总是无法正常连接,导致无法往仓库中推送文件,也算是苦中作乐吧,哈哈

有了自己的个人博客之后,开始关注更多相关领域的个人博客,学着有模有样的发布笔记、装扮博客、交换友链,开始更加注重消息的来源和质量。自己也喜欢看别人的博客和笔记,有种窥探别人日记本闯入他人领地的感觉,遇到和自己 “技术栈” 差不多的站长,会主动联系,加个好友、交换一下友链或者相互鼓励一下,这种感觉很棒,可能也是我不断更新下去的主要原因吧。

博客在这一年里也有一点点的收获,下图为必应搜索引擎的数据报告(因为之前更换了一次域名的原因,这里只显示从 6 月份开始到现在的数据)

05-必应控制台.png

其实我也不在乎这个数据报告,更在乎的是我的🤝朋友们及笔记呀
06-朋友们以及笔记.png

什么是 Git

Git 是一个分布式版本控制工具,主要用于管理开发过程中的源代码文件(Java 类、xml 文件、html 页面等),在软件开发过程中被广泛使用

Git 能做什么

  • 代码回溯:Git 在管理文件过程中会记录日志,方便回退到历史版本
  • 版本切换:Git 存在分支的概念,一个项目可以有多个分支(版本),可以任意切换
  • 多人协作:Git 支持多人协作,即一个团队共同开发一个项目,每个团队成员负责一部分代码,通过 Git 就可以管理和协调
  • 远程备份:Git 通过仓库管理文件,在 Git 中存在远程仓库,如果本地文件丢失还可以从远程仓库获取

我们可以借助互联网上提供的一些代码托管服务来实现,其中比较常用的有 GitHub、码云、GitLab 等,这里以码云为例进行讲解

Git 全局设置

当安装 Git 后首先要做的事情是设置用户名称和 email 地址。这是非常重要的,因为每次 Git 提交都会使用该用户信息。在 Git 命令行中执行下面命令:

  • 设置用户信息
1
2
git config --global user.name "coffeelize"
git config --global user.email "coffeelize@qq.com"

注意:上面设置的 user.name 和 user.email 并不是我们在注册码云账号时使用的用户名和邮箱,此处可以任意设置

  • 查看配置信息
1
git config --list

01-配置用户名和邮箱.png

获取 Git 仓库

要使用 Git 对我们的代码进行管理,首先需要获得 Git 仓库,获取 Git 仓库通常有两种方式

  • 在本地初始化 Git 仓库(不常用)
  • 从远程仓库克隆(常用)

在本地初始化 Git 仓库

  1. 在任意目录下创建一个空目录(例如 repo1)作为我们的本地 Git 仓库
  2. 进入这个目录中,点击右键打开 Git bash 窗口
  3. 执行命令 git init

初始化之后,目录中会多一个 .git 隐藏文件夹,命令行后会多出一个 master 分支
02-master主分支.png

注意:本地仓库不是从远程仓库克隆下来的,且本地仓库中若已经存放了一些文件,此时再从远程仓库拉去文件的时候可能会报如下所示的错误:fatal: refusing to merge unrelated histories(原因是本地仓库中含有本地仓库中文件的历史记录,远程仓库中包含远程仓库中文件的记录信息,这两者间完全没有联系,此时就需要将两者建立起联系)

016-本地仓库首次连接远程仓库报错.png

此时,可以通过如下命令解决

1
git pull origin master --allow-unrelated-histories

从远程仓库克隆

可以通过 Git 提供的命令从远程仓库进行克隆,将远程仓库克隆到本地

1
git clone 远程仓库地址

注:第一次克隆远程仓库时,可能需要登录 Git Credential Manage,即登录 Gitee 的账号,登录之后即可正常操作。同时登录之后,也会将一些验证信息保存到电脑当中,之后再次使用就不用再手动登录了

03-登录Gitee验证.png

工作区、暂存区、版本库

  • 版本库:前面看到的 .git 隐藏文件夹 就是版本库,版本库中存储了很多配置信息、日志信息和文件版本信息等
  • 工作区:包含.git 文件夹的目录就是工作区,也称为工作目录,主要用于存放开发的代码
  • 暂存区:.git 文件夹中有很多文件,其中有一个 index 文件 就是暂存区。暂存区是一个临时保存修改文件的地方

Git 工作区中文件的状态

Git 工作区中的文件存在两种状态:

  • untracked:未跟踪(未被纳入版本控制)
  • tracked:已跟踪(被纳入版本控制)
    • Unmodified 未修改状态
    • Modified 已修改状态
    • Staged 已暂存状态

本地仓库常用操作

  • git status:查看文件状态
  • git add:将文件的修改加入暂存区
  • git reset:将暂存区的文件取消暂存或者是切换到指定版本
  • git commit:将暂存区的文件修改提交到版本库
  • git log:查看日志

git status

可以通过 git status 来查看当前状态
04-status查看状态.png

git add

1
git add fileName

git reset

每次 Git 提交都会产生新的版本号,通过版本号就可以回到历史版本

1
2
git reset --hard 版本号
git reset --hard 76b00c3fd44c92359d70e02cb4ff35c3acf90b40

git commit

1
2
git commit -m msg 文件名
git commit -m "提交一个文件" User.java

红色字体:未跟踪,也就是还没有纳入 git 的版本管理
绿色字体:文件已经放到了缓存区

git log

git log 命令的作用是查看提交日志;通过 git log 命令查看日志,可以发现每次提交都会产生一个版本号,提交时设置的 message、提交人、邮箱、提交时间等信息都会记录到日志中

05-log查看日志.png

远程仓库常用命令

  • git remote:查看远程仓库
  • git remote add:添加远程仓库
  • git clone:从远程仓库克隆
  • git pull:从远程仓库拉取
  • git push:推送到远程仓库

git remote

origin 表示远程仓库的简称

1
2
git remote
git remote -v

06-查看远程仓库简称.png

说明当前本地的仓库已经和远程的仓库之间建立好了连接了。如果输入以上两个命令没有返回值的话,说明这仅仅只是一个本地仓库

git remote add

1
git remote add 简称 远程仓库地址

注意

  • 一个本地仓库可以关联多个远程仓库
  • 这个简称我们习惯命名为 origin

比如我这边初始化了一个本地仓库,然后还有一个远程仓库,想要将这个本地仓库和远程仓库联系起来

1
git remote add origin https://gitee.com/coffeelize/repo1.git

此时再输入命令 git remote -v,即可查看是否已经关联成功

git clone

Git 克隆的是该 Git 仓库服务器上的几乎所有数据(包括日志信息、历史记录等)

1
git clone 远程仓库地址

git push

将本地仓库内容推送到远程仓库

1
2
git push 远程仓库简称 分支名称

先需要将文件提交到本地仓库 (add & commit),再推送到远程仓库

切换远程仓库

如果当前本地仓库需要链接到另一个远程仓库呢,怎么处理?
比如说本地仓库当前绑定的是 repo1 仓库,想要将本地仓库绑定到另外一个远程仓库 hellogit。当前如果已经连接到了一个远程仓库,是无法直接通过添加远程仓库 URL 来覆盖掉原来的 URL,如下图所示:

017-无法直接覆盖远程连接的URL.png

方式一:直接修改远程仓库地址,更换远程仓库地址

1
2
3
git remote set-url origin URL
git remote set-url orgin https://gitee.com/coffeelize/hellogit.git

方式二:先先删除当前连接的远程仓库地址,然后在添加

1
2
git remote rm origin
git remote add origin url

分支操作

分支是 Git 使用过程中非常重要的概念。使用分支意味着你可以把你的工作从开发主线上分离开来,以免影响开发主线。

本地仓库和远程仓库中都有分支,同一个仓库可以有多个分支,各个分支相互独立,互不干扰。通过 git init 命令创建本地仓库时默认会创建一个 master 分支。

查看分支

  • git branch:列出所有本地分支
  • git branch -r:列出所有远程分支
  • git branch -a:列出所有本地分支和远程分支

创建分支

1
2
git branch 分支名称
git branch test

07-创建分支.png

切换分支

1
2
git checkout 分支名称
git checkout test

08-切换分支.png

推送至远程仓库分支

1
2
3
git push 远程仓库简称 分支名称
//将test分支推送到远程仓库
git push origin test

合并分支

1
2
git merge 分支名称
git merge test

分支合并时需注意合并的方向,是将命令中的分支合并到当前所在的分支

09-合并分支.png

在合并分支这种大操作下,会进入 vim 模式要求我们写日志,按照 vim 的操作即可

如果在合并当中遇到文件冲突,比如说主分支对 A 文件进行了修改,测试分支也对 A 文件进行了修改,然后在主分支中合并分支时,报如下错误:Automatic merge failed; fix conflicts and then commit the result.

10-合并分支报错.png

此时冲突的文件会自动加入如下内容。
011-合并后的冲突文件.png

假如我们是想要保留这两行,那么可以把这些自动生成的符号删除,然后还需要将这个有冲突的文件再 add 和 commit 一下,此时,仍然会有报错:
fatal: cannot do a partial commit during a merge.(不能在合并的时候只提交一部分)
012-合并解决冲突文件后再次报错.png

这个时候我们需要在 commit 后面添加一个 -i 参数,此时这个冲突被我们手动解决了,然后就可以正常 push 了

1
2
git commit -m "modify by me" testBranch.txt -i
git push origin master

标签操作

Git 中的标签,指的是某个分支某个特定时间点的状态。通过标签,可以很方便的切换到标记时的状态。比较有代表性的是人们会使用这个功能来标记发布结点(v1.0 、v1.2 等)。

查看标签

1
git tag

创建标签

1
2
git tag 标签名
git tag v0.1

删除本地标签

1
2
git tag -d 标签名
git tag -d v0.1

删除远程标签

1
2
git push origin :refs/tags/标签名
git push origin :refs/tags/v0.1

013-删除远程标签.png

将标签推送至远程仓库

1
2
git push 远程仓库简称 标签名
git push origin v0.1

014-将标签推送至远程仓库.png

检出标签

检出标签时需要新建一个分支来指向某个标签。会自动将某个 tag 中的内容检出到一个新的分支下面

1
2
git checkout -b 新建的一个分支名 标签名
git checkout -b aNewBranch v0.2

015-检出标签.png

Maven 主要功能

Maven 是专门用于管理和构建 Java 项目的工具,它的主要功能有:

  • 提供了一套标准化的项目结构
  • 提供了一套标准化的构建流程(编译,测试,打包,发布…)
  • 提供了一套依赖管理机制

标准化的项目结构

不同 IDE 之间,项目结构不一样,不通用
01-不同IDE之间项目结构不一致.png

所有 IDE 使用 Maven 构建的项目结构完全一样,所有 IDE 创建的 Maven 项目都可以通用

Maven 的项目结构
02-Maven项目结构.png

标准化的构建流程

源代码⇒编译⇒测试⇒打包⇒发布,Maven 提供了一套简单的命令来完成项目构建

提供了一套依赖管理机制

比如 JDBC,需要使用的 MySQL 的驱动包,依赖管理其实就是管理你项目所依赖的第三方资源(Jar 包、依赖),原先我们是如何操作的呢:

  1. 下载 jar 包
  2. 将 jar 包复制到 lib 文件夹里
  3. 右键 jar 包,作为库

那 Maven 是如何管理依赖的呢

  1. Maven 使用标准的 坐标 配置来管理各种依赖
  2. 只需要简单的配置就可以完成依赖管理
    03-通过坐标管理依赖.png

Maven 简介

Apache Maven 是一个项目管理和构建工具,它基于项目对象模型(POM)的概念,通过一小段描述信息来管理项目的构建、报告和文档

仓库分类

  • 本地仓库
  • 中央仓库:有 Maven 团队维护的全球唯一的仓库(免费的开源的 jar 包)
  • 远程仓库(私服):一般有公司团队搭建的私有仓库(可以存放一下自己的公司的和一些可能具有版权的 jar 包)

查找流程:首先会查找本地仓库,如果本地仓库没有,则去中央仓库查找是否有,有的话就会 自动下载 到本地仓库

Maven 的安装和配置

  1. 解压即可安装
  2. 配置环境变量

下图中的 Maven 文件夹呢就是包含 bin 文件夹的文件夹
04-配置Maven环境路径1.png

将 bin 目录添加到 Path 目录中
05-配置Maven环境路径2.png

  1. 配置本地仓库:修改 conf/setting.xml 中的 <localRepository> 为其指定一个目录

06-指定本地仓库路径.png

注意:为了保守起见,在 Intellij 中也对 Maven 的本地路径配置一下:)
12-指定本地仓库路径2.png

  1. 配置阿里云私服:修改 conf/setting.xml 中的 <mirrors> 标签,为其添加如下子标签
1
2
3
4
5
6
<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</mirror>

07-配置阿里云远程仓库.png

Maven 的基本使用

Maven 的常用命令

  • compile:编译
  • clean:清理,删除前面编译产生的 target 目录
  • test:测试,执行 test 文件夹下的代码
  • package:打包
  • install:安装

在含有 pom.xml 文件的目录下,进入 PowerShell

1
2
3
4
5
mvn compile
mvn clean
mvn package
mvn tast
mvn install

Maven 的生命周期

Maven 对项目构建的生命周期划分为三套

  • clean:清理工作
  • default:核心工作,例如编译,测试,打包,安装等
  • site:产生报告,发布站点等

08-Maven的生命周期.png

比如说执行 install,就会自动执行 compile,但是不会自动执行 clean(因为这是两套不同的生命周期)

依赖管理

使用坐标导入 jar 包

  1. 在 pom.xml 中编写 <dependencies> 标签
  2. <dependencies> 标签中使用 <dependency> 来引入坐标
  3. 定义坐标的 groupId,artifactId,version
  4. 点击刷新按钮,是坐标生效(或者对 IDE 进行配置,每次变更自动生效)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 导入 mysql 驱动jar包-->
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.12</version>
</dependency>
</dependencies>

比如去找 MySQL 的 Maven 配置信息(浏览器搜索 mysql maven),比如从以下网页中找到了:MySQL Connector Java

还为我们提供了 maven 需要使用的信息
09-从官网获得Maven的配置信息.png

快速导入坐标信息到 pom.xml

如果本地仓库就有相应的 jar 包,那么直接可以通过搜索 jar 包的名字来导入(即可自动导入模板)

  1. 在 pom.xml 文件中,Alt+Insert
  2. 选择依赖项模板,自动为我们添加模板

10-快速插入依赖项模板.png

依赖范围

通过设置坐标依赖范围(scope),可以设置对应 jar 包的作用范围:编译环境、测试环境、运行环境

编译环境:在主工程 java 文件夹中可以使用
测试环境:在测试文件夹 test 中可以使用
运行环境:

依赖范围的取值有以下六种,默认值是 compile(其实范围也是最大的)

分模块开发与设计

将原始模块按照功能拆分为若干个子模块,方便模块间的相互调用,接口共享

01-分模块开发思想.png

Intellij 中同时导入多个模块方式如下:
02-Intellij同时导入多个模块.png

1、项目准备:之前做好的 SSM 整合的项目 maven_02_ssm 进行讲解
2、新建一个模块:maven_03_pojo

1)新建 com.itheima.domain 包
2)将 maven_02_ssm 中的 domain 下的 Book 实体类剪切至 maven_03_pojo 下的 domain 包下
3)此时 maven_02_ssm 将无法运行,因为缺少了 Book 实体类
4)现在要做到是:如何在 maven_02_ssm 中访问 / 加载 maven_03_pojo 下的 Book 实体类呢?

maven_03_pojo 模块中 pom.xml 的坐标如下

1
2
3
<groupId>com.itheima</groupId>  
<artifactId>maven_03_pojo</artifactId>
<version>1.0-SNAPSHOT</version>

那么我们在 maven_02_ssm 中引入上面的坐标

1
2
3
4
5
6
<!--依赖domain运行-->  
<dependency>
<groupId>com.itheima</groupId>
<artifactId>maven_03_pojo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

此时就将 maven_03_pojo 模块引入进来了,此时 maven_02_ssm 也没有报错了

总结:将一个模块中的一部分功能抽取出来单独做一个模块,然后在原来的使用方去引用这个抽取出来的模块,这样就做成了两个模块

但是此时 02 模块会有问题?为什么呢
因为通过坐标导入,会将相应的资源下载到本地仓库,02 模块引入了 03 模块,但是本地仓库里面却找不到 03 的资源。因此,我们还需要将 03 模块 install 到本地仓库中

03-导入坐标之后需要安装到本地仓库.png

安装完之后,本地仓库中就可以找到模块 03 的资源了。此时我们在 compile 一下 02 模块,若能够编译成功,说明没有问题了

依赖管理

如果一个模块 A 依赖了模块 B,而 B 模块依赖了其他的东西,那么这个 A 模块可以直接使用这些东西

直接依赖:在当前项目中通过依赖配置建立的依赖关系
简介依赖:被依赖的资源如果依赖其他资源,当前项目简介依赖其他资源

依赖冲突

  • 路径优先:当依赖中出现相同的资源时,层级越深,优先级越低,层级越浅,优先级越高
  • 声明优先:当资源在相同层级被依赖时,配置顺序靠前的覆盖配置顺序靠后的
  • 特殊优先:当同级配置类相同资源的不同版本,后配置的覆盖先配置的

04-依赖层级关系.png

可以通过此处查看项目中的依赖关系
05-Intellij中查看依赖层级关系.png

可选依赖

比如说 02 模块引用了 04 模块,04 模块中引用了几个坐标。现在的需求是,我不想让 02 模块能够加载或引用 04 模块中的坐标,怎么处理呢?

那么在 04 模块中,对如下这个坐标进行处理

1
2
3
4
5
6
7
<dependency>
<groupId>com.itheima</groupId>
<artifactId>maven_03_pojo</artifactId>
<version>1.0-SNAPSHOT</version>
<!--可选依赖是隐藏当前工程所依赖的资源,隐藏后对应资源将不具有依赖传递性-->
<optional>true</optional>
</dependency>

其实这个需要就是想要某个坐标没有传递性

排除依赖

比如引用了 maven_04_dao 坐标,但是排除这个坐标下的另外两个坐标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>com.itheima</groupId>
<artifactId>maven_04_dao</artifactId>
<version>1.0-SNAPSHOT</version>
<!--排除依赖是隐藏当前资源对应的依赖关系-->
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
</exclusions>
</dependency>

那么可选和排除有什么区别呢:

  • 用的可选,别人引用了我的坐标,但是不知道我背后引用了哪些坐标;对外隐藏当前所依赖的资源
  • 用的排除,别人是知道我背后引用了那些坐标的;主动断开依赖的资源,被排除的资源无需指定版本

聚合

06-聚合的引入.png

比如:上面的三个模块都是依赖 pojo 模块的,假如我们更新了 pojo 模块,上面三个模块会及时更新吗?如果 pojo 因为更新出现了问题,上面三个模块能够及时发现吗?

聚合:将多个模块组织成一个整理,同时进行项目构建的过程称为聚合(其实有点像事务的概念)
聚合工程:通常是一个不具有业务功能的 “空” 工程(仅有一个 pom 文件)
作用:使用聚合工程可以将多个模块编组,通过对聚合工程进行构建,实现对所包含的模块进行同步构建;当工程中某个模块发生更新时,必须保障工程中与已更新模块关联的模块同步更新,此时可以使用聚合工程来解决批量模块同步构建的问题

1)创建新 Maven 模块
2)聚合工程的特点:在 pom.xml 中,将打包方式设置为 pom

1
<packaging>pom</packaging>

3)设置管理模块的模块名称

1
2
3
4
5
6
7
8
<!--设置管理模块名称
..表示当前文件pom.xml文件的上一级文件
-->
<modules>
<module>../maven_02_ssm</module>
<module>../maven_03_pojo</module>
<module>../maven_04_dao</module>
</modules>

07-聚合-引入子模块.png

08-聚合后各层级的关系.png

4)进行同步编译 compile

09-聚合后进行同步编译.png

继承

概念:继承描述的是两个工程间的关系,与 Java 中的继承相似,子工程可以继承父工程中的配置信息,常见于依赖关系的继承

作用:简化配置;减少版本冲突

1)继承关系在子类中描述

1
2
3
4
5
6
7
<!--配置当前工程继承自parent工程-->  
<parent>
<groupId>com.itheima</groupId>
<artifactId>maven_01_parent</artifactId>
<version>1.0-RELEASE</version>
<relativePath>../maven_01_parent/pom.xml</relativePath>
</parent>

此时,就可以继承父工程中依赖的坐标了

2)父工程中的坐标都必须要被所有子工程继承吗?不一定
可以在父工程 pom 文件中通过 dependencyManagement 来指定这是一个可选的坐标

1
2
3
4
5
6
7
8
9
10
11
<!--定义依赖管理-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>

如果子工程中想要引用的话就在 pom 中加上相应的坐标,但是注意不要加版本号,因为他会自动继承父类中坐标的版本号;而对于其他子工程就不会自动继承引用这个坐标

1
2
3
4
5
<dependency>  
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>

3)子工程继承父工程中的坐标,只要父工程中坐标的版本号一改,所有子工程中对应的版本号都会改

聚合与继承的区别

  • 作用
    • 聚合用于快速构建项目
    • 继承用于快速配置
  • 相同点
    • 聚合与继承的 pom.xml 文件打包方式均为 pom,可以将两种关系制作到同一个 pom 文件中
    • 聚合与继承均属于设计型模块,并无实际的模块内容
  • 不同点
    • 聚合是在当前模块中配置关系,聚合可以感知到参与聚合的模块有哪些
    • 继承是在子模块中配置关系,父模块无法感知哪些子模块继承了自己

属性

10-属性问题的引入.png

1)定义属性

1
2
3
4
<!--定义属性,标签名可以自定义-->  
<properties>
<spring.version>5.2.10.RELEASE</spring.version>
</properties>

2)在定义坐标时,可以直接使用变量

1
2
3
4
5
6
7
8
9
10
11
<dependency>  
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>

3)这样就好了呀,以后可以直接从这儿就可以看到各种坐标的版本

1
2
3
4
5
6
7
<!--定义属性-->  
<properties>
<spring.version>5.2.10.RELEASE</spring.version>
<junit.version>4.12</junit.version>
<mybatis-spring.version>1.3.0</mybatis-spring.version>
<!--<jdbc.url>jdbc:mysql://127.0.0.1:3306/ssm_db</jdbc.url>-->
</properties>

版本管理

比如如下为某一工程 pom.xml 中的坐标,其中的 version 有什么用呢?

1
2
3
4
<groupId>com.itheima</groupId>  
<artifactId>maven_02_ssm</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
  • 工程版本
    • SNAPSHOT(快照版本)
      • 项目开发过程中临时输出的版本,称为快照版本
      • 快照版本会随着开发的进展不断更新
    • RELESE(发布版本)
      • 项目开发到进入阶段里程碑后,向团队外部发布较为稳定的版本,这种版本所对应的构建文件时稳定的,即便进行功能的后续开发,也不会改变当前发布版本的内容,这种版本称为发布版本
  • 发布版本
    • alpha 版
    • beta 版
    • 纯数字版

多环境配置

011-多环境配置.png

maven 提供配置多种环境的设定,帮助开发者使用过程中快速切换环境

1)在父工程 pom.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
25
26
27
28
<!--配置多环境-->
<profiles>
<!--开发环境-->
<profile>
<id>env_dep</id>
<properties>
<jdbc.url>jdbc:mysql://127.1.1.1:3306/ssm_db</jdbc.url>
</properties>
<!--设定是否为默认启动环境-->
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<!--生产环境-->
<profile>
<id>env_pro</id>
<properties>
<jdbc.url>jdbc:mysql://127.2.2.2:3306/ssm_db</jdbc.url>
</properties>
</profile>
<!--测试环境-->
<profile>
<id>env_test</id>
<properties>
<jdbc.url>jdbc:mysql://127.3.3.3:3306/ssm_db</jdbc.url>
</properties>
</profile>
</profiles>

2)对工程进行 install,然后可以查看项目构建好的 war 包,双击 war 包,进入 WEB-INF 文件夹 ⇒ classes 文件夹 ⇒ jdbc.properties 文件,查看配置是否生效

如果要更换环境,可以将设置默认启动环境的那几行代码切换一下位置,比如切换到测试环境中,那么 install 后,默认就是测试环境的配置了

或者可以不用挪动那几行代码,直接使用 Maven 指令来表明我们将使用 env_dep 环境来进行 install,如下图所示
012-Maven指令处理多环境.png

013-Maven指令处理多环境-2.png

跳过测试

跳过测试:跳过所有测试
014-跳过测试.png
跳过测试:指定跳过某些内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12.4</version>
<configuration>
<skipTests>false</skipTests>
<!--排除掉不参与测试的内容-->
<excludes>
<exclude>**/BookServiceTest.java</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

Rest 简介

Rest (Representation State Transfer, 表现形式状态转换),即访问网络资源的格式

传统风格资源描述形式书写如下

1
2
http://localhost/user/getById?id=1
http://localhost/user/saveUser

REST 风格描述形式如下

1
2
http://localhost/user/1
http://localhost/user

特点

  • 隐藏资源的访问行为,无法通过地址得知对资源是何种操作
  • 书写简化
  • 按照 REST 风格访问资源时使用行为动作区分对资源进行了何种操作
  • 根据 REST 风格对资源进行访问称为 RESTful

Rest风格增删改查.png

注意事项

  • 上述行为是约定方式,约定不是规范,可以打破,所以称 REST 风格,而不是 REST 规范
  • 描述模块的名称通常使用复数,也就是加 s 的格式描述,表示此类资源,而非单个资源,例如:users、books 等

Rest 入门案例

1、原先的风格

1
2
3
4
5
6
@RequestMapping("/save")  
@ResponseBody
public String save(){
System.out.println("user save...");
return "{'module':'user save'}";
}

2、REST 风格

1
2
3
4
5
6
7
//设置当前请求方法为POST,表示REST风格中的添加操作  
@RequestMapping(value = "/users",method = RequestMethod.POST)
@ResponseBody
public String save(){
System.out.println("user save...");
return "{'module':'user save'}";
}

删除

1
2
3
4
5
6
7
8
//设置当前请求方法为DELETE,表示REST风格中的删除操作  
//@PathVariable注解用于设置路径变量(路径参数),要求路径上设置对应的占位符,并且占位符名称与方法形参名称相同
@RequestMapping(value = "/users/{id}",method = RequestMethod.DELETE)
@ResponseBody
public String delete(@PathVariable Integer id){
System.out.println("user delete..." + id);
return "{'module':'user delete'}";
}

注意 Postman 中的请求路径:

1
http://localhost//users/1

@PathVariable 表示后面的变量来自路径,但是来自路径中的哪儿呢?
而通过 value = "/users/{id}" 中就指明了路径参数(路径变量)

修改

1
2
3
4
5
6
7
//设置当前请求方法为PUT,表示REST风格中的修改操作  
@RequestMapping(value = "/users",method = RequestMethod.PUT)
@ResponseBody
public String update(@RequestBody User user){
System.out.println("user update..."+user);
return "{'module':'user update'}";
}

根据 id 查询

1
2
3
4
5
6
7
8
//设置当前请求方法为GET,表示REST风格中的查询操作  
//@PathVariable注解用于设置路径变量(路径参数),要求路径上设置对应的占位符,并且占位符名称与方法形参名称相同
@RequestMapping(value = "/users/{id}" ,method = RequestMethod.GET)
@ResponseBody
public String getById(@PathVariable Integer id){
System.out.println("user getById..."+id);
return "{'module':'user getById'}";
}

查询所有

1
2
3
4
5
6
7
//设置当前请求方法为GET,表示REST风格中的查询操作  
@RequestMapping(value = "/users",method = RequestMethod.GET)
@ResponseBody
public String getAll(){
System.out.println("user getAll...");
return "{'module':'user getAll'}";
}

总结

  1. 设定 http 请求动作:@RequestMapping 的 mathod 属性设置请求动作
  2. 设定请求参数(路径变量)
  3. @PathVariable 形参注解,用于绑定路径参数与处理器方法形参间的关系,要求路径参数名和形参名一一对应

@RequestBody:用于接收 json 数据
@RequestParam:接受 URL 地址传参或表单传参
@PathVariable:用于接收路径参数,使用 {参数名称} 描述路径参数

RESTful 快速开发

简化书写

1
2
3
@RequestMapping(value = "/users/{id}" ,method = RequestMethod.GET)
@RequestMapping(value = "/users",method = RequestMethod.GET)
@RequestMapping(value = "/users",method = RequestMethod.PUT)

问题 1:可以看到,以上的这几个中 value = "/users" 都是重复要写的内容,能不能更简化呢?

1
2
3
4
5
@Controller
@RequestMapping("/books")
public class BookController {
//各种处理器方法
}

问题 2:每一个处理器方法前面都带着一个 @ResponseBody 注解,能不能更简化些呢?

@ResponseBody 写到类的前面

1
2
3
4
5
6
@Controller
@ResponseBody
@RequestMapping("/books")
public class BookController {
//各种处理器方法
}

Spring ⇒ 既然每次都得写 @Controller@ResponseBody,那就合二为一吧,使用 @RestController 即可

1
2
3
4
5
@RestController
@RequestMapping("/books")
public class BookController {
//各种处理器方法
}

问题 3:每个处理器方法中都有

1
2
3
4
method = RequestMethod.POST
method = RequestMethod.DELETE
method = RequestMethod.PUT
//...

那么能不能简化书写呢?

1
2
3
4
5
6
7
//@RequestMapping( method = RequestMethod.POST)  
//使用@PostMapping简化Post请求方法对应的映射配置
@PostMapping
public String save(@RequestBody Book book){
System.out.println("book save..." + book);
return "{'module':'book save'}";
}

也就是说,使用注解 @PostMapping 来实现前面 mathod 属性中的功能

那么对于含有路径参数的呢?比如 delete ⇒ @DeleteMapping("/{id}")

1
2
3
4
5
6
//@RequestMapping(value = "/{id}" ,method = RequestMethod.DELETE)  
@DeleteMapping("/{id}") //使用@DeleteMapping简化DELETE请求方法对应的映射配置
public String delete(@PathVariable Integer id){
System.out.println("book delete..." + id);
return "{'module':'book delete'}";
}
1
2
3
4
5
@PostMapping
@DeleteMapping("/{id}")
@PutMapping
@GetMapping
@GetMapping("/{id}")

页面数据展示

非本案例重点,这里省略操作

总结

基于 RESTful 页面数据交互总结

  • 先做后台功能,开发接口并调通接口
  • 再做页面异步调用,确认功能可以正常访问
  • 最后完成页面数据展示

参考资料

1、常用快捷键

操作 快捷键
插入环境 Ctrl+E
插入行内公式 Ctrl+Shift+M
注释 / 取消注释 Ctrl+T
查看 PDF F7
预览行内数学公式 Alt +P
从 PDF 跳转至对应的 Tex 在 PDF 中按 Ctrl 单击

2、设置中文界面

01-Texstudio设置中文界面.png

3、设置编译器与编码

为了正常的输出中文,我们需要把编译器改成 XeLaTeX ,utf-8 编码(默认)

4、显示代码行号

02-Texstudio显示行号.png

5、括号匹配高亮

TexStudio 默认的括号匹配背景色好像是黄色的,有些看不清,不如设置个更亮一点的颜色吧

03-括号匹配高亮1.png

04-括号匹配高亮2.png

卸载前端准备

  1. MySQL 软件安装到哪儿
    01-MySQL8 的安装位置.png

  2. MySQL 的数据库的数据都在哪儿
    02-MySQL8存放数据的位置.png

  3. 右键计算机⇒管理⇒服务和应用程序

找到服务 MySQL80

  1. 右键电脑⇒属性⇒高级系统设置⇒环境变量,在 path 项下

03-MySQL8的环境变量.png

卸载步骤

  1. 停止 MySQL 服务
  2. 通过控制面板卸载程序的方式删除

04-卸载两个程序.png

以上截图中的两个都需要卸载

  1. 删除环境变量

  2. 服务已经自动删除,无需我们在手动删除

  3. 重启电脑

安装 MySQL 5

下载地址:MySQL :: Download MySQL Installer (Archived Versions)

下载 5.7.35 版本的 MSI Installer 安装包
05-选择安装版本以及安装包格式.png

1、选择自定义的方式
06-自定义安装.png

2、选择需要安装的内容
07-需要安装的内容.png

3、进行高级设置
08-高级设定.png

4、配置软件路径及数据库存放位置
09-配置安装路径及数据库数据路径.png

5、端口号什么的不用修改,默认 3306 即可
6、设置账户密码

配置环境变量

将软件安装目录下: bin 文件夹的整个路径复制到系统环境变量的 path 中即可