JoyLau's Blog

JoyLau 的技术学习与思考

错误

MacOS 升级到 10.15 版本时,预览文件出现下面的提示

qlPlugin-not-work

解决方式 1

删除 ~/Library/QuickLook 目录下的隔离属性 (quarantine attribute)

运行下面命令查看属性:

script
1
xattr -r ~/Library/QuickLook

运行下列命令移除这些属性:

script
1
xattr -d -r com.apple.quarantine ~/Library/QuickLook

解决方式 2

  1. 空格预览文件出现下列提示,点击 取消
qlPlugin-not-work
  1. 转到系统设置里
qlPlugin-solution-1

点击 “Allow Anyway”

  1. 使用下列命令打开刚才需要预览的文件
script
1
qlmanage -p /path/to/any/file.js
  1. 此时弹出提示,点击 “open”
qlPlugin-solution-2
  1. 然后就可以预览该后缀名的所有文件了
qlPlugin-solution-3
  1. 如果需要预览其他类型的文件,则将上述步骤重新操作一遍, 换个后缀名即可

最后推荐

推荐下自己使用的预览插件

script
1
brew cask reinstall qlcolorcode qlstephen qlmarkdown quicklook-json qlimagesize suspicious-package quicklookase qlvideo

需要注意的是 qlcolorcode 需要 highlight 库来显示高亮效果, 需要安装: brew install highlight

背景

在一次生产环境部署 elasticsearch 节点时 docker 容器设置了 –restart always,
此时 elasticsearch 的一个节点发生了 java.lang.OutOfMemoryError: Java heap space
容器并没有重启

elasticsearch 已经设置了 -Xms -Xmx

解释

JVM堆内存超出xmx限制,并抛java.lang.OutOfMemoryError: Java heap space异常。堆内存爆了之后,JVM和java进程会继续运行,并不会crash

解决

当JVM出现 OutOfMemoryError,要让 JVM 自行退出, 这样容器就会触发重启

添加新的 jvm 配置: ExitOnOutOfMemoryError and CrashOnOutOfMemoryError

该配置支持 jdk8u92 版本及其之后的版本

地址: https://www.oracle.com/technetwork/java/javase/8u92-relnotes-2949471.html

oracle 官网的原话:

New JVM Options added: ExitOnOutOfMemoryError and CrashOnOutOfMemoryError
Two new JVM flags have been added:

ExitOnOutOfMemoryError - When you enable this option, the JVM exits on the first occurrence of an out-of-memory error. It can be used if you prefer restarting an instance of the JVM rather than handling out of memory errors.

CrashOnOutOfMemoryError - If this option is enabled, when an out-of-memory error occurs, the JVM crashes and produces text and binary crash files (if core files are enabled).

ExitOnOutOfMemoryError: 启用此选项时,JVM在第一次出现内存不足错误时退出。如果您希望重新启动JVM实例而不是处理内存不足错误,则可以使用它。
CrashOnOutOfMemoryError: 如果启用此选项,则在发生内存不足错误时,JVM崩溃并生成文本和二进制崩溃文件(如果启用了核心文件)。

加上配置

ES_JAVA_OPTS = “-XX:+ExitOnOutOfMemoryError”

git 命令显示中文

直接在终端中执行下面的命令
git config –global core.quotepath false

文件回滚

  1. 工作区尚未暂存的文件: git checkout – 文件名
  2. 已添加到暂存区: git reset HEAD 文件名 && git checkout – 文件名
  3. 已提交到本地库, 想要撤销提交,并恢复到之前的文件内容: git reset –hard HEAD^

其中:
git reset 有三种参数:

  • Soft:这个模式仅仅撤销 commit 记录而已,不影响本地的任何文件,即本地修改的文件内容还会存在,也不影响(index)缓存区的任何文件。
  • Hard:不仅撤销 commit 记录,还将本地的文件指向 commit 前的版本,同时 index 也会指向 commit 前的版本。
  • Mixed:回滚 index ,其余的保持不变。

另外 HEAD 后面加上 ~1 代表回滚一次 commit 记录, HEAD^ 代表全部 commit 记录

git fetch 和 git pull

  1. git fetch : 将本地仓库指向的 remote 提交记录更新为和远端一致,即最新,其他的不做任何改变
  2. git pull : 将本地仓库和将本地仓库指向的 remote 都更新为最新, 相当于 fetch + merge

参看: https://blog.csdn.net/qq_37420939/article/details/89736567

git merge 和 git rebase

merge 和 rebase 都是 git pull 时的策略

git rebase 有以下几种使用场景:

  1. 合并本地的多次提交记录

合并最近的 4 次提交纪录

1
2

git rebase -i HEAD~4

在 idea 中可以在 Version Control 中选择最早时间的提交记录, 然后选择 Interactively Rebase from Here
然后除了第一个为pick外,其他选择squash,点击start rebasing,接着输入提交信息就可以把多次commit合并为一次了

  1. 分支合并,可以把本地未push的分叉提交历史整理成直线

将提交合并到其他分支上

有时候我们在某一个分支上提交了一些代码, 需要将这次提交合并到其他分支上, 这时的做法是:

这当前分支上使用 git log 查看需要进行合并的提交记录的 ID

切换到需要合并的分支上: git checkout 分支名

进行合并: git cherry-pick commitId

推送到到上游分支: git push

spring boot 后台的配置

这里记录一些坑
使用 gradle 配置, 其中移除了 Tomcat , 使用的是 Undertow
先引入依赖

implementation ('org.springframework.boot:spring-boot-starter-websocket')

提示报错 web 容器没有实现 JSR356
undertow 肯定是实现了 JSR356, 在 undertow-websockets-jsr 这个依赖里
判断肯定是由于移除 Tomcat 的问题,查看依赖发现 spring-boot-starter-websocket 依赖了 web , 而 web 默认使用的就是 Tomcat
于是移除 web 依赖即可

1
2
3
implementation ('org.springframework.boot:spring-boot-starter-websocket'){
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-web'
}

代码部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// config.enableSimpleBroker("/topic");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins("*")
.withSockJS();
}

}

注意在启动类上加入: @EnableWebSocketMessageBroker

这里使用的是 stomp 协议, 于是也要前端使用 stomp 配合

发送消息可以加入 @SendTo 注解, 还有一种方式, 就是使用 SimpMessagingTemplate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("ws")
public class PushMessage {

private final SimpMessagingTemplate template;

public PushMessage(SimpMessagingTemplate template) {
this.template = template;
}

@GetMapping("/config")
public void configMessage() {
template.convertAndSend("/topic/public", MessageBody.success());
}
}

React 配置

安装组件 npm install react-stomp
自行封装一个组件,如下:

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
import React, {Component} from 'react';
import SockJsClient from "react-stomp";
import {message} from "antd";

class Websocket extends Component {

render() {
return (
<div>
<SockJsClient
url={'ws'}
topics={[]}
onMessage={(payload) => {
console.info(payload)
}}
onConnect={() => {
console.info("websocket connect success")
}}
onConnectFailure={() => {
message.error("websocket 连接失败!")
}}
onDisconnect={() => {
console.info("websocket disconnect")
}}
debug={false}
{...this.props}
/>
</div>
);
}
}

export default Websocket;

子组件使用:

1
2
3
4
5
6
7
<Websocket
topics={['/topic/public']}
debug={false}
onMessage={(payload) => {
// do somthing
}}
/>

遇坑解决

以上方式看起来使用没有问题,但是现实情况往往开发时前后端分离,请求后端接口往往在 node 项目里配置代理, 这里涉及到 websocket 的代理

之前的配置都是在 package.json 配置, 比如:

1
"proxy": "http://localhost:8098"

但是这种方式对 websocket 的代理失败,会发现 websocket 连接不上

解决方式:
在新版的 customize-cra 的使用方式里:
先安装 http-proxy-middleware : npm install http-proxy-middleware
在 src 目录下新建文件 setupProxy.js, 名字不可改,一定要是这个文件名

1
2
3
4
5
6
7
8
9
10
11
12
const proxy = require("http-proxy-middleware");
const pck = require('../package');

module.exports = app => {
app.use(
proxy("/ws",
{
target: pck.proxy,
ws: true
})
)
};

这里开启 ws: true 即可完成 websocket 的代理.

添加多页面配置

之前写过一篇 npm eject 之后的多页面配置,可以往前翻阅 , 现在不想 eject, 该怎么配置多页面?

  1. npm install react-app-rewire-multiple-entry –save-dev

  2. 在 config-overrides.js 中添加配置
    现在 public 里复制一个 html 页面, 在 src 目录下再新增一个目录,里面的文件拷贝 index 的稍微改动下,
    大致目录如下:

-serviceWorker.js
-metadata.js
-metadata.css
-logo.svg
-App.test.js
-App.js
-App.css

基本使用:

1
2
3
4
5
6
7
8
9
10
11
12
const multipleEntry = require('react-app-rewire-multiple-entry')([{
entry: 'src/metadata/metadata.js',
template: 'public/metadata.html',
outPath: '/metadata',
}]);

module.exports = {
webpack: function(config, env) {
multipleEntry.addMultiEntry(config);
return config;
}
};

在 customize-cra 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const multipleEntry = require('react-app-rewire-multiple-entry')([
{
entry: 'src/entry/landing.js',
template: 'public/landing.html',
outPath: '/landing.html'
}
]);

const {
override,
overrideDevServer
} = require('customize-cra');

module.exports = {
webpack: override(
multipleEntry.addMultiEntry
)
};

结合 ant-design 使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const {override, fixBabelImports, addLessLoader} = require('customize-cra');

const multipleEntry = require('react-app-rewire-multiple-entry')([{
entry: 'src/metadata/metadata.js',
template: 'public/metadata.html',
outPath: '/metadata',
}]);


module.exports = override(
multipleEntry.addMultiEntry,
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}),
addLessLoader({
javascriptEnabled: true,
modifyVars: { '@primary-color': '#1890ff' },
}),
);

注意,这样配置的话, 请求的 uri 是 /metadata, 在 build 后会生成 metadata 文件, 将打包后的文件拷贝到服务器上运行效果不好
一般我都注释掉 template, 再将 outPath 写成 /metadata.html

打包不生成 source-map 文件

在 配置文件 config-overrides.js 添加 process.env.GENERATE_SOURCEMAP = "false";
或者

在项目更目录下创建文件 .env, 写入: GENERATE_SOURCEMAP=false 即可.

背景

Nested 类型的数据不多说了,
先看 mapping:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"metaArray": {
"type": "nested",
"properties": {
"key": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"full": {
"type": "keyword"
}
}
},
"value": {
"type": "text",
"analyzer": "ik_max_word",
"fields": {
"full": {
"type": "keyword"
}
}
}
}
},

再看数据:

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
{
"_index":"category_libs_v1.x",
"_type":"category_info",
"_id":"526",
"_version":1,
"_score":1,
"_source":{
"categoryName":"投标文件",
"createTime":"2019-12-23 00:07:15",
"id":"526",
"metaArray":[
{
"value":"Joy",
"key":"作者"
},
{
"value":"txt",
"key":"文件类型"
}
],
"pathName":"企业空间导航/业务条块",
"pids":"|1|525|",
"status":0,
"updateTime":"2019-12-23 00:07:15"
}
}

目的

想查作者是 Joy 并且文件类型是 txt 的记录

方式

使用 nestedQuery + queryStringQuery

语句:

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
{
"from":0,
"size":10,
"query":{
"bool":{
"must":[
{
"nested":{
"query":{
"query_string":{
"query":"metaArray.key.full:作者 AND metaArray.value.full:Joy"
}
},
"path":"metaArray",
"score_mode":"max"
}
},
{
"nested":{
"query":{
"query_string":{
"query":"metaArray.key.full:文件类型 AND metaArray.value.full:txt"
}
},
"path":"metaArray",
"score_mode":"max"
}
}
]
}
}
}

代码:

1
2
3
String key = xxxx
String value = xxxx
nestedQuery("metaArray", queryStringQuery("metaArray.key.full:" + key + " AND metaArray.value.full:" + value), ScoreMode.Max);

背景

之前一直用我弟弟的学生证申请的 license,可惜今年毕业了,无法在续费申请了
早期已经听说 JetBrains 可以使用自己的开源项目进行申请免费的 license
正好使用我的这个博客来申请一波

步骤

  1. 前往 JetBrains 官方提供的申请链接 (https://www.jetbrains.com/shop/eform/opensource?product=ALL)
  2. 填写资料,其中注意,需要在项目的根目录下创建 License 文件,类型没有差别,我使用的是 MIT LICENSE,还有就是邮箱地址和 github profile 页面的邮箱地址一致
  3. 等待了 2 天,收到了 JetBrains 工作人员的回复
  4. 点击邮件中的 Take me to my license(s)
  5. 使用申请的邮箱地址登录已有的账号或者创建新的账号
  6. 登录成功后点击 license tab 页面,会看到你填的项目名
  7. 点击 Active subscriptions, 激活,在点击 Assign分配使用
  8. 看到一些提示成功信息,就说明没有问题了,直接在 IDEA 的激活页面登录账户使用即可

小插曲

我这里因为之前使用 QQ 邮箱创建过 JetBrains 的账号,这次不想使用新的邮箱再次创建账号
于是在第四步点击 Take me to my license(s) 后,我选择授权其他邮箱,填写邮箱地址,之后你的邮箱会收到邮件,点击接受授权的链接
之后的操作都一致了,成功使用我原来的邮箱获取到了 license.

区别

个人理解:
${} : 用于加载外部文件中指定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)

0%