woodwhales's blog

技术改变生活

groovy 学习笔记

Groovy 是一种基于Java 平台的面向对象语言。 Groovy 1.0 于 2007 年 1 月 2 日发布,其中 Groovy 2.4 是当前的主要版本。 Groovy 通过 Apache License v 2.0 发布。

Groovy 官网:https://groovy-lang.org/

阿里云镜像下载:https://mirrors.aliyun.com/apache/groovy/

Groovy 基本语法

官方文档:http://www.groovy-lang.org/syntax.html

  1. 在一个 groovy 文件中可以定义类和脚本。

  2. 变量、方法建议使用 def 定义,而不使用具体的数据类型定义。

  3. groovy 的注释方式和 Java 一样,有单行和多行俩种。

  4. 语句末尾的分号可以省略,groovy 是以换行作为一行代码的结束。

  5. 默认类、方法、字段都是 public 修饰。

  6. 对象属性的操作:

    • 读取:
      • 对象.属性名
      • 对象的 getter 方法
      • 对象[“属性名”]
    • 赋值:
      • 对象.属性名 = xxx
      • 对象的 setter 方法
      • 构造器

    对类属性的操作本质是:通过属性对应的 getter 、setter 方法完成。

  7. 方法操作:

    • 声明时:参数类型、返回值类型可以省略
    • 调用时:() 可以省略
  8. 支持顺序结构、分支结构、循环结构、各种运算符(算术、关系、位、赋值、范围运算符)

  9. 基本类型也是对象,可以直接调用对象的方法

  10. groovy 中的字符串有单引号、双引号、三引号:

    • 单引号:作为字符串常量,没有运算能力
    • 双引号:可以引用变量,使用${}占位,有运算能力
    • 三引号:模板字符串,支持换行
  11. 数据类型:变量、属性、方法、闭包的参数及方法的返回值类型都是可有可无,都是在变量赋值的时候才决定其类型。

IDEA 创建 Groovy 项目

使用 idea 创建 Groovy 项目,主要需要指定本地安装的 groovy sdk:

创建 hello world 程序

groovy 类

在 src 目录下创建 Demo1.groovy 文件:

1
2
3
4
5
6
7
class Demo1 {

static void main(String[] args) {
print('woodwhales.cn')
}

}

通过编译后的 class 文件可以看出,当前 Demo1 类实现了 GroovyObject 接口:

groovy 脚本

在 src 目录下创建 Demo2.groovy 文件:

1
2
3
def website = 'woodwhlaes.cn'

print(website)

通过编译后的 class 文件可以看出,当前 Demo1 类继承了 Script:

groovy 闭包

闭包是一个短的匿名代码块。它通常跨越几行代码。一个方法甚至可以将代码块作为参数。它们是匿名的。

来一个简单的例子:

1
2
3
4
5
6
7
8
class Demo3 {

static void main(String[] args) {
def closure = { println "woodwhales.cn" };
closure.call();
}

}

在上面的例子中,代码{ println "woodwhales.cn" }被称为闭包。此标识符引用的代码块可以使用 call 语句执行。

当我们运行上面的程序,我们将得到以下结果:

1
woodwhales.cn

闭包中的形式参数

闭包也可以包含形式参数,以使它们更有用,就像Groovy中的方法一样。

1
2
3
4
5
6
7
8
class Demo3 {

static void main(String[] args) {
def closure = { website -> println "welcome ${website}" };
closure.call("woodwhales.cn");
}

}

在上面的代码示例中,注意使用${website},这导致 closure 接受一个参数。当通过 closure.call 语句调用闭包时,我们现在可以选择将一个参数传递给闭包。

当我们运行上面的程序,我们将得到以下结果:

1
welcome woodwhales.cn

闭包和变量

更正式地,闭包可以在定义闭包时引用变量。以下是如何实现这一点的示例。

1
2
3
4
5
6
7
8
9
class Demo3 {

static void main(String[] args) {
def str1 = "欢迎访问"
def closure = { website -> println "${str1} ${website}" };
closure.call("woodwhales.cn");
}

}

在上面的例子中,除了向闭包传递参数之外,我们还定义了一个名为 str1 的变量。闭包也接受变量和参数。

当我们运行上面的程序,我们将得到以下结果:

1
欢迎访问 woodwhales.cn

由于参数定义和获取是明确的,{}花括号也可以省略:

1
2
3
4
5
6
7
8
9
class Demo3 {

static void main(String[] args) {
def str1 = "欢迎访问"
def closure = { website -> println "$str1 $website" };
closure.call("woodwhales.cn");
}

}

在方法中使用闭包

闭包也可以用作方法的参数。在 Groovy 中,很多用于数据类型(例如列表和集合)的内置方法都有闭包作为参数类型。

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

def static showMe(closure) {
closure.call("\n版权所有woodwhales.cn");
}

static void main(String[] args) {
def note = "groovy 学习笔记";
def closure = { copyright -> println "${note} ${copyright}" }
Demo4.showMe(closure);
}

}

输出结果:

1
2
groovy 学习笔记 
版权所有woodwhales.cn

gradle 入门学习笔记

安装 gradle

gradle 官网:https://gradle.org/

gradle 所有版本下载:https://gradle.org/releases/

gradle 与 IDEA 版本兼容确认

在 IDEA 安装目录的 plugins\gradle\lib 下有如下gradle相关 jar 文件:

笔者是 7.4 版本,点击下载二进制文件链接即可下载 gradle 文件:

注意:

完整版是包含了 gradle 的文档和源码

安装 gradle 必须要求系统有安装 JDK 1.8 +

官方安装说明:https://docs.gradle.org/current/userguide/installation.html

具体安装教程请移步至:构建工具maven及gradle的安装及IDE配置

gradle 项目目录结构

Gradle 项目默认目录结构和Maven 项目的目录结构一致,都是基于约定大于配置【Convention Over Configuration】。其完整项目目录结构如下所示:

上图说明:

  1. 只有 war 工程才有 webapp 目录,对于普通的 jar 工程并没有 webapp 目录。
  2. gradlew 与 gradlew.bat 执行的 wrapper 目录下的 gradle 指令,不是本地安装的 gradle 指令。
  3. 因此如果仅仅使用本地 gradle 构建项目,可以删除 gradlew、gradlew.bat、wrapper文件(及子文件),但是不是可以删除 gradle 文件目录。
  4. gradle 的 build 目录与 maven 的 target 目录类似,gradle 的 src 目录与 maven 的 src 目录一样。
  5. 一个工程只有一个 settings.gradle 文件,即 maven 的父子工程中的父工程目录下含该文件,其他子工程不含该文件。
  6. gradle 的 build.gradle 文件与 maven 的 pom.xml 文件类似,一个 project 就含有一个该文件。

创建 gradle 项目

使用 spring 或者 aliyun 提供的脚手架创建基础的 gradle 项目:

https://start.aliyun.com/bootstrap.html

https://start.spring.io/

将下载的 gradle 项目压缩文件解压,得到如下目录结构:

上述文件目录中没有 build 目录,因为项目没有编译,等项目编译之后就会有 build 目录了。

gradle 常用命令

gradle 常用命令 作用
gradle clean 清空 build 目录
gradle classes 编译 src 目录下的代码
gradle test 编译测试代码,生成测试报告
gradle build 构建项目
gradle build -x test 构建项目但是跳过构建测试代码

注意:gradle 的指令要在含有 build.gradle 的目录位置才能执行。

编译测试代码之后,在 build\reports\tests\test 文件目录下有测试报告。

build 成功的 jar 包在 build\libs 文件目录下。

添加镜像

Gradle 自带的 Maven 源地址是国外的,该 Maven 源在国内的访问速度是很慢的,除非使用了特别的手段。一般情况下,我们建议使用国内的第三方开放的 Maven 源或企业内部自建 Maven 源。

我们可以在gradle 的init.d 目录下创建以.gradle 结尾的文件,.gradle 文件可以实现在build 开始之前执行,所以可以在这个文件配置一些你想预先加载的操作。

init.d文件夹创建init.gradle文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
allprojects {
repositories {
mavenLocal()
maven { name "Alibaba" ; url "https://maven.aliyun.com/repository/public" }
maven { name "Bstek" ; url "https://nexus.bsdn.org/content/groups/public/" }
mavenCentral()
}

buildscript {
repositories {
maven { name "Alibaba" ; url 'https://maven.aliyun.com/repository/public' }
maven { name "Bstek" ; url 'https://nexus.bsdn.org/content/groups/public/' }
maven { name "M2" ; url 'https://plugins.gradle.org/m2/' }
}
}
}

mavenLocal():指定使用maven本地仓库,而本地仓库在配置maven时settings文件指定的仓库位置。gradle 查找 jar包顺序如下:USER_HOME/.m2/settings.xml >> M2_HOME/conf/settings.xml >> USER_HOME/.m2/repository。

阿里云仓库地址请参考:https://developer.aliyun.com/mvn/guide

启用 init.gradle 文件的方法

  1. 在命令行指定文件,例如:gradle –init-script yourdir/init.gradle -q taskName。可以多次输入此命令来指定多个 init.gradle 文件
  2. 把 init.gradle 文件放到 USER_HOME/.gradle/ 目录下
  3. 把以 .gradle 结尾的文件放到 USER_HOME/.gradle/init.d/ 目录下
  4. 把以 .gradle 结尾的文件放到 GRADLE_HOME/init.d/ 目录下

如果存在上面的 4 种方式的 2 种以上,gradle 会按上面的 1-4 序号依次执行这些文件,如果给定目录下存在多个 init 脚本,会按拼音 a-z 顺序执行这些脚本,每个 init 脚本都存在一个对应的 gradle 实例。

gradle 拉取远程依赖存储过程

gradle 可以通过指定仓库地址为本地 maven 仓库地址和远程仓库地址相结合的方式,避免每次都会去远程仓库下载依赖库。这种方式也有一定的问题,如果本地 maven 仓库有这个依赖,就会从直接加载本地依赖,如果本地仓库没有该依赖,那么还是会从远程下载。但是下载的 jar 不是存储在本地 maven 仓库中,而是放在自己的缓存目录中,默认在 USER_HOME/.gradle/caches 目录,如果我们配置过GRADLE_USER_HOME环境变量,则会放在GRADLE_USER_HOME/caches 目录中。因为 caches下载的文件不是按照 maven 仓库中存放的方式存储,所以gradle 不可以将 gradle caches 指向 maven repository。

Wrapper 包装器

Gradle Wrapper 实际上就是对 Gradle 的一层包装,用于解决实际开发中可能会遇到的不同的项目需要不同版本的 Gradle。

例如:把自己的代码共享给其他人使用,可能出现如下情况:

  1. 对方电脑没有安装 gradle
  2. 对方电脑安装过 gradle,但是版本太旧了

这时候,我们就可以考虑使用官方推荐的 Gradle Wrapper,上有了 Gradle Wrapper 之后,我们本地是可以不配置 Gradle 系统环境变量,下载 Gradle 项目后,使用 gradle 项目中自带的 wrapper 配置可以完成项目构建。

项目中的 gradlew、gradlew.bat 脚本用的就是 wrapper 中规定的gradle版本。

修改 wrapper 中的 gradle 版本

使用指令:

1
gradle wrapper --gradle-version=7.4.2

下述是一个 wrapper 配置:

1
2
3
4
5
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

上述 wrapper 配置意味着第一次使用 gradlew 或者 gradlew.bat 脚本执行项目构建时,从 distributionUrl 配置的地址下载 gradle 压缩包,保存到 zipStoreBase 和 zipStorePath 配置的文件目录中,解压 gradle 压缩包并保存至 distributionBase 和 distributionPath 配置的文件目录中。

wrapper 执行流程

  1. 当我们第一次执行 ./gradlew build 命令的时候,gradlew 会读取 gradle-wrapper.properties 文件的配置信息
  2. 准确的将指定版本的 gradle 下载并解压到指定的位置( GRADLE_USER_HOME 目录下的 wrapper/dists 目录中)
  3. 并构建本地缓存(GRADLE_USER_HOME目录下的caches目录中),下次再使用相同版本的 gradle 就不用下载了
  4. 之后执行的 ./gradlew 所有命令都是使用指定的 gradle 版本。

Vue + Element UI快速入门

项目源码:https://github.com/woodwhales/woodwhales-study/tree/master/woodwhales-vue-demo

脚手架搭建

步骤1:全局安装 vue 2.0

1
cnpm install vue-cli -g

查看 vue 版本命令:vue -V

步骤2:在一个空目录中创建工程

执行 vue 初始化工程命令:

1
vue init webpack my-demo
  • 当提示到是否安装:Install vue-router?,输入y

  • 提示是否安装:Use ESLint to lint your code?,输入n

  • 其他选项全部输入y

安装完成,提示如下信息:

1
2
3
4
5
6
7
8
9
# Project initialization finished!
# ========================

To get started:

cd my-demo
npm run dev

Documentation can be found at https://vuejs-templates.github.io/webpack

根据安装完成的提示,进入 my-demo 目录,执行npm run dev命令,浏览器访问:http://localhost:8080 地址即可看到vue-cli脚手架搭建的基础工程。

步骤3(可选):修改启动端口号

config/index.js配置文件中,配置了项目的启动基础参数:

1
2
3
host # 项目启动的主机名
port # 项目启动的端口号
autoOpenBrowser #是否启动时自动打开浏览器并访问

创建自定义组件

components文件目录下创建Index.vue文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
{{ msg }}
</div>
</template>

<script>

export default {
name: 'Index',
data () {
return {
msg: 'this index page'
}
}
}
</script>

<style>

</style>

router/index.js文件中将Index组件引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/components/Index'

Vue.use(Router)

export default new Router({
routes: [
{
path: '/',
name: 'Index',
component: Index
}
]
})

App.vue文件中将Index组件引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div id="app">
<Index/>
<!-- <router-view/> -->
</div>
</template>

<script>
import Index from './components/Index'

export default {
name: 'App',
components: {
Index
}
}
</script>

具体改动如图:

浏览器访问项目,可以看到首页变成了Index组件中的内容:

引入 element-ui 组件

element-ui 官网:https://element.eleme.cn/#/zh-CN

停止项目,安装element-ui依赖:

1
npm i element-ui -S

安装成功之后,检查package.json配置中是否引入了element-ui

main.js配置中引入element-ui组件:

1
2
3
4
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';

Vue.use(ElementUI);

具体改动如图:

Index.vue组件进行改造,引入 element-ui 的 Container 布局容器:

Container 布局容器:https://element.eleme.cn/#/zh-CN/component/container

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
<template>
<div>
<el-container>
<el-aside width="200px">{{left}}</el-aside>
<el-container>
<el-header>{{header}}</el-header>
<el-main>{{main}}</el-main>
</el-container>
</el-container>
</div>
</template>

<script>

export default {
name: 'Index',
data () {
return {
left: 'this is left aside',
header: 'this is header',
main: 'this is main'
}
}
}
</script>

<style>
.el-header, .el-footer {
background-color: #B3C0D1;
color: #333;
text-align: center;
line-height: 60px;
}

.el-aside {
background-color: #D3DCE6;
color: #333;
text-align: center;
line-height: 200px;
}

.el-main {
background-color: #E9EEF3;
color: #333;
text-align: center;
line-height: 160px;
}

body > .el-container {
margin-bottom: 40px;
}

.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
line-height: 260px;
}

.el-container:nth-child(7) .el-aside {
line-height: 320px;
}
</style>

具体改动如图:

浏览器访问项目,可以看到首页变成了Index组件中的 element-ui 布局样式:

引入 element-ui 导航菜单

NavMenu 导航菜单:https://element.eleme.cn/#/zh-CN/component/menu

Index.vue组件中的左侧栏中添加导航菜单代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
<template>
<div>
<el-container>
<el-aside width="200px">
<el-menu
default-active="2"
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose">
<el-submenu index="1">
<template slot="title">
<i class="el-icon-location"></i>
<span>导航一</span>
</template>
<el-menu-item-group>
<template slot="title">分组一</template>
<el-menu-item index="1-1">选项1</el-menu-item>
<el-menu-item index="1-2">选项2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="1-3">选项3</el-menu-item>
</el-menu-item-group>
<el-submenu index="1-4">
<template slot="title">选项4</template>
<el-menu-item index="1-4-1">选项1</el-menu-item>
</el-submenu>
</el-submenu>
<el-menu-item index="2">
<i class="el-icon-menu"></i>
<span slot="title">导航二</span>
</el-menu-item>
<el-menu-item index="3" disabled>
<i class="el-icon-document"></i>
<span slot="title">导航三</span>
</el-menu-item>
<el-menu-item index="4">
<i class="el-icon-setting"></i>
<span slot="title">导航四</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<el-header>{{header}}</el-header>
<el-main>{{main}}</el-main>
</el-container>
</el-container>
</div>
</template>

<script>

export default {
name: 'Index',
data () {
return {
left: 'this is left aside',
header: 'this is header',
main: 'this is main'
}
},
methods: {
handleOpen(key, keyPath) {
console.log(key, keyPath);
},
handleClose(key, keyPath) {
console.log(key, keyPath);
}
}
}
</script>

<style>
.el-header, .el-footer {
background-color: #B3C0D1;
color: #333;
text-align: center;
line-height: 60px;
}

.el-aside {
background-color: #D3DCE6;
color: #333;
text-align: center;
line-height: 200px;
}

.el-main {
background-color: #E9EEF3;
color: #333;
text-align: center;
line-height: 160px;
}

body > .el-container {
margin-bottom: 40px;
}

.el-container:nth-child(5) .el-aside,
.el-container:nth-child(6) .el-aside {
line-height: 260px;
}

.el-container:nth-child(7) .el-aside {
line-height: 320px;
}
</style>

具体改动如下:

添加导航菜单效果如图:

设置 element-ui 导航菜单动态路由

步骤1:开始导航菜单路由模式

Index.vue组件中,将菜单<el-menu>标签属性中设置开启使用 vue-router 的模式,启用该模式会在激活导航时以 index 作为 path 进行路由跳转,并且在<el-main>标签中增加<router-view/>路由组件:

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
<template>
<div>
<el-container>
<el-aside width="200px">
<el-menu
class="el-menu-vertical-demo"
@open="handleOpen"
@close="handleClose"
:default-active="$route.path"
router>
<el-menu-item index="/linkMgt">
<i class="el-icon-s-grid"></i>
<span slot="title">链接管理</span>
</el-menu-item>
<el-menu-item index="/systemConfig">
<i class="el-icon-s-tools"></i>
<span slot="title">系统配置</span>
</el-menu-item>
</el-menu>
</el-aside>
<el-container>
<el-header>{{header}}</el-header>
<el-main><router-view/></el-main>
</el-container>
</el-container>
</div>
</template>

具体改动如下:

步骤2:创建菜单对应的 vue 组件

创建菜单对应的 vue 组件:

SystemConfig.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<h1>系统配置页面</h1>
</template>

<script>
export default {
name: "SystemConfig"
}
</script>

<style scoped>

</style>

LinkMgt.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<h1>链接管理页面</h1>
</template>

<script>
export default {
name: "LinkMgt"
}
</script>

<style scoped>

</style>

步骤3:配置路由

router/index.js配置中将路由与步骤 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
import Vue from 'vue'
import Router from 'vue-router'
import Index from '@/components/Index'
import LinkMgt from "@/components/LinkMgt";
import SystemConfig from "@/components/SystemConfig";

Vue.use(Router)

export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'Index',
redirect:'LinkMgt',
component: Index
},
{
path: '/linkMgt',
name: 'LinkMgt',
component: LinkMgt
},
{
path: '/systemConfig',
name: 'SystemConfig',
component: SystemConfig
}

]
})

注意:

  • 上述配置中对 Router 对象加了mode: 'history',这样路由地址中就会去除 # 号。
  • 首页路由增加了redirect,访问首页直接重定向到/linkMgt

导航菜单支持动态路由效果如图:

引入 ECharts 组件

ECharts官网:https://echarts.apache.org/zh/index.html

步骤1:安装 ECharts

停止运行中的项目,在项目根据目录安装ECharts依赖:

1
npm install echarts --save

步骤2:将 ECharts 定义到 vue 的原型上

main.js配置文件中将 ECharts 定义到 vue 的原型上:

1
2
3
import * as echarts from 'echarts';

Vue.prototype.$echarts = echarts;

具体改动如下:

步骤3:在组件中引入 ECharts 图表组件

LinkMgt.vue组件中引入ECharts图表组件:

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
<template>
<div ref="myEcharts" style="width: 600px;height:400px;"></div>
</template>

<script>
export default {
mounted() {
this.init();
},
methods: {
init() {
let myChart = this.$echarts.init(this.$refs.myEcharts);
// 绘制图表
let option;
option = {
title: {
text: 'ECharts demo'
},
xAxis: {
data: ["Java", "C", "C++", "Python", "Go", "Ruby"]
},
yAxis: {},
series: [{
name: '熟练度',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}]
};

// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
}
}
}
</script>

<style scoped>

</style>

引入 Echarts 效果如图:

引入 element-ui 动态表格

步骤1:安装 mock 服务

使用 npm 安装json-server服务:

官网地址:https://github.com/typicode/json-server

1
npm install -g json-server

在一个空目录中创建db.json文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"tableData": {
"code": "0",
"msg": "success",
"data": [{
"date": "2017-05-22",
"name": "张三",
"address": "上海市普陀区"
}, {
"date": "2018-11-11",
"name": "李四",
"address": "北京市朝阳区"
}, {
"date": "2019-10-06",
"name": "王五",
"address": "广东省深圳市"
}]
}
}

db.json文件所在目录执行命令,启动 mock 服务:

1
json-server --watch db.json

json-server服务启动成功如图:

浏览器访问http://localhost:3000即可看到`db.json`配置中的 mock 接口:

点击上图中的tableData接口可以看到响应成功的 json 报文:

步骤2:引入axios依赖

停止项目运行,在项目根目录下执行安装命令:

1
npm install axios --save

main.js配置文件中将 axios 定义到 vue 的原型上:

1
2
3
import axios from 'axios';

Vue.prototype.$axios = axios;

具体改动如下:

步骤3:引入动态表格组件

Table 表格:https://element.eleme.cn/#/zh-CN/component/table

SystemConfig.vue组件中将动态表格组件引入:

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
<template>
<el-table
:data="tableData"
style="width: 100%">
<el-table-column
prop="date"
label="日期"
width="180">
</el-table-column>
<el-table-column
prop="name"
label="姓名"
width="180">
</el-table-column>
<el-table-column
prop="address"
label="地址">
</el-table-column>
</el-table>
</template>

<script>
export default {
name: "SystemConfig",
data() {
return {
tableData: null
}
},
mounted() {
this.getTableData();
},
methods: {
getTableData() {
this.$axios.get('http://localhost:3000/tableData')
.then(resp => {
this.tableData = resp.data.data;
})
.catch(error => {
console.log(error)
});
}
}
}
</script>

<style scoped>
</style>

由于<el-main>包裹表格导致表格的表头异常高,可以在Index.vue中将.el-main样式中的line-height样式删除:

整个页面与浏览器边界有“留白”,可以在App.vue中增加样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>vue-demo</title>
<style>
html,
body,
#app {
height: 100%;
margin: 0px;
padding: 0px;
}
</style>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

具体改动如下:

引入动态表格效果如下:

RabbitMQ windows安装

版本选择

RabbitMQ 是使用 Erlang 语言开发的,所以需要系统环境有 Erlang 环境,RabbitMQ 官网提供了版本对应关系:

https://www.rabbitmq.com/which-erlang.html

笔者使用目前 RabbitMQ 最新版本进行安装:

Erlang 安装

环境变量

双击安装 otp_win64_24.2.exe,注意记住 Erlang 安装的路径,需要配置到系统环境变量中:ERLANG_HOME

笔者使用的路径为:C:\programs\erl-24.2.2

将其环境变量配置到 Path 中:%ERLANG_HOME%\bin

验证是否安装成功,在 cmd 窗口中输入 erl 命令,显示 Eshell 信息即可。

安装 RabbitMQ

环境变量

将 RabbitMQ 安装包解压至指定目录并配置到系统变量RABBITMQ_SERVER中即可。

笔者使用的路径为:C:\programs\rabbitmq_server-3.9.13

Path 系统变量中配置:%RABBITMQ_SERVER%\sbin

插件安装

安装 RabbitMQ 管理控制台插件,使用管理员模式下的 CMD 窗口执行命令rabbitmq-plugins.bat enable rabbitmq_management即可。

如果出现此异常:unable to perform an operation on node ‘rabbit@xxxx’. Please see diagnostics information and suggestions below…

解决方法:

将 C:\Users\Administrator.erlang.cookie 文件复制到 C:\Windows\System32\config\systemprofile 下,替换掉 .erlang.cookie。

同时删除:C:\Users\Administrator\AppData\Roaming\RabbitMQ目录,重新执行安装 RabbitMQ 管理控制台插件命令即可。

启动 RabbitMQ

安装完插件,运行rabbitmq-server start命令即可启动 RabbitMQ。

此时,浏览器访问:http://localhost:15672/ 即可看见 RabbitMQ 的控制台登录首页,账号为:guest,密码为:guest

安装 windows 服务

使用管理员模式下的 CMD 窗口执行rabbitmq-service install命令即可。

安装过程中出现安装失败

删除注册表里的HKEY_LOCAL_MACHINE\SOFTWARE\Ericsson\Erlang\ErlSrv,重新使用管理员模式下的 CMD 运行erl命令,再次执行rabbitmq-service install即可。

安装好后,将 RabbitMQ 服务启动即可,这样以后就可以开机自启动啦。

快速排序的俩种实现

算法描述

  1. 每一轮排序选择一个基准点(pivot)进行分区
    1. 让小于基准点的元素的进入一个分区,大于基准点的元素的进入另一个分区
    2. 当分区完成时,基准点元素的位置就是其最终位置
  2. 在子分区内重复以上过程,直至子分区元素个数少于等于 1,这体现的是分而治之的思想 (divide-and-conquer

从以上描述可以看出,一个关键在于分区算法,常见的有单边循环(洛穆托分区方案)、双边循环分区方案、霍尔分区方案

单边循环

核心思路

1,选择数组最右侧元素作为基准点

例如下图中的数组最右侧元素作为基准点。

2,数组从左到右依次遍历元素,比基准点小的放置到基准点左边,比基准点大的放置到基准点右边

达到的效果如下图:

3,步骤 2 之后,基准点将原数组拆分成了俩个子集合。将俩个子集合看作新的集合,重复步骤1,2,直到子集合不能再拆分为止即可完成排序。

代码思路

如图示中,使用俩个游标初始值是数组的最左侧位置。

  1. 选择最右元素作为基准点元素

  2. j 指针负责找到比基准点小的元素,一旦找到则与 i 进行交换,i++

  3. i 指针维护小于基准点元素的边界,也是每次交换的目标索引

  4. 最后基准点与 i 交换,i 即为分区位置

代码实现

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

public static void main(String[] args) {
int[] arr = {5, 3, 7, 2, 9, 8, 1, 4};
System.out.println(Arrays.toString(arr));
// 第一遍排序全数组
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}

private static void quickSort(int[] arr, int low, int high) {
// 地位大于等于高位则不需要进行排序,结束递归
if(low >= high) {
return;
}
// 完成第一遍,将数组按照基准点进行拆分,并得到基准点的索引位置
int p = partition(arr, low, high);
quickSort(arr, low, p - 1);
quickSort(arr, p + 1, high);
}

private static int partition(int[] arr, int low, int high) {
int pv = arr[high];

int i = low;

for (int j = low; j < high; j++) {
if (arr[j] < pv) {
swap(arr, i, j);
i++;
}
}
swap(arr, i, high);
return i;
}

private static void swap(int[] arr, int i, int j) {
if (i != j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
}

双边循环

核心思路

1,选择数组最左侧元素作为基准点

2,使用游标分别从数组的俩端往中间移动,左侧游标找比基准点小的,右侧游标找基准点大的,一旦找到就俩者游标对应的元素互换,直到俩个游标相遇为止。

3,俩个游标相遇的位置就是分区的位置。

代码思路

  1. 选择最左元素作为基准点元素
  2. j 指针负责从右向左找比基准点小的元素,i 指针负责从左向右找比基准点大的元素,一旦找到二者交换,直至 i,j 相交
  3. 最后基准点与 i(此时 i 与 j 相等)交换,i 即为分区位置

要点

  1. 基准点在左边,并且要先 j 后 i

  2. while( i < j && a[j] > pv ) j–

  3. while ( i < j && a[i] <= pv ) i++

代码实现

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

public static void main(String[] args) {
int[] arr = {5, 3, 7, 2, 9, 8, 1, 4};
System.out.println(Arrays.toString(arr));
// 第一遍排序全数组
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}

private static void quickSort(int[] arr, int low, int high) {
// 地位大于等于高位则不需要进行排序,结束递归
if(low >= high) {
return;
}
// 完成第一遍,将数组按照基准点进行拆分,并得到基准点的索引位置
int p = partition(arr, low, high);
quickSort(arr, low, p - 1);
quickSort(arr, p + 1, high);
}

private static int partition(int[] arr, int low, int high) {
int pv = arr[low];

int i = low;
int j = high;
while (i < j) {
// 直到 arr[j] 小于等于基准点
while (i < j && arr[j] > pv) {
j--;
}

// 直到 arr[i] 大于基准点
while (i < j && arr[i] <= pv) {
i++;
}
// 上述俩个while循环保证了 i 和 j 一定会相遇,相遇则互换
swap(arr, i, j);
}

// j 的位置就是分区索引位置
swap(arr, low, j);
return j;
}
}

发布自己的 jar 包到 Maven 中央仓库 ( mvnrepository.com )

笔者以自己的开源项目:https://github.com/woodwhales/woodwhales-common 为例,演示如何将自己的开源 jar 发布至 maven 中央仓库,即可在 https://mvnrepository.com/ 官网中搜索到。

注册 sonatype 的 Jira 帐号

SonaType是什么呢?提供开源项目仓库托管( Open Source Software Repository Hosting,简称 OSSRH )服务。

注意:记录用户名和账号密码,后续发布 jar 文件时会使用到。

注册地址: https://issues.sonatype.org/secure/Signup!default.jspa

注册很简单,重要的是邮箱地址一定要正确,issue 有任何变动都会通过邮件通知。

sonatype 网站设置中文语言

sonatype 的 Jira 帐号注册成功之后,登录进入首页界面如下:

点击头像的个人信息设置:

点击 preferences 设置按钮:

设置语言及时区:

申请创建个人仓库工单

点击页面顶部的创建按钮,创建工单:

项目选择:Community Support - Open Source Project Repository Hosting (OSSRH)

问题类型选择:New Project

填写表单信息:

表单中有如下信息需要填写:

  • Summary:描述自己包的作用
  • Description:可写可不写,也是具体描述的
  • Group Id:笔者有自己的域名,所以填写的是 cn.woodwhales.common,如果没有自己的域名,可以填写:github.xxx.yyy 或者 gitee.xxx.yyy。
  • Project URL:填写你的项目的 github 或者 gitee 地址
  • SCM url:填写你项目的 github 或者 gitee 的克隆地址

读者可以参考笔者的工单进行填写:https://issues.sonatype.org/browse/OSSRH-73359

工单提交之后需要等待管理员审核。审核完成,管理员会在下面提示你让你验证这个域名是你的或者证明域名或者 gitee 或者 github 空间是你的。笔者的要求域名解析配置,并且在 github 仓库中临时创建一个的名称为:OSSRH-73359的公开仓库:

管理员很亲切地给了怎么域名添加解析记录的指导说明:https://central.sonatype.org/faq/how-to-set-txt-record/

笔者的域名在腾讯云平台,进入域名解析平台,添加解析记录,稍等片刻即可收到管理员再次审核通过的信息:

在 github 仓库中创建临时仓库:

因为已经完成了验证,所以笔者在完成本博文之后就删除了这个临时仓库。

当上述步骤按照管理员要求验证之后,稍等片刻,就会收到管理员验证通过的信息:

到此,我们可以按照管理员给的配置指南进行下一步操作了,可以不再使用 sonatype 网站。

值得注意是的是,工单的状态也变为已解决状态。

发布指南

参考管理员给的配置指南:https://central.sonatype.org/publish/publish-guide/#deployment,笔者是 maven 工程,所以点击 maven 工程配置指南:https://central.sonatype.org/publish/publish-guide/#deployment

读者如果是其他版本控制工具,找到对应的文档自行配置即可,思路大致相同。

配置 maven 的 settings.xml

在自己本机的 maven 配置文件 settings.xml 中配置<server>

1
2
3
4
5
6
7
8
9
<settings>
<servers>
<server>
<id>ossrh</id>
<username>sonatype 的 Jira 帐号</username>
<password>sonatype 的 Jira 帐号的密码</password>
</server>
</servers>
</settings>

笔者建议这个<id>ossrh</id> 不要修改,就使用官方指南的即可。

GPG 签名配置

下载 GPG

sonatype 要求发布的 jar 需要使用 GPG 签名,所以需要安装 GPG,并生成密钥对。

GPG 下载地址:

GPG 官网有各个环境的安装包下载地址:https://gnupg.org/download/index.html

安装 GPG

安装 GPG 很简单,一直下一步即可安装成功。安装过程中要记得安装目录,后续会有使用到这个路径。

笔者安装在了 c 盘符下:

值得注意的是 GPG 会自动注册到系统环境变量中,所以我们可以直接在 cmd 窗口中验证是否安装成功:

展示出 GPG 版本信息表示安装成功。另外,安装成功之后,桌面会出现一个快捷图标:Kleopatra

生成 GPG 证书(密钥对)

关于密码学相关知识,请移步至笔者的另一篇博文:漫谈密码技术

可以使用 GPG 命令在 cmd 窗口生成,也可以打开 Kleopatra 桌面程序生成。

方式1:命令生成

命令行生成生成秘钥: gpg --gen-key

按提示输入名字, 如:woodwhales,邮箱 woodwhales@163.com,然后选择 (O) kay,输入英文字母 o

弹出设置密码框,记住这个密码,稍后会使用到:

密码输入完成之后,程序生成密钥对,并展示出 GPG 公钥签名:

查看本系统已生成的所有密钥对信息: gpg --list-keys

上图中有俩个公钥信息,第一个是笔者写本博文之前创建的,第二个是本博文操作创建的。

方式2:可视化生成

可视化的生成密钥对比命令行生成友好很多,并且直接展示已生成的所有密钥对信息:

点击创建个人密钥对:

输入密钥名称、邮箱,勾选设置密码:

设置密码:

生成成功

系统中又多了一个密钥对。

如果确认证书不需要使用了,可以右键点击删除。注意:一定要确认好,物理删除了可创建不会回来相同的密钥对了!

上传公钥证书至公钥服务器

使用 cmd 命令执行:

1
gpg --keyserver hkp://pool.sks-keyservers.net --send-keys 公钥签名

验证是否成功地上传到了两个服务器:

1
gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 公钥签名

可能出现的上传失败问题

命令行上传至 hkp://pool.sks-keyservers.net 可能出现[gpg: keyserver receive failed: No name](https://stackoverflow.com/questions/66217436/gpg-keyserver-receive-failed-no-name)问题。

解决方案1

可以参考:https://central.sonatype.org/publish/requirements/gpg/#distributing-your-public-key 文档中的镜像替换,即将hkp://pool.sks-keyservers.net 替换为镜像列表中的镜像。

1
2
3
keyserver.ubuntu.com
keys.openpgp.org
pgp.mit.edu
解决方案2

笔者创建至 hkp://pool.sks-keyservers.net 失败了,可以使用 Kleopatra 导出公钥,浏览器访问公钥服务器:https://keys.openpgp.org/ 官网,然后上传公钥证书,上传成功之后,服务器会给你的邮件发一封确认邮件。

在搜索框中搜索邮箱得到信息,则表示上传成功:https://keys.openpgp.org/search?q=woodwhales%40163.com

再次配置 maven 的 settings.xml

在自己本机的 maven 配置文件 settings.xml 中再次配置<profile>

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

<profiles>
<profile>
<id>ossrh</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<gpg.executable>C:\programs\GnuPG\bin\gpg.exe</gpg.executable>
<gpg.passphrase>上述步骤中创建的密钥对的密码</gpg.passphrase>
</properties>
</profile>
</profiles>

上述配置中:

<id>ossrh</id>:和<server>中 sonatype 账号的 id 一致

<gpg.executable>:gpg.exe 绝对路径,如果不配置,可能在后续发布的时候 maven-gpg-plugin 插件报找不到 gpg.exe 错误。

<gpg.passphrase>:上述步骤中创建的密钥对的密码

配置项目中的 pom.xml

在要发布的 maven 工程的 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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>cn.woodwhales.common</groupId>
<artifactId>woodwhales-common</artifactId>
<version>3.5.0</version>
<name>woodwhales-common</name>

<!-- 项目描述 -->
<description>https://github.com/woodwhales</description>
<url>https://github.com/woodwhales/woodwhales-common</url>

<properties>
<java.version>1.8</java.version>
<!-- 文件拷贝时的编码 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>

<licenses>
<!-- 开源协议 -->
<license>
<name>The Apache Software License, Version2.0</name>
<url>http://www.apache.org/licenses/</url>
<distribution>repo</distribution>
</license>
</licenses>

<!-- scm -->
<scm>
<tag>master</tag>
<url>https://github.com/woodwhales/woodwhales-common</url>
<connection>scm:git:git://github.com:woodwhales/woodwhales-common.git</connection>
<developerConnection>scm:git:git://github.com:woodwhales/woodwhales-common.git</developerConnection>
</scm>

<!-- issues -->
<issueManagement>
<system>GitHub Issue Management</system>
<url>https://github.com/woodwhales/woodwhales-common/issues</url>
</issueManagement>

<!-- 开发者信息 -->
<developers>
<developer>
<id>woodwhales</id>
<name>woodwhales</name>
<email>woodwhales@163.com</email>
<url>https://woodwhales.cn/</url>
<timezone>+8</timezone>
</developer>
</developers>

<dependencies>
...
</dependencies>

<!-- distributionManagement -->
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>ossrh</id>
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>

<build>
<plugins>
<!-- maven 打包时附带源码文件插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 生成 javadoc 插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9.1</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- sonatype staging 插件 -->
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.6.7</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://s01.oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<!-- maven-gpg 插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.5</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>

对 maven 工程执行即可发布 jar 至 maven 仓库,注意工程版本不要带有SNAPSHOT

1
mvn clean deploy

发布成功之后,可以在 search.maven.org 中搜索到自己刚刚上传的 jar 文件。

并在 https://s01.oss.sonatype.org/#welcome 中使用 sonatype 的 Jira 账号登录,从 https://s01.oss.sonatype.org/content/groups/public/https://s01.oss.sonatype.org/content/repositories/releases/ 中可以看到发布的 jar 文件。

另外在 https://mvnrepository.com/ 中已可以确认一下 maven central 中也已经同步了:

maven central 仓库:

通知管理员关闭 issue

上述步骤操作完毕之后,能顺利发布 jar 至 releases 仓库,则需要在最开始的工单中评论,请管理员关闭工单。

并将工单的是否已同步至 central 状态改为 Yes:

填写已关闭 issuse

笔者上传自定义开源 jar 文件至 maven 仓库成功之后,在隔一天之后可在 https://mvnrepository.com 网站搜索到该 jar 信息:https://mvnrepository.com/search?q=woodwhales

参考资料

The Central Repository Documentation

发布自己的jar包到Maven中央仓库

GPG 使用 Maven 签名后发送到中央仓库提示 Upload your public key and try the operation

jar maven 中央仓库 GPG问题 deploy 400

gpg: keyserver receive failed: No name

更新JAR到maven仓库 gpg: keyserver send failed: No name

认识 MySQL 的快照读、当前读

基本概念

普通读

普通读,也称快照读,英文名:Consistent Read。

普通读就是单纯的 select 语句,不包括下面这两类语句:

1
2
select ... for update 
select ... lock in share mode

当前读

当前读,也称锁定读,Locking Read。

当前读,读取的是最新版本,并且需要先获取对应记录的锁,如以下这些 SQL 类型:

1
2
3
4
5
select ... for update
select ... lock in share mode
update ...
delete ...
insert ...

实验操作

环境准备:

一个 MySQL Server,俩个 MySQL 客户端(笔者使用的是 windows mysql 客户端),一个数据库,一张数据库表,表中存储了些许测试数据。

数据库环境约定

MySQL 版本及默认事务隔离级别约定

MySQL Server 版本

查看 MySQL Server 版本:

1
select version();

MySQL Server 版本为 5.7.27

windows 环境安装MySQL 教程请移步至笔者这篇博文:MySQL 5.7 绿色安装(for windows)

事务隔离级别

查看事务隔离级别:

1
select @@tx_isolation;

InnoDB 存储引擎的事务隔离级别默认为:可重复读。

关闭自动提交

MySQL 客户端默认是开启自动提交的,需要手动关闭。

查看自动提交状态

方式1:

1
select @@autocommit;

1 表示开启自动提交,0 表示关闭自动提交

方式2:

1
show variables like 'autocommit'\G

ON 表示开启自动提交,OFF 表示关闭自动提交

关闭自动提交

方式1:

1
set autocommit=0;

方式2:

1
set autocommit=OFF;

创建数据库表及测试数据

执行如下 sql 创建数据库表及测试数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 清空已存在的 woodwhales_test_db 数据库
drop database if exists woodwhales_test_db;

-- 创建 woodwhales_test_db 数据库
create database woodwhales_test_db;

-- 使用 woodwhales_test_db 数据库
use woodwhales_db;

-- 创建 test 数据库表,存储引擎使用 innodb
CREATE TABLE `test_table` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`age` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;

-- 插入测试数据
begin; -- 开启事务
use woodwhales_test_db; -- 使用 woodwhales_test_db 数据库
insert into test_table(age) values (1),(2); -- 插入测试数据
commit; -- 提交事务

快照读-实验

步骤1

客户端1客户端2分别开启事务;

步骤2

客户端1客户端2分别查询测试数据库表;

此时,客户端1客户端2在各自的事务中看到的库表数据是一样的。

步骤3

客户端2执行更新语句,但不执行 commit 或者 rollback 命令;

客户端2更新 id = 1 的数据:

1
update test_table set age = 10 where id = 1;

步骤4

客户端1客户端2分别查询测试数据库表;

1
select * from test_table;

俩个客户端执行上述命令之后的结果如下图:

从上图结果可知:

客户端1在自己的事务中查询到的结果与客户端 2 在自己的事务中查询到结果不一致:

  • 客户端1查询到的还是开启事务之前的版本
  • 客户端2查询到的是自己操作数据之后的版本

出现读取数据不一致的现象,是因为客户端1的 sql 语句是一个快照读,读取的是事务开启时的库表快照。

读者可能会怀疑,为什么没有读取到客户端2操作后的数据,是不是因为客户端2还没有提交事务呢?

别忘了笔者在数据库环境约定中指明了数据库的隔离级别为可重复读,因此客户端2就算执行了提交事务操作,也不会影响客户端1在当前未提交事务中所读的数据,因为只要事务还没有提交,读取的就是事务开启时的库表快照。

实验证明:客户端2提交事务,并重新查询数据库表:

当前读-实验

防止俩个客户端有未提交的事务,影响实验结果。使用其中一个客户端更新数据为初始状态。并将俩个客户端都执行一下 commit 命令,保证俩个客户端开启事务是在“同一起跑线”上。

下述的前三个步骤与上小节的三个步骤一致。

步骤1

客户端1客户端2分别开启事务;

步骤2

客户端1客户端2分别查询测试数据库表;

步骤3

客户端2执行更新语句,但不执行 commit 或者 rollback 命令;

客户端2更新 id = 1 的数据:

1
update test_table set age = 10 where id = 1;

步骤4

客户端1使用当前读查询:

1
select * from test_table for update

客户端1执行上述命令之后,命令行窗口会处于阻塞状态,而客户端2执行上述命令无任何阻塞:

注意此时的客户端1不会无限阻塞,会阻塞一段时间之后,报出ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction错误。

InnoDB 存储引擎中innodb_lock_wait_timeout参数控制着锁等待时长,默认获取锁等待 50s,一旦数据库锁超过这个时间就会报错。

1
2
3
4
-- 查看锁等待超时时间,单位:秒
show global variables like 'innodb_lock_wait_timeout';
-- 设置锁等待超时时间,单位:秒
set global innodb_lock_wait_timeout=500;

由于上述步骤 4 中客户端1查询的是全表的当前最新数据记录,我们知道当前全表中的 id = 1 的数据是客户端2修改了并且没有提交事务,由于事务隔离级别的约束,所以客户端1只能阻塞等待。

步骤5

如果步骤 4 中的客户端1查询的不是客户端1已修改但未提交的记录,那么客户端1执行当前读语句是不会阻塞的:

步骤6

先明确当前读的概念,当前读表示强制读取数据的最新状态,并且这个数据状态一定是已事务提交后的数据状态。由于事务隔离级别决定了,不允许产生脏读,所以无论是快照读还是当前读都不可能读取到其他事务未提交的数据状态。

如果步骤 4 中的客户端1执行当前读之前,客户端2进行了事务提交,那么客户端1进行当前读的时候就会读取到数据的最新状态:

从上图可以看出,客户端1没有被阻塞,而是查询到了数据,但是数据和开启事务时的数据状态不一致,出现了幻读的情况。总结得出,在可重复读的事务隔离级别下,快照读和当前读混用可能会造成幻读。

浅析String#intern

String#intern() 方法简述

String#intern() 方法的官方注释如下:

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 final class String
implements java.io.Serializable, Comparable<String>, CharSequence {

/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java&trade; Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
*/
public native String intern();

}

String#intern 是一个 native 方法,注释大意为:如果常量池中存在当前字符串,就会直接返回当前字符串对象引用。如果常量池中没有此字符串,会将此字符串放入常量池中后,再将其字符串对象引用返回。

字符串常量池(String pool 或 String Literal pool)

字符串常量池,或许叫全局字符串池更容易了解它的功能,这个池子里存储的内容是:类加载(验证,准备阶段)完成之后在堆中生成字符串对象实例的引用值,因此 String pool 中存的是引用值而不是具体的实例对象,具体的实例对象存放在堆中开辟的一块空间(具体来说是在运行时常量池中)。

在 HotSpot VM 里实现 String pool 功能的是一个 StringTable 类,它是一个哈希表,里面存的是驻留字符串的引用(也就是我们常说的用双引号括起来的字符串对象实例的引用,而不是驻留字符串实例本身),因此在堆中的某些字符串实例被这个 StringTable 引用之后就等同被赋予了“驻留字符串”的身份。

这个 StringTable 在每个 HotSpot VM 的实例只有一份,被所有的类共享。

字符串常量池由一个固定容量的 hashmap 实现,每个元素包含相同 hash 值的字符串列表。

在早期的 java 6 中它是一个常量,从 Java 6u30 以后变成了可配置的,你需要通过-XX:StringTableSize=N来设置字符串常量池中 map 的大小。为了性能考虑请确保它是一个质数

在 Java 6 到 7u40 中 -XX:StringTableSize 参数的默认值是 1009。在 Java 7u40 以后默认值为 60013(java 8 仍然支持此参数设置以兼容 Java 7)。

Class 文件常量池( Class constant pool )

程序员编写好的 Java 文件需要编译成 Class 文件,再由 JVM 加载运行,注意的是,每一个类都会生成对应的一个 Class 文件,Class 文件中的存储的信息格式十分严格,Class 文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池( Constant pool table ),用于存放编译器生成的各种字面量( Literal )和符号引用( Symbolic References )。

字面量就是我们所说的常量概念,如文本字符串、被声明为 final 的常量值等。

符号引用是一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可(它与直接引用不同:直接引用一般是指向方法区的本地指针,相对偏移量或是一个能间接定位到目标的句柄)。一般包括下面三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

1
2
3
4
5
public class Test {
public static void main(String[] args) {
String str = "木鲸鱼";
}
}

反编译上述代码,执行javap -verbose Test,查看 Class 文件编译内容结果:

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
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#13 // java/lang/Object."<init>":()V
#2 = String #14 // 木鲸鱼
#3 = Class #15 // Test
#4 = Class #16 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 main
#10 = Utf8 ([Ljava/lang/String;)V
#11 = Utf8 SourceFile
#12 = Utf8 Test.java
#13 = NameAndType #5:#6 // "<init>":()V
#14 = Utf8 木鲸鱼
#15 = Utf8 Test
#16 = Utf8 java/lang/Object
{
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String 木鲸鱼
2: astore_1
3: return
LineNumberTable:
line 4: 0
line 5: 3
}
SourceFile: "Test.java"

Test.java文件被编译之后,也就会生成了上述的 Class 文件内容,可以看到其中保存了"木鲸鱼"的字面量在 Class 文件常量池(Constant pool)中。

具体存储的方式是:"木鲸鱼"字面量会拆分成CONSTANT_Utf8CONSTANT_String,前者是一个指针,指向一个Symbol类型的 C++ 对象,内容是跟 Class 文件同样格式的 UTF-8 编码的字符串,后者表示的是常量的类型,它持有一个index,这个index可以指向另一个常量池(运行时常量池)中类型为CONSTANT_Utf8的常量。

重要概念说明:

Class 文件常量池中不仅仅存储着字符串的字面量,它可以存储着 14 种常量类型的项目表,比如上面反编译结果中的CONSTANT_Methodref_infoCONSTANT_Class_infoCONSTANT_NameAndType_infoCONSTANT_String_infoCONSTANT_Utf8_info都是常量类型表,分别存储着不同的信息,它们之间的关系如图:

这里再多说一下和字符串字面量相关的两个常量类型表:

symbol 对象的符号引用,在未解析的时候状态为CONSTANT_UnresolvedString,解析成功之后将转变为CONSTANT_internString

1
2
3
4
CONSTANT_String_info {  
u1 tag; // 常量池项目类型标志,其中 CONSTANT_String_info 的 tag 为:8
u2 index; // 指向字符串字面量的索引
}

CONSTANT_Utf8_info字符串字面量属于 Class 常量池项目中的字符串项目类型

1
2
3
4
5
CONSTANT_Utf8_info {  
u1 tag; // 常量池项目类型标志,其中 CONSTANT_Utf8_info 的 tag 为:1
u1 length; // UTF-8编码的字符串长度(字节单位)
u2 bytes_length; // UTF-8编码的字符串长度(字节单位)
}

从上面反编译的结果可以看出:常量池中的第 4 项是CONSTANT_String_info类型的信息,指向了常量池中的第 14 项的项目内容,即类型为CONSTANT_Utf8_info的字面量内容。

运行时常量池(runtime constant pool)

当 Java 文件被编译成 Class 文件之后,其文件中就会生成上面所说的 Class 文件常量池,那么这个 Class 文件常量池是用来干什么的呢?

JVM 在执行某个类的时候,必须经过加载、链接、初始化,而链接又包括验证、准备、解析三个阶段。当某个类加载到 JVM 内存之后,JVM 就会将这个类中的 Class 文件常量池中的内容存放到运行时常量池中。由此可知,运行时常量池也是每个类都有一个。

上个概念已经解释:Class 文件常量池中存的是字面量和符号引用,也就是说 Class 文件常量池存的并不是对象的实例,而是对象的符号引用值。当被加载完成字面量和符号引用经过解析(resolve)之后,也就是把符号引用替换为直接引用,Class 文件常量池中字面量内容才正式被 JVM 使用,解析完成之后就会去查询全局字符串池(上述 HotSpot VM 中的 StringTable),以保证运行时常量池所引用的字符串与全局字符串池中所引用的是一致的。

讲到这里,不知道读者有没有意识到上述两个常量池的明显区别:Class 文件常量池是在编译之后就已经确定好了内容及内存大小,因此它是静态的常量池,而运行时常量池相对于 Class文件常量池 的另外一个重要特征是具备动态性,Java 语言并不要求常量一定只有编译期才能产生,也就是并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也允许新的常量放入池中,这种特性被开发人员利用比较多的就是 String 对象的 intern() 方法。

在 JDK 7 中,运行时常量池已经在 Java 堆上分配内存,执行 String#intern() 方法时,JVM 会检查当前常量池中是否存在调用 intern() 方法的 String 对象是否,如果常量池已经存在该字符串,则直接返回字符串引用,否则复制该字符串对象的引用到常量池中并返回,所以在 JDK 7 中,可以重新考虑使用intern方法,减少 String 对象所占的内存空间。

运行时常量池和 Class 文件常量池 及 全局常量池的关系

以下引用 1,引用 2 摘自:《请问,jvm实现读取class文件常量池信息是怎样呢?》http://hllvm.group.iteye.com/group/topic/26412 ;引用 3 摘自:《运行时常量池与Class文件常量池的区别》http://hllvm.group.iteye.com/group/topic/40008

各个类型的常量是混在一起放在常量池(运行时常量池)里的,跟 Class 文件里的基本上一样。

最不同的是在这个运行时常量池里,symbol 是在类之间共享的;而在 Class 文件的常量池里每个 Class 文件都有自己的一份 symbol 内容,没共享。

以上述表述中可以得知:Class 文件常量池中的字面量内容(symbol 内容),会在类加载完毕之后,就会进入该类的运行时常量池中,并且这份 symbol 内容是全局通用的。

这些 Utf8 常量在 HotSpot VM 里以 symbolOopDesc 对象(下面简称 symbol 对象)来表现;它们可以通过一个全局的 SymbolTable 对象找到。注意:constantPool 对象并不“包含”这些 symbol 对象,而只是引用着它们而已;或者说,constantPool 对象只存了对 symbol 对象的引用,而没有存它们的内容。

@Rednaxelafx 提到的 SymbolTable 就是全局字符串常量池,因为 StringTable 里存储的就是 symbol 对象的引用,从 openjdk 源码中即可验证,openjdk 源码在文末附引用中。

以上述表述中可以得知:全局常量池中保存的是 symbol 对象实例的引用,一旦被保存进来,那么这个 symbol 引用维护的对象实例就有了“全局字符串常量的身份”。

Class 文件常量池只是 .class 文件中的、静态的;而运行时常量池,是在运行时将所有 Class 文件常量池中的东西加载进来?

前半句对,后半句半对。运行时常量池是把 Class 文件常量池加载进来,每个类有一个独立的。刚开始运行时常量池里的链接都是符号链接,跟在 Class 文件里一样;边运行边就会把用到的常量转换成直接链接,例如说要 Class A 调用 Foo.bar() 方法,A.class 文件里就会有对该方法的 Methodref 常量,是个符号链接(只有名字没有实体),加载到运行时常量池也还是一样是符号链接,等真的要调用该方法的时候该常量就会被 resolve 为一个直接链接(直接指向要调用的方法的实体)。

以上表述可以得知:Class 文件常量池中存储的是字面量内容,等到类加载完成之后就会将这些字面量标记未解析,存储在运行时常量池中,注意,此时类只是加载,并且加载后的内容不能直接使用,需要解析之后才能使用。因此只有在调用者要使用的时候才进行解析,变成可使用状态,并且是解析机制是调用多少就解析多少,不会用到的不会解析,因此运行时常量池是动态加载的。

创建字符串对象

首先 String 不属于 8 种基本数据类型,String 是对象。对象的默认值是 null,所以 String 对象的默认值也是 null;但它又是一种特殊的对象,有其它对象没有的一些特性;

new String() 和 new String(“”) 都是申明一个新的空字符串,是空串不是 null;

创建 String 对象有两种创建方式:

第一种,直接使用双引号直接显示声明:

1
2
3
4
5
public class Test {
public static void main(String[] args) {
String str = "木鲸鱼";
}
}

JVM 编译 Test.java 文件之后,生成一个 Test.class 文件(反编译命令:javap -verbose Test),从反编译的内容中可以看出(这里笔者就不贴图了):"木鲸鱼"这个字符串已经被编译器编译成了的文本字符串字面量,当这个类加载之后,JVM 会将 class 文件常量池中的字面量内容全部加载到这个类的运行时常量池中,等待解析,再进行后续的程序操作。

第二种,使用 new 创建 String 对象:

1
2
3
4
5
public class Test {
public static void main(String[] args) {
String str = new String("木鲸鱼");
}
}

通过反编译可以发现:"木鲸鱼"字面量存储在 Class 文件常量池中,并观察执行语句可以看出,在 str 变量赋值之前,会进行ldc指令操作(加粗标绿部分),它的作用是从常量池中拉取字符串常量值的引用。

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
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Class #16 // java/lang/String
#3 = String #17 // 木鲸鱼
#4 = Methodref #2.#18 // java/lang/String."<init>":(Ljava/lang/String;)V
#5 = Class #19 // Test
#6 = Class #20 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Test.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Utf8 java/lang/String
#17 = Utf8 木鲸鱼
#18 = NameAndType #7:#21 // "<init>":(Ljava/lang/String;)V
#19 = Utf8 Test
#20 = Utf8 java/lang/Object
#21 = Utf8 (Ljava/lang/String;)V
{
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String 木鲸鱼
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: return
LineNumberTable:
line 3: 0
line 4: 10
}
SourceFile: "Test.java"

ldc:Push item from run-time constant pool,从常量池中加载指定项的引用到栈。

astore_<n>:Store reference into local variable,将引用赋值给第n个局部变量。

从上面两个反编译的结果可以看出,只要是双引号引起来的字符串都会被编译器编译成字面量,待类加载完成的之后,就会加载到运行时常量池中,但是注意,没有解析之前,加载的内容是不可以使用的。

牛刀小试

「 代码一 」

1
2
3
4
5
public static void main(String[] args) {
String str = new String("木鲸鱼");
String intern = str.intern();
System.out.println(intern == str); //false
}

通过反编译的内容结果可以发现:"木鲸鱼"已经被编译到了 Class 文件中的Constant pool中,在类加载完成的时候,就把"木鲸鱼"字面量内容加载到运行时常量池中,并标注为未解析状态JVM_CONSTANT_UnresolvedString

执行第一行代码,在栈中的 str 变量在 new操作之前,使用了ldc指令,ldc指令是去全局字符串常量池中查询并获取结果,此时查询之前需要注意,这时的"木鲸鱼"JVM_CONSTANT_String状态是unresolved(未解析的状态),此时的全局常量池没有此字符串的直接引用,只有"木鲸鱼"字面量的 symbol 对象的引用,于是 JVM 根据"木鲸鱼"的字面量内容中 symbol 对象创建出"木鲸鱼"的 String 对象,并把这个 symbol 符号引用(C++层面的符号引用,所有虚拟机都认识的,不能被虚拟机直接引用)直接转换成直接引用(不用虚拟机的直接引用的值的实现不同),并把这个直接引用返回给查询者,当成功获取到结果之后,就会把"木鲸鱼"字面量的项目类将会改为"JVM_CONSTANT_internString",因此解析完毕。

进一步证明一下:

「 代码二 」

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
String str1 = "木鲸鱼";
String str2 = new String("木鲸鱼");
String str3 = str1.intern();
String str4 = str2.intern();
System.out.println(str1 == str2); // false
System.out.println(str1 == str3); // true
System.out.println(str3 == str4); // true
}

str1 上来就 ldc 了,所以常量池中驻留的引用一定是 “木鲸鱼” 的引用。

「 代码三 」

1
2
3
4
5
6
7
8
public static void main(String[] args) {
String str1 = new String("木鲸鱼");
String str2 = "木鲸鱼";
String intern = str1.intern();
System.out.println(intern == str1); //false
System.out.println(intern == str2); //true
System.out.println(str1 == str2); //false
}

第一行代码的执行过程不在赘述,直接看第二行代码执行过程: str2 变量赋值之前,ldc 指令需要从全局字符串常量池中拉取结果,因为在第一行的代码执行完成之后,"木鲸鱼"已经被解析并驻留了,因此不会再创建新的 String 对象,而是直接去全局常量池中查询,发现能获取到直接引用,因此不再将 symbol 对象转换成直接引用了(解析)。

「 代码四 」

1
2
3
4
5
public static void main(String[] args) {
String str = new String("鲸") + new String("鱼");
String intern = str.intern();
System.out.println(intern == str); //true
}

上面这个结果,看起来很正常,因为 Class 文件常量池中没有"鲸鱼",所以 str 变量在 intern 的时候,JVM 会先去全局字符串常量池中找一下,看有没有这个"鲸鱼"字符串,发现没有,就将 str 创建的"鲸鱼"对象的堆地址并驻留到全局常量池中,也就是此时,str 有了全局字符串常量的“身份”。

注意:String str = new String("鲸") + new String("鱼");执行语句的过程是:先创建new StringBuilder()对象保存到 str_0 变量中,再创建两个隐性的变量 str_1 和 str_2 分别引用new String("鲸")new String("鱼"),再将 str_1 和 str_2 局部变量当作参数值,带入 append() 方法进行字符串拼接,拼接完成之后 str_0 变量的值为new StringBuilder("鲸鱼")对象堆地址引用,类的类型为 StringBuilder,使用 toString() 方法进行类型转换,这个 toString() 会创建新的 String 对象。(反编译的内容就不放了,读者自己动手尝试吧)

「 代码五 」

先使用字符串赋值再 intern() 。

1
2
3
4
5
6
7
8
public static void main(String[] args) {
String str1 = new String("鲸") + new String("鱼");
String str2 = "鲸鱼";
String intern = str1.intern();
System.out.println(intern == str1); //false
System.out.println(intern == str2); //true
System.out.println(str1 == str2); //false
}

str2 先执行的ldc指令,先去全局常量池中查询,发现没有,于是驻留了自己。

「 代码六 」

在代码四的基础上,将 3、4 行代码互换位置:先 intern() 再直接使用字符串赋值。

1
2
3
4
5
6
7
8
public static void main(String[] args) {
String str1 = new String("鲸") + new String("鱼");
String intern = str1.intern();
String str2 = "鲸鱼";
System.out.println(intern == str1); //true
System.out.println(intern == str2); //true
System.out.println(str1 == str2); //true
}

str1 先去全局常量池中查询,发现没有,于是驻留了自己。

centos 7 环境编译 openJDK 8 JVM 源码

centos 7 环境编译 openJDK 8 JVM 源码

centos 7 系统

官方镜像下载列表:https://www.centos.org/download/mirrors/

笔者下载的是 CentOS-7-x86_64-DVD-2009.iso,下载地址:http://mirrors.163.com/centos/7/isos/x86_64/CentOS-7-x86_64-DVD-2009.iso

centos 7 安装至 VMware 教程可参见笔者的博文:虚拟机安装 centos 7 系统

openJDK 8 源码包

openJDK 源码全部使用 Mercurial 管理,网址为:https://hg.openjdk.java.net/

选择 jdk8u

部署文档:

从上述图片中可以看到并没有源码,需要讲该项目下载下来,并执行 get_source.sh 文件。笔者尝试了多次,均不能成功下载源码。建议使用如下方式下载,访问:https://jdk.java.net/

笔者下载地址:https://download.java.net/openjdk/jdk8u41/ri/openjdk-8u41-src-b04-14_jan_2020.zip

JDK 1.7 安装包

官方要求编译 jdk8 源码需要使用到 jdk7,因此需要下载 jdk7:

官方地址:https://www.oracle.com/java/technologies/javase/javase7-archive-downloads.html

镜像地址:https://mirrors.huaweicloud.com/java/jdk/

笔者下载的版本为:https://mirrors.huaweicloud.com/java/jdk/7u80-b15/

编译环境

将上述下载好的:openJDK 8 源码包(openjdk-8u41-src-b04-14_jan_2020.zip)和 JDK 1.7 安装包(jdk-7u80-linux-x64.tar.gz)上传至 centos 系统。

笔者在系统根目录下创建了 /data 文件目录,上述俩个文件均上传至此目录,后续操作均在此目录下完成。

由于 centos 是最小安装,没有相关 unzip 解压工具,执行如下命令安装 unzip 工具:

1
yum install -y unzip

执行如下命令,将上述俩个压缩包进行解压:

1
2
tar -zxvf /data/jdk-7u80-linux-x64.tar.gz
unzip /data/openjdk-8u41-src-b04-14_jan_2020.zip

得到/data/jdk1.7.0_80文件目录和/data/openjdk文件目录:

安装依赖工具

使用 yum 包管理器安装下述相关工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 安装 c 语言开发相关编译器及调试器
yum install -y gcc
yum install -y gcc-c++
yum install -y gdb

# 安装编译 JVM 相关依赖
yum install -y cups
yum install -y cups-devel
yum groupinstall -y "Development Tools"
yum install -y libXtst-devel libXt-devel libXrender-devel
yum install -y freetype
yum install -y freetype-devel
yum install -y alsa-lib-devel
yum install -y fontconfig-devel

进入/data/openjdk 文件目录、设置 configure 为可执行文件,再执行预编译:

1
2
3
4
5
cd /data/openjdk

chmod +x ./configure

./configure --with-boot-jdk=/data/jdk1.7.0_80/ --with-target-bits=64 --with-debug-level=release

参数含义:

–with-boot-jdk-jvmargs:指定 boot-jdk 目录

–with-target-bits:系统的位数:32 或者 64

–with-debug-level:可选择:release, fastdebug, or slowdebug

上述命令执行成功,会提示如下信息:

上述步骤完成之后,使用 make 工具编译 JVM:

1
make all

编译预计半小时,其中出现警告不需要处理,直到出现如下信息表示编译成功:

自己编译好的 JDK 就在/data/openjdk/build/linux-x86_64-normal-server-release/jdk目录下:

查看 jdk 版本:

1
/data/openjdk/build/linux-x86_64-normal-server-release/jdk/bin/java -version

从版本信息中可以看到自已编译的版本信息:

Python函数定义、函数参数定义、函数相关变量知识点总结

函数

函数的定义

1
2
3
def 函数名([输入参数]):
函数体
[return xxx]

上述输入参数表示的是形式参数,简称形参

调用函数的调用者传入的数值,称为实际参数,简称实参。实参的位置是函数的调用处。

函数的参数传递

位置实参

根据形参对应的位置进行实参传递

1
2
3
4
5
6
7
# 自定义函数
def calc(x, y):
print('x=', x, 'y=', y)
return x + y

# 位置实参
print(calc(10, 20))

关键字实参

根据形参名称进行实参传递

1
2
3
4
5
6
7
# 自定义函数
def calc(x, y):
print('x=', x, 'y=', y)
return x + y

# 关键字实参
print(calc(y=40, x=30))

函数的参数传递——可变实参与不可变实参

1
2
3
4
5
6
7
8
9
10
11
12
13
# 自定义函数
def fun(arg1, arg2):
arg1 = 10
arg2.append(10)
return arg1, arg2

a = 20
b = [1, 2, 3, 4]

print(fun(a, b)) # 输出(10, [1, 2, 3, 4, 10])

print(a) # 输出20
print(b) # 输出[1, 2, 3, 4, 10]

上述代码中,fun() 函数中的形参 arg1 的作用域只在函数体内,fun() 函数中的形参 arg2 是数组的地址,因此操作数组地址对应的数组内容,则影响所有使用到该数组地址的地方。

使用图解描述上述的现象:

函数的返回值

  1. 如果函数没有返回值,return 可以省略不写。如果调用者一定要接受没有返回值的函数,则返回None

  2. 函数的返回值,如果是 1 个,直接返回类型。

  3. 函数的返回值,如果是多个,返回的数据类型为元组。

无返回值,示例:

1
2
3
4
5
6
# 自定义函数
def fun(arg1, arg2):
print(arg1, arg2)


print(fun(10, 20)) # 没有返回值的函数,返回 None

只有一个返回值,示例:

1
2
3
4
5
6
7
# 自定义函数
def fun(arg1, arg2):
list1 = [arg1, arg2]
return list1

print(fun(10, 20)) # 输出 [10, 20]
print(type(fun(10, 20))) # 输出 <class 'list'>

多个返回值,示例:

1
2
3
4
5
6
# 自定义函数
def fun(arg1, arg2):
return arg1, arg2

print(fun(10, 20)) # 输出 (10, 20)
print(type(fun(10, 20))) # 输出 <class 'tuple'>

函数参数

位置参数

1
2
3
4
5
6
7
8
9
def power(x, n):
s = 1
while n > 0:
n = n - 1
s = s * x
return s


print(power(3, 3)) # 输出 27

默认参数

1
2
3
4
5
6
7
8
9
def power(x, n=2):
s = 1
while n > 0:
n = n - 1
s = s * x
return s


print(power(3)) # 输出 9

设置默认参数时,有几点要注意:

  • 必选参数在前,默认参数在后,否则Python的解释器会报错。

  • 如何设置默认参数:当函数有多个参数时,把变化大的参数放前面,变化小的参数放后面。变化小的参数就可以作为默认参数。

个数可变的位置参数

  • 定义函数时,可能无法事先确定传递的位置实参的个数时,使用可变的位置参数

  • 使用*定义个数可变的位置形参

  • 结果为一个元组

1
2
3
4
5
6
7
8
9
def fun(*args):
print(args)
print(type(args))


fun(1, 2, 3)
# 输出
# (1, 2, 3)
# <class 'tuple'>

如果想把 list 中每一个元素传入 fun() 函数中,则需要在 list 变量之前加*

1
2
3
4
5
6
7
8
9
10
def fun(*args):
print(args)
print(type(args))


list1 = [4, 5, 6, 7]
fun(*list1)
# 输出
# (4, 5, 6, 7)
# <class 'tuple'>

个数可变的关键字参数

  • 定义函数时,可能无法事先确定传递的关键字实参的个数时,使用可变的关键字参数
  • 使用**定义个数可变的关键字形参
  • 结果为一个字典
1
2
3
4
5
6
7
8
9
def fun(**kw):
print(kw)
print(type(kw))


fun(a=1, b=2, c=3)
# 输出
# {'a': 1, 'b': 2, 'c': 3}
# <class 'dict'>

如果想把 dict 中每一个元素传入 fun() 函数中,则需要在 dict 变量之前加**

1
2
3
4
5
6
7
8
9
10
def fun(**kw):
print(kw)
print(type(kw))


dict1 = {'x': 1, 'y': 2, 'z': 3}
fun(**dict1)
# 输出
# {'a': 1, 'b': 2, 'c': 3}
# <class 'dict'>

命名关键字参数

命名关键字参数需要一个特殊分隔符**后面的参数被视为命名关键字参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
def fun(x, y, *, name, age):
print('x', x)
print('y', y)
print('name=', name)
print('age=', age)


'''
fun(10, 20, 'woodwhales', 40)
上述这种调用方式会报错,因为 fun() 函数的最后俩个参数指定了 name 和 age
'''
fun(10, 20, name='woodwhales', age=20)
fun(10, 20, age=20, name='woodwhales')

如果函数定义中已经有了一个可变参数,后面跟着的命名关键字参数就不再需要一个特殊分隔符*了:

1
2
def person(name, age, *args, city, job):
print(name, age, args, city, job)

多种参数的组合

在 Python 中定义函数,可以用必选参数、默认参数、可变参数、关键字参数和命名关键字参数,这5种参数都可以组合使用。但是请注意,参数定义的顺序必须是:必选参数、默认参数、可变参数、命名关键字参数和关键字参数。

比如定义一个函数,包含上述若干种参数:

1
2
3
4
5
def f1(a, b, c=0, *args, **kw):
print('a =', a, 'b =', b, 'c =', c, 'args =', args, 'kw =', kw)

def f2(a, b, c=0, *, d, **kw):
print('a =', a, 'b =', b, 'c =', c, 'd =', d, 'kw =', kw)

变量的作用域

函数体外的变量

函数体之外的变量可以作用于函数体内外:

1
2
3
4
5
6
7
name = 'woodwhales.cn'


def fun():
print('name=', name)

fun() # 输出 name= woodwhales.cn

函数体内的变量

函数体内的变量只作用于函数体内,称为局部变量。如果想让函数体内的变量能在函数体之外被使用到,则需要给局部变量前面加global

1
2
3
4
5
6
def fun():
global name
name = 'woodwhales'

fun() # 注意想使用全局变量时,需要调用一次函数
print(name) # 输出 woodwhales