JoyLau's Blog

JoyLau 的技术学习与思考

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)

背景

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”) ,否则启动会报错,我踩过这个坑
0%