JoyLau's Blog

JoyLau 的技术学习与思考

区别

个人理解:
${} : 用于加载外部文件中指定key的值
#{} : 功能更强大的SpEl表达式,将内容赋值给属性
#{…}${…} 可以混合使用,但是必须#{}外面,${}在里面,#{ ‘${}’ } ,注意单引号,注意不能反过来

#{} 功能

  1. 直接量表达式: “#{‘Hello World’}”
  2. 使用java代码new/instance of: 此方法只能是java.lang 下的类才可以省略包名 #{“new Spring(‘Hello World’)”}
  3. 使用T(Type): 使用“T(Type)”来表示java.lang.Class实例,同样,只有java.lang 下的类才可以省略包名。此方法一般用来引用常量或静态方法 ,#{“T(Integer).MAX_VALUE”}
  4. 变量: 使用“#bean_id”来获取,#{“beanId.field”}
  5. 方法调用: #{“#abc.substring(0,1)”}
  6. 运算符表达式: 算数表达式,比较表达式,逻辑表达式,赋值表达式,三目表达式,正则表达式
  7. 判断空: #{“name?:’other’”}

实例

springboot 和 elasticsearch 的整合包里有一个注解
@Document(indexName = “”, type = “”)
indexName 和 type 都是字符串
这个注解写在实体类上,代表该实体类是一个索引
现在, indexName 和 type 不能为固定写死,需要从配置文件读取,
于是想到了 spring 的 el 表达式
使用
@Document(indexName = “${xxxx}”, type = “${xxxx}”)
启动后
无效,spring 直接将其解析成了字符串
于是,查看 @Document 这个注解实现的源码
在这个包中 org.springframework.data.elasticsearch.core.mapping 找到了实现类 SimpleElasticsearchPersistentEntity
其中

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
public SimpleElasticsearchPersistentEntity(TypeInformation<T> typeInformation) {
super(typeInformation);
this.context = new StandardEvaluationContext();
this.parser = new SpelExpressionParser();

Class<T> clazz = typeInformation.getType();
if (clazz.isAnnotationPresent(Document.class)) {
Document document = clazz.getAnnotation(Document.class);
Assert.hasText(document.indexName(),
" Unknown indexName. Make sure the indexName is defined. e.g @Document(indexName=\"foo\")");
this.indexName = document.indexName();
this.indexType = hasText(document.type()) ? document.type() : clazz.getSimpleName().toLowerCase(Locale.ENGLISH);
this.useServerConfiguration = document.useServerConfiguration();
this.shards = document.shards();
this.replicas = document.replicas();
this.refreshInterval = document.refreshInterval();
this.indexStoreType = document.indexStoreType();
this.createIndexAndMapping = document.createIndex();
}
if (clazz.isAnnotationPresent(Setting.class)) {
this.settingPath = typeInformation.getType().getAnnotation(Setting.class).settingPath();
}
}

@Override
public String getIndexName() {
Expression expression = parser.parseExpression(indexName, ParserContext.TEMPLATE_EXPRESSION);
return expression.getValue(context, String.class);
}

@Override
public String getIndexType() {
Expression expression = parser.parseExpression(indexType, ParserContext.TEMPLATE_EXPRESSION);
return expression.getValue(context, String.class);
}

我们看到了 SpelExpressionParserParserContext.TEMPLATE_EXPRESSION
那么这里就很肯定 indexName 和 type 是支持 spel 的写法了,只是怎么写,暂时不知道
再看
ParserContext.TEMPLATE_EXPRESSION 的源码是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* The default ParserContext implementation that enables template expression
* parsing mode. The expression prefix is "#{" and the expression suffix is "}".
* @see #isTemplate()
*/
ParserContext TEMPLATE_EXPRESSION = new ParserContext() {

@Override
public boolean isTemplate() {
return true;
}

@Override
public String getExpressionPrefix() {
return "#{";
}

@Override
public String getExpressionSuffix() {
return "}";
}
};

看到上面的注释,知道是使用 #{}
接着
新建一个类,使用 @Configuration 和 @ConfigurationProperties(prefix = “xxx”) 注册一个 bean
再在实体类上加上注解 @Component 也注册一个bean
之后就可以使用 #{bean.indexName} 来读取到配置属性了

Spring Boot 中手动解析表达式的值

有时候我们会在注解中使用 SPEL 表达式来读取配置文件中的指定值, 一般会使用类似于 【${xxx.xxx.xxx}】这样来使用
如果在代码中手动解析该表达式的值,可以使用 Environment 的以下方法

environment.resolvePlaceholders(cidSpel) 或者 environment.resolveRequiredPlaceholders(cidSpel)

背景

docker 容器启动, 通过 docker logs -f container 可以实时查看日志

但是控制台输出的日志太多,会怎么样,容器里控制台输出的日志在宿主机什么位置?

有时容器输出太多,运行时间长了后,会把磁盘撑满…

解释

docker 里容器的日志都属于标准输出(stdout)
每个 container 都是一个特殊的进程,由 docker daemon 创建并启动,docker daemon 来守护和管理

docker daemon 有一个默认的日志驱动程序,默认为json-file
json-file 会把所有容器的标准输出和标准错误以json格式写入文件中,这个文件每行记录一个标准输出或标准错误并用时间戳注释

修改配置

  1. vim /etc/docker/daemon.json

  2. 增加一条:{“log-driver”: “none”} (也可以添加{“log-opts”: {“max-size”: “10m” }} 来控制log文件的大小)

  3. 重新加载配置文件并重启docker服务: systemctl daemon-reload

docker-compose 配置

1
2
3
4
    logging: 
# driver: "json-file"
options:
max-size: "1g"

这样就不需要修改 daemon.json 配置文件了

查看日志位置

  1. docker inspect container_id | grep log
  2. 进入上述目录
  3. du -sh *

解决

在 idea 以前的版本里,在 Preferences | Build, Execution, Deployment | Gradle 去掉勾选 Offline work 即可

但是在最新版 2019.2 里,需要点击 gradle 面板里最上面一排小扳手左边一个图标,取消离线模式

  1. fork 模式下
  • 使用命令参数 pm2 start app.js --node-args="--harmony"
  • json 文件添加配置: "node_args" : "--harmony"
  1. cluster 模式下
    使用上一篇的方法 require("babel-register");
    在更改配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"apps": [
{
"name": "my_name",
"cwd": "./",
"script": "bin/start",
"instances" : "max",
"exec_mode" : "cluster",
"log_date_format": "YYYY-MM-DD HH:mm Z",
"error_file": "./logs/error.log",
"watch": ["routes"]
}
]
}

这里需要注意:

  1. exec_mode 要改为 cluster, instances 为实例数, max 为 CPU 的核心数,
  2. script 里配置的直接就是 js 文件,不需要加 node 命令(如 “script”: “node bin/start”) ,否则启动会报错,我踩过这个坑

  1. package.json 添加
1
2
3
4
5
6
7
8
9
10
"babel": {
"presets": [
"es2015"
]
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-preset-es2015": "^6.24.1",
"babel-register": "^6.26.0"
}
  1. npm install

  2. 有 2 种方法可配置

  • 第一种: 启动命令改为: ./node_modules/.bin/babel-node app.js
  • 第二种: 在 app.js 头部里添加 require("babel-register");

lombok 依赖编译报错

在gradle4.7以后对于加入依赖lombok方式发生变化,gradle4.7版本以前,可以直接如下引用:

1
compile("org.projectlombok:lombok:1.18.2")或者compileOnly("org.projectlombok:lombok:1.18.2")

在gradle5.0这种方式会产生警告,在gradle5.0里面会直接报编译错误

有 2 中解决方式:

  1. 官方推荐

开发依赖:

1
2
3
4
annotationProcessor 'org.projectlombok:lombok:1.18.2'

compileOnly 'org.projectlombok:lombok:1.18.2'

测试依赖:

1
2
3
testAnnotationProcessor 'org.projectlombok:lombok:1.18.2'

testCompileOnly 'org.projectlombok:lombok:1.18.2'
  1. gradle-lombok插件方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
repositories {                 
mavenCentral()
}


plugins {

id 'net.ltgt.apt' version '0.10'

}

dependencies {

compileOnly 'org.projectlombok:lombok:1.18.2'

apt "org.projectlombok:lombok:1.18.2"
}

log4j 报错

错误信息:

1
2
3
Errors occurred while build effective model from /Users/joylau/.gradle/caches/modules-2/files-2.1/log4j/log4j/1.2.16/88efb1b8d3d993fe339e9e2b201c75eed57d4c65/log4j-1.2.16.pom:
'build.plugins.plugin[io.spring.gradle.dependencymanagement.org.apache.maven.plugins:maven-antrun-plugin].dependencies.dependency.scope' for junit:junit:jar must be one of [compile, runtime, system] but is 'test'. in log4j:log4j:1.2.16

这是因为 Log4J 1.2.16 的 pom 中存在一个Bug。1.2.16 已经在 2010 年停止更新了
可以通过声明对 log4j:log4j:1.2.17 的显式依赖
或通过依赖关系管理确保使用 1.2.17 来解决

1
implementation("log4j:log4j:1.2.17")

容器启动时初始化数据的方法

  1. 编写好脚本,支持 .sql;.sh;.sql.gz
  2. 容器启动时, 将脚本挂载到容器的 /docker-entrypoint-initdb.d 目录下即可

可就是这么简单的操作,我却没有成功…

注意

该方法只在初始化数据库的时候起作用,意思是,当你想把 mariadb 的数据目录 /var/lib/mysql 挂载到本地盘上,那么 该目下有文件时,放置的脚本将不会执行

Electron 自动更新的方法

  1. 使用 Electron 自己提供的 autoUpdater 模块
  2. 使用更新服务器
  3. 自己实现自动更新逻辑

为什么说经过了一系列的折腾呢, 因为前 2 中方式都没有解决我的问题,最后我是自己实现了自动更新的逻辑
没有解决我的问题是因为我需要兼顾到 mac 平台和 Windows 平台,然而 mac 平台比较麻烦,代码需要签名
我自己亲测方式一和方式二在 mac 平台上都需要代码签名, 而签名代码需要注册苹果开发者账号,需要付年费
于是这 2 条路就走不通了

最后我决定自己实现更新的逻辑

更新逻辑分析

  1. 自动触发或手动触发软件更新检查
  2. 服务器版本号大于本地版本才出现更新提示
  3. 对于更新,无非就是卸载之前的版本,安装新下载的安装包
  4. 软件的打包我选择的是 Electron Builder, 分别打成 dmg , setup.exe , app.zip
  5. 更新的时候先从服务器下载新版本
  6. 下载完成后对于安装包的安装分平台来说

Windows 下的更新

  1. Windows 下的安装包是 exe 可执行文件,安装包本身是有处理逻辑在里面的
  2. 于是我们只需要将安装包下载到临时目录,然后再软件里打开它,再退出软件,剩下的安装步骤交给用户
  3. 有一点需要注意的是,NSIS 的新安装包在安装前会自动卸载掉之前的版本,不过不会提示用户,我们可以在 nsis 脚本里加一个提示

MacOS 下的更新

  1. 相比于 Windows 下的安装包, macOS 下的 dmg 安装包就没有什么逻辑了,直接打开,然后将 app 文件拖到 Applications 目录中即可完成安装
  2. 于是有 2 中方法可选
  3. 一. 挂载 dmg, 找到挂载目录,在 mac 下是 /Volumes 目录下; 删除 /Applications 下的 app, 将 /Volumes 下的 app 拷贝到 /Applications 目录下; 再卸载 dmg; 重启应用即可,该方法可实现类似无缝更新的效果
  4. 二. 和方法一一个道理,只不过不是挂载 dmg 来查找 app, 直接解压 app.zip 压缩文件即可得到 app ,在使用相同的方式覆盖即可.

软件的版本控制

可以采取一个 json 文件来记录个版本的更新记录, 这里给个参考:

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
[
{
"version": "1.1.0",
"force": false,
"time": "2019-09-14",
"download": {
"winSetup": "",
"dmg": "",
"appZip": ""
},
"description": [
"1. 修复若干 BUG,稳定性提升"
]
},
{
"version": "1.0.0",
"force": false,
"time": "2019-09-01",
"download": {
"winSetup": "",
"dmg": "",
"appZip": ""
},
"description": [
"1. 全新界面,主体功能完成"
]
}
]

代码参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
import $ from 'jquery';
import semver from 'semver';
import request from 'request';
import progress from 'request-progress';

//global.fs = require('fs');
//global.cp = require('child_process');
const fs = window.fs;
const cp = window.cp;
const electron = window.electron;
const {app, shell} = electron.remote;

state = {
check: true,
latest: {},
// wait,download,install,error
update: 'wait',
downloadState: {}
};

// 检查更新
$.ajax({
url: appConfig.updateCheckURL,
timeout: 10000,
type: 'GET',
cache:false,
success: function (data) {
let latest = data[0];
if(semver.satisfies(latest.version, '>' + app.getVersion())){
if (latest.force) {
that.updateVersion();
}
}
},
complete: function (XMLHttpRequest, status) {
that.setState({
check: false
})
}
});


updateVersion(){
let that = this;
const platform = osInfo.platform();
try {
const downloadUrl = platform === 'darwin' ? this.state.latest.download.dmg : platform === 'win32' ? this.state.latest.download.winSetup : '';
if (downloadUrl === '') return;

const downloadUrlArr = downloadUrl.split("/");

const filename = downloadUrlArr[downloadUrlArr.length-1];

const savePath = osInfo.tmpdir() + '/' + filename;

const _request = request(downloadUrl);
progress(_request, {
// throttle: 2000, // Throttle the progress event to 2000ms, defaults to 1000ms
// delay: 1000, // Only start to emit after 1000ms delay, defaults to 0ms
// lengthHeader: 'x-transfer-length' // Length header to use, defaults to content-length
})
.on('progress', function (state) {
// The state is an object that looks like this:
// {
// percent: 0.5, // Overall percent (between 0 to 1)
// speed: 554732, // The download speed in bytes/sec
// size: {
// total: 90044871, // The total payload size in bytes
// transferred: 27610959 // The transferred payload size in bytes
// },
// time: {
// elapsed: 36.235, // The total elapsed seconds since the start (3 decimals)
// remaining: 81.403 // The remaining seconds to finish (3 decimals)
// }
// }
that.setState({downloadState: state})
})
.on('error', function (err) {
that.setState({
downloadState:{
error: true
}
})
})
.on('end', function () {
if (that.state.update === 'error') return;
that.setState({
update: 'install',
});

setTimeout(function () {
if (platform === 'darwin'){
const appName = pjson.build.productName;
const appVersion = app.getVersion();
console.info(appName,appVersion);
// 挂载
cp.execSync(`hdiutil attach '${savePath}' -nobrowse`, {
stdio: ['ignore', 'ignore', 'ignore']
});

// 覆盖原 app
cp.execSync(`rm -rf '/Applications/${appName}.app' && cp -R '/Volumes/${appName} ${appVersion}/${appName}.app' '/Applications/${appName}.app'`);

// 卸载挂载的 dmg
cp.execSync(`hdiutil eject '/Volumes/${appName} ${appVersion}'`, {
stdio: ['ignore', 'ignore', 'ignore']
});

// 重启
app.relaunch();
app.quit();
}

if (platform === 'win32') {
shell.openItem(savePath);
setTimeout(function () {
app.quit();
},1500)
}
},2000)
})
.pipe(fs.createWriteStream(savePath));

that.setState({update:'download'});
} catch (e) {
console.info(e);
that.setState({
update: 'error',
});
}
}
0%