woodwhales's blog

技术改变生活

Spring Boot 工程常见开发技巧

service 层参数 @Valid 校验

在 Spring Boot 工程中,一般开发中会 controller 层的接口参数的请求体对象属性中增加注解校验。在 service 层中,也可以对其进行 @Valid 注解校验:

在 service 接口实现类中增加 @Validated 注解,并在需要校验参数实体前增加 @Valid 注解即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;

import javax.validation.Valid;

/**
* @author woodwhales
* @date 2020-12-26 17:17
*/
@Service
@Validated
public class TempServiceImpl implements TempService {

@Override
public void save(@Valid TempData tempData) {
System.out.println(tempData);
}

}

参数实体对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import lombok.Data;

import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
* @author woodwhales
* @date 2020-12-26 17:18
*/
@Data
public class TempData {

@NotNull
private Integer id;

@NotBlank
private String name;

}

controller 调用时,springboot 自动帮我们做了属性校验:

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class TemController {

@Autowired
private TempService tempService;

@GetMapping("/temp")
public void temp() {
tempService.save(new TempData());
}
}

手写业务数据处理小工具

本文源码可运行的 JDK 版本要求:JDK 1.8 +

本文源码参见:https://github.com/woodwhales/woodwhales-common

一般在业务开发中,开发者会先通过 ORM 框架查询数据库数据得到数据的 List 集合,再该业务数据集合做如下处理:

List 集合转 Map 集合

按照 List 集合元素中某个属性将 List 集合转 Map 集合

在 JDK 1.8 版本中可以使用 stream 快速将 list 转 map:

1
2
3
4
5
6
7
8
9
// 第一种 直接返回user本身
Map<Long, User> map = userList.stream().collect(Collectors.toMap(User::getId, user -> user));

// 第二种 Function中有一个static方法identity 返回本身
Map<Long, User> map = userList.stream().collect(Collectors.toMap(User::getId, Function.identity()));

// 如果list中的age存在相同的时候,转化map的时候就会出错Duplicate key
// toMap的重载,定义key2覆盖key1的值
Map<Integer, Users> map3 = list.stream().collect(Collectors.toMap(Users::getId, Function.identity(), (key1,key2) -> key2));

笔者利用上述代码进行二次封装,简化了判空及转 map 时填写 .stream().collect… 等重复的代码。

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

/**
* list 转 map 集合
* @param source 数据源集合
* @param keyMapper map 集合中的 key 获取规则
* @param valueMapper map 集合中的 value 获取规则
* @param <K> map 集合中的 key 类型
* @param <S> 数据源集合中元素的类型
* @param <T> map 集合中的 value 类型
* @return
*/
public static <K, S, T> Map<K,T> toMap(List<S> source,
Function<? super S, ? extends K> keyMapper,
Function<? super S, ? extends T> valueMapper) {
if(null == source || source.size() == 0) {
return Collections.emptyMap();
}

return source.stream().collect(Collectors.toMap(keyMapper, valueMapper));
}
}

源码参见:src/main/java/org/woodwhales/business/DataTool.java

List 集合分组

List 集合按照某种规则进行分组

在 JDK 1.8 版本中可以使用 stream 快速将 list 分组:

1
Map<String, List<User>> group = list.stream().collect(Collectors.groupingBy(User::getType));

笔者利用上述代码进行二次封装,简化了判空及转 map 时填写 .stream().collect… 等重复的代码。

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

/**
* 将 list 集合分组
* @param source 数据源集合
* @param classifier 分组规则
* @param <K> map 集合中的 key 类型
* @param <S> map 集合中的 value 类型
* @return
*/
public static <K, S> Map<K, List<S>> groupingBy(List<S> source,
Function<? super S, ? extends K> classifier) {
if(null == source || source.size() == 0) {
return Collections.emptyMap();
}

return source.stream().collect(Collectors.groupingBy(classifier));
}
}

源码参见:src/main/java/org/woodwhales/business/DataTool.java

业务数据去重

List 集合按照指定属性去重(其中可能存在要去重的属性不存在,即“无效的”数据)。

利用 Map 集合存入元素方法 put() 的特性:已存在则返回旧的元素,不存在则返回空。

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 DataTool {
/**
* 对集合数据进行去重器
* 非线程安全
* @param source 数据源集合
* @param deduplicateInterface 去重器接口
* @param <K> 去重属性的类型
* @param <T> 数据源集合中元素的类型
* @return
*/
public static <K, T> DeduplicateResult<T> deduplicate(List<T> source,
DeduplicateInterface<K, T> deduplicateInterface) {
if(null == source || source.size() == 0) {
return new DeduplicateResult<T>(source, emptyList(), emptyList(), emptyList());
}

Map<K, T> container = new LinkedHashMap<>();
// 无效的数据集合
List<T> invalidList = new LinkedList<>();
// 重复的数据集合
List<T> repetitiveList = new LinkedList<>();

for (T data : source) {
if (!deduplicateInterface.isValid(data)) {
invalidList.add(data);
} else {
K deduplicatedKey = deduplicateInterface.getDeduplicatedKey(data);
T putData = container.put(deduplicatedKey, data);
if(Objects.nonNull(putData)) {
repetitiveList.add(putData);
}
}
}

// 已去重的数据集合
List<T> deduplicatedList = new ArrayList<>(container.values());

return new DeduplicateResult<T>(source, invalidList, deduplicatedList, repetitiveList);
}
}

源码参见:src/main/java/org/woodwhales/business/DataTool.java

枚举转 map 集合

根据枚举中某个属性进行枚举转 map 集合。

利用 EnumSet 类的 allOf() 方法,可以根据枚举的 Class 类型获取该枚举的所有枚举实例。当拿到集合枚举数据就可以使用 stream 进行集合转 map。

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

/**
* 枚举转 map 集合
* @param sourceEnumClass 数据源枚举 Class类
* @param keyMapper map 集合中的 key 获取规则
* @param <K> map 集合中的 key 类型
* @param <T> map 集合中的 value 类型
* @return
*/
public static <K, T extends Enum<T>> Map<K, T> enumMap(Class<T> sourceEnumClass,
Function<? super T, ? extends K> keyMapper) {
EnumSet<T> enumSet = EnumSet.allOf(sourceEnumClass);
return enumSet.stream().collect(Collectors.toMap(keyMapper, Function.identity()));
}
}

源码参见:src/main/java/org/woodwhales/business/DataTool.java

Visual Studio Code(vsCode) 下 C/C++ 环境搭建

本文教程参考官方文档:https://code.visualstudio.com/docs/cpp/config-mingw

安装 Visual Studio Code

Visual Studio Code 下载地址:https://code.visualstudio.com/download

安装插件:C/C++ extension for VS Code

安装插件:Code Runner

安装 Mingw-w64

MinGW-w64 下载地址:https://sourceforge.net/projects/mingw-w64/files/

可以下载在线安装器:MinGW-W64-install.exe,笔者下载的是完整版,省得在线安装缓慢。

下载好的安装包名为:x86_64-8.1.0-release-win32-seh-rt_v6-rev0.7z,使用 7-Zip 解压缩工具解压该安装包。进入解压后的文件目录,里面 mingw64 文件目录,将其剪切到指定的不含空格及中文目录下。

配置系统环境变量:Mingw-w64

将创建好的环境变量配置到 Path 中:

配置好 Mingw-w64 系统环境变量之后,打开 cmd 或者 windows terminal 终端窗口,执行如下命令可可以查看 gcc/g++ 编译器版本:

1
2
3
gcc -v
# 或者执行
g++ -v

出现版本信息则表示 Mingw-w64 安装成功:

Visual Studio Code 常用设置

Visual Studio Code 设置自动保存

在设置中,找到文件,设置失去焦点自动保存:

Code Runner 插件配置

防止运行 code runner 插件时,出现:vscode 中出现无法在只读编辑器中编辑问题,设置如下:

运行 C 文件

步骤1:使用 vsCode 创建一个 c 文件:

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("woodwhales.cn");
return 0;
}

步骤2:创建配置任务文件:在 “终端” 菜单中选择 “配置任务”

点击 “C/C++”,vsCode 自动生成配置文件:

步骤3:执行 C 文件:

也可以点击按钮执行:

步骤5:生成可执行文件:ctrl + shift + B

Spring 源码(spring-5.2.9.RELEASE)编译及 IDEA 环境搭建

安装包及环境准备

下载 spring-framework 源码

spring-framework 源码仓库在 github 平台维护,读者可以自行搜索如何将 github 仓库源码克隆至 gitee 平台。

笔者已经克隆一版,偷懒的读者可以直接拉取:https://gitee.com/woodwhales/spring-framework.git

警告!:spring-framework 源码存放的文件目录,一定不要含有特殊字符、汉字、空格等,以免编译失败。

使用 git 命令或者 source tree 工具等切换代码分支的 tag 为:5.2.9.RELEASE

切换成功之后,在项目根目录中的gradle.properties文件中显示了 5.2.9 版本:

gradle 下载与安装

移步至笔者的博文:构建工具maven及gradle的安装及IDE配置

gadle 编译配置

步骤一:build.gradle

在项目根目中找到build.gradle文件,在大约 280 行处的 repositories 配置里增加:

1
2
maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' }
maven { url 'https://maven.aliyun.com/nexus/content/repositories/jcenter'}

步骤二:settings.gradle

在项目根目中找到settings.gradle文件,在第 2 行处的 pluginManagement 配置里增加:

1
2
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
maven { url "https://maven.aliyun.com/repository/public" }

步骤三:gradle.properties

修改gradle.properties文件,设置org.gradle.jvmargs参数值为:-Xmx2048M

1
2
3
4
5
6
7
8
9
10
11
version=5.2.9.RELEASE
## 设置此参数主要是编译下载包会占用大量的内存,可能会内存溢出
org.gradle.jvmargs=-Xmx2048M
## 开启 Gradle 缓存
org.gradle.caching=true
## 开启并行编译
org.gradle.parallel=true
## 启用新的孵化模式
org.gradle.configureondemand=true
## 开启守护进程 通过开启守护进程,下一次构建的时候,将会连接这个守护进程进行构建,而不是重新fork一个gradle构建进程
org.gradle.daemon=true

IDEA 环境编译

在项目根目录下,右键选择:Git Bash Here,打开 Git Bash 窗口。

笔者安装了 git 工具,读者也可打开 windows terminal 程序或者 cmd 程序,执行命令为:

gradlew :spring-oxm:compileTestJava

执行如下命令:

1
./gradlew :spring-oxm:compileTestJava

命令,当出现如下字样时:

1
2
Downloading https://services.gradle.org/distributions/gradle-6.6.1-bin.zip
..

按 ctrl + c 停止该下载任务,将 gradle 包地址在浏览器中输入,浏览器会自动下载 gradle 包。

将下载好的 gradle 包放到:%GRADLE_USER_HOME%\wrapper\dists\gradle-6.6.1-bin\du4tvj86lhti6iga1v8h7pckb 文件夹目录下。在这个文件夹里有:上述下载任务时创建的,进入该目录下,将里面的未下载完成文件删除。

GRADLE_USER_HOME 是在系统的环境变量里配置的,笔者配置的值就是maven本地仓库的根目录。

再次执行./gradlew :spring-oxm:compileTestJavaa命令编译源码。

编译成功之后,使用 IDEA 导入项目即可。

常见错误

文件目录问题

如果读者按照上述步骤严格执行,一般不会出现编译失败问题。如果出现如下错误,很可能由于 spring 源码所在目录有问题。

笔者在两台电脑中执行了上述相同操作,一个成功,一个失败。让笔者郁闷很久,笔者一度怀疑编译失败的电脑操作系统出现了问题,特此重装了系统,结果还是编译失败。细细琢磨之后严重怀疑是源码存放目录的问题,果不其然,因此笔者在上述 “下载 spring-framework 源码” 章节里重点标注了警告提醒。

AnnotationCacheAspect 找不到符号

这是因为 AnnotationCacheAspect.aj 不是java文件需要另外的aspectj进行处理,可以按照以下步骤解决这个问题

执行 java -jar aspectj-1.9.5.jar 将 aspectj-1.9.5.jar 安装到本地某个目录。并在 IDEA 中安装 Aspectj weaver 插件。

详细参见:https://www.cnblogs.com/qubo520/p/13264036.html?utm_source=tuicool

参考资料

https://www.cnblogs.com/qubo520/p/13264036.html

https://www.cnblogs.com/liuyangfirst/p/13526619.html

https://gitee.com/zhong96/spring-framework-analysis

https://gitee.com/zhong96/spring-framework-analysis/blob/master/docs/Spring%E6%BA%90%E7%A0%81%E7%8E%AF%E5%A2%83%E6%90%AD%E5%BB%BA.md

centos 环境安装 confluence 详解

confluence 官方安装指南:https://confluence.atlassian.com/alldoc/confluence-documentation-directory-12877996.html

confluence 官方安装环境要求指南:https://confluence.atlassian.com/doc/system-requirements-126517514.html

笔者安装的 confluence 版本为 6.13.3:https://bitbucket.org/atlassian/confluence_docs/downloads/Confluence_6-13-0_CompleteGuide.pdf

安装建议:笔者使用的centos为 VMware 虚拟机环境搭建的,设置内存大小为 2G,依然运行有“卡顿”现象,如果可用内存小于 2GB,不建议安装使用 confluence。

环境准备

MySQL 软件

笔者使用的 MySQL 版本为:5.7.31,使用 docker 安装:

MySQL 官方 docker 镜像地址:https://hub.docker.com/_/mysql?tab=tags

拉取 MySQL 5.7.13 镜像:

1
docker pull mysql:5.7.31

MySQL 驱动

官方 MySQL 驱动:https://downloads.mysql.com/archives/c-j/

maven 仓库里下载 MySQL 驱动 :https://mvnrepository.com/artifact/mysql/mysql-connector-java

笔者使用的驱动版本为:mysql-connector-java-5.1.48.jar

因为官网文档已经说明了:https://confluence.atlassian.com/conf613/database-jdbc-drivers-964961299.html

代理包

笔者使用的代理包为:atlassian-agent.jar

代理包官方下载地址:

https://gitee.com/pengzhile/atlassian-agent

https://zhile.io/2018/12/20/atlassian-license-crack.html

confluence 软件

官方下载地址:https://www.atlassian.com/software/confluence/download-archives

笔者使用的 confluence 版本为:atlassian-confluence-6.13.3-x64

也是直接在浏览器访问:https://product-downloads.atlassian.com/software/confluence/downloads/atlassian-confluence-{version}.bin 下载 confluence 安装文件。

形如:https://product-downloads.atlassian.com/software/confluence/downloads/atlassian-confluence-6.13.3-x64.bin

MySQL 安装

上述拉取 MySQL 镜像之后,查看本地 docker 镜像:

1
docker images

创建临时 MySQL 容器

先创建临时 MySQL 容器

1
docker run --rm --name mysqlTemp -it -v /usr/local/mysql/:/var/lib/mysql  -e MYSQL_ROOT_PASSWORD=123456 {IMAGE ID} /bin/bash

上述命令中:{IMAGE ID} 替换成上图中的 IMAGE ID。

上述命令参数说明:

  • -- rm

    退出后就删除该容器

  • -v /usr/local/mysql/:/var/lib/ mysql

    装主机目录/usr/local/mysql/映射Docker中的/var/lib/mysql目录

  • -it

    交互模式

  • /bin/bash

    进入 bash 命令模式

上述命令执行成功之后,就进入了 docker 容器内部。查看 my.cnf 文件:

1
cat /etc/mysql/my.cnf

出现形如下图的内容:

如果是完整的 my.cnf 文件,直接把该 my.cnf 复制到 docker 容器外面去。如果不是,则需要把 includedir 目录一起复到主机。

从容器中获取 MySQL 配置文件到宿主机

把 docker 中的 /etc/mysql/my.cnf 文件拷贝到 docker 的/var/lib/mysql 目录中:及主机的 /usr/local/mysql/ 目录中:

1
cp /etc/mysql/my.cnf /var/lib/mysql

把 docker 中 includedir 目录复制出去

1
cp -R /etc/mysql/conf.d/ /var/lib/mysql/conf.d

退出 bash,docker 中临时用的 mysqlTemp 会被删除。此时查看主机中的 /usr/local/mysql/ 目录下有 mysql 所有的配置文件:

1
2
[root@node04 ~]# ls /usr/local/mysql/
conf.d my.cnf

修改宿主机 MySQL 配置

修改 MySQL 配置,在 my.cnf 文件中增加配置:

1
2
3
4
5
6
7
8
[mysqld]
character-set-server=utf8
collation-server=utf8_bin
default-storage-engine=INNODB
max_allowed_packet=512M
innodb_log_file_size=2GB
transaction-isolation=READ-COMMITTED
binlog_format=row

创建 MySQL 容器

改完之后,创建正式的 mysql 容器:

1
docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -v /usr/local/mysql/data:/var/lib/mysql -v /usr/local/mysql/my.cnf:/etc/mysql/my.cnf -v /usr/local/mysql/conf.d:/etc/mysql/conf.d --restart=always -d {IMAGE ID}

{IMAGE ID} 表示 MySQL 镜像的镜像 ID。

如果 my.cnf 是完整的配置,这里不需要映射 conf.d 和 mysql.conf.d 这俩个目录:

1
docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -v /usr/local/mysql/data:/var/lib/mysql -v /usr/local/mysql/my.cnf:/etc/mysql/my.cnf -d {IMAGE ID}

参数说明:

  • --name

    容器名称

  • -p

    端口映射

  • -v

    配置文件映射:将 宿主机上修改的自定义配置文件,映射到 容器中

  • -e

    MYSQL_ROOT_PASSWORD 设置root密码

  • -d

    守护进程模式运行

安装 confluence

将下载好的 confluence 安装包、MySQL 驱动、代理包都上传到 centos 上,笔者自己创建了 /data/confluence 目录:

上图中:atlassian-agent-v1.2.3.tar.gz 文件使用:tar -zxvf atlassian-agent-v1.2.3.tar.gz 命令解压即可的得到:atlassian-agent-v1.2.3 文件目录。

添加运行权限

刚上传的 confluence 安装包需要添加运行权限:

1
chmod +x atlassian-confluence-6.13.3-x64.bin

运行文件得到下面的内容,需要注意的是Confluence的安装路径,
默认程序:/opt/atlassian/confluence
默认数据:/var/atlassian/application-data/confluence

安装

运行 atlassian-confluence-6.13.3-x64.bin:

1
./atlassian-confluence-6.13.3-x64.bin

运行日志如下:

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
[root@node04 confluence]# ./atlassian-confluence-6.13.3-x64.bin 
Unpacking JRE ...
Starting Installer ...

This will install Confluence 6.13.3 on your computer.
OK [o, Enter], Cancel [c]
o
Click Next to continue, or Cancel to exit Setup.

Choose the appropriate installation or upgrade option.
Please choose one of the following:
Express Install (uses default settings) [1],
Custom Install (recommended for advanced users) [2, Enter],
Upgrade an existing Confluence installation [3]
1

See where Confluence will be installed and the settings that will be used.
Installation Directory: /opt/atlassian/confluence
Home Directory: /var/atlassian/application-data/confluence
HTTP Port: 8090
RMI Port: 8000
Install as service: Yes
Install [i, Enter], Exit [e]
i

Extracting files ...


Please wait a few moments while we configure Confluence.

Installation of Confluence 6.13.3 is complete
Start Confluence now?
Yes [y, Enter], No [n]
n

Installation of Confluence 6.13.3 is complete
Finishing installation ...

安装完成之后:

1
2
# netstat -tunlp | grep 8090
tcp6 0 0 :::8090 :::* LISTEN 2573/java

Confluence 日志文件位置:/usr/local/atlassian/confluence/logs/catalina.out

启动 confluence

windows 环境

进入到服务管理的界面(我的电脑 -> 右键 -> 管理 -> 服务或者services.msc)找到confluence 的服务,在这里启动关闭服务。

linux 环境

root 权限
1
2
3
4
5
6
7
8
9
10
11
#需要管理员权限
#关闭服务
service confluence stop
#启动服务
service confluence start

#需要管理员权限
#关闭服务
/etc/init.d/confluence stop
#启动服务
/etc/init.d/confluence start
非 root 权限

如果不是 root 用户安装的话,confluence 这个服务可能不会被注册到 service 上,所以上面的方法都不能用。

1
2
3
4
5
#非root用户安装,以我的安装路径为例:
#关闭服务
/opt/atlassian/confluence/bin/stop-confluence.sh
#启动服务
/opt/atlassian/confluence/bin/start-confluence.sh

笔者安装的环境是 linux,启动日志如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[root@node04 confluence]# service confluence start
Warning: confluence.service changed on disk. Run 'systemctl daemon-reload' to reload units.

To run Confluence in the foreground, start the server with start-confluence.sh -fg
executing using dedicated user: confluence
If you encounter issues starting up Confluence, please see the Installation guide at http://confluence.atlassian.com/display/DOC/Confluence+Installation+Guide

Server startup logs are located in /opt/atlassian/confluence/logs/catalina.out
---------------------------------------------------------------------------
Using Java: /opt/atlassian/confluence/jre//bin/java
2020-08-01 15:23:57,626 INFO [main] [atlassian.confluence.bootstrap.SynchronyProxyWatchdog] A Context element for ${confluence.context.path}/synchrony-proxy is found in /opt/atlassian/confluence/conf/server.xml. No further action is required
---------------------------------------------------------------------------
Using CATALINA_BASE: /opt/atlassian/confluence
Using CATALINA_HOME: /opt/atlassian/confluence
Using CATALINA_TMPDIR: /opt/atlassian/confluence/temp
Using JRE_HOME: /opt/atlassian/confluence/jre/
Using CLASSPATH: /opt/atlassian/confluence/bin/bootstrap.jar:/opt/atlassian/confluence/bin/tomcat-juli.jar
Using CATALINA_PID: /opt/atlassian/confluence/work/catalina.pid
Tomcat started.

上述日志可以看出,运行日志在:/opt/atlassian/confluence/logs/catalina.out 文件中。

代理

在 /opt/atlassian/confluence/bin/setenv.sh 文件中增加如下命令:

1
2
3
# vim /opt/atlassian/confluence/bin/setenv.sh
# 在文件最末尾增加一行下面的代码
export JAVA_OPTS="-javaagent:/data/confluence/atlassian-agent.jar ${JAVA_OPTS}"

笔者的代理包路径是:/data/confluence/atlassian-agent.jar,读者可以更换成自己的代理包位置。本站下载

再次启动 confluence,查看启动日志,看到=== agent working ===字样,表示代理成功。也可以使用命令:

1
ps aux | grep javaagent

检查代理是否成功。

防火墙设置

笔者使用的环境是虚拟机中的环境,因此直接把防火墙关闭了,如果需要开放某个具体的端口,参考下面的命令:

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
# 查看防火墙状态
systemctl status firewalld.service
# 开启防火墙:
systemctl start firewalld.service
# 关闭防火墙:
systemctl stop firewalld.service
# 重启防火墙:
systemctl restart firewalld.service
# 禁止开机启动:
systemctl disable firewalld.service
# 开启开机启动:
systemctl enable firewalld.service
# 查看防火墙所有开放的端口
firewall-cmd --zone=public --list-ports
# 开放8090端口
firewall-cmd --zone=public --add-port=8090/tcp --permanent
# 配置立即生效
firewall-cmd --reload

# 查看监听的端口
# centos7默认没有 netstat 命令,需要安装 net-tools 工具,yum install -y net-tools
netstat -lnpt

# 检查端口被哪个进程占用
netstat -lnpt |grep 8090

# 查看进程的详细信息
ps 进程号

# 中止进程
kill -9 进程号

激活设置

启动 confluence 之后,浏览器输入 ip:8090 打开 confluence 的配置页面,第一次访问浏览器出现初始化配置页面,笔者选择中文环境,选择产品安装,出现如下界面:

上图中的服务器 ID 很关键,一定要记录好。

当你试着执行 java -jar /data/confluence/atlassian-agent.jar 时应该可以看到输出的KeyGen参数帮助。由于此处是破解confluence,选择conf即可,具体命令如下:

将server ID复制(-m 邮箱 -n 用户名 -o 公司名 -s SERVER ID)

1
java -jar /data/confluence/atlassian-agent.jar -p conf -m woodwhales@163.com -n woodwhales -o https://woodwhales.cn/ -s AAAA-BBBB-CCCC-DDDD

设置数据库

选择 MySQL 数据库,需要安装 MySQL 驱动:

将下载的 mysql-connector-java-5.1.42-bin.jar 复制到 /opt/atlassian/confluence/confluence/WEB-INF/lib 目录并重启服务:

1
2
cp /data/confluence/mysql-connector-java-5.1.42.jar /opt/atlassian/confluence/confluence/WEB-INF/lib/
service confluence restart

性能优化

设置JVM运行内存参数

1
2
3
4
vim /opt/atlassian/confluence/bin/setenv.sh

# 修改 Xms 和 Xmx 参数
CATALINA_OPTS="-Xms4096m -Xmx8192m -XX:+UseG1GC ${CATALINA_OPTS}"

设置数据库连接池参数

1
2
3
4
5
6
7
8
9
vim /var/atlassian/application-data/confluence/confluence.cfg.xml

# 修改数据库连接池参数
<property name="hibernate.c3p0.acquire_increment">10</property> #默认为1
<property name="hibernate.c3p0.idle_test_period">100</property>
<property name="hibernate.c3p0.max_size">60</property>
<property name="hibernate.c3p0.max_statements">60</property> #默认为0
<property name="hibernate.c3p0.min_size">20</property>
<property name="hibernate.c3p0.timeout">120</property> #默认为30

数据链接 useSSL 配置

由于使用了 MySQL 5.7.31,所以数据库链接配置必须显示设置 useSSL

1
<property name="hibernate.connection.url">jdbc:mysql://127.0.0.1:3306/confluence?useSSL=false</property>

卸载 confluence

confluence 默认安装路径为 /opt/atlassian/confluence/:

到安装路径下,运行 uninstall:

1
/opt/atlassian/confluence/uninstall

然后手动删除两个路径的文件

1
2
rm -rf /var/atlassian/
rm -rf /opt/atlassian/confluence/

另,进入 /etc/init.d/ 删除多余的 confluence 开机启动项

windows上也是这么卸载的,但是在Windows上的话需要重启电脑。

插件安装

官方插件市场:https://marketplace.atlassian.com/

Gliffy Diagrams

官方安装指南:https://support.gliffy.com/hc/en-us/articles/217895178-Confluence-Plugin-Installation-Update-Instructions-

插件下载地址:https://marketplace.atlassian.com/plugins/com.gliffy.integration.confluence/versions

Balsamiq Wireframes

插件下载地址:https://marketplace.atlassian.com/apps/256/balsamiq-wireframes-confluence-server/version-history

PlantUML

插件下载地址:https://marketplace.atlassian.com/apps/41025/plantuml-for-confluence/version-history

参考资料

https://liangfu.wang/2020/01/22/CentOS-7-6-安装-Confluence-7-2/

https://www.tracymc.cn/archives/1507

虚拟机安装 centos 7 系统

安装包准备

centos官方地址:https://www.centos.org/

阿里云镜像:https://mirrors.aliyun.com/centos/

网易云镜像:http://mirrors.163.com/centos/

笔者推荐:centos 7 版本

vmware 网络配置

步骤1:打开虚拟网络编辑器

步骤2:选择设置NAT模式

点击更改设置进行网络设置。

笔者设置的配置为:

子网IP:192.168.100.0

子网掩码:255.255.255.0

网关IP:192.168.100.2

步骤3:设置 VMnet8 网卡网络配置

笔者设置了阿里云的 DNS 中的 223.6.6.6 或者 223.5.5.5。

虚拟机网络设置

安装 centos 之后,设置虚拟机的网络

安装 centos

虚拟机配置

设置centos网络

设置阿里云镜像

阿里云镜像地址:https://developer.aliyun.com/mirror/centos

最小安装版的 centos 系统没有任何常用工具,如 ifconfig、wget 等。

使用 yum 安装:

1
2
yum install -y net-tools
yum install -y wget

步骤1:备份

1
mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup

步骤2:替换镜像配置

1
2
3
wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo
或者
curl -o /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo

步骤3:生成缓存

1
yum makecache

步骤4:其他

非阿里云ECS用户会出现 Couldn’t resolve host ‘mirrors.cloud.aliyuncs.com’ 信息,不影响使用。用户也可自行修改相关配置:

1
sed -i -e '/mirrors.cloud.aliyuncs.com/d' -e '/mirrors.aliyuncs.com/d' /etc/yum.repos.d/CentOS-Base.repo

Guava EventBus 使用介绍及自实现详解

使用 guava 工具,先在 pom 中引入依赖:https://mvnrepository.com/artifact/com.google.guava/guava/

1
2
3
4
5
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>

Guava EventBus 的使用

简单使用

先编写监听者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.google.common.eventbus.Subscribe;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.2 22:08
* @description:
*/
public class GoogleEventBusListener {

@Subscribe
public void method1(String event) {
System.out.println("GoogleEventBusListener.method1, event = " + event);
}

@Subscribe
public void method2(Integer event) {
System.out.println("GoogleEventBusListener.method2, event = " + event);
}
}

再创建 EventBus 对象,将上述监听者注册到 EventBus 对象实例中,调用 post() 发送消息即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.google.common.eventbus.EventBus;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.2 22:08
* @description: EventBus 简单示例
*/
public class SimpleEventBusExample {

public static void main(String[] args) {
EventBus eventBus = new EventBus();

// 监听器注册到 eventBus
eventBus.register(new GoogleEventBusListener());

// 发送消息
eventBus.post("this message from eventBus");
eventBus.post(200);
}

}

EventBus 构造函数中,如果不显示指定名称,则使用:default 作为默认名称。

注意:消息类型必须是包装类型,不能是基本数据类型。

多个监听者存在继承关系

当只注册监听器中的父类中也监听了某些消息的时候,父类的监听方法也会执行。如果父类的监听方法被子类覆盖监听,则只执行子类监听器的方法。

类监听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.google.common.eventbus.Subscribe;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.2 22:18
* @description: 父类监听器
*/
public class FatherListener {

@Subscribe
public void accept(String event) {
System.out.println("FatherListener.accept, event = " + event);
}

@Subscribe
public void consume(String event) {
System.out.println("FatherListener.consume, event = " + event);
}
}

子类监听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.google.common.eventbus.Subscribe;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.2 22:19
* @description: 子类监听器
*/
public class ChildListener extends FatherListener {

@Subscribe
@Override
public void accept(String event) {
System.out.println("ChildListener.accept, event = " + event);
}

@Subscribe
public void consumeTask(String event) {
System.out.println("ChildListener.consumeTask, event = " + event);
}
}

注册监听器并发送消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.google.common.eventbus.EventBus;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.2 22:21
* @description: 多个监听器监听消息
* 当只注册监听器中的父类中也监听了某些消息的时候,父类的监听方法也会执行。
* 如果父类的监听方法被子类覆盖监听,则只执行子类监听器的方法
*
*/
public class MultipleListenerExample {

public static void main(String[] args) {
EventBus eventBus = new EventBus();
// 监听器注册到 eventBus
eventBus.register(new ChildListener());

// 发送消息
eventBus.post("this message from eventBus");
}
}

日志输出:

1
2
3
ChildListener.accept, event = this message from eventBus
ChildListener.consumeTask, event = this message from eventBus
FatherListener.consume, event = this message from eventBus

多个事件存在继承关系

event 如果有父类,此时某个监听器监听的event是这个基类,那么消息是这个子类的时候,该监听父类的监听器方法也会执行。

父类事件对象:

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
import com.google.gson.Gson;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.2 22:33
* @description: 父类事件对象
*/
public class FruitEvent {

String name;

public FruitEvent() {

}

public FruitEvent(String name) {
this.name = name;
}

@Override
public String toString() {
return new Gson().toJson(this);
}
}

子类事件对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.google.gson.Gson;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.2 22:35
* @description: 子类事件对象
*/
public class AppleEvent extends FruitEvent {

public AppleEvent() {
}

public AppleEvent(String name) {
super(name);
}

@Override
public String toString() {
return new Gson().toJson(this);
}

}

监听器对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.google.common.eventbus.Subscribe;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.2 22:32
* @description: 一个监听子类对象,一个监听父类对象
*/
public class SimpleListener {

@Subscribe
public void acceptFruitEvent(FruitEvent fruitEvent) {
System.out.println("SimpleListener.acceptFruitEvent => fruitEvent " + fruitEvent);
}

@Subscribe
public void acceptAppleEvent(AppleEvent appleEvent) {
System.out.println("SimpleListener.acceptAppleEvent => appleEvent " + appleEvent);
}
}

单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.google.common.eventbus.EventBus;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.2 22:32
* @description: event 如果有父类,此时某个监听器监听的event是这个基类,
* 那么消息是这个子类的时候,该监听父类的监听器方法也会执行
*/
public class MultipleEventExample {

public static void main(String[] args) {
EventBus eventBus = new EventBus();

eventBus.register(new SimpleListener());

eventBus.post(new AppleEvent("apple"));

eventBus.post(new FruitEvent("fruit"));
}
}

日志输出:

1
2
3
SimpleListener.acceptAppleEvent => appleEvent {"name":"apple"}
SimpleListener.acceptFruitEvent => fruitEvent {"name":"apple"}
SimpleListener.acceptFruitEvent => fruitEvent {"name":"fruit"}

异常处理

多个监听器同时监听相同消息,其中一个监听器有异常,那么不会影响其他监听器消费消息。可以在EventBus构造函数中传入一个异常处理器,当监听器有异常抛出时,会执行这个异常处理器。

思考点:如果异常处理很长时间,会不会导致后面的监听器等待执行?

异常处理器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import com.google.common.base.Throwables;
import com.google.common.eventbus.SubscriberExceptionContext;
import com.google.common.eventbus.SubscriberExceptionHandler;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.2 22:54
* @description: 异常处理器
*/
public class MySubscriberExceptionHandler implements SubscriberExceptionHandler {

@Override
public void handleException(Throwable exception, SubscriberExceptionContext context) {
System.out.println("======== handleException start =========");
System.out.println("exception => " + Throwables.getStackTraceAsString(exception));
System.out.println("EventBus => " + context.getEventBus());
System.out.println("Subscriber => " + context.getSubscriber().getClass().getName());
System.out.println("SubscriberMethod => " + context.getSubscriberMethod());
System.out.println("======== handleException end =========");
}
}

监听器对象:

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
import com.google.common.eventbus.Subscribe;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.2 22:49
* @description:
*/
public class MyListener {

@Subscribe
public void method1(Integer sum) {
System.out.println("MyListener.method1 => " + sum);
}

@Subscribe
public void method2(Integer sum) {
System.out.println("MyListener.method2 => " + sum);
}

@Subscribe
public void method3(Integer sum) {
int i = sum / 0;
System.out.println("MyListener.method3 => " + sum);
}

}

单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.google.common.eventbus.EventBus;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.2 22:48
* @description: 多个监听器同时监听相同消息,其中一个监听器有异常,那么不会影响其他监听器消费消息
* 可以在EventBus构造函数中传入一个异常处理器,当监听器有异常抛出时,会执行这个异常处理器
*
* 思考点:如果异常处理很长时间,会不会导致后面的监听器等待执行
*
*/
public class ExceptionListenerExample {

public static void main(String[] args) {

EventBus eventBus = new EventBus(new MySubscriberExceptionHandler());

eventBus.register(new MyListener());

eventBus.post(Integer.MAX_VALUE);
}
}

日志输出:

1
2
3
4
5
6
7
8
9
10
MyListener.method2 => 2147483647
MyListener.method1 => 2147483647
======== handleException start =========
exception => java.lang.ArithmeticException: / by zero
…… // 打印日志栈信息

EventBus => EventBus{default}
Subscriber => org.woodwhales.guava.eventbus.demo4.MyListener
SubscriberMethod => public void org.woodwhales.guava.eventbus.demo4.MyListener.method3(java.lang.Integer)
======== handleException end =========

DeadEvent 实现类型

监听器监听 DeadEvent 类型事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.google.common.eventbus.DeadEvent;
import com.google.common.eventbus.Subscribe;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.2 23:05
* @description:
*/
public class DeadEventListener {

@Subscribe
public void handle(DeadEvent deadEvent) {
System.out.println("DeadEventListener.handle deadEvent => " + deadEvent);
System.out.println("DeadEventListener.handle event => " + deadEvent.getEvent());
System.out.println("DeadEventListener.handle source => " + deadEvent.getSource());
}
}

单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.google.common.eventbus.EventBus;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.2 23:07
* @description: 监听DeadEvent消息类型,那么可以从这个DeadEvent对象中获取事件源和eventBus
*/
public class DeadEventExample {

public static void main(String[] args) {
EventBus eventBus = new EventBus("DEAD-EVENT-BUS");

eventBus.register(new DeadEventListener());

eventBus.post("message");
}
}

日志输出:

1
2
3
DeadEventListener.handle deadEvent => DeadEvent{source=EventBus{DEAD-EVENT-BUS}, event=message}
DeadEventListener.handle event => message
DeadEventListener.handle source => EventBus{DEAD-EVENT-BUS}

自实现 EventBus

Bus 接口设计

自己实现一个 EventBus 前需要先设计好需要多少功能,即多核心少接口:将监听者注册bus,将监听者从bus中移除,发送消息,发送带有主题的消息,获取当前 bus 名称等:

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
/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.4 13:55
* @description: Bus 接口
*/
public interface MyBus {

/**
* 注册到 bus
* @param subscriber 订阅者
*/
void register(Object subscriber);

/**
* 从bus中取消注册
* @param subscriber
*/
void unRegister(Object subscriber);

/**
* 发送消息
* @param event
*/
void post(Object event);

/**
* 发送消息
* @param event
* @param topic
*/
void post(Object event, String topic);


/**
* 关闭bus
*/
void close();

/**
* 获取当前bus名称
* @return
*/
String getBusName();

}

Bus 接口实现

实现 Bus 接口,重要的是怎么来保存注册到中的监听者,怎么在消息发布者调用 post() 发布消息的时候,根据消息的类型找到对应的要监听的方法并执行,如果有异常处理器,则执行异常处理器。

MyEventBus 实现 MyBus 接口,在其类中增加以下属性:MyEventBus 的构造函数仅仅是设置属性的入口,消息存储和消息消费,均由 MyRegistry 和 MyDispatcher 来执行。

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
public class MyEventBus implements MyBus {

private final MyRegistry registry = new MyRegistry();

private String busName;

private static final String DEFAULT_BUS_NAME = "DEFAULT";

private static final String DEFAULT_TOPIC = "default-topic";

private final MyDispatcher dispatcher;

public MyEventBus() {
this(DEFAULT_BUS_NAME, null, MyDispatcher.SEQ_EXECUTOR_SERVICE);
}

public MyEventBus(MyEventExceptionHandler eventExceptionHandler) {
this(DEFAULT_BUS_NAME, eventExceptionHandler, MyDispatcher.SEQ_EXECUTOR_SERVICE);
}

public MyEventBus(String busName, MyEventExceptionHandler eventExceptionHandler, Executor executor) {
if(Objects.isNull(busName) || busName.isEmpty()) {
busName = DEFAULT_BUS_NAME;
}
this.busName = busName;
this.dispatcher = MyDispatcher.newDispatcher(executor, eventExceptionHandler);
}

@Override
public void register(Object subscriber) {
this.registry.bind(subscriber);
}

@Override
public void unRegister(Object subscriber) {
this.unRegister(subscriber);
}

@Override
public void post(Object event) {
this.post(event, DEFAULT_TOPIC);
}

@Override
public void post(Object event, String topic) {
this.dispatcher.dispatch(this, registry, event, topic);
}

@Override
public void close() {
this.dispatcher.close();
}

@Override
public String getBusName() {
return this.busName;
}
}

Dispatcher 实现

Dispatcher 用于执行消息消费,因此对于 Dispatcher,它需要直到要消费的消息信息及异常处理器:

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
134
135
136
137
138
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.4 14:33
* @description: 消息执行器
*/
public class MyDispatcher {

private final Executor executorService;

private final MyEventExceptionHandler eventExceptionHandler;

public static final Executor SEQ_EXECUTOR_SERVICE = SeqExecutorService.INSTANCE;

public static final Executor PER_THREAD_EXECUTOR_SERVICE = PerThreadExecutorService.INSTANCE;

private MyDispatcher(Executor executorService, MyEventExceptionHandler eventExceptionHandler) {
this.executorService = executorService;
this.eventExceptionHandler = eventExceptionHandler;
}

public void dispatch(MyBus bus, MyRegistry registry, Object event, String topic) {
ConcurrentLinkedQueue<MySubscriber> subscribers = registry.scanSubscriber(topic);
if(Objects.isNull(subscribers)) {
if(Objects.nonNull(eventExceptionHandler)) {
eventExceptionHandler.handle(new IllegalArgumentException(String.format("The topic [%s] not bind yet", topic)),
new DefaultMyContext(bus.getBusName(), null, event));
}

return;
}

// 遍历所有的监听者,拿到对应的事件参数对象,比对消息的事件类型和监听者参数事件类型是否一致
subscribers.stream().filter(subscriber -> !subscriber.isDisabled())
.filter(subscriber -> {
Method subscribeMethod = subscriber.getSubscribeMethod();
Class<?> parameterTypeClass = subscribeMethod.getParameterTypes()[0];
return parameterTypeClass.isAssignableFrom(event.getClass());
})
.forEach(subscriber -> invokeRealSubscribe(bus, subscriber, event));
}

private void invokeRealSubscribe(MyBus bus, MySubscriber subscriber, Object event) {

Method subscribeMethod = subscriber.getSubscribeMethod();
Object subscribeObject = subscriber.getSubscribeObject();

this.executorService.execute(() -> {
try {
subscribeMethod.invoke(subscribeObject, event);
} catch (Exception e) {
if(Objects.nonNull(this.eventExceptionHandler)) {
this.eventExceptionHandler.handle(e, new DefaultMyContext(bus.getBusName(), subscriber, event));
}
}
});
}


public static MyDispatcher newDispatcher(Executor executorService, MyEventExceptionHandler eventExceptionHandler) {
return new MyDispatcher(executorService, eventExceptionHandler);
}

public static MyDispatcher newSeqDispatcher(MyEventExceptionHandler eventExceptionHandler) {
return new MyDispatcher(SEQ_EXECUTOR_SERVICE, eventExceptionHandler);
}

public static MyDispatcher newPerThreadDispatcher(MyEventExceptionHandler eventExceptionHandler) {
return new MyDispatcher(PER_THREAD_EXECUTOR_SERVICE, eventExceptionHandler);
}

public void close() {
if(executorService instanceof ExecutorService) {
((ExecutorService) executorService).shutdown();
}
}

private static class SeqExecutorService implements Executor {

private static final SeqExecutorService INSTANCE = new SeqExecutorService();

@Override
public void execute(Runnable command) {
command.run();
}
}

private static class PerThreadExecutorService implements Executor {

private static final PerThreadExecutorService INSTANCE = new PerThreadExecutorService();

@Override
public void execute(Runnable command) {
new Thread(command).start();
}
}

public static class DefaultMyContext implements MyEventContext {

private final String busName;

private final MySubscriber subscriber;

private final Object event;

public DefaultMyContext(String busName, MySubscriber subscriber, Object event) {
this.busName = busName;
this.subscriber = subscriber;
this.event = event;
}

@Override
public String getSource() {
return this.busName;
}

@Override
public Object getSubscriberObject() {
return Objects.nonNull(subscriber) ? subscriber.getSubscribeObject() : null;
}

@Override
public Method getSubscribeMethod() {
return Objects.nonNull(subscriber) ? subscriber.getSubscribeMethod() : null;
}

@Override
public Object getEvent() {
return this.event;
}
}
}

MyRegistry 实现

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
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.4 14:04
* @description: 已订阅bus的订阅者容器对象
*/
public class MyRegistry {

private final ConcurrentHashMap<String, ConcurrentLinkedQueue<MySubscriber>> subscriberContainer = new ConcurrentHashMap<> ();

/**
* 绑定
* @param subscriber
*/
public void bind(Object subscriber) {
List<Method> subscriberMethods = getSubscriberMethods(subscriber);
subscriberMethods.forEach(method -> tireSubscriber(subscriber, method));
}

private void tireSubscriber(Object subscriber, Method method) {
MySubscribe mySubscribe = method.getDeclaredAnnotation(MySubscribe.class);
String topic = mySubscribe.topic();
subscriberContainer.computeIfAbsent(topic, key -> new ConcurrentLinkedQueue<>());
subscriberContainer.get(topic).add(new MySubscriber(subscriber, method));
}

private List<Method> getSubscriberMethods(Object subscriber) {
List<Method> methods = new ArrayList<>();

Class<?> subscriberClass = subscriber.getClass();
while (Objects.nonNull(subscriberClass)) {
Method[] declaredMethods = subscriberClass.getDeclaredMethods();

Arrays.stream(declaredMethods).filter(method ->
method.isAnnotationPresent(MySubscribe.class) &&
method.getParameterCount() == 1 &&
method.getModifiers() == Modifier.PUBLIC
).forEach(methods::add);

// 获取当前 subscriber 的父类
subscriberClass = subscriberClass.getSuperclass();
}

return methods;
}

/**
* 解绑
* @param subscriber
*/
public void unBind(Object subscriber) {
subscriberContainer.forEach((key, queue) -> {
queue.forEach(subscriberItem -> {
if(subscriberItem.getSubscribeObject() == subscriber) {
subscriberItem.setDisabled(false);
}
});
});
}

/**
* 根据topic获取已经绑定的监听对象
* @param topic
* @return
*/
public ConcurrentLinkedQueue<MySubscriber> scanSubscriber(final String topic) {
return subscriberContainer.get(topic);
}

}

EventExceptionHandler 异常处理器

当有异常发生时,用户可以自己实现异常处理器接口,当有异常发生时会回调该接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.4 14:14
* @description: 异常处理器
*/
public interface MyEventExceptionHandler {

/**
* 对 event 异常处理
* @param cause
* @param eventContext
*/
void handle(Throwable cause, MyEventContext eventContext);

}

消息上下文对象

异常产生,需要告知消费者,产生异常的相关上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.lang.reflect.Method;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.4 14:28
* @description:
*/
public interface MyEventContext {

String getSource();

Object getSubscriberObject();

Method getSubscribeMethod();

Object getEvent();

}

Subscribe 监听器注解

监听器注解用于标记要监听并执行的方法:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.4 15:00
* @description: 自定义监听器订阅方法注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MySubscribe {

String topic() default "default-topic";
}

监听者信息对象

为了便于管理监听者对象,需要对监听者的信息进行封装,并存储到 MyRegistry 的 ConcurrentHashMap<> 容器中:

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
import java.lang.reflect.Method;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.4 15:05
* @description: 监听者信息对象
*/
public class MySubscriber {

private final Object subscribeObject;

private final Method subscribeMethod;

private boolean disabled = false;

public MySubscriber(Object subscribeObject, Method subscribeMethod) {
this.subscribeObject = subscribeObject;
this.subscribeMethod = subscribeMethod;
}

public Object getSubscribeObject() {
return subscribeObject;
}

public Method getSubscribeMethod() {
return subscribeMethod;
}

public boolean isDisabled() {
return disabled;
}

public void setDisabled(boolean disabled) {
this.disabled = disabled;
}
}

小结

自实现一个简单的 EventBus,要涉及到以下核心组件:

核心接口

MyBus

通过构造器接收监听者(使用 @MySubscribe 注解了某些方法)和自定义异常处理器(自实现了 MyEventExceptionHandler 接口)

MyEventExceptionHandler

用于消息过程出现异常时,处理异常

MyEventContext

异常处理过程中,将产生异常的数据封装起来

核心类

MyEventBus

MyBus 接口的实现类,其内部维护了 MyDispatcher 和 MyRegistry。

MyDispatcher

用于真正指定消费逻辑,内部维护了 MyEventExceptionHandler 实现(从 MyEventBus 构造器传入的)。

MyRegistry

用于对所有加了 @MySubscribe 注解的监听者信息进行管理,包括注册和注销。

MySubscriber

对注册的监听者信息进行封装,方便 MyRegistry 存储,也方便 MyDispatcher 消费执行。

核心注解

MySubscribe

提供要监听消息的方法注解标记。

Java 的四种引用

在Java中除了基础的数据类型以外,其它的都为引用类型。从JDK1.2版本开始,把对象的引用分为四种级别,从而使程序更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。 (虚引用又称为幻影引用)

一般情况下平时开发中基本上只用到强引用类型,而其他的引用类型较少用,但是它们依旧很重要,作为 Javaer 必须掌握清楚。

强引用

我们平日里面的用到的new了一个对象就是强引用,例如:

1
Object obj = new Object();

上述Object这类对象就具有强引用,属于不可回收的资源,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠回收具有强引用的对象,来解决内存不足的问题。

值得注意的是:如果想中断或者回收强引用对象,可以显式地将引用赋值为null,这样的话 JVM 就会在合适的时间,进行垃圾回收。

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
/**
* 测试强引用
*
* 测试时,注意设置JVM参数:-Xms128m -Xmx128m -XX:+PrintGCDetails
*
* 因为不断的创建 MyReference 对象,而这些对象是强引用,一直在被 list 维护着,
* 所以在程序运行期间,就算发生垃圾回收,也不会将list维护的强引用对象回收,
* 因此堆内存会不断被占用,直到内存溢出。
*
* @throws Exception
*/
private static void testStrongReferences() throws Exception {
List<MyReference> list = new ArrayList<>();

int index = 1;

while (true) {
int currentIndex = index++;
list.add(new MyReference(currentIndex));
System.out.println("the index = " + index + " MyReference inserted to list");
TimeUnit.MILLISECONDS.sleep(500);
}
}

private static class MyReference {

private final int index;

private int[] data = new int[1024 * 1024];

private MyReference(int index) {
this.index = index;
}

/**
* Java回收该类的一个对象时,就会调用这个已经被重写的finalize()方法。
* 标记当前对象在下一次GC的时候会被垃圾回收,如果不想被回收,那么可以重写这个方法,拯救这个对象和 root 强关联。
* @throws Throwable
*/
@Override
protected void finalize() throws Throwable {
System.out.println("index = [ "+ index +"] will be GC");
}
}

软引用(SoftReference)

如果一个对象只具有软引用,那么它的性质属于可有可无的那种。如果此时内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

1
2
3
4
5
Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
SoftReference reference = new SoftReference(obj, queue);
//强引用对象滞空,保留软引用
obj = null;

当内存不足时,软引用对象被回收时,reference.get()为null,此时软引用对象的作用已经发挥完毕,这时将其添加进ReferenceQueue 队列中

如果要判断哪些软引用对象已经被清理:

1
2
3
4
SoftReference ref = null;
while ((ref = (SoftReference) queue.poll()) != null) {
//清除软引用对象
}

单元测试:

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
/**
* 软引用测试
*
* 测试时,注意设置JVM参数:-Xms128m -Xmx128m -XX:+PrintGCDetails
*
* 当内存快要耗尽时,GC 会回收掉软引用。
*
* 注意:由于内存占用太快,导致当内存快要耗尽时,GC还没来得及回收掉软引用时,也会出现OOM异常。
*
* @throws Exception
*/
public static void testSoftReferences() throws Exception {
List<SoftReference<MyReference>> list = new ArrayList<>();

int index = 1;

while (true) {
int currentIndex = index++;
list.add(new SoftReference<>(new MyReference(currentIndex)));
System.out.println("the index = " + index + " MyReference inserted to list");
//TimeUnit.MILLISECONDS.sleep(500);
// 当设置吃内存的速度慢一点的时候,即软引用在GC时来得及回收,那么内存会一直有新的空间可以使用,OOM 的情况能得到缓解
TimeUnit.SECONDS.sleep(1);
}
}

弱引用(WeakReference)

如果一个对象具有弱引用,那其的性质也是可有可无的状态。

而弱引用和软引用的区别在于:弱引用的对象拥有更短的生命周期,只要垃圾回收器扫描到它,不管内存空间充足与否,都会回收它的内存。

同样的弱引用也可以和引用队列一起使用。

1
2
3
4
5
Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
WeakReference reference = new WeakReference(obj, queue);
//强引用对象滞空,保留软引用
obj = null;

单元测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 虚引用测试
* 测试时,注意设置JVM参数:-Xms128m -Xmx128m -XX:+PrintGCDetails
*
* 虚引用比软引用更脆弱,只要有GC时,虚引用就会被回收
*
* @throws Exception
*/
public static void testWeakReferences() throws Exception {
List<WeakReference<MyReference>> list = new ArrayList<>();

int index = 1;

while (true) {
int currentIndex = index++;
list.add(new WeakReference<>(new MyReference(currentIndex)));
System.out.println("the index = " + index + " MyReference inserted to list");
TimeUnit.MILLISECONDS.sleep(500);
}
}

虚引用(PhantomReference)

虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

注意:虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。

程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

1
2
3
4
5
Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
PhantomReference reference = new PhantomReference(obj, queue);
//强引用对象滞空,保留软引用
obj = null;

单元测试:

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
/**
* 幻影引用
* 测试时,注意设置JVM参数:-Xms128m -Xmx128m -XX:+PrintGCDetails
*
* 幻影引用必须配合 ReferenceQueue 使用,
* 因为使用 PhantomReference 对象本身是拿不到该具体实例,当这个幻影引用被回收时,就会放到 ReferenceQueue 队列中
*
* 最佳实践:
* @See org.apache.commons.io.FileCleaningTracker
* org.apache.commons.io.FileCleaningTracker.Tracker
*/
private static void testPhantomReferences() throws Exception {
int index = 10;
MyReference reference = new MyReference(index);
ReferenceQueue referenceQueue = new ReferenceQueue<>();
MyPhantomReference phantomReferences = new MyPhantomReference(reference, referenceQueue, index);

reference = null;
System.gc();

// 等待真正GC一下, 保证虚引用被回收
TimeUnit.SECONDS.sleep(2);
// phantomReferences.get() 拿不到对象
System.out.println(phantomReferences.get());
// 虚引用被回收会进入 ReferenceQueue 队列中
Reference object = referenceQueue.remove();
((MyPhantomReference)object).doAction();
}

private static class MyPhantomReference extends PhantomReference<Object> {

private final int index;

public MyPhantomReference(Object referent, ReferenceQueue<? super Object> queue, int index) {
super(referent, queue);
this.index = index;
}

public void doAction() {
System.out.println("index = [ "+ index +"] will be GC");
}
}

总结

  1. 对于强引用,平时在编写代码时会经常使用。

  2. 而其他三种类型的引用,使用得最多就是软引用和弱引用,这两种既有相似之处又有区别,他们都来描述非必须对象。

  3. 被软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收。

LRU算法的Java实现(两种实现+增强实现)

LRU 算法

LRU 全称是 Least Recently Used,即最近最久未使用的意思。

LRU算法的设计原则是:如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

LRU 接口设计

有了上面的设计原则,就可以得出设计一个带有 LRU 功能的缓存,应该具备以下功能:

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
public interface LruCache<K, V> {

/**
* 插入数据
* 当容器满的时候,将最久访问的数据从容器中移除,
* 并将新加入的数据放入容器中
* @param key
* @param value
*/
void put(K key, V value);

/**
* 获取数据
* @param key
* @return
*/
V get(K key);

/**
* 将指定元素从容器中移除
* @param key
*/
void remove(K key);

/**
* 清空缓存
*/
void clear();

/**
* 缓存当前已存有效数据的容量大小
* @return
*/
int size();

/**
* 当前缓存容器的最大可缓存容量
* @return
*/
int limit();
}

说明:在实现 LRU 算法容器过程中,对参数校验笔者使用到了 guava 工具包:https://mvnrepository.com/artifact/com.google.guava/guava

1
2
3
4
5
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>

实现1:利用 LinkedHashMap 实现

在 Java 中,LinkedHashMap 天然实现了 LRU 算法,因此我们可以设计一个 Cache 对象,内部自己维护一个 LinkedHashMap 容器,重写掉 removeEldestEntry() 方法即可。

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
import com.google.common.base.Preconditions;

import java.util.LinkedHashMap;
import java.util.Map;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.4 21:02
* @description: 使用LinkedHashMap实现带LRU的缓存
* 本类不是一个线程安全的
*/
public class LinkedHashMapLruCache<K, V> implements LruCache<K, V> {

private final int limit;

private final InternalLruLinkedHashMapCache<K, V> cache;

@Override
public void put(K key, V value) {
this.cache.put(key, value);
}

@Override
public V get(K key) {
return this.cache.get(key);
}

@Override
public void remove(K key) {
this.cache.remove(key);
}

@Override
public void clear() {
this.cache.clear();
}

@Override
public int size() {
return this.cache.size();
}

@Override
public int limit() {
return this.limit;
}

@Override
public String toString() {
return this.cache.toString();
}

private static class InternalLruLinkedHashMapCache<K, V> extends LinkedHashMap<K, V> {
private int limit;

public InternalLruLinkedHashMapCache(int limit) {
super(limit, 0.75f, true);
this.limit = limit;
}

@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return this.size() > this.limit;
}
}

public LinkedHashMapLruCache(int limit) {
Preconditions.checkArgument(limit > 0, "this limit must big than zero.");
this.limit = limit;
this.cache = new InternalLruLinkedHashMapCache<>(limit);
}

}

为什么 LinkedHashMapLruCache 不直接继承 LinkedHashMap,而是在内部维护一个静态内部类呢?因为保证接口实现的“纯洁性”,LinkedHashMapLruCache 的使用者随便继承使用或直接使用,也只能使用上述定义的缓存接口中的方法,而不会使用到 LinkedHashMap 的方法。

上述这种实现很简单,完全依靠 LinkedHashMap 来维护缓存数据,重点在于内部类 InternalLruLinkedHashMapCache 一定要重写 removeEldestEntry() 方法,当达到容器容量满时,触发 LinkedHashMap 对最久访问数据的移除。

单元测试:

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 org.woodwhales.guava.lru.LruCache;
import org.woodwhales.guava.lru.LinkedHashMapLruCache;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.4 21:43
* @description: 使用LinkedHashMap实现带LRU的缓存测试
*/
public class LinkedHashMapLruCacheExample {

public static void main(String[] args) {
LruCache<String, Integer> cache = new LinkedHashMapLruCache<>(3);

cache.put("1", 1);
cache.put("2", 2);
cache.put("3", 3);

System.out.println(cache);

cache.put("4", 4);

System.out.println(cache);

// 由于使用了一次 2,因此原本 2 是最老的,现在是 3 是最老的
System.out.println(cache.get("2"));

System.out.println(cache);
}
}

日志输出:

1
2
3
4
{1=1, 2=2, 3=3}
{2=2, 3=3, 4=4}
2
{3=3, 4=4, 2=2}

日志输出结果符合预期。

实现2:利用 LinkedList + HashMap 实现

上面一种实现完全利用 Java 自带的 LinkedHashMap 容器实现,如果不允许使用 LinkedHashMap,则需要自己实现 LinkedHashMap 类似的功能:LinkedList + HashMap

使用 LinkedList 维护着缓存中元素的 key,保证key的顺序就可以真正的数据存放在 HashMap 中。

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
import com.google.common.base.Preconditions;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.stream.Collectors;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.4 21:59
* @description: 使用LinkedList+HashMap实现带LRU的缓存
*/
public class LinkedListLruCache<K, V> implements LruCache<K, V> {

private final int limit;

private final LinkedList<K> keys;

private final HashMap<K, V> cache;

public LinkedListLruCache(int limit) {
Preconditions.checkArgument(limit > 0, "the limit big than zero.");
this.limit = limit;
this.keys = new LinkedList<K> ();
this.cache = new HashMap<>(limit);
}

@Override
public void put(K key, V value) {
Preconditions.checkNotNull(key, "this key must nut null");
Preconditions.checkNotNull(value, "this value must nut null");
if(cache.size() >= limit) {
K firstKey = keys.getFirst();
keys.remove(firstKey);
cache.remove(firstKey);
}

keys.addLast(key);
cache.put(key, value);
}

@Override
public V get(K key) {

boolean exist = keys.remove(key);
if(!exist) {
return null;
}

keys.addLast(key);
return cache.get(key);
}

@Override
public void remove(K key) {
boolean exist = keys.remove(key);
if(exist) {
cache.remove(key);
}
}

@Override
public void clear() {
keys.clear();
cache.clear();
}

@Override
public int size() {
return keys.size();
}

@Override
public int limit() {
return this.limit;
}

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("{");
builder.append(keys.stream().map(key -> key + "=" + cache.get(key)).collect(Collectors.joining(",")));
builder.append("}");
return builder.toString();
}
}

单元测试:

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 org.woodwhales.guava.lru.LinkedListLruCache;
import org.woodwhales.guava.lru.LruCache;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.4 21:43
* @description: 使用LinkedList+HashMap实现带LRU的缓存测试
*/
public class LinkedListLruCacheExample {

public static void main(String[] args) {
LruCache<String, Integer> cache = new LinkedListLruCache<>(3);

cache.put("1", 1);
cache.put("2", 2);
cache.put("3", 3);

System.out.println(cache);

cache.put("4", 4);

System.out.println(cache);

// 由于使用了一次 2,因此原本 2 是最老的,现在是 3 是最老的
System.out.println(cache.get("2"));

System.out.println(cache);
}
}

日志输出:

1
2
3
4
{1=1,2=2,3=3}
{2=2,3=3,4=4}
2
{3=3,4=4,2=2}

实现3:软引用缓存(增强版缓存)

上述两种实现的容器,缓存中的对象都是强引用,可能会存在一种极端情况:当开发者在内存中使用了大量的缓存,而这些缓存中的内容一旦“满员”,那么这个缓存容器就会维护着大量的强引用对象,这些对象会一直得不到垃圾回收。从而导致堆内存溢出(OOM)。

Java 中的引用有四种:强引用、软引用、弱引用、幻影(虚)引用。

软引用会在内存快要 OOM 的时候被 GC 回收。因此我们可以利用这个特性,对缓存中的数据做软引用,而不是使用强引用。

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
import com.google.common.base.Preconditions;

import java.lang.ref.SoftReference;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.5 00:45
* @description: 使用软引用对缓存进行增强
*/
public class LinkedHashMapSoftReferencesLruCache<K, V> implements LruCache<K, V> {

private final int limit;

private final InternalLruLinkedHashMapCache<K, V> cache;

private class InternalLruLinkedHashMapCache<K, V> extends LinkedHashMap<K, SoftReference<V>> {
private int limit;

public InternalLruLinkedHashMapCache(int limit) {
super(limit, 0.75f, true);
this.limit = limit;
}

@Override
protected boolean removeEldestEntry(Map.Entry<K, SoftReference<V>> eldest) {
return this.size() > this.limit;
}
}

public LinkedHashMapSoftReferencesLruCache(int limit) {
Preconditions.checkArgument(limit > 0, "this limit must big than zero.");
this.limit = limit;
this.cache = new InternalLruLinkedHashMapCache<>(limit);
}

@Override
public void put(K key, V value) {
this.cache.put(key, new SoftReference<>(value));
}

@Override
public V get(K key) {
SoftReference<V> softReference = this.cache.get(key);
if(Objects.isNull(softReference)) {
return null;
}
return softReference.get();
}

@Override
public void remove(K key) {
this.cache.remove(key);
}

@Override
public void clear() {
this.cache.clear();
}

@Override
public int size() {
return this.cache.size();
}

@Override
public int limit() {
return this.limit;
}
}

上述实现是在实现1的基础上做了一点点变动,即 LinkedHashMap 中存储的数据对象是 SoftReference

单元测试:

测试时,注意设置JVM参数:-Xms128m -Xmx128m -XX:+PrintGCDetails

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
import org.woodwhales.guava.lru.LinkedHashMapLruCache;
import org.woodwhales.guava.lru.LinkedHashMapSoftReferencesLruCache;
import org.woodwhales.guava.lru.LruCache;

import java.util.concurrent.TimeUnit;

/**
* @projectName: guava
* @author: woodwhales
* @date: 20.7.5 00:58
* @description: 使用软引用对缓存进行增强测试
* 测试时,注意设置JVM参数:-Xms128m -Xmx128m -XX:+PrintGCDetails
*/
public class LinkedHashMapSoftReferencesLruCacheTest {

public static void main(String[] args) throws Exception {
testSoftReferencesCache();
//testStrongReferenceCache();
}

/**
* 当使用了带软引用的缓存的时候,相比强引用缓存,在不频繁写入缓存情况下,很难出现OOM
* @throws Exception
*/
private static void testSoftReferencesCache() throws Exception {
LruCache<String, byte[]> cache = new LinkedHashMapSoftReferencesLruCache(100);

for (int i = 1; i <= 100; i++) {
cache.put(String.valueOf(i), new byte[1024 * 1024 * 2]);
System.out.println("i = " + i + " was cached");
TimeUnit.MILLISECONDS.sleep(500);
}
}

/**
* 设置了最大堆内存是 128M
* 每次创建对象并缓存,则增加 2M 内存,因此在大约缓存第60次的时候,就会出现OOM
* @throws Exception
*/
private static void testStrongReferenceCache() throws Exception {
LruCache<String, byte[]> cache = new LinkedHashMapLruCache<>(100);

for (int i = 1; i <= 100; i++) {
cache.put(String.valueOf(i), new byte[1024 * 1024 * 2]);
System.out.println("i = " + i + " was cached");
TimeUnit.MILLISECONDS.sleep(500);
}
}
}

上述单元测试中,由于设置了堆内存最大上限,因此在使用强引用的缓存时,堆内存会被缓存不断地占满,最终导致堆内存全部被占满,即每 500 毫秒创建 2MB 的内存在堆中,堆的最大容量是 128 MB,因此当循环到达 60 次左右的会出现 OOM。而软引用缓存中,当堆内存不够的时候,创建新的对象到堆的频率不够快,GC 来得及回收掉部分软引用对象,那么循环会一直进行下去,不会出现 OOM。

Docker 安装与入门

Docker 是一个开源的商业产品,有两个版本:社区版(Community Edition,缩写为 CE)和企业版(Enterprise Edition,缩写为 EE)。企业版包含了一些收费服务,个人开发者一般用不到。

安装 Docker-CE(centos 7)

Docker CE 安装官方文档参考:

注意:docker 要求 centos 系统必须是 centos 7 版本以上,windows 系统必须是 win10 专业版、企业版或教育版。

卸载旧的 docker

如果系统中存在老版本的 docker ,可以使用下面命令卸载旧的 docker:

1
2
3
4
5
6
7
8
$ sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine

安装 docker

yum 阿里云镜像设置:https://developer.aliyun.com/mirror/centos

步骤1:安装 docker 软件依赖的工具

1
yum install -y yum-utils device-mapper-persistent-data lvm2

步骤2:设置 docker 安装源,使用阿里云镜像安装源:

1
yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

步骤3:设置 yum 使用最快的安装源

1
yum makecache fast

步骤4:执行下面命令,安装 docker-ce

1
yum install -y docker-ce

安装成功之后,控制台提示类似如下信息:

docker 服务相关命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
systemctl start docker.service	 			# 启动docker服务
systemctl stop docker.service # 停止docker服务
systemctl restart docker.service # 重启docker服务

systemctl status docker.service # 查看docker服务运行状态

systemctl enable docker.service # 设置开机自动启动docker服务
systemctl disable docker.service # 设置禁止开机自动启动docker服务

systemctl is-enabled docker.service # 查看docker服务是否自启动
systemctl list-dependencies docker.service # 列出docker服务层级和依赖关系

# 其他命令
systemctl list-units --type=service # 列出系统所有服务的启动情况
systemctl list-unit-files|grep enabled # 列出所有自启动服务

设置阿里云容器镜像代理

访问:https://cr.console.aliyun.com/

注意的是,上述地址需要注册阿里云账号,并开通:容器镜像服务。

按照阿里云文档选择对应的操作系统脚本执行,即可设置阿里云镜像加速服务:

拉取 docker 镜像并运行:

1
2
docker pull hello-world
docker run hello-world

运行效果界面如下:

Docker 的基本概念

Docker 中包括三个基本的概念:

  • Image(镜像)
  • Container(容器)
  • Repository(仓库)

镜像是 Docker 运行容器的前提,仓库是存放镜像的场所,可见镜像更是 Docker 的核心。

Image (镜像)

docker 官网镜像地址:https://hub.docker.com/

Docker 镜像可以看作是一个特殊的文件系统,除了提供容器运行时所需的程序、库、资源、配置等文件外,还包含了一些为运行时准备的一些配置参数(如匿名卷、环境变量、用户等)。镜像不包含任何动态数据,其内容在构建之后也不会被改变。

镜像(Image)就是一堆只读层(read-only layer)的统一视角,也许这个定义有些难以理解,下面的这张图能够帮助读者理解镜像的定义。

从左边我们看到了多个只读层,它们重叠在一起。除了最下面一层,其它层都会有一个指针指向下一层。这些层是Docker 内部的实现细节,并且能够在主机的文件系统上访问到。统一文件系统 (union file system) 技术能够将不同的层整合成一个文件系统,为这些层提供了一个统一的视角,这样就隐藏了多层的存在,在用户的角度看来,只存在一个文件系统。我们可以在图片的右边看到这个视角的形式。

Container (容器)

容器 (container) 的定义和镜像 (image) 几乎一模一样,也是一堆层的统一视角,唯一区别在于容器的最上面那一层是可读可写的。

由于容器的定义并没有提及是否要运行容器,所以实际上,容器 = 镜像 + 读写层。

Repository (仓库)

Docker 仓库是集中存放镜像文件的场所。镜像构建完成后,可以很容易的在当前宿主上运行,但是, 如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry (仓库注册服务器)就是这样的服务。有时候会把仓库 (Repository) 和仓库注册服务器 (Registry) 混为一谈,并不严格区分。Docker 仓库的概念跟 Git 类似,注册服务器可以理解为 GitHub 这样的托管服务。实际上,一个 Docker Registry 中可以包含多个仓库 (Repository) ,每个仓库可以包含多个标签 (Tag),每个标签对应着一个镜像。所以说,镜像仓库是 Docker 用来集中存放镜像文件的地方类似于我们之前常用的代码仓库。

通常,一个仓库会包含同一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本 。我们可以通过<仓库名>:<标签>的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签.。

仓库又可以分为两种形式:

  • public(公有仓库)
  • private(私有仓库)

Docker Registry 公有仓库是开放给用户使用、允许用户管理镜像的 Registry 服务。一般这类公开服务允许用户免费上传、下载公开的镜像,并可能提供收费服务供用户管理私有镜像。

除了使用公开服务外,用户还可以在本地搭建私有 Docker RegistryDocker 官方提供了 Docker Registry 镜像,可以直接使用做为私有 Registry 服务。当用户创建了自己的镜像之后就可以使用 push 命令将它上传到公有或者私有仓库,这样下次在另外一台机器上使用这个镜像时候,只需要从仓库上 pull 下来就可以了。

我们主要把 Docker 的一些常见概念如 ImageContainerRepository 做了详细的阐述,也从传统虚拟化方式的角度阐述了 docker 的优势,我们从下图可以直观地看到 Docker 的架构:

Docker 使用 C/S 结构,即客户端/服务器体系结构。 Docker 客户端与 Docker 服务器进行交互,Docker服务端负责构建、运行和分发 Docker 镜像。 Docker 客户端和服务端可以运行在一台机器上,也可以通过 RESTfulstock 或网络接口与远程 Docker 服务端进行通信。

这张图展示了 Docker 客户端、服务端和 Docker 仓库(即 Docker HubDocker Cloud ),默认情况下Docker 会在 Docker 中央仓库寻找镜像文件,这种利用仓库管理镜像的设计理念类似于 Git ,当然这个仓库是可以通过修改配置来指定的,甚至我们可以创建我们自己的私有仓库。

常用命令

Docker容器信息

1
2
3
docker version	# 查看docker容器版本
docker info # 查看docker容器信息
docker --help # 查看docker容器帮助

镜像操作

提示:对于镜像的操作可使用镜像名、镜像长ID和短ID。

镜像查看

1
2
3
4
5
6
7
docker images				# 列出本地images
docker images -a # 含中间映像层
docker images -q # 只显示镜像ID
docker images -qa # 含中间映像层
docker images --digests # 显示镜像摘要信息(DIGEST列)
docker images --no-trunc # 显示镜像完整信息
docker history -H redis # 显示指定镜像的历史创建;参数:-H 镜像大小和日期,默认为true;--no-trunc 显示完整的提交记录;-q 仅列出提交记录ID

镜像搜索

1
2
3
4
docker search mysql						# 搜索仓库MySQL镜像
docker search --filter=stars=600 mysql # --filter=stars=600:只显示 starts>=600 的镜像
docker search --no-trunc mysql # --no-trunc 显示镜像完整 DESCRIPTION 描述
docker search --automated mysql # --automated :只列出 AUTOMATED=OK 的镜像

镜像下载

1
2
3
docker pull redis			# 下载Redis官方最新镜像,相当于:docker pull redis:latest
docker pull -a redis # 下载仓库所有Redis镜像
docker pull bitnami/redis # 下载私人仓库镜像

镜像删除

1
2
3
4
docker rmi redis					# 单个镜像删除,相当于:docker rmi redis:latest
docker rmi -f redis # 强制删除(针对基于镜像有运行的容器进程)
docker rmi -f redis tomcat nginx # 多个镜像删除,不同镜像间以空格间隔
docker rmi -f $(docker images -q) # 删除本地全部镜像

镜像操作

提示:对于容器的操作可使用CONTAINER ID 或 NAMES。

容器启动

1
2
3
4
docker run -i -t --name mycentos	# 新建并启动容器,参数:-i  以交互模式运行容器;-t  为容器重新分配一个伪输入终端;--name  为容器指定一个名称
docker run -d mycentos # 后台启动容器,参数:-d 已守护方式启动容器
docker start redis # 启动一个或多个已经被停止的容器
docker restart redis # 重启容器

查看容器

1
2
3
4
5
6
7
8
9
docker ps				# 查看正在运行的容器
docker ps -q # 查看正在运行的容器的ID
docker ps -a # 查看正在运行+历史运行过的容器
docker ps -s # 显示运行容器总文件大小
docker ps -l # 显示最近创建容器
docker ps -n 3 # 显示最近创建的3个容器
docker ps --no-trunc # 不截断输出
docker inspect redis # 获取镜像redis的元信息
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' redis # 获取正在运行的容器redis的 IP

容器的停止与删除

1
2
3
4
5
6
7
8
docker stop redis					# 停止一个运行中的容器
docker kill redis # 杀掉一个运行中的容器
docker rm redis # 删除一个已停止的容器
docker rm -f redis # 删除一个运行中的容器
docker rm -f $(docker ps -a -q) # 删除多个容器
docker ps -a -q | xargs docker rm # 删除多个容器
docker rm -l db # -l 移除容器间的网络连接,连接名为 db
docker rm -v redis # -v 删除容器,并删除容器挂载的数据卷

# ##查看docker容器信息 docker info ##查看docker容器帮助 docker –help

参考文档

10分钟快速掌握Docker必备基础知识

Docker 入门教程

Docker — 从入门到实践

这可能是最为详细的Docker入门吐血总结