0%

Spring源码学习之IoC(走马观花)

什么是IoC

  • 调用方不去new被调用方,而是由第三方去new

IoC和DI的区别

首先,给出结论:IOC是目的,DI是手段

如何理解这个结论呢?

  • 引用Martin Fowler原话
1
As a result I think we need a more specific name for this pattern. Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussionwith various IoC advocates we settled on the name Dependency Injection.

大意是

1
已经存在某种模式,该模式被称为IoC,但IoC太普遍,会让人产生疑惑,为了让表意更明确,决定用DI来精确指称那个模式。

IoC要做的事情就是让控制反转,对象实例不再由程序员来new,这是它的目的,而IoC主要的实现方式有两种:依赖查找,依赖注入。

DI即Dependency Injection就是依赖注入,它只是IoC这个目的的一种实现手段,只是因为依赖注入相对于依赖查找是一种更可取的手段,使得它成了更流行的那一种,所以才导致人们总是把IoC和DI混为一谈。

Bean是如何被初始化的

  • 首先编写一个简易demo,方便我们快速的找到IoC所做的事情

spring-ioc-1

  • 从图中我们可以知道,ClassPathXmlApplicationContext是实例化Bean的关键,它把Bean加载到context,并通过getBean方法拿到Bean实例。那我们接着看看ClassPathXmlApplicationContext到底做了什么

    spring-ioc-2

  • 首先来看一下它的类图,大概了解一下其大致框架

  • ClassPathXmlApplicationContext的构造函数看,最核心的就是refresh()函数,其他只是设一些值。
    而这个refresh()是调用父类AbstractApplicationContext中的refresh()
    根据它的注解可知它是加载刷新了整个context,并且加载所有Bean定义和创建对应的单例。

spring-ioc-refresh-1

那我们来看一下这个refresh函数做了什么

spring-ioc-refresh-2

里面有许多对于beanFactory的操作方法,重点看下obtainFreshBeanFactory()(重新获取一个BeanFactory)。

它里面有个核心的方法refreshBeanFactory()

spring-ioc-refreshBeanFactory

如果已有BeanFactory,先删除所有Bean,然后关闭BeanFactory。
然后创建一个新的ListableBeanFactory,这个工厂里会预先加载所有的Bean。
最后核心的就是loadBeanDefinitions(beanFactory)

spring-ioc-load-1

它是加载Bean的定义,实现交给了下面这个方法

spring-ioc-load-1

用的是XmlBeanDefinitionReader直接读配置文件加载Bean Definition(Bean定义)到BeanFactory。它里面一步步把xml的配置文件拆解读取,把一个个Bean Definition加载到BeanFactory里。

至此,已经有用一个加载好Bean Definition的BeanFactory了。
其他方法也是围绕BeanFactory后置处理和Context的配置准备。

Bean又是如何被获取的

找到一开始的context.getBean(),查看源码找到对应的DefaultListableBeanFactory中的实现

spring-ioc-getBean-1

可以看出 拿到Bean的关键在于resolveBean方法,再次深入查看其源码

spring-ioc-resolveBean

如果namedBean不为空,直接从其中获取到实例返回,如果为空则获取父类的,那么我们再进到resolveNamedBean中看看

spring-ioc-resolveNamedBean

这里通过getBeanNamesForType获取到当前Type下所有BeanName,看看spring是如何做的

spring-ioc-getBeanNamesForType

两个return最终都是调用了doGetBeanNamesForType,继续往里看

spring-ioc-doGetBeanNamesForType

可以看到,它是遍历BeanFactory里面维护的beanDefinitionNames和manualSingletonNames成员变量,找出命中的beanName返回。(方法很长,截图未能展示manualSingletonNames部分)

然后拿着这个beanName去找具体的bean实例。这里的代码比较长,在AbstractBeanFactory里面的doGetBean()中实现。

spring-ioc-doGetBean

大意是先尝试去找手动添加bean的单例工厂里找有没有对应的实例,没有的话就往父类beanFactory里面找,最后没有的话就生成一个。

总结

  • 本文只是走马观花的了解IoC的大致过程,知道其中调用了哪些类等等
  • 重点需要了解BeanFactory中的DefaultListableBeanFactory实现,在Spring中,实际上是把DefaultListableBeanFactory作为一个默认的功能完整的IoC容器来使用的。包含了基本IoC容器所具有的重要功能。
  • 知道了大致过程后接下来应该仔细阅读,了解其中所运用的设计模式,以及各个类在整个架构中担任了什么功能

Solidity语言学习

Solidity基础特性

​ 任何编程语言都有其规范的代码结构,用于表达在一个代码文件中如何组织和编写代码,Solidity也一样。

本节,我们将通过一个简单的合约示例,来了解智能合约的代码结构。

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
pragma solidity ^0.4.25;
contract Sample {

//State variables
address private _admin;
uint private _state;

//Modifier
modifier onlyAdmin() {
require(msg.sender == _admin, "You are not admin");
_;
}

//Events
event SetState(uint value);

//Constructor
constructor() public {
_admin = msg.sender;
}

//Functions
function setState(uint value) public onlyAdmin {
_state = value;
emit SetState(value);
}

function getValue() public view returns (uint) {
return _state;
}

}

上面这段程序包括了以下功能:

  • 通过构造函数来部署合约
  • 通过setValue函数设置合约状态
  • 通过getValue函数查询合约状态

整个合约主要分为以下几个构成部分:

  • 状态变量 - _admin, _state,这些变量会被永久保存,也可以被函数修改
  • 构造函数 - 用于部署并初始化合约
  • 事件 - SetState, 功能类似日志,记录了一个事件的发生
  • 修饰符 - onlyAdmin, 用于给函数加一层”外衣”
  • 函数 - setState, getState,用于读写状态变量

下面将逐一介绍上述构成部分。

状态变量

状态变量是合约的骨髓,它记录了合约的业务信息。用户可以通过函数来修改这些状态变量,这些修改也会被包含到交易中;交易经过区块链网络确认后,修改即为生效。

1
uint private _state;

状态变量的声明方式为:[类型] [访问修饰符-可选] [字段名]

构造函数

构造函数用于初始化合约,它允许用户传入一些基本的数据,写入到状态变量中。

在上述例子中,设置了_admin字段,作为后面演示其他功能的前提。

1
2
3
constructor() public {
_admin = msg.sender;
}

和java不同的是,构造函数不支持重载,只能指定一个构造函数。

函数

函数被用来读写状态变量。对变量的修改将会被包含在交易中,经区块链网络确认后才生效。生效后,修改会被永久的保存在区块链账本中。

函数签名定义了函数名、输入输出参数、访问修饰符、自定义修饰符。

1
function setState(uint value) public onlyAdmin;

函数还可以返回多个返回值:

1
2
3
function functionSample() public view returns(uint, uint) {
return (1,2);
}

在本合约中,还有一个配备了view修饰符的函数。这个view表示了该函数不会修改任何状态变量。

与view类似的还有修饰符pure,其表明该函数是纯函数,连状态变量都不用读,函数的运行仅仅依赖于参数。

1
2
3
function add(uint a, uint b) public pure returns(uint) {
return a+b;
}

如果在view函数中尝试修改状态变量,或者在pure函数中访问状态变量,编译器均会报错。

事件

事件类似于日志,会被记录到区块链中,客户端可以通过web3订阅这些事件。

定义事件

1
event SetState(uint value);

构造事件

1
emit SetState(value);

这里有几点需要注意:

  • 事件的名称可以任意指定,不一定要和函数名挂钩,但推荐两者挂钩,以便清晰地表达发生的事情.

  • 构造事件时,也可不写emit,但因为事件和函数无论是名称还是参数都高度相关,这样操作很容易笔误将事件写成函数调用,因此不推荐。

1
2
3
4
5
6
function setState(uint value) public onlyAdmin {
_state = value;
//emit SetState(value);
//这样写也可以,但不推荐,因为很容易笔误写成setState
SetState(value);
}
  • Solidity编程风格应采用一定的规范。关于编程风格,建议参考这里

修饰符

修饰符是合约中非常重要的一环。它挂在函数声明上,为函数提供一些额外的功能,例如检查、清理等工作。

在本例中,修饰符onlyAdmin要求函数调用前,需要先检测函数的调用者是否为函数部署时设定的那个管理员(即合约的部署人)。

1
2
3
4
5
6
7
8
9
10
11
//Modifer
modifier onlyAdmin() {
require(msg.sender == _admin, "You are not admin");
_;
}

...
//Functions
function setState(uint value) public onlyAdmin {
...
}

值得注意的是,定义在修饰符中的下划线“_”,表示函数的调用,指代的是开发者用修饰符修饰的函数。在本例中,表达的是setState函数调用的意思。

Solidity语言运行

运行方法

通过在线ide remix来进行合约的部署与运行, remix的地址在这

编译

首先,在remix的文件ide中键入代码后,通过编译按钮来编译。成功后会在按钮上出现一个绿色对勾:

solidityStudy-1

部署

编译成功后就可进行部署环节,部署成功后会出现合约实例。

solidityStudy-2

setState

合约部署后,我们来调用setState(4)。在执行成功后,会产生一条交易收据,里面包含了交易的执行信息。

solidityStudy-3

在这里,用户可以看到交易执行状态(status)、交易执行人(from)、交易输入输出(decoded input, decoded output)、交易开销(execution cost)以及交易日志(logs)。

在logs中,我们看到SetState事件被抛出,里面的参数也记录了事件传入的值4。

如果我们换一个账户来执行,那么调用会失败,因为onlyAdmin修饰符会阻止用户调用。

solidityStudy-4

getState

调用getState后,可以直接看到所得到的值为4,正好是我们先前setState所传入的值:

solidityStudy-5

Solidity数据类型

整数系列

Solidity提供了一组数据类型来表示整数, 包含无符号整数与有符号整数。每类整数还可根据长度细分,具体细分类型如下。

类型 长度(位) 有符号
uint 256
uint8 8
uint16 16
uint256 256
int 256
int8 8
int16 16
int256 256

定长bytes系列

Solidity提供了bytes1到bytes32的类型,它们是固定长度的字节数组。

用户可以读取定长bytes的内容。

1
2
3
4
5
6
7
function bytesSample() public {

bytes32 barray;
//Initialize baarray
//read brray[0]
byte b = barray[0];
}

并且,可以将整数类型转换为bytes。

1
2
uint256 s = 1;
bytes32 b = bytes32(s);

这里有一个关键细节,Solidity采取大端序编码,高地址存的是整数的小端。例如,b[0]是低地址端,它存整数的高端,所以值为0;取b[31]才是1。

1
2
3
4
5
6
7
function bytesSample() public pure returns(byte, byte) {

uint256 value = 1;
bytes32 b = bytes32(value);
//Should be (0, 1)
return (b[0], b[31]);
}

变长bytes

从上文中,读者可了解定长byte数组。此外,Solidity还提供了一个变长byte数组:bytes。使用方式类似数组,后文会有介绍。

string

Solidity提供的string,本质是一串经UTF-8编码的字节数组,它兼容于变长bytes类型。

目前Solidity对string的支持不佳,也没有字符的概念。用户可以将string转成bytes。

1
2
3
4
5
6
function stringSample() public view returns(bytes) {
string memory str = "abc";
bytes memory b = bytes(str);
//0x616263
return b;
}

要注意的是,当将string转换成bytes时,数据内容本身不会被拷贝,如上文中,str和b变量指向的都是同一个字符串abc。

address

address表示账户地址,它由私钥间接生成,是一个20字节的数据。同样,它也可以被转换为bytes20。

1
2
3
4
5
6
function addressSample() public view returns(bytes20) {

address me = msg.sender;
bytes20 b = bytes20(me);
return b;
}

mapping

mapping表示映射, 是极其重要的数据结构。它与java中的映射存在如下几点差别:

  • 它无法迭代keys,因为它只保存键的哈希,而不保存键值,如果想迭代,可以用开源的可迭代哈希类库
  • 如果一个key未被保存在mapping中,一样可以正常读取到对应value,只是value是空值(字节全为0)。所以它也不需要put、get等操作,用户直接去操作它即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
contract Sample {

mapping(uint=>string) private values;

function mappingSample() public view returns(bytes20) {
//put a key value pair
values[10] = "hello";

//read value
string value = values[10];

}

}

数组

如果数组是状态变量,那么支持push等操作:

1
2
3
4
5
6
7
8
9
10
11
contract Sample {

string[] private arr;

function arraySample() public view {
arr.push("Hello");
uint len = arr.length;//should be 1
string value = arr[0];//should be Hello
}

}

数组也可以以局部变量的方式使用,但稍有不同:

1
2
3
4
5
6
function arraySample() public view returns(uint) {
//create an empty array of length 2
uint[] memory p = new uint[](2);
p[3] = 1;//THIS WILL THROW EXCEPTION
return p.length;
}

struct

Solidity允许开发者自定义结构对象。结构体既可以作为状态变量存储,也可以在函数中作为局部变量存在。

1
2
3
4
5
6
7
8
9
10
11
struct Person {
uint age;
string name;
}

Person private _person;

function structExample() {
Person memory p = Person(1, "alice");
_person = p;
}

本节中只介绍了比较常见的数据类型,更完整的列表可参考Solidity官方网站

全局变量

示例合约代码的构造函数中,包含msg.sender。它属于全局变量。在智能合约中,全局变量或全局方法可用于获取和当前区块、交易相关的一些基本信息,如块高、块时间、合约调用者等。

比较常用的全局变量是msg变量,表示调用上下文,常见的全局变量有以下几种:

  • msg.sender:合约的直接调用者。

    由于是直接调用者,所以当处于 用户A->合约1->合约2 调用链下,若在合约2内使用msg.sender,得到的会是合约1的地址。如果想获取用户A,可以用tx.origin.

  • tx.origin:交易的”始作俑者”,整个调用链的起点。

  • msg.calldata:包含完整的调用信息,包括函数标识、参数等。calldata的前4字节就是函数标识,与msg.sig相同。

  • msg.sig:msg.calldata的前4字节,用于标识函数。

  • block.number:表示当前所在的区块高度。

  • now:表示当前的时间戳。也可以用block.timestamp表示。

这里只列出了部分常见全局变量,完整版本请参考这里

docker

docker安装

1
2
3
4
5
6
7
8
9
10
# 1、yum 包更新到最新
yum update
# 2、 安装需要的软件包,yum-util 提供yum-config-manager功能,另外两个是devicemapper驱动依赖的
yum install -y yum-utils device-mapper-persistent-data lvm2
# 3、 设置yum源
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# 4、 安装docker,出现输入的界面都按y
yum install -y docker-ce
# 5、 查看docker版本,验证是否安装成功
docker -v

docker架构

image-20200416205059220

  • daemon: docker在机器上以守护进程(后台运行的进程)的形式存在,客户端发送docker命令操作该进程
  • image(镜像): 镜像就是一堆只读层(read-only layer)的统一视角,不可变
  • container:(容器): 容器 = 镜像 + 读写层,关系就像类与对象的关系;一个运行态容器(running container)被定义为一个可读写的统一文件系统加上隔离的进程空间和包含其中的进程,注意,一个容器只能有一个进程隔离空间
  • repository: 仓库,类似maven仓库,也有公共仓库和私有仓库

docker镜像加速

  • 打开阿里云
  • 搜索容器镜像服务
  • 找到镜像加速器,复制粘贴配置即可

docker命令

服务相关命令

  • 启动docker服务

    1
    systemctl start docker
  • 停止docker服务

    1
    systemctl stop docker
  • 重启docker服务

    1
    systemctl restart docker
  • 查看docker服务状态

    1
    systemctl status docker
  • 设置开机启动docker服务

    1
    systemctl enable docker

镜像相关命令

  • 查看镜像

    1
    2
    3
    4
    # 1、 查看所有镜像详细信息
    docker images
    # 2、 查看所有镜像id
    docker images -q
  • 搜索镜像(查看有哪些可以下载)

    1
    docker search
  • 拉取,下载镜像

    1
    docker pull
  • 删除镜像

    1
    2
    3
    4
    # 1、删除单个镜像
    docker rmi <imageid>|<repository:tag>
    # 2、 删除所有镜像
    docker rmi `docker images -q`

容器相关命令

  • 创建并运行容器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    docker run 
    ######## 可配置参数
    -i 保持容器运行
    -t 分配一个终端,exit容器就会关闭
    --name 起名
    --expose 公开一个或多个端口
    -d 以(守护)后台模式运行容器,并打印容器id,进入终端需要输入 docker exec -it <name> /bin/bash,exit容器不会关闭
    -l 添加元数据
    -p <主机port>:<容器port> 将容器端口映射到主机端口
    -P 随机映射端口
    --rm 当容器退出时自动移除这个容器
    centos:7 <镜像名>:<tag>/<imageid>
    /bin/bash 初始化运行命令
  • 查看正在运行的容器

    1
    docker ps
  • 查看全部容器

    1
    docker ps -a
  • 启动容器

    1
    docker start
  • 停止容器

    1
    docker stop
  • 删除容器

    1
    docker rm
  • 删除所有不在运行的容器

    1
    docker rm `docker ps -aq`
  • 查看容器信息

    1
    docker inspect

深入理解docker命令

docker容器的数据卷

数据卷概念

image-docker-data-volume

思考:

  • Docker容器删除后,容器中产生的数据是否会随之销毁?
  • Docker容器与外部机器是否可以直接交换文件?
  • 容器之间是否可以进行数据交互?

数据卷:

  • 数据卷是宿主机中的一个目录或文件
  • 当容器目录和数据卷目录绑定后,对方的修改会立即同步
  • 一个数据卷可以被多个容器同时挂载
  • 一个容器也可以被挂载多个数据卷

注意事项:

  • 目录必须是绝对路径
  • 如果目录不存在,会自动创建

作用:

  • 容器数据持久化
  • 外部机器和容器间接通信
  • 容器之间数据交换

配置数据卷

1
docker run -it --name=test -v /root/data:/root/data centos:7

数据卷容器

多容器进行数据交换

  • 多个容器挂载同一个数据卷
  • 数据卷容器

image-docker-volume-container

配置数据卷容器

1
2
3
4
5
# 1、 创建启动c3数据卷容器,使用-v参数设置数据卷
docker run -it --name=c3 -v /volume centos:7
# 2、 创建启动c1、c2容器,使用--volumes-from参数设置数据卷
docker run -it --name=c1 --volumes-from c3 centos:7
docker run -it --name=c2 --volumes-from c3 centos:7

docker应用部署

mysql部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1、搜索mysql镜像
docker search mysql
# 2、拉取mysql镜像
docker pull mysql:5.6
# 3、创建容器,设置端口映射、目录映射
## 在/root目录下创建mysql目录用于存出mysql数据信息
mkdir ~/mysql
cd ~/mysql

## -p:端口映射 -v:目录映射 -e:环境
docker run -id \
-p 3306:3306 \
--name=c_mysql \
-v $PWD/conf:/etc/mysql/conf.d \
-v $PWD/logs:/logs \
-v $PWD/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
mysql:5.6

dockerfile

镜像原理

思考:

  • docker镜像的本质是什么?
  • docker中一个centos镜像为什么只有200M,而一个centos操作系统的iso文件要几个G?
  • docker中一个tomcat镜像为什么有500M,而tomcat的安装包才70多M?
  • docker镜像是由特殊的文件系统叠加而成

    ​ 最底端是bootfs,并使用宿主机的bootfs

    ​ 第二层是root文件系统rootfs,称为base image

    ​ 然后再往上可以叠加其他的镜像文件

  • 统一文件系统(Union File System)技术能够将不同层整合成一个文件系统,为这些层提供了一个统一的视角,这样就隐藏了多层的存在,在用户的角度看来,只存在一个文件系统

  • 一个镜像可以放在另一个镜像的上面,位于下面的镜像称为父镜像,最底部的镜像称为基础镜像

  • 当从一个镜像启动容器时,docker会在最顶层加载一个读写文件系统作为容器

image-docker-image

镜像制作

容器转为镜像

1
2
3
4
docker commit 容器id 镜像名称:版本号
## 镜像不能直接传输,需要先压缩
docker save -o 压缩文件名称 镜像名称:版本号
docker load -i 压缩文件名称

dockerfile概念

  • dockerfile是一个文本文件
  • 每一条指令构建一层,基于基础镜像,最终构建成一个新的镜像

dockerfile制作

1
2
3
4
5
1、定义父镜像: FROM centos:7
2、定义作者信息: MAINTAINER iddunk<iddunk@example.com>
3、执行安装vim命令: RUN yum install -y vim
4、定义默认的工作目录: WORKDIR /usr
5、定义容器启动执行的命令: CMD /bin/bash

springboot项目部署

1
2
3
4
5
1、制作父镜像: FROM java:8
2、定义作者信息: MAINTAINER: iddunk<iddunk@example.com>
3、将jar包添加到容器: ADD springboot.jar app.jar
4、定义容器启动执行的命令: CMD java -jar app.jar
5、通过dockerfile构建: docker build -f dockerfile文件路径 -t 镜像名称:版本

docker compose

docker compose概念

docker compose是一个编排多容器分布式部署的工具,提供命令集管理容器化应用的完整开发周期,包括服务构建,启动和停止。使用步骤:

  • 利用dockerfile定义运行环境镜像
  • 使用docker-compose.yml定义组成应用的各服务
  • 运行docker-compose up启动应用