CrazyAirhead

疯狂的傻瓜,傻瓜也疯狂——傻方能执著,疯狂才专注!

0%

背景

上一篇说到使用vue-element-admin来做后端管理,经同事的介绍可以使用Muse-ui来做前端应用。查看了Muse的官网后,准备踩坑下。

快速入门

在Github上看到MuseUI的模板nuxt-template

1
2
3
4
vue init museui/nuxt-tempalte myproject
cd my-project
yarn instll
yarn run dev

一切看起来很美好,http://localhost:3000可直接看页面了。

Nuxt.js

一看源码,连App.vue都没有,刚有点基础的我慌了。赶紧查查是什么?

简单点说Nuxt.js是一个基于Vue.js的通用应用框架,重点关注应用的UI渲染(支持客户端和服务端渲染),另外它像Maven一样规定了项目的目录结构,对于初学者来说,看懂文档,还是相对容易创建一个新的项目的。这里有它的文档,可以自行了解。

将项目转成TypeScript

之前说过,ts更接近java,为了统一,我准备将项目转换成ts的,顺便多熟悉下Nuxt。

创建

1
mkdir myproject2

用VSCode打开空目录,myproject2

新建package.json文件

package.json 文件用来设定如何运行 nuxt:

1
2
3
4
5
6
{
"name": "myproject2",
"scripts": {
"dev": "nuxt"
}
}

安装Nux

1
yarn add nuxt

pages目录

Nuxt.js 会依据 pages 目录中的所有 *.vue 文件生成应用的路由配置。

创建 pages 目录:

1
mkdir pages

创建我们的第一个页面 pages/index.vue

1
2
3
<template>
<h1>Hello world!</h1>
</template>

启动

1
yarn run dev

此时我们的应用运行在 http://localhost:3000 上运行。

支持TypeScript

为了能够在项目中使用TypeScript,您需要将@nuxt/typescriptts-node作为开发依赖项安装:

1
2
yarn add -D @nuxt/typescript
yarn add ts-node

需要创建一个空的tsconfig.json文件,tsconfig.json文件将在您第一次运行nuxt命令时使用默认值自动更新。

1
touch tsconfig.json

配置文件

为了能够在配置文件中使用TypeScript,您只需要将nuxt.config.js重命名为nuxt.config.ts

使用ESLint

1
yarn add -D @typescript-eslint/eslint-plugin

然后,通过添加@typescript-eslint插件并使@typescript-eslint/parser作为默认解析器来编辑ESLint配置(.eslintrc.js)。

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
plugins: ['@typescript-eslint'],
parserOptions: {
parser: '@typescript-eslint/parser'
},
extends: [
'@nuxtjs'
],
rules: {
'@typescript-eslint/no-unused-vars': 'error'
}
}

最后,添加或编辑package.jsonlint脚本:

1
"lint": "eslint --ext .ts,.js,.vue --ignore-path .gitignore ."

使用MuseUI

拷贝pages文件

将myproject下的about.vue和index.vue拷到pages下

拷贝plugins文件

将myproject下的muse-ui.js拷贝过来并重命名为muse-ui.ts

修改配置文件

nuxt.config.js拷贝过来重命名为nuxt.config.ts。其中去掉eslint的相关配置。修改vendor为

1
2
3
vendor: [
'~/plugins/muse-ui'
]

问题

此时通过yarn run dev已经可正常编译,但运行时会页面会提示错误。大致错误

1
2
3
4
5
6
SyntaxError
Invalid or unexpected token

vm.js
Missing stack framesJS
new Script@80:7

查官网issues,说是还不支持ssr,调整plugins配置就可以了。

1
2
3
4
5
6
plugins: [
{
src: '~plugins/muse-ui',
ssr: false
}
]

最终的nuxt.config.ts文件

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
module.exports = {
/*
** Headers of the page
*/
head: {
title: 'myproject2',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: 'Nuxt.js Muse-UI project' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons' }
]
},
/*
** Customize the progress bar color
*/
loading: { color: '#3B8070' },
plugins: [
{
src: '~plugins/muse-ui',
ssr: false
}
],
/*
** Build configuration
*/
build: {
babel: {
plugins: [
['import', {
libraryName: 'muse-ui',
libraryDirectory: 'lib',
camel2DashComponentName: false
}]
]
},
vendor: [
'~/plugins/muse-ui'
],
extractCSS: true
}
}

经过以上的修改已经可以通过nuxt来管理vue项目,并能过ts来写代码了。

背景

因为公司目前暂时没的前端,于是准备自己用Vue和TypeScript入手公司项目的管理后台。因为Vue已经有很多UI框架了(比如Element),而TypeScript呢,引入了类等概念,对于Java开发人员来说是比较容易入手的。而后端开发人员可能最头疼的是CSS布局吧,于是找了vue-element-admin来抄,当然有人改了个TypeScript版本vue-typescript-admin-template

问题

经过一段时间的熟悉yarn和代码结构,我们开始了服务端的调用。

跨域问题

本地服务开发我们是可以设置Chrome的跨域的(–disable-web-security
–user-data-dir=),但为了后面更好的开发和及时发现问题,我们还是使用了Proxy方式,按vue-cli的文档,增加vue.config.js的devServer配置就要可以,如下:

1
2
3
4
5
6
7
8
9
module.exports = {
/**
* vue-cli3跨域的全配置!
*/
devServer: {
proxy: 'http://172.26.0.252:8881'
}
}

但我们还是出现了问题,原来在request.ts中也设置了服务器地址

1
2
3
4
const service = axios.create({
baseURL: 'http://172.26.0.252:8881',
timeout: 5000
})

其实呢这个值应该设置在.env中,设置VUE_APP_BASE_API

1
VUE_APP_BASE_API=http://172.26.0.252:8881

远程调用

解决了跨域之后,开始了实际的接口调用,修改的是登录页面,一测试,报这样的错。

1
2
3
4
5
6
7
8
9
10
login error:Error: ERR_ACTION_ACCESS_UNDEFINED: Are you trying to access this.someMutation() or this.someGetter inside an @Action? 
That works only in dynamic modules.
If not dynamic use this.context.commit("mutationName", payload) and this.context.getters["getterName"]
Error: Could not perform action Login
at Store.eval (webpack-internal:///./node_modules/vuex-module-decorators/dist/esm/index.js:333:37)
at step (webpack-internal:///./node_modules/vuex-module-decorators/dist/esm/index.js:114:23)
at Object.eval [as throw] (webpack-internal:///./node_modules/vuex-module-decorators/dist/esm/index.js:95:53)
at rejected (webpack-internal:///./node_modules/vuex-module-decorators/dist/esm/index.js:86:65)
Error: error with code: undefined
at __webpack_exports__.default (webpack-internal:///./src/utils/request.ts:48:133)

嗯,完全没在头绪,查了下呢看到这篇Rejected action throws ERR_ACTION_ACCESS_UNDEFINED,说是增加rawError,因为对Vuex不熟悉,也还是不知道怎么加。

之后查看了vuex-module-decorators的基本文档。

回头重新看issues,搜索ERR_ACTION_ACCESS_UNDEFINED,记录还是不少的,一条条的看,终于看到rawError如何设置。

1
@Action({ rawError: true })

修改代码,测试这个时候报错

1
code undifined

问题于是又定位到了request.ts上。

查看代码,如下:

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
service.interceptors.response.use(
(response) => {
// Some example codes here:
// code == 20000: valid
// code == 50008: invalid token
// code == 50012: already login in other place
// code == 50014: token expired
// code == 60204: account or password is incorrect
// You can change this part for your own usage.
const res = response.data
if (res.code !== 20000) {
Message({
message: res.message,
type: 'error',
duration: 5 * 1000
})
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
MessageBox.confirm(
'你已被登出,可以取消继续留在该页面,或者重新登录',
'确定登出',
{
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
UserModule.FedLogOut().then(() => {
location.reload() // To prevent bugs from vue-router
})
})
}
return Promise.reject(new Error('error with code: ' + res.code))
} else {
return response.data
}
},
(error) => {
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)

是的,我们的返回值没有返回code,错误被上一层的Module给捕获了,接下来改代码就好了。

小结

实操才会碰到问题,才能解决问题,看文档时你是不会知道有多少坑的,你以为你会了实际上你还不会。

点:如高峰坠石

右点是写好点的基础,空中落笔,不可太长。垂点像露珠,仰点是自左上到右下落笔顿笔后往左路上挑。

横:如千里阵云

起笔要稳,略顿,收笔要顿笔回锋。长横要舒展,行笔速度稍快,中间微微上凸。短横顶着斜向上。

竖:如万岁枯藤

垂露竖起笔向左上方伸出,顿笔下行,轻顿收笔。悬针竖运笔沉着形如银针,中锋行笔。

撇:如陆断犀象

长撇要舒展而飘逸,短撇要迅速而有力。写好撇,是关键的是要撑握好角度和弧度。

捺:如崩浪雷奔

捺的出锋要重顿快收,力量要用得沉稳。斜捺是轻入笔,右下行笔出书锋,不要太直,忌生硬。

折:如劲弩筋节

折在书写时候,要有一个明显的停顿,然后转笔和力量的变化,方圆兼备。

提:如策马扬鞭

顿笔斜向上,形状像匕首,速度要快,有力量,有策马扬鞭的气势,但不要甩。

勾:如百钧弩发

横勾出钩迅速,钩身宜短,出锋方向指向字心。卧钩像一口锅,向上环抱。斜钩像一张弓,行笔迅疾,力拔山河。

图

今天启动Spring boot的一个项目时,提示“Process finished with exit code 1”,项目无法启动。
初步分析可能是没有配置环境变了,检查了下是有的,加了debug=true,也没出提示日志。

于是上网查了下,确实说的是环境变量spring.profiles.active没在配置,重新检查配置时发现,之前的环境变量居然是spring.active.profiles。因为换电脑,没有在这台机器运行不同环境,没有发现错误。

提醒下自己,检查配置内容还是要认真些。

参考

解决 Spring boot 启动报 Process finished with exit code 1 问题

概要

MM理论被称为是“公司金融学奠基石的理论”,这个理论是关于融资决策的。香帅把这个理论称为企业融资的地平线理论。

两个M分别是两位作者名字的首个字母,米勒Miller和莫迪利亚尼Modiogian。米勒也因为他在MM理论中的贡献,在1990年和马科维茨、夏普分享了诺贝尔奖。莫迪利亚尼是因为在之前过世了,所以没有拿到诺奖。

推论

  • 一个企业的市场价格决定于它未来能创造的价值。
  • 在有税收存在的情况下,一个企业的价值会随着债务比例的上升而上升。而增加的那部分价值,就恰好等于利息乘以公司的税率。
  • 当债务存在着破产风险的时候,一个企业的价值会随着债务比例的上升而下降,而下降的价值就等于公司总价值乘以破产的概率。

为什么是地平线理论

这个理论看起来挺简单的,为什么这么一个听上去很简单的理论,就拿了诺奖,而且还被誉为“公司金融的奠基性的理论呢”?

一个好理论的评价标准就是能否用一个最简洁(parsimonious)的框架模型解释最多(rich)的现象。

MM定理很明显,完美地做到了这点,它先假设一个没有摩擦的世界,然后一步步地加入这个摩擦,让你可以估算这个摩擦对公司价值的影响。然后从里作为起点,你就可以动态地思考下去,用这个理论作为标尺(benchmark),去度量和评估不同条件下的融资决策。从一个最简洁,最干净的设定,得到最丰富的现实解释。这是理论的力量,也是MM定理做得特别好的地方。

那更牛的地方在哪里呢?就是在MM定理动态演化发展的过程中,学界慢慢形成了一套企业投融资角度的体系化思想。比如说在股东价值最大化的目标下,我们该如何投资项目,该如何选择融资结构,该怎么考虑分红政策,分司的内部治理,哪种选择会更加提升公司的价值。这也是为什么香帅会说MM定理就像公司的地平线理论。

思考

金融学思维和架构设计的思维有时是相关的,比如KISS,Less Is More。基于一个好理论的评价标准,我们能否用来定义一个好技术架构呢?用一个最简单的框架服务最多的应用场景。

来源

本文为我对《香帅的北大金融学课》的摘抄和整理,更多详细内容请在得到订阅的《香帅的北大金融学课》。

In addition to indexing HBase updates in near-real-time, it’s also possible to run a batch indexing job that will index data already contained within an HBase table. The batch indexing tool operates with the same indexing semantics as the near-real-time indexer, and it is run as a MapReduce job.

In its most basic running mode, the batch indexing can be run as multiple indexers that run over HBase regions and write data directly to Solr, as follows:

1
2
3
4
hadoop jar hbase-indexer-mr-*-job.jar \ 
--hbase-indexer-zk zk01 \
--hbase-indexer-name docindexer
--reducers 0

It is also possible to generate offline index shards in HDFS by supplying -1 or a positive integer for the –reducers argument, as shown below:

1
2
3
4
5
hadoop jar hbase-indexer-mr-*-job.jar \ 
--hbase-indexer-zk zk01 \
--hbase-indexer-name docindexer \
--reducers -1 \
--output-dir hdfs://namenode/solroutput

Finally, indexing shards can be generated offline and then merged into a running SolrCloud cluster using the –go-live flag as follows:

1
2
3
4
hadoop jar hbase-indexer-mr-*-job.jar \
--hbase-indexer-zk zk01 \
--hbase-indexer-name docindexer \
--go-live

The HBase Indexer publishes a number of metrics for each of its indexer processes. These metrics can be useful for getting an idea of how much data is being indexed, keeping track of the health of indexing processes, and troubleshooting problems.

Hbase Indexer发布了一些索引生成时的指标数据。些指标可用于了解索引了多少数据、跟踪索引进程的运行状况和故障排除问题。

By default, all metrics are published via JMX. The indexer can also be configured to publish metrics to Ganglia (see below).

默认情况下,所有的指标是通过JMX发布的。也可以通过配置将指标发布到Ganglia中(配置看下文)。

Besides these metrics, you can monitor the replication status using the SEP tools.

除了这些指标,还可以通过SEP tools来监控复制集的状态。

可用指标一览(Overview of available metrics)

HBase to Solr mapping

Each indexer has a metric under hbaseindexer/DefaultResultToSolrMapper/<indexer_name>/HBase Result to Solr Mapping time.

每个索引在hbaseindexer/DefaultResultToSolrMapper/<indexer_name>/HBase Result to Solr Mapping time下用一个指标。

This metric lists information about the amount of time taken (in milliseconds) for converting an HBase update event into a Solr document, as well as a rate (in documents/second) for the general throughput of the process.

这个指标列出了将Hbase转换成Solr文档的总耗费时间(毫秒),以及文档吐吞率(文档数/秒)。

Incoming and applicable Events

Under hbaseindexer/(Row|Column)BasedIndexer/<indexer_name>, there are a pair of metrics about incoming events and applicable events. These metrics list the rate of incoming HBase update events, as well as the rate of incoming HBase events that are considered applicable for indexing.

hbaseindexer/(Row|Column)BasedIndexer/<indexer_name>下, 是一对关于输入事件和可用事件的指标。这些指标列出了输入HBASE更新事件的速率,以及被认为可用索引的输入HBASE事件的速率。

Solr writing

Under hbaseindexer/SolrWriter/<indexer_name>, there are a number of metrics around the rate of adds and delete to Solr.

hbaseindexer/SolrWriter/<indexer_name>下,是一些关于数据添加和删除到Solr速率的指标。

There are also metrics about “document add/delete errors” and “Solr add/delete errors”. Document-based errors are errors that have occurred where the problem has been determined to be in the Solr document itself. Solr-based errors are errors that have occurred within Solr.

也包含了“文档添加/删除错误”和“Solr添加/删除错误”。基于文档发错误是指发生在Solr文档本身中的错误。基于Solr的错误是在Solr中发生的错误。

Ganglia配置(Ganglia)

Reporting of metrics to Ganglia can be configured in the hbase-indexer-site.xml file (in the conf directory).

通过配置hbase-indexer-site.xml文件(在conf目录下)可以把指标生成到Ganglia中。

In order to configure reporting in Ganglia, the following three configuration keys must be supplied:

  • hbaseindexer.metrics.ganglia.server - Ganglia server to report to
  • hbaseindexer.metrics.ganglia.port - port on the Ganglia server
  • hbaseindexer.metrics.ganglia.interval - interval for reporting, in seconds

为了配置Ganglia,下面三个配置需要被指定:

  • hbaseindexer.metrics.ganglia.server - Ganglia服务器地址
  • hbaseindexer.metrics.ganglia.port - Ganglia服务器端口
  • hbaseindexer.metrics.ganglia.interval - 生成报告时间间隔

An example Ganglia reporting configuration would look like this
以下是一个生成Ganglia报告的配置:

1
2
3
4
5
6
7
8
9
10
11
12
<property>
<name>hbaseindexer.metrics.ganglia.server</name>
<value>mygangliaserver</value>
</property>
<property>
<name>hbaseindexer.metrics.ganglia.port</name>
<value>8649</value>
<property>
<property>
<name>hbaseindexer.metrics.ganglia.interval</name>
<value>60</value>
</property>

The HBase Indexer includes a number of command line tools for adding, updating, listing, and deleting indexers. All of these tools are executed by supplying a command name to the hbase-indexer tool. For example, to use the list-indexers command, you would enter the following:

1
./bin/hbase-indexer list-indexers

Running any of the commands below with the –help parameter will show a listing of possible command parameters.

add-indexer

The “add-indexer” command is used for configuring a new indexer.

When adding an indexer, you need to supply at least the following:

  • the name of the indexer
  • the indexer’s definition (in the form of an xml file)
  • connection information to the ZooKeeper used by Solr

In order to add a basic indexer named “myindexer” whose definition is saved in a file called myindexer.xml, with ZooKeeper running on a server named myzkhost:2181/solr and a Solr collection named mycollection, you would execute the following:

1
2
3
4
5
./bin/hbase-indexer add-indexer \
--name myindexer \
--indexer-conf myindexer.xml \
--cp solr.zk=myzkhost:2181/solr \
--cp solr.collection=mycollection

update-indexer

The update-indexer command is used for updating the definition or the state of an indexer.

This command is most typically used for updating the definition of an indexer – for example, changing the list of fields to be indexed.

The parameters to the update-indexer tool are generally the same as those for the add-indexer tool, but none of them are mandatory except for the name field.

To update the definition of the indexer that was created in the example for add-indexer, you would run the following:

1
./bin/hbase-indexer update-indexer --name myindexer --indexer-conf myupdatedindexer.xml

Any parameters (other than the name parameter) that you supply to the update-indexer command will overwrite existing parameters for the indexer.

list-indexers

The list-indexer command lists all currently configured indexers, including various status information about them.

This command can be run without any additional parameters.

delete-indexer

The delete-indexer command removes an indexer and stops all indexing processes for that indexer.

The –name parameter is mandatory for this command.

问题

之前基于Docker搭建了ES环境,也能正常使用,这周突然程序能正常启动,但实际发生ES调用时出错,错误如下:

1
2
3
4
5
6
7
8
9
10
org.elasticsearch.client.transport.NoNodeAvailableException: None of the configured nodes are available: [{#transport#-1}{zYhliWDFSRGpbaB371OV-g}{172.26.0.251}{172.26.0.251:9300}]
at org.elasticsearch.client.transport.TransportClientNodesService.ensureNodesAreAvailable(TransportClientNodesService.java:349)
at org.elasticsearch.client.transport.TransportClientNodesService.execute(TransportClientNodesService.java:247)
at org.elasticsearch.client.transport.TransportProxyClient.execute(TransportProxyClient.java:60)
at org.elasticsearch.client.transport.TransportClient.doExecute(TransportClient.java:381)
at org.elasticsearch.client.support.AbstractClient.execute(AbstractClient.java:407)
at org.elasticsearch.client.support.AbstractClient.execute(AbstractClient.java:396)
at org.elasticsearch.action.ActionRequestBuilder.execute(ActionRequestBuilder.java:46)
at org.elasticsearch.action.ActionRequestBuilder.get(ActionRequestBuilder.java:53)
...

现在将问题做下记录,主要是client.transport.sniff的使用问题。

环境

基于Docker的ES,官网

1
docker run -d --name elasticsearch -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:6.5.4

客户端

客户端代码基本参照了ES官网

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
@Bean(destroyMethod = "close")
public TransportClient client() throws Exception {
TransportClient client = new PreBuiltTransportClient(settings());

String clusterNodes = elasticsearchProperties.getClusterNodes();
Assert.hasText(clusterNodes, "[Assertion failed] clusterNodes settings missing.");

for (String clusterNode : split(clusterNodes, ElasticsearchProperties.COMMA)) {
String hostName = substringBeforeLast(clusterNode, ElasticsearchProperties.COLON);
String port = substringAfterLast(clusterNode, ElasticsearchProperties.COLON);
Assert.hasText(hostName, "[Assertion failed] missing host name in 'clusterNodes'");
Assert.hasText(port, "[Assertion failed] missing port in 'clusterNodes'");
logger.info("adding transport node : " + clusterNode);
client.addTransportAddress(new TransportAddress(InetAddress.getByName
(hostName), Integer.valueOf(port)));
}
client.connectedNodes();
return client;
}

private Settings settings() {
return Settings.builder()
.put("cluster.name", elasticsearchProperties.getClusterName())
.put("client.transport.sniff", elasticsearchProperties.getClientTransportSniff())
.put("client.transport.ignore_cluster_name", elasticsearchProperties
.getClientIgnoreClusterName())
.put("client.transport.ping_timeout", elasticsearchProperties
.getClientPingTimeout())
.put("client.transport.nodes_sampler_interval", elasticsearchProperties
.getClientNodesSamplerInterval())
.build();
}

问题处理

因为之前可以访问,例常的检查了cluster.name和配置的服务器和端口,一切正常。

经过一些搜索后发现这篇文章,NoNodeAvailableException,其中提到了配置了client.transport.snifftrue

检查代码,自己默认配置也是true。调整client.transport.snifffalse,一切正常了。

分析及处理

client.transport.sniff的作用是:使客户端去嗅探整个集群的状态,把集群中其它机器的ip地址加到客户端中。这样做的好处是,一般你不用手动设置集群里所有集群的ip到连接客户端,它会自动帮你添加,并且自动发现新加入集群的机器。

但使用该参数时有个注意事项。当ES服务器监听(publish_address )使用内网服务器IP,而访问(bound_addresses )使用外网IP时,不要设置client.transport.sniff为true。不设置client.transport.sniff时,默认为false(关闭客户端去嗅探整个集群的状态)。因为在自动发现时会使用内网IP进行通信,导致无法连接到ES服务器。因此此时需要直接使用addTransportAddress方法把集群中其它机器的ip地址加到客户端中。
然后检查了下Docker的启动日志:

1
2
3
4
[2019-04-16T05:45:56,427][INFO ][o.e.n.Node               ] [ehrCloud] starting ...
[2019-04-16T05:45:56,648][INFO ][o.e.t.TransportService ] [ehrCloud] publish_address {172.17.0.3:9300}, bound_addresses {0.0.0.0:9300}
[2019-04-16T05:45:56,796][INFO ][o.e.x.s.t.n.SecurityNetty4HttpServerTransport] [ehrCloud] publish_address {172.17.0.3:9200}, bound_addresses {0.0.0.0:9200}
[2019-04-16T05:45:56,797][INFO ][o.e.n.Node ] [ehrCloud] started

之后做了如下尝试,修改配置:

1
network.public_host: 172.26.0.251

重新运行程序,此时无论client.transport.sniff的配置为何,都可正常使用。

不过为什么之前可以,现在不行这个问题还是不清楚。

参考链接

ES版本问题

elasticsearch中client.transport.sniff的使用方法和注意事项

背景

刚接触Jfinal时就对ActiveRecord印象深刻,但公司整体使用SpringBoot框架,使用不多,但一直保持着关注。当Jfinal推出Model生成器,SQL管理和动态生成之后,拿了个项目来练手,找到了淋漓尽致的感觉,惊呼顺畅。如果你使用过Spring Data JPA,只要一对比,你会发现过多的封装就是在挖坑。哦对,就是那个KISS(Keep it Simple and Stupid)原则,异或奥卡姆剃刀原理(如无必要,勿增实体),概念越多,越复杂,越手足无措。回归本质你会得到更多,是的Less Is More。而此时公司项目正在进行一次代码重构,在SpringBoot框架不变的前提下(体系相对完善,有积累),如果能集成Jfinal的ActiveRecord快速搞定增删改查,开发就会变得更简单。是的,我们还在开始使用Kotlin,也是遵循简单,快速原则。

我把整个的集成过程称为SpringBoot和Jfinal的合体,主要分为3部分1.集成ActiveRecord,2.实现代码生成。3.使用方式。

集成ActiveRecord

参考Jfinal的官方文档独立使用ActiveRecord。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ActiveRecordTest {
  public static void main(String[] args) {
    DruidPlugin dp = new DruidPlugin("localhost""userName""password");
    ActiveRecordPlugin arp = new ActiveRecordPlugin(dp);
    arp.addMapping("blog", Blog.class);
    
    // 与 jfinal web 环境唯一的不同是要手动调用一次相关插件的start()方法
    dp.start();
    arp.start();
    
    // 通过上面简单的几行代码,即可立即开始使用
    new Blog().set("title""title").set("content""cxt text").save();
    Blog.dao.findById(123);
  }
}

我们可以看到,基本上是可以直接使用,在SpringBoot的中,我们只要把ActiveRecordPlugin用一个Bean对象的方式来创建就可以了。
于是,有如下代码(当然还有一些配套工作,这里省略了):

1
2
3
4
5
6
7
8
9
10
11
12
13
@Bean
Public ActiveRecordPlugin activeRecordPlugin() {
DruidPlugindruidPlugin = druidPlugin();
druidPlugin.start();

ActiveRecordPluginarp = new ActiveRecordPlugin(druidPlugin);
arp.setTransactionLevel(Connection.TRANSACTION_READ_COMMITTED);
arp.setShowSql(false);

arp.getEngine().setSourceFactory(newClassPathSourceFactory());

return arp;
}

实现代码生成

IDEA可以为JPA后成Entity实体类代码,如果了解Groovy,还可对生成的代码进行一些调整。而Jfinal在框架上就提供了Model的自动生成,对于表更新比较频繁时非常好用。然后查看了下Jfinal的代码生成逻辑,发现了更多的可玩性,于是就不满足于只生成Model了,我们可以通过Jfinal的Enjoy模板引擎,生成基于整个数据库的整个Module(这里指MVC,而非整个工程,工程可通过IDEA的SpringBoot向导来创建),同时为单表提供基本的增删改查(单个查询和分页查询)等基础接口。“有表就有服务”,就是这样简单,之后我们可以更专注于复杂业务逻辑的开发。
代码生成逻辑主要分为代码目录结构设计,代码生成逻辑和代码模板修改。

目录结构

SpringBoot的一个原则,是约定大于配置,而微服务架构也强调统一原则,所以必要的目录结构设计或者规范是需要的。

  1. 所有Model统一放在common/model下。
  2. 按业务名优先原则,也就是”业务名/controller”,”业务名/serivce”的方式。
  3. 以ModelNameEndpoint,ModelNameService方式来命名。

代码逻辑

生成代码的逻辑,主要过程生成Model(同时建构业务分组表信息),生成通用SQL模板,根据不同业务生成Controller和Serivce。我的做法是把Jfinal里面的BaseModelGenerator拷贝出来,做为自己的基类,为不同的模块生成不同的子类,做简单的目录规划和生成模板的调整就可以了,这部分逻辑不复杂。

1
2
3
4
5
6
7
8
9
10
11
12
/**
*生成MVC
*/
Public void generate() {
generateModel();
genSql();

groupTables.forEach((groupName, dbTables) -> {
genEndpoint(groupName,phrMetaBuilder);
genService(groupName,phrMetaBuilder);
});
}

业务分组信息

我这里讲下业务分组信息的提取,继承MetaBuilder重写buildTableNames方法,除了供本生的Model生成,就是提取出来分组表信息,供Controller和Sevice生成器使用。

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
@Override
Protected void buildTableNames(List<TableMeta> ret) throws SQLException {
if(groupTables == null || groupTables.size() == 0) {
super.buildTableNames(ret);
return;
}

ResultSet rs = getTablesResultSet();
Kv kv = Kv.create();
While (rs.next()) {
StringtableName=rs.getString("TABLE_NAME");
Stringremarks=rs.getString("REMARKS");
kv.set(tableName,remarks);
}
rs.close();

groupTables.forEach((group, tables) -> {
ArrayList<TableMeta> tableMetas = new ArrayList<>();
String[] tableList = tables.split(",");
for (Stringtable Name : tableList) {
TableMetatableMeta = new TableMeta();
tableMeta.name = tableName;
tableMeta.remarks = kv.getStr(tableName);
tableMeta.modelName = buildModelName(tableName);
tableMeta.baseModelName = buildBaseModelName(tableMeta.modelName);
tableMetas.add(tableMeta);
}

ret.addAll(tableMetas);
groupTableMeta.put(group,tableMetas);
});
}

代码模板

代码模板基本的实现方式就是通过一个工程,书写出来实际可用的代码,之后改成.jf。放到模板路径下。同时根据表信息,调整模板内容。其间不断的生成加修改就好。这里只截取部分代码。

使用方式

创建工程

使用IDEA向导创建SpringBoot工程,注意使用Maven(Gradle的父POM配置方式还没搞定),调整POM文件(如引入父POM,增加依赖等)。

生成代码

在生成的Test目录,增加如下代码,运行生成代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@RunWith(SpringRunner::class)
@SpringBootTest
class SvrPhrApplicationTests {
@Autowired
private val phrModuleGenerator: PhrModuleGenerator? = null

@Test
fun contextLoads() {
}

@Test
fun genModule() {
phrModuleGenerator?.setBasePackageName("com.jkzl.phr.phr")
?.addGroupTables("adapter",
"adapter_data_element," +
"adapter_dataset")
?.addGroupTables("standard",
"std_dataset," +
"std_data_element")
?.setKotlin(true)
?.generate()
}

}

启用ActiveRecord

通过实现CommanLineRunner接口,在ActiveRecordPlugin Bean对象创建,设置SQL模板,关联Model并启动。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootApplication
class SvrPhrApplication : CommandLineRunner {
Override fun run(vararg args: String?) {
if(arp != null) {
//代码生成后增加
arp.addSqlTemplate("/sql/all.sql")
_MappingKit.mapping(arp)

arp.start()
} else {
throw NullPointerException("arpisnull")
}
}

@Autowired
Private val arp : ActiveRecordPlugin? = null
}

运行

启动项目,我们就可以通过Swagger来查看微服务提供的基础服务了。
swagger

小结

虽然生成代码模板还在改进之处,多次生成代码覆盖问题没有解决,但重要的是,我们已经起步了,同时通过集成Jfinal的ActiveRecord,吸收Jfinal的Model代码生成,让Springboot开发更简单,而使用Kotlin又简化了一些Java的复杂性。之后你会感叹,居然开发可以如此简单。借Jfinal的口号“为您节约更多时间,去陪恋人、家人和朋友 :)”来结束本文。

参考资料

[1]Jfinal官网